注册表
监听事件让你得以重塑 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 才会去播种 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。
注册一件灵气吸收物品
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());
- **旗帜是一个粒子系统,不是一种颜色。**引擎「生成时着色」的字段并不可靠,因此旗帜的颜色是烘焙进
.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 —— 见事件。
注册一套配色
<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());
- 所声明的文档,是按裸文件名在
documentRoot下匹配的,因此每一个被覆盖的文件都必须置于那同一个根目录下。你未声明的页面,会刻意回落到 Cultivation 自己的版本 —— 一个无法解析的.ui路径会让整个页面加载失败且不留日志,因此注册表绝不会凭空捏造一个。 - 九个天赋树分支的光晕是全有或全无的:要么传入每一个
SkillTreeBranch,要么一个都不传 ——build()会拒绝只传一部分。 CultivationPalette.DEFAULT_KEY("cultivation:default")即内置的默认外观。getPalette(store, ref)对它返回null;document(palette, basePath)会依玩家当前所处的配色解析出对应路径。- 与已佩戴的称号一样,玩家所选的配色存放在其设置上,存档之间共享。
上调上限
<span class="tag">v0.7.0</span> 有两项上限并非配置键,而是可由扩展提升的 —— 走的是同一套注册/撤销结构:
| 方法 | 默认值 | 上限 | 提升的对象 |
|---|---|---|---|
registerTechniquePresetCap(String key, int cap) | 3 | 8 | 玩家可保留的功法配置组数目。 |
registerProfileCap(String key, int cap) | 3 | 6 | 玩家可保留的存档数目。试炼存档沙盒绝不计入其中。 |
key 是你模组的一个 id(例如 "jadeSlip")。生效值 —— getMaxTechniquePresets() / getMaxProfiles() —— 取的是所有注册中最高的那个上限,绝非总和:两个扩展都把它提到六,结果是六,不是十二。上限下调时不会销毁任何东西。一名已填满六个位置、随后失去了那个准许它的扩展的玩家,仍会保留全部六个;在他重新落回生效上限之内前,只是新增会被拒绝。请在 setup() 中注册,在 shutdown() 中以对应的 unregister*Cap 撤销。
哪些不可注册
并非每个扩展点都是一个 Java 注册表。CultivationAPI 为种族、功法、灵气吸收物品、管理设置、菜单页、典籍条目、称号、宗门旗帜、配色,以及那两项上限,都暴露了 register* 方法。天赋树节点、秘籍与灵兽物种改为在 Cultivation 自己的 JSON 配置中声明 —— 见配置页 —— 且无法从代码注册。不过你仍可经由事件触及这三者:节点解锁经由 CultivationEvents.PreSkillUnlockEvent,秘籍经由 ItemEvents.PreManualReadEvent,灵兽物种则经由每个 BeastEvents 事件上的 species() getter。