开发者/注册表·

注册表

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

CultivationAPI 暴露以下这些注册方法(外加一个供功法规则使用的构建辅助方法):

方法返回说明
CultivationAPI.registerRace(String id, String displayName, String translationKey, CultivationRealm unlockRealm, Supplier<RaceConfig> stats)PlayerRace注册一个全新的种族,玩家修为达到 unlockRealm 后即可选择。
CultivationAPI.registerTechnique(String id, String displayName, String nameKey, String descriptionKey, TechniqueRule defaultRule, TechniqueEffect effect)Technique注册一门修士可施展的全新功法
CultivationAPI.newTechniqueRule(String id, boolean enabled, boolean daoSpecific, String requiredElement, String elements, String damageType, String unlockRealm, float qiCost, float cooldownSeconds, Object... params)TechniqueRule为你传给 registerTechnique 的规则提供的便捷构建器。
CultivationAPI.registerQiAbsorptionItemModifier(String itemId, float multiplier)注册或覆写某件物品的灵脉吸收倍率 —— 当它位于打坐玩家当前快捷栏槽位中时生效。
CultivationAPI.registerTitle(CultivationTitle title) <span class="tag">v0.7.0</span>在称号页放上你自己的一个纯装饰称号
CultivationAPI.registerSectBanner(SectBanner banner) <span class="tag">v0.7.0</span>添加一面宗门可悬挂于其大殿之上的旗帜。
CultivationAPI.registerPalette(CultivationPalette palette) <span class="tag">v0.7.0</span>添加一套能为每一个 Cultivation 菜单与 HUD 重新上色的配色。

它们全都可安全地从你自己插件的 setup() 中调用,相对于 Cultivation 自身的 setup() 无论何种加载顺序皆然。注册表是普通的静态 map,在玩家真正交互之前 —— 打开种族菜单、打坐、施展功法 —— 无人读取,而那只会发生在每个插件加载完毕许久之后。

以同一 id 重复注册一个种族或一门功法是空操作:它返回已有的条目而非报错,因此在你的插件重载时也是安全的。

注册一个种族

@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 才会去播种 RaceConfigUnlock-Realm —— 这样一来,用自己 JSON 文件(其本身可能设了 Unlock-Realm)来支撑属性的调用方,仍保持服主可编辑。

RaceConfig 为它的每一个可调项都提供了 getter 与 setter:getDescription / setDescriptiongetUnlockRealm / setUnlockRealmgetHealthBonusPercent / setHealthBonusPercentgetDamageBonusPercent / setDamageBonusPercentgetQiGainRatePercentBonus / setQiGainRatePercentBonusgetBreakthroughDurationPercentReduction / setBreakthroughDurationPercentReduction,以及 getQiAlignmentYinBiasPercent / setQiAlignmentYinBiasPercent。它们与配置页内置种族配置文件中的 Health-Bonus-PercentDamage-Bonus-PercentQi-Gain-Rate-Percent-BonusBreakthrough-Duration-Percent-ReductionQi-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)

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

参数说明
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)
参数说明
requiredElementdaoSpecific 为 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.PreTechniquePerformEventTechniquePerformEvent,因此其他扩展也能为你的功法重新定价或将其否决。

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

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

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

注册一件灵气吸收物品

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

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

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

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

注册一个称号

<span class="tag">v0.7.0</span> 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 组件写入的,因此一个同样会写入它的改名模组,会与称号相互覆写。

注册一面宗门旗帜

<span class="tag">v0.7.0</span> 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());

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

注册一套配色

<span class="tag">v0.7.0</span> 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());

上调上限

<span class="tag">v0.7.0</span> 有两项上限并非配置键,而是可由扩展提升的 —— 走的是同一套注册/撤销结构:

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

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

哪些不可注册

并非每个扩展点都是一个 Java 注册表。CultivationAPI 为种族、功法、灵气吸收物品、管理设置、菜单页、典籍条目、称号、宗门旗帜、配色,以及那两项上限,都暴露了 register* 方法。天赋树节点、秘籍与灵兽物种改为在 Cultivation 自己的 JSON 配置中声明 —— 见配置页 —— 且无法从代码注册。不过你仍可经由事件触及这三者:节点解锁经由 CultivationEvents.PreSkillUnlockEvent,秘籍经由 ItemEvents.PreManualReadEvent,灵兽物种则经由每个 BeastEvents 事件上的 species() getter。

另见