SirenHytale
本节页面
  1. 修真
  2. 开服指南
  3. 注册表

注册表

监听事件让你得以重塑 Cultivation 已有的行为。而此处的注册表,让你得以添加模组随后会视如己出的内容 —— 一个出现在种族菜单里的种族、一门可经由每种功法触发器施展的功法、导航栏上的一个页面、典籍中的一篇条目、选择页上的一个称号、宗门可悬挂的一面旗帜、能为每个菜单重新上色的一套配色。

CultivationAPI 共有 30 个 register* 方法(计入重载为 33 个),其中 23 个有对应的 unregister*。下面逐一列出,附上它注册什么,以及讲解它的章节:

方法返回所注册的章节
registerRace(String id, String displayName, String translationKey, CultivationRealm unlockRealm, Supplier<RaceConfig> stats)PlayerRace一个可玩的种族。注册一个种族
registerTechnique(String id, String displayName, String nameKey, String descriptionKey, TechniqueRule defaultRule, TechniqueEffect effect)Technique一门可施展的功法。注册一门功法
registerTechniqueGate(String id, String failureKey, TechniqueGate.Condition condition)(另有 1 个重载)TechniqueGate功法可要求的一个运行时条件。功法关卡
registerQiAbsorptionItemModifier(String itemId, float multiplier)某件持握之物的灵脉吸收倍率。注册一件灵气吸收物品
registerAdminConfigSection(AdminConfigSection section)管理菜单配置页上的一组可调行。注册管理设置
registerPlayerAdminAction(PlayerAdminAction action)管理菜单玩家页上的一个逐玩家行。注册逐玩家的管理操作
registerIdentitySection(IdentitySection section)身份页上位于种族与称号之后的一个标签页。身份页分节
registerMenuPage(CultivationMenuPage page)每个 Cultivation 菜单所带导航栏上的一个按钮。菜单页
registerCodexEntry(CodexEntry entry)游戏内典籍中的一篇条目。典籍条目
registerCodexCategory(CodexCategory category)典籍索引中的一个标题。典籍分类
registerRevealableSystem(RevealableSystem system)渐进式解锁可将其隐藏、直至它变得相关的一个系统。可揭示的系统
registerTitle(CultivationTitle title)一个装饰性的称号。注册一个称号
registerSectBanner(SectBanner banner)宗门可悬挂于其大殿之上的一面旗帜。注册一面宗门旗帜
registerPalette(CultivationPalette palette)一套菜单与 HUD 配色。注册一套配色
registerPaletteDefault(String key, CultivationPaletteDefault rule)为尚未选择的玩家挑选配色的一条规则。配色默认与配色锁
registerPaletteLock(CultivationPaletteLock lock)对某些玩家强制指定配色的一条规则。配色默认与配色锁
registerMeditationAura(CultivationMeditationAura aura)一种打坐、突破与晋阶的粒子外观。打坐气息
registerOffRingRoot(DaoRoot root)对一个环外 DaoElement 槽位的认领。环外灵根
registerPersonalDao(PersonalDao dao)一个个人之道(经由事迹领悟的一门道)。个人之道
registerBeastArt(String id, String displayName, String nameKey, String descriptionKey, BeastArtRule defaultRule, BeastArtEffect effect)BeastArt一门伙伴一侧的兽技。兽技
registerSectBuildingType(SectBuildingType type)一种宗门建筑。宗门建筑
registerLifeBoundTrait(LifeBoundTrait trait)本命法宝可以显出的一种性。本命法宝之性
registerMasteryStage(MasteryStageRule stage)boolean功法境界阶梯上额外的一级。功法境界的阶梯
registerModifierSource(String key, CultivationModifierSource source)覆盖 Cultivation 各系统的、逐玩家的常驻修正。常驻修正
registerStoreBenefit(StoreBenefit benefit)与游戏内权益相绑的一件网店商品。宝阁权益
registerTechniquePresetCap(String key, int cap)玩家可保留的快捷键配置组数目的更高上限。上调上限
registerProfileCap(String key, int cap)玩家可保留的存档数目的更高上限。上调上限
registerUpdateCheck(String modId, String displayName, String currentVersion, String manifestUrl, String downloadUrl)(另有 1 个重载)UpdateCheck你的模组,加入更新检查。更新检查
registerBuildCheck(String modId, String displayName, String version, Path jarPath, String manifestUrl)BuildCheck你的模组,加入构建检查。构建检查
registerCompatCheck(String modId, String displayName, String version, String manifestUrl, Consumer<CompatCheck> onVerdict)(另有 1 个重载)CompatCheck你的扩展,加入兼容性检查。兼容性检查

它们全都可安全地从你自己插件的 setup() 中调用,相对于 Cultivation 自身的 setup() 无论何种加载顺序皆然。注册表是普通的静态结构,在玩家真正交互之前 —— 打开种族菜单、打坐、施展功法、打开典籍 —— 无人读取,而那只会发生在每个插件加载完毕许久之后。请在你的 shutdown() 中以对应的 unregister* 撤下你所注册的一切,使重载之后不致留下指向已不存在的类的条目。

以同一 id 重复注册一个种族、一门功法或一门兽技是空操作:它返回已有的条目而非报错,因此在你的插件重载时也是安全的。对其余大多数注册表,以已存在的 id 注册会替换先前的条目 —— 模组也正是以此重新装点或接管 Cultivation 自己的某个内置项,而不是在旁边另添一个。

有两种整层接管是安装而非注册的 —— setProgressionProvider 与 setTheme —— 见编写扩展。包含读取方法在内的完整方法清单,见接口参考。

注册一个种族

@Nonnull
public static PlayerRace registerRace(@Nonnull String id, @Nonnull String displayName, @Nullable String translationKey,
                                      @Nonnull CultivationRealm unlockRealm, @Nonnull Supplier<RaceConfig> stats)
参数说明
id一个稳定、唯一、且不展示给玩家的 id。请以你模组的名字作命名空间 —— "MyMod:Vampire" —— 以免与另一模组同短名的种族相撞。
displayName未给出 translationKey 时界面所显示的名称;若该键在玩家的语言环境下无法解析,也会作为回退。
translationKey用于本地化名称的 server.lang 键;填 null 则始终以纯文本显示未翻译的 displayName。
unlockRealm玩家修为须达到的境界,方可在菜单中选择此族。
stats每当需要时,供给此族的属性加成。

stats 供给器是实时调用的,因此你可以用自己插件的 withConfig(name, RaceConfig.codec(...)) 为其提供一份服主可编辑的 JSON 文件,或者对固定数值直接用 () -> myConstantConfig。仅当所供给的配置尚未指定时,unlockRealm 才会去播种 RaceConfig 的 Unlock-Realm —— 这样一来,用自己 JSON 文件(其本身可能设了 Unlock-Realm)来支撑属性的调用方,仍保持服主可编辑。

RaceConfig 为它的每一个可调项都提供了 getter 与 setter:getDescription / setDescription、getUnlockRealm / setUnlockRealm、getHealthBonusPercent / setHealthBonusPercent、getDamageBonusPercent / setDamageBonusPercent、getQiGainRatePercentBonus / setQiGainRatePercentBonus、getBreakthroughDurationPercentReduction / setBreakthroughDurationPercentReduction,以及 getQiAlignmentYinBiasPercent / setQiAlignmentYinBiasPercent。它们与配置页内置种族配置文件中的 Health-Bonus-Percent、Damage-Bonus-Percent、Qi-Gain-Rate-Percent-Bonus、Breakthrough-Duration-Percent-Reduction 与 Qi-Alignment-Yin-Bias-Percent 诸键一一对应。

完整示例 —— 一个于元婴期解锁、属性固定的吸血鬼族:

import plugin.siren.API.CultivationAPI;
import plugin.siren.ECS.Races.PlayerRace;
import plugin.siren.ECS.Realms.CultivationRealm;
import plugin.siren.Utils.Config.RaceConfig;

public final class MyRaces {

    public static PlayerRace VAMPIRE;

    public static void register(){
        VAMPIRE = CultivationAPI.registerRace(
                "MyMod:Vampire",
                "Vampire",
                "server.mymod.race.vampire",
                CultivationRealm.NASCENT_SOUL,
                MyRaces::vampireStats);
    }

    private static RaceConfig vampireStats(){
        RaceConfig config = new RaceConfig();
        config.setDescription("嗜血者。体魄孱弱,聚气之速却倍于常人。");
        config.setHealthBonusPercent(-10.0F);
        config.setDamageBonusPercent(15.0F);
        config.setQiGainRatePercentBonus(100.0F);
        config.setBreakthroughDurationPercentReduction(0.0F);
        config.setQiAlignmentYinBiasPercent(80.0F);
        return config;
    }
}

Qi-Alignment-Yin-Bias-Percent 的值会直接汇入天道页所述的阴阳之衡,因此以此法注册的种族,也会左右其族人朝哪一条德行之途漂移。

注册一门功法

@Nonnull
public static Technique registerTechnique(@Nonnull String id, @Nonnull String displayName, @Nullable String nameKey,
                                          @Nullable String descriptionKey, @Nonnull TechniqueRule defaultRule,
                                          @Nonnull TechniqueEffect effect)

这与内置的「一步千里」所用的是同一套系统。一经注册,该功法便自动可经由每一种功法触发器施展:

  • /cultivation technique <id> 指令及其列表(见指令);
  • 任何 CultivationActivateTechnique 交互以此 id 作为其 TechniqueId 的启用物品;
  • 你自己的触发器 —— 一个按键绑定、另一件物品、一个事件 —— 经由 CultivationAPI.performTechnique。
参数说明
id一个稳定、唯一的 id。它同时充当配置键与启用物品的 TechniqueId。请加命名空间 —— "MyMod:flame_step"。
displayName未给出 nameKey 时显示;若该键无法解析,也作为回退。
nameKey用于本地化名称的 server.lang 键,或 null 表示直接显示 displayName。
descriptionKey用于描述的 server.lang 键,或 null。
defaultRule该功法据以运行的规则。除非服主在 Cultivation 的 TechniqueConfig.json 中添加了一条相符的覆写条目,否则这是你功法规则的唯一来源。
effect施展它会做什么。仅在所有关卡通过、灵气消耗与冷却均已施加之后才会被调用。

