注册表
监听事件让你得以重塑 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) 可为其中任何一种添加一句话说明。
上面的每个 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,因此无法翻译。
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、howToComprehend | name 为原始键 | 各接受一个 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)。
从功法施展监听器抵达的显化,包括蓄力释放路径,运行于该存储的处理锁之下,拿到的是裸的 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。
把导航栏放到你自己的页面上
- 给你的
.ui文档一个占位:Group #NavBar { LayoutMode: Left; Anchor: (Bottom: 4, Height: 46); }。 - 在你页面的
build中调用CultivationAPI.buildMenuNav(commandBuilder, eventBuilder, playerRef, "myMod:alchemy", store, ref)。带store与ref的重载以观看者的配色绘制这条栏;较短的那个总是以默认的绯红与金色绘制,在一个换了主题的页面上,那就成了一条旧颜色的条带。 - 在你的事件 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) - 在
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:DaoSeals | Seal-Min-Realm |
cultivation:daoDomains | 是 | 是,在己身一栏 | Cultivation:DaoDomains | Domain-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) | 3 | 8 | 玩家可保留的功法快捷键配置组数目。 |
registerProfileCap(String key, int cap) | 3 | 6 | 玩家可保留的存档数目。试炼存档沙盒绝不计入其中。 |
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。