开发指南
调试与排错
10.1 日志:ctx.getLogger()
ctx.getLogger() 返回名为 seecmd.mod.<模组id> 的 java.util.logging.Logger。宿主为该 logger 装配了单行格式的输出处理器,日志行前缀为 [<模组id>],因此:
- 在游戏日志里直接搜
[<你的模组id>](或搜模组 id)即可定位自己的全部输出; - 宿主把标准输出 / 标准错误双写入
data/log/run_<启动时间>.log,控制台看不到内容(例如双击 exe 启动)时以这个文件为准; - 多个模组混在一起排错时,前缀是你唯一的归属标记 —— 不要用
System.out.println打关键信息而不带前缀。
java
@Override
public void onInit(ModContext ctx) {
ctx.getLogger().info("初始化完成");
ctx.getLogger().warning("贴图缺失:" + NOTE_TEXTURE);
}10.2 排错清单
| 现象 | 常见原因 | 怎么查 / 怎么改 |
|---|---|---|
模组状态 LOAD_FAILED,原因写「此模组要求 API X,宿主支持 Y」 | manifest 的 Seecmd-Api-Version 与宿主支持的模组 API major 不匹配 | 改用与宿主支持的模组 API 主版本一致的 API jar 重新编译;或改对 manifest 字段 |
LOAD_FAILED,原因含「引用了内核内部实现」 | 代码 import 了 com.seecmd.* 下的宿主 / 内核类 | 只依赖 com.seecmd.mod.api.*;需要宿主做某件事时走能力接口 |
LOAD_FAILED,找不到入口类 | manifest 的 Seecmd-Mod-Class 写错,或类未打进 jar | 核对全限定类名与 src/main/java 下的实际包名;用 jar tf 确认 class 在 jar 里 |
LOAD_FAILED,原因含加载时抛异常 | 入口类无参构造 / info() / onPreInit 里抛了异常 | 把 info() 保持为纯数据返回;onPreInit 中只做注册,不做 IO |
抛 RegistryFrozenException | 注册写在了 onInit / onPostInit / 渲染回调等非注册窗口 | 把全部 ctx.registries().xxx().register(...) 移到 onPreInit,不要捕获该异常来规避 |
抛 IllegalArgumentException:非法命名空间 / 非法路径 | ResourceLocation 的 namespace 只接受小写 [a-z0-9_.-](长度 1~64),path 只接受小写 [a-z0-9/._-](长度 1~255) | 全部改小写;命名空间约定 = 模组 id |
抛 IllegalArgumentException:该 location 已注册 | 同一 ResourceLocation 注册了两遍(重复注册是显式失败,不静默覆盖) | 注册前用 Registry.contains(location) 自检;不同内容用不同 path |
抛 IllegalArgumentException:键 / 值 / 类型为 null,或 RegistryKey 的泛型与目标注册表不匹配 | 把某类内容注册进了另一个注册表 | 核对 RegistryKey.of(ns, path, XxxDefinition.class) 的第三个参数与目标注册表一致 |
加载图片 / 文本时抛 ResourceNotFoundException | 资源没放在 src/main/resources/assets/<模组id>/...,或路径写错 | 1.0 没有裸路径回退,路径必须对应 assets/<命名空间>/<路径>;可用 ResourceManager.list("mymod:") 列出 jar 内实际条目 |
ctx.language().get(key) 在 onInit 里读不到自己的译文 | 宿主在本模组 onInit 返回之后才扫描 jar 内的语言文件 | 放到 onPostInit(或更晚)再读;也可改用 ctx.language().add(locale, entries) 主动注册 |
| 改了模组却「没生效」 | 注册在启动期一次性完成 | 重启游戏;模组列表里的启用 / 停用开关只在下次启动生效;改了模组 jar 同样要重启游戏 |
| 事件监听器抛异常后「什么都没发生」 | 总线对每个监听器独立 try/catch(Throwable),异常被隔离并记日志 | 到日志里搜该模组的 logger 名;异常不会影响其它模组、不会中断游戏,也不会重试 |
10.3 事件监听器抛异常的表现
事件总线对每个监听器独立 try/catch(Throwable):单个监听器抛异常只记日志,不影响同事件的其它监听器,也绝不冒泡到宿主循环 —— 一个模组崩不了游戏。需要注意的推论:
- 异常不会被重试:本次事件分发直接跳过该监听器的后续逻辑,下一次同类事件才会再次调用;
- 因此监听器内的关键副作用(写文件、置状态位)要么放在 try 之外先完成,要么自行在监听器内部做异常处理;
- 分发是同步的:
post在调用线程内当场完成全部分发并返回,监听器回调运行在游戏 / 渲染线程,不得在回调里做阻塞 IO。
10.4 「需要重启才生效」的两件事
- 启用 / 停用模组需要重启游戏:注册在进程生命周期内一次性完成、不再撤销;模组列表里的开关只在下次启动生效。
- 改了模组 jar 需要重启游戏:宿主不会热替换已在运行的模组代码,重新打包后必须重启才能看到新行为。