TechniqueEffect 是一个只有 void execute(TechniqueContext context) 一个方法的 @FunctionalInterface,因此一个 lambda 或方法引用便已足够。效果所需的一切都在 context 上:getAccessor()、getRef()、getPlayerRef()、getTechnique()、getRule()、getParam(String key, float fallback)、getCultivation()、getRealmIndex()、getStageIndex()、getPosition()、getLookDirection()、getWorld()、teleport(Vector3d)、spawnParticle(String particleId, Vector3d position) 与 sendMessage(Message)。其中的向量是 org.joml.Vector3d,故以 x()、y()、z() 读取其分量;实体没有 transform 时 getPosition() 返回 null,因此正式代码中使用前请先做判空。

请用辅助方法而非构造函数来构建规则:

@Nonnull
public static TechniqueRule newTechniqueRule(@Nonnull String id, boolean enabled, boolean daoSpecific,
                                             @Nullable String requiredElement, @Nullable String elements,
                                             @Nullable String damageType, @Nonnull String unlockRealm,
                                             float qiCost, float cooldownSeconds, @Nonnull Object... params)
参数说明
requiredElement当 daoSpecific 为 true 时,一个 DaoElement 枚举名,例如 "WIND";否则填 "" 或 null。
elements以逗号分隔的、该功法所「携带」的 DaoElement 名称 —— 元数据与风味 —— 或 ""。
damageType伤害类功法所用的 DamageCause 资产 id,无则填 ""。
unlockRealm使用它所需的 CultivationRealm 枚举名,例如 "QI_CONDENSATION"。
params交替出现的键/值对,你的效果以 context.getParam(key, fallback) 读回。参数个数必须为偶数:String, float, String, float, ...。

完整示例 —— 一门携带火属性、将修士向前传送的「烈焰步」:

import org.joml.Vector3d;
import plugin.siren.API.CultivationAPI;
import plugin.siren.ECS.Technique.Technique;
import plugin.siren.Utils.Config.TechniqueRule;

public final class MyTechniques {

    public static Technique FLAME_STEP;

    public static void register(){
        TechniqueRule rule = CultivationAPI.newTechniqueRule(
                "MyMod:flame_step",
                true,                 // enabled
                true,                 // daoSpecific
                "FIRE",               // requiredElement
                "FIRE",               // 所携带的属性
                "",                   // damageType —— 无
                "QI_CONDENSATION",    // unlockRealm
                25.0F,                // qiCost
                8.0F,                 // cooldownSeconds
                "Distance", 12.0F);   // params

        FLAME_STEP = CultivationAPI.registerTechnique(
                "MyMod:flame_step",
                "Flame Step",
                "server.mymod.technique.flame_step",
                "server.mymod.technique.flame_step.desc",
                rule,
                context -> {
                    float distance = context.getParam("Distance", 12.0F);
                    Vector3d from = context.getPosition();
                    Vector3d look = context.getLookDirection();
                    context.teleport(new Vector3d(
                            from.x() + look.x() * distance,
                            from.y(),
                            from.z() + look.z() * distance));
                    context.spawnParticle("MyMod:FlameStepBurst", new Vector3d(from));
                });
    }
}

由于 daoSpecific 为 true 且 requiredElement 为 "FIRE",唯有行火之道的修士方可施展它 —— 见大道。施展它会像内置功法一样,如实触发 TechniqueEvents.PreTechniquePerformEvent 与 TechniquePerformEvent,因此其他扩展也能为你的功法重新定价或将其否决。

若要从你自己的触发器施展它:

boolean performed = CultivationAPI.performTechnique(accessor, ref, playerRef, MyTechniques.FLAME_STEP);

它会跑完每一道关卡 —— 系统已启用、该功法已启用、境界解锁、道之相符、灵气消耗、冷却 —— 成功时扣除灵气、盖上冷却戳并运行效果。无论成败玩家都会收到消息,或是效果的成功提示,或是失败的缘由。唯有功法确实施展成功时它才返回 true。

功法还可以要求只有另一个模组才答得出的运行时条件:见功法关卡。

功法关卡

v0.9.1 TechniqueGate 让另一个模组能依据只有它才看得见的条件否决一门功法 —— 例如「这名玩家此刻是否变成了某个其他模组的生灵形态」。功法在其规则上点出它所需的关卡 id;关卡的主人在施展时作答。

// 1. 注册关卡。
CultivationAPI.registerTechniqueGate(
        "mymod:song_ready",                    // 稳定 id,以你模组的名字作命名空间
        "server.mymod.gate.songNotReady",      // 拒绝施展时的聊天消息;接收一个 `technique` 参数
        "server.mymod.gate.songStatus",        // 功法列表与快捷键界面所用的可选简短标签
        (accessor, ref, technique) -> MySongs.isReady(accessor, ref));

// 2. 在某门功法的规则上要求它。
TechniqueRule rule = CultivationAPI.newTechniqueRule(/* ... */)
        .requiresGates("mymod:song_ready");
  • TechniqueGate.Condition 只有一个方法,boolean test(ComponentAccessor<EntityStore> accessor, Ref<EntityStore> ref, Technique technique)。
  • 两个消息键都是完整的 server.lang 键,含 server. 前缀。若省略状态键(三参数的重载),列表便显示通用的 server.cultivation.technique.status.gateRefused 标签。
  • 关卡是失败即放行的:抛出异常的条件视为通过,并按关卡 id 记录一次日志。因此,依赖某道关卡的功法效果,也应重新核验它自己的真实条件,使一次瞬时故障只落得一次白费的施展,而不是一门永远无法施展的功法。
  • 以同一 id 再次注册会替换该关卡;unregisterTechniqueGate(id) 撤下它。

注册一件灵气吸收物品

public static void registerQiAbsorptionItemModifier(@Nonnull String itemId, float multiplier)

这正是内置聚气符背后的确切机制:当该物品位于打坐玩家当前快捷栏槽位中时,其灵脉吸收会乘以 multiplier。为一个已有修正的 id 再次注册会将其覆写,因此你也可以借此重调内置的那一项。

CultivationAPI.registerQiAbsorptionItemModifier("MyMod:JadePendant", 1.75F);

服主可在配置页的 Qi-Absorption-Item-Modifiers 键下查看并编辑内置条目;机制本身则记载于聚灵采气。

注册管理设置

v0.6.1 扩展可以把自己的可调数值放进 Cultivation 的管理菜单,它们会与 Cultivation 自己的并列于分节栏中,编辑与保存的方式完全相同。两个宿主页面都以 cultivation.admin 为门槛,所以分节可以暴露真正的平衡数值。

CultivationAPI.registerAdminConfigSection(
    CultivationAPI.newAdminConfigSection("MyMod:power",
            "server.mymod.admin.power", "server.mymod.admin.powerHint",
            AdminConfigSection.SORT_LAST, config::save,
            List.of(
                CultivationAPI.newAdminConfigField("MyMod:BaseXp",
                        Message.translation("server.mymod.admin.baseXp"),
                        () -> config.get().getBaseXp(),
                        value -> config.get().setBaseXp((float) value)),
                CultivationAPI.newAdminBooleanField("MyMod:Enabled",
                        Message.translation("server.mymod.admin.enabled"),
                        () -> config.get().isEnabled(),
                        config.get()::setEnabled))));

共有五种行可用,每一种都由对应形状的原版控件支撑:

工厂方法控件
newAdminConfigField小数输入框。默认类型,也是 0.6.1 之前每个字段的样子。
newAdminIntField整数字段 —— 用于计数、等级上限或档位,那里的两位小数只是噪音。
newAdminBooleanField真正的复选框。在此之前,总开关只得伪装成 0/1 数字,这也是 Cultivation 自己的许多开关只能在配置文件里改的原因。
newAdminChoiceField基于 AdminConfigChoice 的下拉框 —— 凡枚举型的值都该用它,文本框会让管理员键入解析不出任何东西的内容。
newAdminTextField自由文本。

withTooltip(field, String) 可为其中任何一种添加一句话说明。

经由 supplier 读取,绝不捕获配置对象

上面的每个 getter 与 setter,都是在被调用的那一刻才经由持有者去取配置的。一次重载会替换持有者背后的实例,而被捕获的配置对象届时便成了一份被丢弃的副本,对它的编辑无处可去。这与 CultivationConfigs 所记载的规则相同。

提示框是一个普通的 String,因此无法翻译 —— 客户端背后的属性就是字符串,向它推送富消息会让玩家掉线。凡须在每种语言下都可读的内容,请放进字段的标签里,标签接受 Message。

v0.6.1 Cultivation 自己的 80 个分节(57 个内置,另有 23 个由其各子系统注册;v0.11.2 其中十个是新增的 —— 门客、神识探查与威压、灵石、死亡惩罚、建筑总览,以及五处罕见之地各一;v0.11.1 在它们之前新增了八个 —— 突袭与首领之赏、道印、道域、宗门大阵、灵脉罗盘、首领道域、道域共鸣与引脉阵)都是这同一注册表里的普通注册项,所以扩展可以重排、隐藏或替换其中任何一个 —— 以已有的键注册即替换它。分节还带有 getSortOrder()(Cultivation 自己的从 SORT_BUILTIN_FIRST 起、以 100 为步长往上占位)与 isVisible(),Endless Leveling 分节在未安装该模组时便是借此隐藏自己。

注册逐玩家的管理操作

v0.9.1 registerPlayerAdminAction 是上文注册管理设置的逐玩家对应物:它不是服务器级的数值,而是让一个拥有全然属于自己的概念的扩展 —— 一条血脉、一朵结契之火,任何 Cultivation 没有内置行的东西 —— 在 /cultivation admin 的玩家页上得到一行,作用于管理员所选中的那名玩家。

PlayerAdminAction 是一个由你实现的接口;它没有工厂方法。

CultivationAPI.registerPlayerAdminAction(new PlayerAdminAction(){
    @Override public String getKey(){ return "MyMod:setFlame"; }
    @Override public Message getLabel(){ return Message.translation("server.mymod.admin.setFlame"); }
    @Override public Message getButtonLabel(){ return Message.translation("server.mymod.admin.setFlameButton"); }
    @Override public String getTooltip(){ return null; }
    @Override public List<AdminConfigChoice> getChoices(){ return MyFlames.asChoices(); }
    @Override public String getDefaultValue(){ return "none"; }

    @Override
    public void apply(Store<EntityStore> targetStore, Ref<EntityStore> targetRef, PlayerRef targetPlayerRef,
                      PlayerRef actingAdmin, boolean targetingSelf, String value){
        // 直接经由 targetStore 施加改动 —— 见下方说明
    }
});

