环境搭建与第一个模组
第一次接触本项目:建议先跟 教程:序章:把环境搭起来 与 教程:第一个模组 走一遍,把工程跑通。本篇是同一批内容的细节参考(构建脚本做了什么、manifest 每个字段的协商规则、目录约定),适合回头查阅。
本节的目标是让一个从零开始的开发者把「能编译、能进游戏、能在模组列表里看到 ACTIVE」这条最短路径走通。当前版本的模组只依赖三个外部件:JDK 17+、mod-api-3.2.jar(编译期 API)、gson-*.jar(如 gson-2.10.1.jar,com.google.gson.JsonObject 出现在 API 签名里,所以编译期需要它;运行期由宿主提供)。
模组编译期只应依赖 com.seecmd.mod.api.*(含子包)这一套瘦身 API,不得把宿主或游戏内核的类打进自己的 jar。
1.1 构建路线一:build.bat(无需 Maven,推荐)
官方模板 mods-template\ 已带好脚本与依赖,最快的路径是复制模板目录再改:
- 把
mod-api-3.2.jar与gson-*.jar放在工程根目录(与build.bat同级); - 编辑
build.bat顶部的变量:MOD_ID、MOD_NAME、MOD_VERSION、MOD_CLASS(入口类全限定名)、API_VERSION; - 在
src\main\java下写代码,在src\main\resources下放资源; - 双击
build.bat,产物为target\<MOD_ID>-<MOD_VERSION>.jar。
脚本内部做四件事,值得知道它到底依赖什么:
- 用
javac --release 17 -encoding UTF-8 -cp "<mod-api jar>;<gson jar>"编译src\main\java下的全部.java; - 把
src\main\resources整个拷进输出目录(因此资源在 jar 里的路径就是assets/...); - 写入 manifest:
Seecmd-Mod-Class/Seecmd-Mod-Id/Seecmd-Mod-Name/Seecmd-Mod-Version/Seecmd-Mod-Authors/Seecmd-Mod-Description/Seecmd-Api-Version; - 用
jar cfm打包。
前提:本机装了 JDK 17+,且 javac、jar 在 PATH 中。脚本找不到 mod-api-*.jar 时会直接报错退出(也可以在脚本里手工指定 API_JAR);gson-*.jar 缺省时会尝试从 %USERPROFILE%\.m2 里找一个可用的。
1.2 构建路线二:Maven(pom.xml)
模板同样带 pom.xml,两条路线产物一致,都是带完整 manifest 的模组 jar。使用前需要先把 API jar 安装进本地仓库(只需一次):
mvn install:install-file -Dfile=mod-api-3.2.jar -DgroupId=com.seecmd -DartifactId=mod-api -Dversion=3.2 -Dpackaging=jar然后 mvn package,产物在 target\ 下。关键配置有两点:
- 依赖的
scope必须是provided(mod-api与gson都是),表示编译期引用、运行期由宿主提供;把宿主类打进模组 jar 会引发类加载冲突; - manifest 字段由
maven-jar-plugin的<manifestEntries>写入,改类名/模组 id 时要同步改这里,否则游戏会因为找不到入口类而加载失败。
编译目标版本用 maven.compiler.release 设为 17(模板已配好)。
1.3 目录布局与资源放哪
my-mod/
├── mod-api-3.2.jar # 编译期依赖(必需)
├── gson-2.10.1.jar # 编译期依赖(必需,spi 签名用到 JsonObject)
├── build.bat # 路线一
├── pom.xml # 路线二(二选一)
└── src/main/
├── java/com/example/hellomod/HelloMod.java
└── resources/assets/hello_mod/
├── lang/zh.json # 自带语言文件,宿主自动加载
├── lang/en.json
└── textures/note.png资源约定只有一种:文件放在 src/main/resources/assets/<模组id>/<路径>,代码里用 ResourceLocation.of("<模组id>", "<相对路径>") 引用。例如上面的贴图对应 ResourceLocation.of("hello_mod", "textures/note.png")。
当前版本没有裸路径回退:宿主按「data/mods/<模组id>/assets/... 磁盘覆盖 → 模组 jar 内 assets/...」两级查找,找不到会抛受检异常 ResourceNotFoundException,所以加载资源的调用点必须显式处理(try/catch 或声明抛出),不能靠判空。
语言文件放在 assets/<模组id>/lang/<locale>.json,键名以模组 id 作前缀(如 "hello_mod.greeting"),宿主在装配后自动扫描加载。模组数据目录是 data/mods/<模组id>/,经 ctx.getDataDir() 拿到;配置文件经 ctx.config() 读写。
1.4 manifest 字段
| 键 | 是否必须 | 说明 |
|---|---|---|
Seecmd-Mod-Class | 必须 | 入口类全限定名,必须是 Mod 或 GameplayMod 的实现 |
Seecmd-Mod-Id | 可选 | 仅在加载失败时作为失败记录的键(缺失时退化为 jar 文件名)。真正的模组 id 由 ModInfo.id() 提供,两者不一致时以 ModInfo 为准 |
Seecmd-Mod-Name / Seecmd-Mod-Version / Seecmd-Mod-Authors / Seecmd-Mod-Description | 宿主不读取 | 模组列表展示的是 ModInfo 的对应字段。这些 manifest 字段只作为 jar 元信息,方便人工识别,不参与加载 |
Seecmd-Api-Version | 建议 | 期望的 mod-api 版本(如 3.2,判定按 major / minor 解析) |
Seecmd-Api-Version 的判定规则:
| 声明情况 | 宿主行为 |
|---|---|
| 键缺失或为空白 | 按宿主当前版本处理(不因缺键被拒绝) |
| 无法解析 | 拒绝加载 |
| major 不同 | 拒绝加载,并给出「此模组要求 API X,宿主支持 Y」的明确提示 |
| minor 更高(major 相同) | 告警放行(宿主保证 minor 向后兼容) |
| patch 不同 | 不参与判定 |
校验发生在创建模组类加载器之前,所以不会出现「加载到一半才炸」。major 不匹配属于最容易踩的坑:仓库里放着旧版 API jar 编译出来的模组,进游戏就会直接 LOAD_FAILED。
1.5 最小可运行模组
最小模组只需实现 Mod,提供 info(),并按需覆写生命周期回调。下面这段是可直接编译的参考骨架;带日志、数据目录与玩家提示的完整走查见 教程 · 第一个模组。
package com.example.hellomod;
import com.seecmd.mod.api.Mod;
import com.seecmd.mod.api.ModContext;
import com.seecmd.mod.api.ModInfo;
import java.util.List;
public class HelloMod implements Mod {
/** 模组 id:小写下划线,必须与 manifest 的 Seecmd-Mod-Id 一致。 */
private static final String MOD_ID = "hello_mod";
@Override
public ModInfo info() {
return ModInfo.of(
MOD_ID, // id
"Hello Mod", // 显示名称
"0.1.0", // 模组自己的版本号(与 mod-api 的 1.0 无关)
List.of("YourName"), // 作者列表
"我的第一个 Theseus 模组"); // 描述
}
@Override
public void onPreInit(ModContext ctx) {
// 唯一允许注册内容的阶段。本模组暂时没有自有内容要注册。
// 不要在这里做 IO / 耗时操作:全部模组的 onPreInit 一结束宿主就冻结注册表。
ctx.getLogger().info("注册窗口:暂无内容注册");
}
@Override
public void onInit(ModContext ctx) {
// 注册表已冻结:读配置、读资源、建缓存、订阅事件都在这里。
ctx.getLogger().info("初始化完成,数据目录:" + ctx.getDataDir());
ctx.notifier().info("Hello Mod 已加载");
}
}说明几点:
info()是接口里唯一的抽象方法,必须实现;onPreInit/onInit/onPostInit/onUnload全都是默认空实现。ctx.getLogger()返回名为seecmd.mod.<模组id>的java.util.logging.Logger(宿主已装配输出处理器),排查问题优先看它。- 需要接管游玩 HUD 覆盖或「按键 → 轨道」映射时,把入口类改成实现
GameplayMod(它继承Mod),再覆写createGameplayHud()/createLaneInputMapper()/replacesDisorderProgressBar()。其余写法不变。
1.6 安装到游戏
把构建产物 target\<MOD_ID>-<MOD_VERSION>.jar 放进游戏根目录的 mods\ 文件夹,重启游戏即可自动加载;在模组列表里能看到它,状态为 ACTIVE。
- 停用 / 启用模组需要重启游戏才生效:注册在启动期一次性完成,运行期不再变动。
- 状态含义见
ModLoadStatus:LOAD_FAILED表示启用但加载失败(依赖缺失 / 循环依赖 / 生命周期回调抛异常 / API 版本不匹配等),REGISTERED表示已注册未启用,ACTIVE表示已激活。看不到自己的模组时先确认状态是ACTIVE而不是LOAD_FAILED。