API 参考 / 入口与生命周期
生命周期:四个阶段
宿主按下列顺序推进生命周期,全部回调在 Mod 中都有默认空实现,模组按需覆写:
| 阶段 | 何时调用 | 允许做什么 | 禁止做什么 | 抛异常后果 |
|---|---|---|---|---|
onPreInit(ModContext) | 模组加载时最先调用,注册表开放 | 经 ctx.registries() 注册全部自有内容(音符类型 / 游戏模式 / 判定等级 / 谱面扩展段 / 渲染滤镜 / 背景渲染器 / 打击特效样式 / 判定策略 / 判定调整器 / 收集品 / 皮肤等) | 把注册写在其它回调或运行期 | 该模组不被激活,记入加载失败(LOAD_FAILED),其余模组继续 |
| 宿主冻结注册表 | 全部模组的 onPreInit 结束后立即执行 | 宿主内部动作 | 模组不得依赖此动作作为回调 | 不适用(宿主动作) |
onInit(ModContext) | 注册表已冻结、全部模组的 onPreInit 已完成之后 | 读配置、建缓存、订阅事件、创建运行期资源、读取其它模组已注册的内容(此时注册表内容已定型,可安全遍历) | 任何注册写入(抛 RegistryFrozenException) | 该模组不被激活,记入加载失败 |
onPostInit(ModContext) | 全部模组的 onInit 已完成之后 | 发布初始事件、自检、建立跨模组交互(此时可确信依赖模组已完成初始化)、读取自有译文 | 任何注册写入 | 保持激活,仅记录错误(此时运行期订阅已经建立,判成失败会让订阅变孤儿) |
| 运行期 | onPostInit 之后直至卸载 | 事件回调、渲染、快捷键、网络等 | 任何注册写入 | 不在本节讨论范围 |
onUnload() | 模组卸载时调用一次 | 释放资源 | — | 异常被宿主隔离,仅记录日志 |
要点补充:
- 注册窗口一次性:宿主在全部模组的
onPreInit结束后立即冻结所有注册表;此后(含onInit、运行期、渲染回调内)任何注册都抛com.seecmd.mod.api.registry.RegistryFrozenException。把注册写在别处是错误用法。 - 注册在进程生命周期内一次性完成、不再撤销,因此玩家在模组列表里启用/停用模组需要重启才生效。冻结之后的启用列表变更只被记录,宿主不会热激活/热停用。
- 注册不等于激活:
ModManager.register(Mod)只做登记;真正激活由宿主在启动装配时统一推进(依赖校验 → 排序 → 全部onPreInit→ 冻结 → 全部onInit→ 全部onPostInit)。 - 注册键的命名空间必须等于模组 id(宿主用
ctx.registries()返回的 per-模组视图做归属校验),错配抛IllegalArgumentException;同一注册键重复注册同样抛IllegalArgumentException。
阶段推进粒度:宿主在每一个阶段内部都按依赖拓扑顺序遍历全部模组,即「全部模组的 onPreInit(按依赖序)→ 冻结 → 全部 onInit(按依赖序)→ 全部 onPostInit(按依赖序)」,而不是「逐个模组跑完三段」。因此 onInit 里可以确信所有模组的注册内容都已定型,onPostInit 里可以确信所有模组的 onInit 都已完成。
语言文件加载时机:宿主在本模组的 onInit 返回之后才自动扫描加载其 jar 内的语言文件。因此在 onInit 中读取自己的译文会读到空值,读取自有译文应放到 onPostInit。
事件订阅不在入口接口里:统一走 ctx.eventBus(),在 onInit 中订阅即可收到游玩事件与全局事件,不再有「游玩总线 / 全局总线」之分。GameplayMod 中历史上的注册型钩子(渲染滤镜、背景渲染器、打击特效样式、判定策略、判定调整器)已全部下线,统一改为在 onPreInit 内经 ctx.registries() 注册,不再有 per-模组 的 registerXxx(registry) 回调。