这一行渲染成一个由 getChoices() 构建的下拉框,外加一个内联的应用按钮 —— 而不是普通文本框。选项请用 AdminConfigChoice.translated(id, labelKey)、AdminConfigChoice.raw(id, displayName) 或 AdminConfigChoice.of(enumConstant, labelKey) 构建。选项每次渲染都会重读,所以取决于你的模组已注册何物的选项集始终是最新的。有两个方法带默认实现:getSortOrder()(AdminConfigSection.SORT_LAST)与 isVisible()(true,每次渲染都会读取,所以属于已关闭子系统的操作可以自行隐藏)。

它的键兼作按钮回传的动作字符串,且必须含有 :,也不得等于保留的内置键之一("setrealm"、"setlevel" ……)。坏键会被拒绝并记录一条警告,而不会让面板崩溃。getTooltip() 是普通 String,因此无法翻译。

在这里直接经由 Store 写入

apply 拿到的是真正的 Store,不是 CommandBuffer —— 直接经由它写入,包括 Store.putComponent,对这一个接口而言是正确而既定的做法,不同于适用于 tick 系统与监听器的「结构性写入须经 CommandBuffer」规则。apply 还运行在目标玩家的世界线程上,那可能不同于管理员的线程,所以只能通过 PlayerRef#sendMessage 汇报结果。value 来自客户端:请对照你自己已知的取值重新解析,而不要信任它。

以同一键注册两次,后者替换前者;请在你的 shutdown() 中以 unregisterPlayerAdminAction(key) 撤下它。

注册一个称号

v0.7.0 registerTitle 会把你自己的一个纯装饰称号放上称号页 —— 在那里它会像内置称号一样被佩戴、悬浮于头顶、显示于聊天栏,并出现在排行榜上。称号纯属装饰 —— 此注册表中的任何东西都不会授予属性、权限副作用,或任何玩法上的改变。

CultivationAPI.registerTitle(CultivationTitle.builder("MyMod:dragonslayer")
        .name("server.mymod.title.dragonslayer")          // server.lang 键
        .section("server.mymod.title.section")            // 选择页的分组标题
        .unlocked((store, ref, player) -> MyDeeds.hasSlainDragon(player))
        .hint("server.mymod.title.dragonslayer.hint")     // 未获得前灰显
        .build());

CultivationTitle.builder(key) 接受:

构建器方法说明
name(String) / name(Supplier<Message>)显示名称 —— 一个 server.lang 键,或一个用于带参数名称的供给器(内置的元素称号即以此实现「{element}道」)。
section(String)在选择页上将称号分组的标题键。仅当它与前一条不同时才会被绘出,因此请把同一分组的注册项放在一起。
unlocked(UnlockCheck)获得门槛:boolean test(Store, Ref, PlayerRef)。未解锁的称号仍留在选择页上、呈灰显,并以提示语说明如何获得它。省略此项,则任何能看到它的人皆可直接使用。
hint(String) / hint(Supplier<Message>)显示在未解锁图块上的那一行提示。
permission(String) / visible(Predicate<PlayerRef>)可见门槛:二者必须皆通过,否则该称号会对该玩家完全隐藏。这是给服主/功能开关用的 —— 凡玩家可以努力去争取的,请用 unlocked。

读取:getTitles()、getTitle(String),以及用于取得玩家已佩戴称号的 getTitle(store, ref)。以既有的 key 再次注册会替换它;unregisterTitle 可撤下一个。请注意已佩戴的称号存放在玩家的设置上,而非其存档 —— 切换存档不会丢失它 —— 且模组在读取时刻意不会重新核验 unlocked,因此一个在佩戴时够格获得的称号,此后会一直显示。

有一处引擎层面的注意事项值得了解:头顶名字是经由共享的 PersistentDisplayName 组件写入的,因此一个同样会写入它的改名模组,会与称号相互覆写。

注册一面宗门旗帜

v0.7.0 registerSectBanner 会添加一面宗门可悬挂于其大殿之上的旗帜,与六种内置旗帜并列。

CultivationAPI.registerSectBanner(SectBanner.builder("MyMod:crimson")
        .name("server.mymod.banner.crimson")
        .section("server.mymod.banner.section")
        .swatch(0xD8452E)                       // 选择页图块与地图标记上的 RGB
        .particle("MyMod_HallBanner_Crimson")   // 必填 —— 那道灯火本身
        .build());
  • 旗帜是一个粒子系统,不是一种颜色。引擎「生成时着色」的字段并不可靠,因此旗帜的颜色是烘焙进 .particlesystem 资产本身的;swatch 只为选择页图块与大殿的地图标记着色。build() 会拒绝一面没有粒子的旗帜。
  • 资产必须自行燃尽。为它设定有限的 TotalParticles,并给出一个明确的系统 LifeSpan,使其能在 Sect-Hall-Beacon-Interval-Seconds(默认 2.5 秒)之内结束 —— 一个没有上限的系统,会在每一个曾路过大殿的客户端上永久泄漏一个实例。
  • permission(String) / visible(Predicate) 限定谁可以悬挂它 —— 六种内置旗帜均未设限。
  • 卸载是安全的。宗门存的是旗帜的id,从不存旗帜本身。一个不再被任何人注册的 id,只会解算为大殿朴素的灵脉品阶灯火,而这份选择会保留,以待模组归来的那一天。

读取:getSectBanners()、getSectBanner(String)(容忍 null)。变更一面旗帜会触发 SectEvents.PreSectBannerChangeEvent / SectBannerChangeEvent —— 见事件。

注册一套配色

v0.7.0 registerPalette 让一个模组能为选中它的玩家,重新为每一个 Cultivation 菜单与 HUD 上色。配色主要是重新上色的 .ui 文档,而非十六进制色值:你在同一个根目录下发布 Cultivation 各页面的重新上色副本,并声明自己覆盖了哪些文件。

CultivationAPI.registerPalette(CultivationPalette.builder("MyMod:moonlit")
        .name("server.mymod.palette.moonlit")
        .swatch(0x8FA8FF)                                  // 选择页图块
        .documentRoot("Common/UI/Custom/Pages/MyMod/Moonlit/")
        .documents(Set.of("CultivationPage.ui", "CultivationHud.ui"))  // 你所覆盖的文件
        .halo(SkillTreeBranch.VITALITY, 0x9BD8A0)          // 九个分支,全给或全不给
        // ……其余八个分支……
        .build());
  • 所声明的文档,是按裸文件名在 documentRoot 下匹配的,因此每一个被覆盖的文件都必须置于那同一个根目录下。你未声明的页面,会刻意回落到 Cultivation 自己的版本 —— 一个无法解析的 .ui 路径会让整个页面加载失败且不留日志,因此注册表绝不会凭空捏造一个。
  • 九个天赋树分支的光晕是全有或全无的:要么传入每一个 SkillTreeBranch,要么一个都不传 —— build() 会拒绝只传一部分。
  • CultivationPalette.DEFAULT_KEY("cultivation:default")即内置的默认外观。getPalette(store, ref) 对它返回 null;document(palette, basePath) 会依玩家当前所处的配色解析出对应路径。
  • 与已佩戴的称号一样,玩家所选的配色存放在其设置上,存档之间共享。

配色默认与配色锁

v0.10.0 玩家自己的配色选择周围,有两条可选的规则(锁出现于 0.10.0,默认出现于 0.10.2)。默认为尚未选择的玩家提供一种外观 —— 比如某个种族包为自己的新人着装。锁则对某些玩家强制指定一种外观,并彻底覆盖他们的选择 —— 比如某个环外大道之根索要自己的袍服。CultivationAPI.getPalette(accessor, ref) 先问锁,再看玩家自己所存的选择,最后才问默认。

// 一条默认:只填补玩家什么也没选的空缺。
CultivationAPI.registerPaletteDefault("mymod:newcomers",
        (accessor, ref) -> MyMod.isNewcomer(accessor, ref) ? "mymod:moonlit" : null);

// 一把锁:在其适用期间,胜过一切。
CultivationAPI.registerPaletteLock(new CultivationPaletteLock(){
    @Override
    public String lockedPaletteKey(ComponentAccessor<EntityStore> accessor, Ref<EntityStore> ref){
        return MyMod.holdsSignature(accessor, ref) ? "mymod:robes" : null;
    }

    @Override
    public String reasonKey(){
        return "server.mymod.palette.lockReason";   // 显示在主题下拉框通常的提示语的位置
    }
});
  • 默认,即 CultivationPaletteDefault,是一个函数式接口:String defaultPaletteKey(accessor, ref)。返回要使用的配色的键,或返回 null 表示「无意见」。可以注册多条;第一条点出已注册配色者胜出,依注册顺序。
  • 默认必须自行把关。 对它的答案,CultivationPalette.isAvailableTo 不会重新核验,因为这条路径在每次绘制时都会运行。对你的外观不适用的任何人,请返回 null。函数体请只做一次组件读取与一次比较。
  • 玩家要回绝一条被推荐的默认,只需选择 CultivationPalette.NONE_KEY,它解析为 Cultivation 自己的外观,并使每一条默认都不再被询问。选择 CultivationPalette.DEFAULT_KEY 则回到该外观,并允许默认再次推荐。
  • 锁,即 CultivationPaletteLock,有两个方法:lockedPaletteKey(accessor, ref)(不适用时为 null)与 reasonKey()(提示语的语言键,或 null)。第一把点出已注册配色的锁胜出。有锁适用时,setPalette 会拒绝,设置页则显示你给的理由。
  • 二者都在观看玩家的世界线程上求值,所以尽可读取其组件,但绝不要写入 Store。指向无人注册配色的键,对两者皆算「无意见」,所以配色缺失的规则绝不会困住玩家。
  • unregisterPaletteDefault(key) 接受你注册时所用的键;unregisterPaletteLock(lock) 接受锁对象本身。resolvePaletteDefault 与 resolveActivePaletteLock 读取当前的答案,getExplicitPaletteKey 则报告玩家实际所存的 id,在任何锁或默认之前。

打坐气息

v0.10.0 配色为玩家所看的菜单与状态栏上色;打坐气息则是玩家在打坐、突破或晋阶时看起来如何。二者的选择相互独立:玩家可以穿着默认的绯红与金色菜单,却在别人的气息之下打坐。

CultivationAPI.registerMeditationAura(
        CultivationMeditationAura.builder("myMod:frostBreath")
                .name("server.myMod.meditationAura.frostBreath")
                .section("server.myMod.meditationAura.section.winter")
                .swatch(0x9FD8F0)
                .auraPrefix("MyMod_MeditationAura_")
                .build());
构建器方法说明
name(String)选择页图块上的文字,一个 server.lang 键。
section(String)把图块归入选择页上一个带标题的分节。
swatch(int)选择页图块上绘制的颜色(默认 0xD9A63E)。
auraPrefix(String)你的打坐 tick 粒子的 id 前缀。随着一次打坐渐深,模组会请求 MyMod_MeditationAura_Stirring、_Gathering 与 _Converging,与它自己的三档做法相同。
breakthroughParticle(String) / advancementParticle(String)为每种仪式的脉动提供的单个可选覆写。你不设置的那一个,回落到 Cultivation 自己的。
permission(String) / visible(Predicate<PlayerRef>)对未通过任一道门的玩家隐藏该气息。两者须皆通过;施加气息时会重新核验 isAvailableTo。

build() 会拒绝一个什么都不覆写的气息 —— 前缀与两个粒子至少要设置一个,否则它会坐在选择页上看似可选,却什么也不改变。

  • 不会对照真实资产做任何校验。 一个不指向任何粒子的 id,在那一刻既不会失败也不会报任何错,所以新的气息请在游戏中测试。
  • 卸载是安全的。玩家的选择存的是 id,而非对象;无人认领的 id 会解析回 Cultivation 自带的内置粒子。
  • 以 getMeditationAuras()、getMeditationAura(key) 与 getMeditationAura(accessor, ref) 读取(null 表示原版外观)。setMeditationAura(store, ref, key) 在应用之前,会对照注册表与 isAvailableTo 重新核验该键,因为客户端事件可以伪造 id;传 null 或空串则回到内置粒子。CultivationMeditationAura.DEFAULT_KEY 是仅用于选择页的「未选择气息」标记,永远不会被存储。

环外灵根

v0.10.0 DaoElement 是一个普通的 Java 枚举,所以模组无法给它添加常量。因此 Cultivation 预先声明了模组可以认领的环外槽位 —— 目前只有玩笑性质的 DUCK —— 并对它们不持其他意见。在有模组认领之前,该元素处于休眠:无论 Dao-Mutant-Roots-Enabled 怎么设,都没有玩家能选它。

CultivationAPI.registerOffRingRoot(
        DaoRoot.builder(DaoElement.DUCK)
                .name("server.myMod.dao.element.duck")
                .counterBonusPercent(35f)
                .build());
  • name(String) 是一个 server.lang 键。counterBonusPercent(float) 是可选的:设置后,每当该元素在战斗中克制另一元素,它就取代服务器的 Dao-Counter-Bonus-Percent;省略则采用服务器自己的数字。
  • build() 会拒绝并非环外的元素(DaoElement.isOffRing()),所以你无法认领主环的灵根。
  • 命名、称号、典籍条目与图标一概留给认领它的模组:元素称号循环、「X 克 Y」的清单,以及大道页的两个环行,都会直接跳过环外元素。若灵根应自带袍服,请将认领与 registerPaletteLock 配对。
  • 认领一个已被认领的元素会替换该认领。unregisterOffRingRoot(element) 释放它;正行此道的修士不会被迁移,该元素只是重新变得不可选。getOffRingRoot(element) 与 isOffRingRootClaimed(element) 读取注册表。

个人之道

v0.9.0 个人之道是修士经由事迹领悟、而非选择的一门道 —— 它是与元素大道、天道并列的开放注册表。内置有七门(剑道、杀戮道、时空道、灵兽道、魔道、炼体道、元素道);模组可再注册更多。领悟本身存放在玩家的 DaoComprehensionComponent 上,且只经由 CultivationAPI.addPersonalDaoComprehension 写入;你注册的 PersonalDao 描述这门道,而非某名玩家在其上的进度。事件:DaoComprehensionEvents。

CultivationAPI.registerPersonalDao(PersonalDao.builder("mymod:moon")
        .name("server.mymod.dao.moon")
        .description("server.mymod.dao.moon.desc")
        .howToComprehend("server.mymod.dao.moon.how")    // 必填
        .manifestThreshold(150f)
        .maxComprehension(600f)
        .manifestation(new PersonalDao.Manifestation(){
            @Override
            public float outgoingDamageMultiplier(ComponentAccessor<EntityStore> accessor,
                    Ref<EntityStore> attackerRef, Ref<EntityStore> targetRef, float fraction){
                return 1f + 0.2f * fraction;             // fraction 是 领悟/上限,0 到 1
            }
        })
        .build());
构建器方法默认值说明
name、description、howToComprehendname 为原始键各接受一个 server.lang 键、一个 Message,或一个 Supplier<Message>。howToComprehend 为必填 —— 一门没有说明如何得之的道无法游玩,build() 会抛出异常。
sortOrder(int)SORT_DEFAULT(1000)SORT_FIRST 为 100;越小越靠前。
manifestThreshold(float)100此道显化所需的领悟值。
maxComprehension(float)500上限。
hiddenUntilComprehended(boolean)false在玩家有了一定领悟之前,于菜单中隐藏此道。
visible(Predicate<PlayerRef>)所有人对该判定所拒绝的玩家隐藏它。
manifestation(Manifestation)什么也不做显化之道的回报是什么。

PersonalDao.Manifestation 有四个方法,每个默认都是「无意见」:outgoingDamageMultiplier(accessor, attackerRef, targetRef, fraction)(运行于伤害管线之内 —— 请保持轻量且只读)、priceTechnique(accessor, ref, PreTechniquePerformEvent event, fraction)(为一次功法施展重新定价)、onManifested(accessor, ref, playerRef) 与 onSetAside(accessor, ref, playerRef)。

onManifested 可能收到裸 Store

从功法施展监听器抵达的显化,包括蓄力释放路径,运行于该存储的处理锁之下,拿到的是裸的 Store,而非 CommandBuffer。切勿在 onManifested 中调用 putComponent,或创建可能尚不存在的组件:那会抛出 "Store is currently processing!" 并把世界线程带崩。请读取或修改你确知存在的组件,或发送消息与事件。

以 getPersonalDaos() 与 getPersonalDao(key) 读取注册表;getPersonalDaoComprehension、isPersonalDaoManifested 与 addPersonalDaoComprehension 接受一个 key。以同一 key 注册两次,后者替换前者。unregisterPersonalDao(key) 撤下它。

注册一门兽技

兽技是功法在伙伴一侧的孪生,其注册表之所以开放,缘由相同:Java 枚举无法在运行时增添常量。

BeastArt frostFang = CultivationAPI.registerBeastArt(
        "mymod:frost_fang",
        "Frost Fang",
        "mymod.beast.art.frost_fang.name",          // 或 null 表示直接显示文本
        "mymod.beast.art.frost_fang.description",
        new BeastArtRule("mymod:frost_fang", true, "FOUNDATION_ESTABLISHMENT", 12f, "Ice",
                new TechniqueParam[]{
                        new TechniqueParam("Radius", 4f),
                        new TechniqueParam("BaseDamage", 8f),
                        new TechniqueParam("DamagePerLevel", 0.7f)
                }),
        context -> {
            // 羁绊的两端:getBeastRef() 是施展者,getOwnerRef() 是受益者。
            context.sendMessage(Text.of("mymod.beast.art.frost_fang.playerMsg.hit"));
        });
注册一门兽技,并不会把它给任何人

兽技属于一个物种。它的 id 必须出现在 BeastConfig.json 中该物种的 Arts 列表里,任何生灵才能习得。这是刻意的 —— 让你只管提供兽技,哪些生灵得到它们,由服主决定。

规则的 UnlockRealm 是灵兽的境界,绝不是它主人的。其 damageType 指向一个 DamageCause 资产(原版的 Fire、Ice、Poison……,或 Cultivation 的某个 Cultivation_*);解析不到任何东西的名字,会悄然回落成物理伤害而不报错,所以一处笔误会让你白白丢掉元素,且日志中没有一行提示。

读取与驱动:getBeastArts()、getBeastArt(id)、getBeastArtRule(art)、getKnownBeastArts(accessor, ref),以及 performBeastArt(accessor, ref, art, playerRef) —— 它会跑完模组自己的调用方所跑的每一道关卡,所以你永远不必重复它们。

注册一种宗门建筑

一种宗门建筑,附其打坐倍率、它所等候的宗门等级,以及决定其地面是否承载宗门大道的开关:

CultivationAPI.registerSectBuildingType(
        new SectBuildingType("mymod:frost_terrace", 1.12f, 2, false));
//                            id                   medMult  level  daoActiveByDefault

最后一个参数最值得斟酌。false 使它成为中立之地 —— 任何元素的门人都可在此修炼,而不会被扭向宗门的大道,宗门正是借此接纳走着不同元素的朋友。意在作客舍的类型,应默认为 false。

注册一种类型,只是让它为人所知;宗门能否建造它,仍由服主决定。你的条目永远不会覆盖同 id 的配置条目 —— 服主的永远优先。

注册一种本命法宝之性

一件本命法宝可能呈现的性。它在绑定时掷定一次,此后永不重掷:

CultivationAPI.registerLifeBoundTrait(
        new LifeBoundTrait("mymod:frostbite", LifeBoundTrait.Slot.WEAPON,
                           1f, 0.5f, 25f, 1.2f)      // 基础、每级、上限、掷骰权重
                .unlocks("frozen_domain", 9));       // 持握时借出的一门功法,自 9 级起

有两点后果值得预先考虑。你的特质立刻进入加权掷骰,所以会改变今后的绑定可能产出的东西 —— 已绑定的法宝保留其所掷出的,模组中任何地方都没有重掷。而 Slot 是真正的限制:WEAPON 特质永远不会落在胸甲上,这正是掷骰不致有一半时候令人失望的缘故。

以 getLifeBoundTraitAmount(stack, "lifesteal") 读回其一 —— 若该法宝是另一种性则返回 0,因此对任何物品询问任何特质都是安全的 —— 并以 getLifeBoundGrantedTechnique(stack) 取得它当前借出的功法。

注册一级功法境界

功法境界的阶梯通常就是服务器所配置的五级。扩展可以追加一级:

boolean added = CultivationAPI.registerMasteryStage(
        new MasteryStageRule("Transcendent", "SOUL_FORMATION", 2500f, 8, 2.6f, 0.7f, 0.65f));

阶梯已满时,它返回 false,而不是悄悄无视你 —— 阶梯上限为五级,因为界面与语言键只覆盖这么多。请检查返回值。

注册常驻修正

0.7.4 新增。凡要改变一名修士是什么、而非他做什么的,皆挂在此处 —— 血脉、体质、特殊体质。一次注册即回答一组具名的类型化通道,Cultivation 则在自己各处关口逐一相询:

CultivationAPI.registerModifierSource("mymod:bloodlines", new CultivationModifierSource() {
    @Override
    public float qiGainMultiplier(ComponentAccessor<EntityStore> accessor, Ref<EntityStore> ref) {
        return Bloodline.of(accessor, ref) == Bloodline.DRAGON ? 1.15f : 1f;
    }

    @Override
    public float damageTakenMultiplier(ComponentAccessor<EntityStore> accessor, Ref<EntityStore> ref) {
        return Bloodline.of(accessor, ref) == Bloodline.DRAGON ? 0.95f : 1f;
    }
});

每个方法都有一个「不予置评」的默认实现,所以你只需实现自己在意的通道。共有十八条类型化通道,另有两条并不改动数值,而是向总览页添入内容:

通道组合方式缩放或决定的对象
qiGainMultiplier相乘来自任何来源的每一点灵气获取。
meditationQiMultiplier(accessor, ref, boolean inLava)相乘专指打坐所汲取的灵气,叠加在上一条之上。
damageDealtMultiplier相乘所施加的一切伤害 —— 兵器、功法与拳脚一视同仁。
unarmedDamageMultiplier相乘赤手所施加的伤害,叠加在上一条之上。绝不作用于兵器。
damageTakenMultiplier相乘所受伤害。低于 1 为庇护,高于 1 为脆弱。
isImmuneToDamageCause(accessor, ref, String damageCauseId)取或某种伤害成因是否被彻底无视。是硬性免疫 —— 请慎用。
chargeSecondsMultiplier相乘一门蓄力功法须蓄多久。低于 1 为更快。
ignoresDaoElementLock取或修士能否施展一门锁于其未曾领悟之元素的大道功法。
daoAffinityMultiplier(accessor, ref, DaoElement element)相乘对某一大道元素所得的亲和。
alignmentShiftMultiplier(accessor, ref, boolean towardYin)相乘单次阴阳偏移能使其移动多远,分方向。
yinYangBalanceToleranceMultiplier相乘被视为阴阳平衡的区带有多宽。
ritualDifficultyMultiplier相乘突破或晋阶仪式有多难:其时长与天劫落雷。低于 1 为更易。
canMeditateInLava取或他们能否站在岩浆中打坐。
talismanPotencyMultiplier相乘符箓的效力,叠加在品阶自身的量级倍率之上。
beastBonusMultiplier(accessor, ownerRef, BeastSpecies species)相乘召出灵兽的加成倍率,叠加在其物种与元素共鸣之上。ownerRef 是主人,而非灵兽。
lootRollMultiplier(accessor, ref, ItemEvents.LootType type)相乘一次修炼掉落究竟能否掷中的几率 —— 即运气通道。乘积夹紧在 0 到 1 之内,它缩放的是概率,绝不是数量。
qiRequirementReductionPercent相加从下一次升级所耗的存蓄灵气中削去的百分比。见下文。
auraScaleMultiplier相乘境界气息的大小。仅为装饰。
bonuses(accessor, ref)列表以 CultivationAPI.newBonus 构建的总览行。无所授予时返回空列表。
traits(accessor, ref)列表供总览页「体质」分节使用的、具名的 Message 行,例如「荒古圣体」。没有任何来源贡献时,该分节自行隐藏。

lootRollMultiplier 目前在四处掷骰点被询问 —— 魔道修士所掉的秘籍、被斩杀修士遗落的秘籍、灵石击杀掉落,以及宝藏窖的材料掷骰。修炼核心的击杀掉落不在其内(它掷的是哪一档,而不是掉不掉);要为那一项重新定价,请用 ItemEvents.PreLootDropEvent。LootType 会随时间增添常量,所以请加一个 default 分支,而不要穷举 switch。

qiRequirementReductionPercent 是唯一相加而非相乘的通道,因为它汇入的是一个本就相加的区块:天赋树的灵气消耗削减与修行传承的传人增益相加,总和再对 Legacy-Min-Qi-Cost-Floor-Percent 夹紧一次。你返回的任何值都加入该总和,并受同一下限约束,所以两个来源无法让一次突破变成免费。在替换用的 ProgressionProvider 治下它不起作用,因为后者拥有自己的曲线 —— 请在你自己的界面里说明这一点。

为何是此处而非事件

事件适于回应一个瞬间;此处所对的是关于一名玩家的一桩常驻事实,它会一举重定十几套彼此无关的系统。若以事件为之,便要在每一套上各挂一个监听器,且各自反复推导同一桩事实。

其余每个倍率都在所有已注册来源之间相乘,布尔值则取或,所以两个扩展各加 10%,得到的是 ×1.21 而非 ×1.20 —— 这与 Cultivation 自身内部倍率的叠加方式一致。抛出异常的来源会被捕获、记录一次,此后不再理会,故一个有缺陷的扩展无法拖垮整条伤害管线。

你的方法在世界线程上被调用,有时是从伤害管线或某个逐 tick 的系统之内,所以请只做一次组件读取与一些算术,且绝不要经由 accessor 写入。当无人注册时,每条通道都会立即返回其中性值,所以没有此类扩展的服务器,每个关口只付出一次判空的代价。以同一 key 注册两次,后者替换前者;请在你的 shutdown() 中以 unregisterModifierSource 撤下,否则一次重载会留下一个指向已不存在的类的来源。

另有两个辅助方法建立在它之上:CultivationAPI.newBonus(sourceKey, statKey, amount, percent) 构建一行总览行(凡有合适的,请沿用 Cultivation 自己的属性键,使你的行与模组的相加;在 percent 上不一致的行绝不会相加),而 CultivationAPI.applyStatBonus(accessor, ref, statIndex, key, amount) 对玩家的某项引擎属性施加一个带键的固定加成 —— 以同一个键再次调用会替换该键的贡献,0 则撤回。

圣体(Sacred Bodies)全然建于此注册表之上,别无其他,值得当作可仿的范式来读:它以一次组件读取回答全部通道,并向总览页添入自己的具名条目,使体质显现为可读的文字,而非玩家数值上一处无从解释的变动。

注册一种天象

0.8.0 新增。天象是一个开放注册表。四种内置异象 —— 灵潮、流星、血月与星陨汇聚 —— 正是以此方式注册的,而扩展模组的第五种,所用的权限并不比它们更多:

CelestialManager.registerEventType(new CelestialEventType(
        "mymod:frost_veil",                        // id —— 小写、稳定,即 /celestial start 的参数
        "server.mymod.celestial.name.frostVeil",   // 显示名的语言键
        "MyMod_Weather_FrostVeil",                 // 其天穹的天气资源 id
        14f,                                       // 持续分钟数
        1f,                                        // 在已启用异象中的抽取权重
        true));                                    // 调度器是否可以选中它
它是一份轻描述,而非回调接口

注册所买到的是免费的调度、天穹与通报 —— 一切对任何异象都通用的部分。它不承载你的玩法效果,因为三种内置异象自己的效果也足够特化,同样是 CelestialManager 上的普通静态查询。你的系统读取 CelestialManager.active(),并在你的 id 正在运行期间施加任何你想要的效果。这与 SectBuildingType 用于扩展宗门建筑的形状完全一致。

权重小于等于零者永不被选中 —— 等于无须注销即已禁用;而 enabled: false 只拦住自动出场:管理员精确的 /celestial start <id> 无论如何仍然有效,这与 Tribulation-Storm-Enabled 之于风暴本身是同一种关系。注册一个已存在的 id 会替换它。

另有三种类型,是由拥有它们的子系统注册的,而非 CelestialManager 自己:heavenly_schism 由正魔之战注册,beast_tide 由兽潮注册,beast_omen 由灾劫兽注册。后两者以零权重注册,所以调度器永远不会选中它们;由它们各自的管理器来启动。监听 CelestialEvents 即可看到每一种。

注册一项宝阁权益

0.8.0 新增。把宝阁中的一件商品,与游戏内发生的某件事绑在一起,且在任何运行 Cultivation 的服务器上都生效:

CultivationAPI.registerStoreBenefit(
        StoreBenefit.builder("myMod:store:crown", "my-mod-crown")
                .name("server.myMod.store.benefit.crown")
                .title("server.myMod.title.store.crown")
                .hint("server.myMod.title.storeHint.crown")
                .build());

第一个参数是你的权益键,第二个是商店发布其权益名单所用的别名 —— 可以是商品别名(my-mod-crown),也可以是 sub_<familyId>(如 sub_cultivation),用于绑定整个订阅家族而非单一商品。对于两种方式皆可购得的商品(订阅内含或单独买断,如 Sacred Bodies),也可以改用 once_<slug>(如 once_cultivation-sacred-bodies)专门要求买断购买,排除仅持有家族订阅的玩家 —— 若该商品并无买断选项,则返回 404。name 是 /cultivation store status 所打印的标签;title 则可选地指定一个因拥有它而解锁的称号。

v0.10.3 .hint(...) 可选地指定一个语言键,在称号尚未获得时显示于其灰显图块上 —— 与其他注册称号相同的「灰显并附说明」处理,而非一个孤零零的锁形图标。不设置则回退到通用的「来自宝阁」提示。

取回、6 小时缓存、退款撤销与失败即放行,都由共用的同步逻辑代劳 —— 你所声明的只是这件商品是什么。Cultivation 自家的商品正是经由这同一套公开接口注册的,而非某条私有路径,这是「公开途径就是真正的途径」的一贯保证。以此方式注册的称号,只要同步本身被关闭(Web-Store-Enabled: false),或其所依赖的具体别名被列入 Disabled-Benefits,便会从选择页上消失 —— 本服拒绝了该项权益的服务器,绝不会展示它本可解锁的称号。

注册一种任务计量

0.8.0 新增。宗门任务榜的 Metric 字段是一个自由字符串而非封闭枚举,因此即便随附的记入来源是固定的,名字空间仍是开放的:

MissionManager.creditProgress(player, "myMod:eventId", amount);

而后在 SectConfig.json 的 Sect-Mission-Types 中添一条同名条目,它便成为一项真正的、按门第设限、带冷却、以贡献点计酬的任务。Cultivation 自身钩子所记入的有四种计量 —— gather_item、slay_beast、meditate_seconds 与 win_duel。v0.11.1 第五种 cultivation:clear_breach,在每一次哥布林突袭、哥布林宝匣、虚空猎杀或地宫首领的酬赏发到某位玩家手中时,给他记一次(见突袭与首领之赏),Cultivation 还为它注册了一项任务:清剿哥布林突袭,id 为 clear_goblin_breaches,清剿 3 场得 40 点贡献点,冷却 24 小时,各品阶皆可接。服主自己的同 id 条目优先。任务 Id 中不可含 :、; 或 |,那是持久化进度行所用的分隔符。

v0.6.0 每个 Cultivation 菜单都带有一条共享的导航栏。registerMenuPage 在其上添一个按钮,使模组自己的界面与总览、种族、大道等并列,而不必躲在它自己的指令后面。这条栏可横向滚动,所以能添加多少页,实际上没有上限。

CultivationAPI.registerMenuPage(
        CultivationMenuPage.builder("myMod:alchemy")
                .label("server.myMod.nav.alchemy")
                .permission("myMod.alchemy")
                .onOpen((store, ref, playerRef) ->
                        CultivationAPI.openMenuPage(store, ref, new MyAlchemyUIPage(playerRef)))
                .build());
构建器方法说明
label(String) / label(Message) / label(Supplier<Message>)按钮文字。请优先用翻译键的形式:它在每次绘制时解析,所以会本地化,也会采纳已安装的 CultivationTheme。
sortOrder(int)在栏上的位置,越小越靠前。SORT_FIRST 为 100,SORT_DEFAULT(默认)为 1000,SORT_LAST 为 9000 —— 内置的管理按钮就在那里。
permission(String)对没有该权限的玩家隐藏按钮,并拒绝为他们打开页面。
visible(Predicate<PlayerRef>)权限无法表达的门槛,例如一个只有玩家加入宗门后才有意义的页面。与 permission 合并;两者须皆通过。
onOpen(Opener)点击所做之事。必填:缺了它或缺了 id,build() 都会抛出异常。
  • Id。 id 就是点击时回传的东西,所以请加命名空间("myMod:alchemy")。Cultivation 自己的十个 id 是裸词:overview、settings、race、skilltree、dao、rankings、codex、sense、admin 与 info。另有六个 id 能打开真正的页面,却没有按钮 —— bonuses(并入 overview)、titles(并入 race,这正是该按钮如今写作「身份」的缘故)、profiles 与 keybinds(经由 settings 抵达)、daomastery 与 daoinheritance(经由 dao 上的一行链接抵达)。你自己注册其中任何一个,都会让按钮回来,因为这条栏完全是依注册表绘制的。
  • 替换内置项。 以已有的 id 注册会替换它,模组也正是以此接管 Cultivation 自己的某一项。
  • 隐藏就是隐藏。 没有置灰状态 —— 这条栏已经用「禁用」来标记你所在的页面。可见性在页面打开之前会再次核验,所以手工构造的数据包无法抵达按钮已被隐藏的页面。
  • 线程。 getLabel、isVisibleTo 与 open 都运行在拥有观看玩家的世界线程上,即他们的页面被构建或被换掉的时候。尽可读取组件;绝不要写入 Store。

把导航栏放到你自己的页面上

  1. 给你的 .ui 文档一个占位:Group #NavBar { LayoutMode: Left; Anchor: (Bottom: 4, Height: 46); }。
  2. 在你页面的 build 中调用 CultivationAPI.buildMenuNav(commandBuilder, eventBuilder, playerRef, "myMod:alchemy", store, ref)。带 store 与 ref 的重载以观看者的配色绘制这条栏;较短的那个总是以默认的绯红与金色绘制,在一个换了主题的页面上,那就成了一条旧颜色的条带。
  3. 在你的事件 codec 里,于 CultivationAPI.MENU_NAV_EVENT_KEY 之下添一个普通的 Codec.STRING 字段。它携带的是字面页面 id,所以不带 @ 前缀: .addField(new KeyedCodec<>(CultivationAPI.MENU_NAV_EVENT_KEY, Codec.STRING), (data, value) -> data.nav = value, data -> data.nav)
  4. 在 handleDataEvent 中,把该字段交给 CultivationAPI.handleMenuNav(store, ref, playerRef, data.nav)。当事件来自你自己的某个按钮、你的字段持有 null 时,或遇到玩家无权打开的 id 时,调用它都是安全的。tryHandleMenuNav 返回是否打开了某个页面;如果你是在一个会锁定界面的导航绑定中调用它,则得到 false 时请自行以 sendUpdate(null, null, false) 应答这次点击,否则客户端会一直停在加载状态。

身份页分节

v0.10.0 导航栏的 race 按钮打开的是身份页,其上有种族标签页与称号标签页。registerIdentitySection 会添上第三个、第四个等等,始终排在两个内置标签页之后。种族与称号不是这个注册表中的条目 —— 它们仍写死在代码里,因为各自带有专属于其内容的局部更新逻辑 —— 但每个已注册的分节都有自己的标签按钮,切换方式相同。

CultivationAPI.registerIdentitySection(
        CultivationAPI.newIdentitySection("MyMod:classes",
                Message.translation("server.mymod.identity.tabClasses"),
                IdentitySection.SORT_DEFAULT,
                context -> {
                    context.getCommandBuilder().append(context.getContainerSelector(), myDocument);
                    context.bindAction(CustomUIEventBindingType.Activating, "#SomeButton", "pick");
                },
                (context, action, value) -> {
                    if("pick".equals(action)){
                        // 处理这次点击
                    }
                }));

newIdentitySection 由一个构建回调与一个可选的动作处理器构建分节;四参数的重载(无处理器)用于没有交互内容的分节。你若愿意,也可以自行实现 IdentitySection 接口:

方法说明
getId()稳定、唯一、带命名空间的 id。不得等于 "race" 或 "titles"。它是标签点击时回传的东西,也是 handleAction 据以分派的依据。
getLabel()标签按钮的文字。请使用 Message.translation(...) 以便本地化。
build(IdentitySectionContext)每次打开页面构建一次该分节的内容,绝不在切换标签时重建 —— 切换只是翻转你容器上的 .Visible,所以重建会丢掉你已注册的事件绑定。
handleAction(context, action, value)处理来自你所绑定控件的点击或值推送。默认什么也不做。纯点击时 value 为 null。
isVisible(PlayerRef)这名玩家究竟能否得到这个标签,每次构建都会核验。默认为 true。
getSortOrder()已注册分节之间的顺序(种族与称号不在其内)。默认为 SORT_DEFAULT,即 1000。

IdentitySectionContext 向 build 提供它所需的一切:getCommandBuilder()、getEventBuilder()、getStore()、getRef()、getPlayerRef()、getPalette()(请经由 CultivationAPI.document 解析你自己的文档)、getContainerSelector() 与 getSectionId()。用 selector(childSelector) 把选择器限定在你的容器之内 —— 绝不要寻址一个裸的、无限定的 id,因为 UI 的 id 对已载入的页面是全局的,另一个分节可能复用同一个控件名。bindAction(type, childSelector, action) 把一次点击连同 null 值路由给 handleAction;bindValueAction(childSelector, action) 则路由一次携带控件实时值的 ValueChanged,下拉框或文本框用的正是这种形状。

所有调用都运行在观看玩家的世界线程上:读取他们的组件,绝不要写入 Store。抛出异常的分节会被捕获并记录,而不会让页面对所有玩家都失败。

典籍条目

v0.6.0 游戏内典籍是一个开放注册表。一篇条目是为每位读者现写的,这正是游戏内典籍胜过 wiki 页面之处:它能陈述这台服务器实际运行的数值,以及读者实际所达到的境地。

CultivationAPI.registerCodexEntry(
        CodexEntry.builder("myMod:soulRings")
                .title("server.myMod.codex.soulRings.title")
                .summary("server.myMod.codex.soulRings.summary")
                .category(CodexCategory.SELF)
                .body(page -> page
                        .heading("server.myMod.codex.soulRings.heading")
                        .paragraph("server.myMod.codex.soulRings.body")
                        .stat("server.myMod.codex.soulRings.maxRings", config.getMaxRings())
                        .noteIf(!MySouls.hasAwakened(page.getAccessor(), page.getRef()),
                                "server.myMod.codex.soulRings.locked")));
构建器方法说明
title(String) / title(Supplier<Message>)条目的名称,见于索引与其页面顶部。每次绘制时解析。
summary(String) / summary(Supplier<Message>)标题下的一行,说明该条目涵盖什么。
category(String)CodexCategory 的 id:四个内置之一,或你自己的。默认为 CodexCategory.PATH。
sortOrder(int)其分类之内的顺序。默认为 SORT_DEFAULT,即 10000 —— 排在内置条目之后,内置条目彼此相隔 100。
visible(Predicate<PlayerRef>)对其所拒绝的读者隐藏该条目。建议在正文里写「尚未解锁」:一本把你未解锁之物藏起来的典籍,也就无法告诉你如何解锁。
body(Body)必填。 针对一个 CodexPage 写出条目。缺了正文或 id,build() 都会抛出异常。

CodexPage 依次收集各个块,典籍决定每一块的样子。每个方法都返回该页,所以调用可以链式书写:

方法块
heading(key)条目内的一个分节标题。
paragraph(key)一段正文。会自动换行。
stat(labelKey, value)一个标签,及其在这台服务器上所解析出的值。value 可以是 Message、String、int 或 float(修剪到尽可能少的小数位)。
note(key) / noteIf(condition, key)一句调暗的旁注 —— 一条提醒,或仅对这名读者成立的事。
recipe(itemId)一份合成配方,实时取自该物品自己的资产。没有配方的物品什么也不渲染。
icon(iconPath, nameKey, descriptionKey)一幅图,连同它的名字与含义。iconPath 是自资产根起算的资产路径,绝不是相对文档的 ../ 路径。
divider()一条细线。

每个文本方法都接受完整的语言键(含 server. 前缀)或一个 Message。该页还携带读者 —— getAccessor()、getRef()、getPlayerRef() —— 以及两个静态辅助方法 CodexPage.formatNumber(float) 与 formatPercent(float),它们按典籍展示数字的方式来格式化已配置的数字(1.5,而不是 1.50)。

  • 命名空间与替换。 请以你模组的名字给 id 加命名空间。以已有的 id 注册会替换它,模组也正是以此改写一篇内置条目。
  • 线程。 正文运行在读者的世界线程上,即其页面被构建的时候:尽可读取其组件,不要写入 Store。
  • getCodexEntries() 按排序序号再按注册顺序返回条目;分组是调用者的事。getCodexEntry(id) 与 unregisterCodexEntry(id) 使这一套完整。

典籍分类

一个 CodexCategory 是典籍索引中为条目分组的标题。模组自带四个,彼此相隔 100,使模组能在它们之间嵌入自己的一组:CodexCategory.PATH(修行之路:境界、灵气、突破、天劫)、SELF(己身:种族、大道、功法、伙伴)、WORLD(天地:灵脉、宗门、洞府、阵法、其他修士)与 CRAFT(炼制:炼丹、炼器、秘籍)。归入其中之一,通常对读者更好,好过再添第五个 —— 想找灵兽的人,希望在「己身」下找到它们,而不是在恰好添加了它们的那个模组的名字之下。

CultivationAPI.registerCodexCategory(CodexCategory.of("myMod:winter", "server.myMod.codex.category.winter", 450));

CodexCategory.of(id, translationKey, sortOrder) 接受一个语言键;第二个重载接受一个 Supplier<Message>。标签每次绘制时解析,所以已安装的 CultivationTheme 可以重新措辞。以已有的 id 注册会替换它,分组也正是这样被改名或挪动的。getCodexCategories() 按显示顺序返回各标题,unregisterCodexCategory(id) 移除一个。

可揭示的系统

v0.10.3 渐进式解锁会让一个系统不出现在菜单与典籍中,直到它与某名玩家相关为止 —— 默认是直到其历来最高的境界达到某道门槛。registerRevealableSystem 让扩展把自己的内容也纳入其中,方式与它贡献一篇典籍条目相同。

CultivationAPI.registerRevealableSystem(
        RevealableSystem.builder("myMod:soulRings")
                .name("server.myMod.reveal.soulRings.name")
                .hint("server.myMod.reveal.soulRings.hint")
                .revealRealm(CultivationRealm.QI_CONDENSATION)
                .codexEntries("myMod:soulRings")
                .build());
构建器方法说明
name(String) / hint(String)二者皆必填。 简短名称(进度罗盘「新近开启」的标题)与一句话说明其意义的语言键。
revealRealm(CultivationRealm) / revealRealm(IntSupplier) / neverByRealm()境界门槛。IntSupplier 形式返回一个实时读取的境界序号,所以管理员的修改无需重启即可生效。neverByRealm() 意为只有下述判定才能让它浮现。
unlockedBy(Predicate<PlayerRef>)揭示该系统并公告之;一旦为真即永久有效。
engagedBy(Predicate<PlayerRef>)无视境界、悄然揭示它;一旦为真即永久有效,自身绝不公告。
enabledWhen(BooleanSupplier)该系统是否开启。关闭意为永不公告,也永不计作被隐藏。
codexEntries(String...)该系统为其可见性做出贡献的典籍条目 id。一篇条目仅在每一个声称它的系统都被隐藏时才隐藏。
opensCodex(String) / opensMenuPage(String)罗盘或通告的「打开」动作所深链到的地方。
openedNote(String)一个语言键,说明它是如何被开启的(「由飞升开启」);默认是朴素的「于某境界开启」一行。
sortOrder(int)系统之间的顺序。默认为 10000。
  • id 会被持久化。 它必须匹配 ^[A-Za-z0-9_.:-]{1,64}$,且一经发布就绝不可改名:它被原样存入玩家的已揭示与未读集合,改名会悄然让每个已挣得它的账号重新失去该系统的揭示。坏 id 会让 build() 抛出异常。
  • 判定要轻、只读,且绝不创建状态。 请使用 store.getComponent 式的读取;绝不要在其中调用 getOrCreate 类的访问器。抛出异常的判定视为已通过(系统被揭示),并按 id 记录一次日志。
  • isSystemRevealed(accessor, ref, id) 答复玩家当前是否看得见某系统 —— 对未注册的 id 为 true,因为没有东西可以隐藏它。getHighestRealmReached(accessor, ref) 返回渐进式解锁所读取的永久最高境界序号,只有挣来的进度才会抬高它。

v0.11.1 Cultivation 自己的系统也是经由这同一个调用注册的。0.11.1 新增了两个,每个都会打开一篇同 id 的典籍条目。另有两篇条目与它们一同到来,却不属于任何可显现系统,所以渐进式解锁永不隐藏它们(一篇条目,只有当宣称它的每个系统都被隐藏时才会隐藏):

id可显现系统典籍条目管理员配置分节开启于
cultivation:daoSeals是是,在己身一栏Cultivation:DaoSealsSeal-Min-Realm
cultivation:daoDomains是是,在己身一栏Cultivation:DaoDomainsDomain-Min-Realm
cultivation:sectArray否是,在天地一栏Cultivation:SectArray-
cultivation:veinCompass否是,在天地一栏Cultivation:VeinCompass-

当一个系统自己的开关(Dao-Seals-Enabled、Dao-Domains-Enabled)关闭时,它既不会被宣告,也不会被算作隐藏。若你自己的某项功能与它们并肩而立,请以 isSystemRevealed(accessor, ref, "cultivation:daoSeals") 为它把关,让它在 Cultivation 自己的显现时一同出现。突袭与首领之赏也有一个管理员分节(键为 breachRewards),但没有典籍条目,也不属于任何可显现系统。

v0.11.2 又新增了三篇典籍条目:cultivation:retainers、cultivation:auraSight 与 cultivation:pressure。它们都没有可显现系统,因此渐进显现永远不会把它们隐藏。

更新检查

v0.7.1 registerUpdateCheck 把你的模组放进 Cultivation 的更新检查:它在后台线程上拉取你所指的清单,当已安装版本落后于其中所发布的版本时,在服务器管理员加入时告知他们。你的模组会在 Cultivation 自己的更新所出现的同一条消息里得到一行,所以一台在三个模组上都落后两个版本的服务器,只会被告知一次。

CultivationAPI.registerUpdateCheck("SoulRings", "Soul Rings",
        this.getManifest().getVersion().toString(),
        "https://xianxia.dev/api/get/version/SoulRings.json",
        "https://xianxia.dev/");        // 到哪里获取新版本;仅五参数的重载有此项

清单是一个静态的 JSON 对象。release 是你认为当前的版本;ignore 列出那些即便落后也永不通知的已安装版本 —— 一个你宁愿放着不管的 beta,或一个升级路径尚未就绪的构建:

{ "release": "0.7.1", "ignore": ["0.7.0"] }
  • 加载顺序无关紧要。 在 Cultivation 开始其扫描之前注册,会并入那轮扫描;在其后注册,则单为你的模组触发一次拉取。以同一 id 注册两次,后者替换前者。
  • 失败即放行。 缺字段、格式错误的响应体、失联的主机或超时,一律意味着「无更新」,绝不会有人看到错误。
  • 服主的 Update-Check-Enabled 开关约束每一个已注册的模组,包括你的:关闭时,注册会保留,但永不拉取。
  • 以 getUpdateChecks() 取得每一项注册(包括 Cultivation 自己的),以 getAvailableUpdates() 仅取得已知落后且未被压制者 —— 在第一轮扫描落定(启动后数秒)之前为空。unregisterUpdateCheck(modId) 移除你的。

UpdateCheck 是你取回的只读句柄。它的 getter 在任何线程上都安全,并回答最近一次完成的拉取:

方法返回
getModId()、getDisplayName()、getCurrentVersion()、getManifestUrl()你所注册的内容。显示名是纯文本,不是语言键。
getDownloadUrl()你指定的去处,或 null。
getLatestVersion()最近一次成功拉取所报告的 release;若尚无成功则为 null。
isUpdateAvailable()已安装版本是否落后且未被压制 —— 加入消息就是据此构建的标志。
isSuppressed()是否存在更新,但已安装版本在清单的 ignore 列表中。
getLastCheckedMillis()最近一次拉取何时完成,或 0。

构建检查

v0.7.1 registerBuildCheck 对你 jar 的已编译类做哈希,并将结果与你发布为官方的摘要比对,使得一个被反编译再重建的 jar 能与你发行的那个区分开来。它搭载同一条后台线程与同一个 Update-Check-Enabled 开关。

CultivationAPI.registerBuildCheck("SoulRings", "Soul Rings",
        this.getManifest().getVersion().toString(),
        this.getFile(),                                  // 你插件自己的 jar
        "https://xianxia.dev/api/get/build/SoulRings.json");
{ "builds": { "0.7.1": ["a3f2...9c"], "0.7.0": ["71bd...04", "5e90...11"] } }

每个版本一个数组,所以一个你重建过、却仍视为官方的版本,可以带不止一个摘要。

检测,而非保护

任何能重建你模组的人,同样能删掉这一调用。它能抓到的,是那些没人费心去剔除它的情形 —— 被重新打包的上传、被再分发的付费扩展、损坏的下载。请把 BuildStatus.UNOFFICIAL 当作信号,绝不当作锁,并且要知道,以它为功能设限,多半惩罚的是诚实的服务器。结论写入控制台与 getBuildStatus(modId);游戏中什么都不会改变,也不会通知任何玩家。

BuildStatus 有三种状态,因为「我们无从判断」是一个真实而常见的答案:

值含义
UNKNOWN什么也没能确立:检查尚未运行、请求失败,或已发布的名单中没有该版本的条目。绝不是任何事情的证据。
OFFICIAL该 jar 的代码与为其版本发布为官方的某个摘要相符。
UNOFFICIAL名单带有该版本的摘要,而已安装的 jar 与其中任何一个都不符。这是关于字节的事实,不是对某个人的指控。

它失败即放行:端点不可达、jar 无法读取,或你从未为该版本发布摘要,结论一律是 UNKNOWN。getBuildStatus(modId) 报告某一个模组(该 id 下没有注册任何东西则为 UNKNOWN),getBuildStatus() 报告 Cultivation 自身,而 isOfficialBuild() 把后者化简成一个布尔值,仅在确凿不符时为 false —— 它刻意无法分辨「已验证」与「无从判断」。getBuildChecks() 列出每一项注册,unregisterBuildCheck(modId) 移除你的。

BuildCheck 的 getter 有 getModId()、getDisplayName()、getVersion()、getManifestUrl()、getLocalDigest()(jar 无法读取则为 null,这会使状态为 UNKNOWN)、getStatus() 与 getLastCheckedMillis()。

兼容性检查

v0.7.4 兼容性检查发布的是一个扩展的已安装版本,与哪些 Cultivation 版本共同工作,使得一个事后证明有问题的搭配,能在两个 jar 都已发布之后得到更正。面向玩家的说明:模组兼容。

这刻意是第二层。第一层是你自己的 manifest.json,其 Dependencies 条目接受完整的 semver 区间,而不只是下限:

"Dependencies": { "Siren:Cultivation": ">=0.11.2 <0.12.0" }

那才是真正的门槛:引擎会在你的一行代码运行之前、且无需任何网络,就拒绝让你的扩展对着该区间之外的 Cultivation 加载。已发布的清单所做不到的,是改变主意 —— 它只能编码发布当日已知的东西。已发布的矩阵则是事后仍可编辑的那一部分,无需 jar,也无需发行。

CultivationAPI.registerCompatCheck("MyAddon", "My Addon",
        this.getManifest().getVersion().toString(),
        "https://xianxia.dev/api/get/compat/MyAddon.json",
        check -> {
            if(check.getStatus() == CompatStatus.INCOMPATIBLE){
                // 在日志里说明,并撤下你所注册的一切。绝不要抛出异常。
                MyAddon.standDown(check.getRequiredRange(), check.getCultivationVersion());
            }
        });

矩阵以你的版本为键。min 含端点,below 不含,blocked 点出该区间之内、事后证明确实不行的个别 Cultivation 版本。每个字段都是可选的:只有 min 的区间没有上限,二者都没有则意为「任何 Cultivation,被封禁者除外」:

{ "compatible": { "0.1.2": { "min": "0.7.4", "below": "0.8.0", "blocked": [] } } }

CompatStatus 有三个值:

值含义
UNKNOWN一无所知,或一无所获:第一次检查落定之前,以及答案不可信的任何时候(主机失联、超时、响应体格式错误,或矩阵对这个扩展版本只字未提)。当作「继续」处理 —— 这是每台离线服务器永久所处的状态。
COMPATIBLE矩阵为这个扩展版本点出了一个区间,而 Cultivation 在其中。
INCOMPATIBLE矩阵点出了一个区间而 Cultivation 在其外,或点名这个确切的 Cultivation 版本被封禁。扩展唯一应当据以行动的结论,仅由针对这一确切搭配的肯定陈述得出 —— 绝不由缺席得出,也绝不由失败的请求得出。
  • 对 INCOMPATIBLE,应撤下你所注册的一切并在日志里说明 —— 而不是抛出异常。 从 setup() 抛出异常的插件会把整台服务器带崩,况且等到结论落定时,你已经运行了半分钟。
  • onVerdict 不在世界线程上。 一旦该搭配解析为 UNKNOWN 以外的任何结果,它就在检查自己的守护线程上运行一次。请触碰注册表与日志,绝不要触碰实体状态。传入四参数的重载(无回调),改为轮询 getCompatStatus(modId)。
  • 它在每个方向上都失败即放行,所以离线服务器绝不会被告知它的模组彼此不合。
  • getCompatChecks() 列出每一项注册,unregisterCompatCheck(modId) 移除你的。

CompatCheck 是你取回的句柄:getModId()、getDisplayName()、getVersion()(在矩阵中查找所用的键)、getManifestUrl()、getCultivationVersion()(实际运行的那个)、getRequiredRange()(形如 ">=0.7.4 <0.8.0" 的文字,矩阵未点出则为 null)、getStatus() 与 getLastCheckedMillis()。

上调上限

v0.7.0 有两项上限是可由扩展提升的,而不是配置键,走的是同一套注册/撤销结构:

方法默认值上限提升的对象
registerTechniquePresetCap(String key, int cap)38玩家可保留的功法快捷键配置组数目。
registerProfileCap(String key, int cap)36玩家可保留的存档数目。试炼存档沙盒绝不计入其中。

key 是你模组的一个 id("jadeSlip")。生效值 —— getMaxTechniquePresets() / getMaxProfiles() —— 取的是所有注册中最高的那个上限,绝非总和:两个扩展都把它提到六,结果是六,不是十二。上限下调时不会销毁任何东西。一名已填满六个位置、随后失去了那个准许它的扩展的玩家,仍会保留全部六个;在他重新落回生效上限之内前,只是新增会被拒绝。请在 setup() 中注册,在 shutdown() 中以对应的 unregister*Cap 撤销。

公开类型

plugin.siren.API 中,上述注册表所用的每一个公开类型,汇于一处。事件见事件;配置持有者见接口参考。

类型它是什么
UpdateCheck, BuildCheck, CompatCheck由 registerUpdateCheck、registerBuildCheck 与 registerCompatCheck 返回的只读句柄。
BuildStatus, CompatStatus它们的结论枚举:UNKNOWN、OFFICIAL、UNOFFICIAL 与 UNKNOWN、COMPATIBLE、INCOMPATIBLE。
CodexCategory, CodexEntry, CodexPage一个典籍标题、一篇条目,以及其正文所接收的块写入器。
CultivationMenuPage共享导航栏上的一个按钮,以 CultivationMenuPage.builder(id) 构建。
IdentitySection, IdentitySectionContext身份页上的一个标签页,及其所接收的构建期上下文。
CultivationMeditationAura一种打坐、突破与晋阶的外观。
CultivationPaletteDefault, CultivationPaletteLock两种配色规则。
DaoRoot对一个环外大道元素的认领。
RevealableSystem一个渐进式解锁可以隐藏的系统。
CultivationBonus一个记录 (sourceKey, statKey, amount, percent):一行总览加成,以 CultivationAPI.newBonus 构建,并由 CultivationModifierSource.bonuses 返回。
CultivationModifierSource常驻修正接口。
PlayerAdminAction, AdminConfigChoice逐玩家的管理行及其下拉选项。
TechniquePresetSnapshot一份脱离玩家的快捷键布局值;见下文。
EventBus每个 *Events 类背后的监听器列表底层:newListenerList()、dispatch(...) 与 fire(...)。不是稳定的接口面 —— 请改经由 *Events 类来注册。
AuctionListingType一件物品在拍卖行中可被挂出的两种方式:FIXED("fixed",一口价挂单)与 TIMED("timed",竞价拍卖)。getId()、getTranslationKey() 与 fromId(String)(未知或 null 回落为 FIXED)。刻意封闭,因为 AuctionManager 处处据此分支;要观察或否决一次出价,请用 MarketEvents.PreAuctionBidEvent。
MarketCategory拍卖行分类筛选行中的一个标签页:一个 id、一个每次绘制时解析的标签与一个排序序号,外加一个决定物品是否属于它的 Predicate<ItemStack>。MarketCategory.of(id, translationKey, sortOrder, matcher)。
SectRelationKind宗门间的邦交阶梯,每一级都是下一级的超集:NON_AGGRESSION(互不侵犯,双方都不得宣战)、TRADE(外加成员之间折扣的拍卖行抽成)与 ALLIANCE(外加受信任访客的领地通行与天劫相助)。SectEvents.SectRelationChangedEvent 携带它;token() 是玩家在 /sect ally propose 处键入的小写单词。

MarketCategory 是一个开放注册表,但与其余不同,它的注册点不在门面上:它位于 plugin.siren.Utils.Market.MarketCategories.register(MarketCategory),那是一个内部类,可能随版本变动。内置的标签 id 有 spirit、alchemy、manuals、talismans、treasures 与 sundries。

TechniquePresetSnapshot

一个 TechniquePresetSnapshot 是一组快捷键布局,脱离写下它的玩家:不可变,可放心持有,并能随着物品、配置或存档迁移。快照不是授予 —— 它记录一组布局指向了哪些功法,却从不授予它们。

TechniquePresetSnapshot snapshot = CultivationAPI.exportTechniquePreset(accessor, ref, 0);
String wire = snapshot.serialize();      // 一个字符串,可放进单个 Codec.STRING 物品字段

// ……之后,在另一名玩家身上……
TechniquePresetSnapshot back = TechniquePresetSnapshot.parse(wire);       // 无法读取则为 null;绝不抛出异常
TechniquePresetSnapshot.ImportResult result =
        CultivationAPI.importTechniquePreset(accessor, ref, back, "Imported layout");

该记录是 (name, authorName, slots),恰有四个 Slot(modifier, key, techniqueId) 值;TechniquePresetSnapshot.of(name, author, slots) 是宽容的构造方法,会用空槽补足过短的列表。boundSlotCount() 与 techniqueIds() 概括它,其线上形式是 1|name|author|NONE:KEY_6:,ALT:KEY_7:cloud_step,NONE:KEY_8:,NONE:KEY_9: —— 开头的 1 是格式版本。authorName 是标签,而非身份:不要据它授权任何事。

importTechniquePreset 只保留这样的功法:本服务器已注册、其规则已启用、且修士已习得。ImportResult 记录携带 status()、index()、finalName()、imported()、droppedUnknown() 与 droppedDisabled(),外加 isAdded() 与 dropped()。Status 是下列之一:ADDED、AT_CAP(他们已保留 getMaxTechniquePresets() 组布局;什么都没有丢,快照可再次导入)、EMPTY_AFTER_FILTER(每门功法都被筛掉了,布局会空着抵达)或 DISABLED(功法系统已关闭)。

哪些不可注册

并非每个扩展点都是门面上的 register* 方法。天象注册于 CelestialManager.registerEventType,拍卖行分类标签页注册于 MarketCategories.register,宗门任务计量则经由 MissionManager.creditProgress 记入。有两整层 —— 进阶与用词 —— 以 setProgressionProvider 与 setTheme 安装(编写扩展)。

天赋树节点、秘籍与灵兽物种改为在 Cultivation 自己的 JSON 配置中声明 —— 见配置页 —— 且无法从代码注册。(兽技可以从代码注册,但哪个物种习得它,仍由配置说了算。)不过你仍可经由事件触及这三者:节点解锁经由 CultivationEvents.PreSkillUnlockEvent,秘籍经由 ItemEvents.PreManualReadEvent,灵兽物种则经由每个 BeastEvents 事件上的 species() getter。

另见