打包与发布
一个能被游戏加载、能分享给别人的模组 jar,含版本与依赖声明。
代码能在你的工程里编译,不代表别人双击就能玩。发布要回答三件事:游戏怎么找到入口类;jar 里哪些该带、哪些绝不能带;换了宿主版本还能不能加载。
这三件事没有一件写在 Java 业务代码里,全部落在 jar 的 manifest 和一两个构建变量上。写错任何一处,表现都不是编译错误,而是模组列表里的 LOAD_FAILED。
- jar:一个 ZIP 包,里面装编译后的
.class、资源和META-INF/MANIFEST.MF。游戏只认这一个文件。 - manifest 属性:jar 根目录
META-INF/MANIFEST.MF里的键值对。加载器靠它找入口类、判断 API 版本。 - 模组依赖:写在代码
ModInfo.dependsOn里的模组 id 列表,不是 manifest 字段。 - API 主版本:
3.2里的3。宿主只保证同一主版本内兼容,主版本不同的 jar 会被直接拒绝。
前置条件
- 已经有一个能编译的模组工程(模板工程或自己的)。没跑通过就先看 第一个模组。
javac -version至少 17,且javac、jar在PATH里;走 Maven 路线还需要mvn。- 分得清两个「根目录」:模组根是放
build.bat和src\的目录;游戏根是放游戏本体和mods\的目录。
打包本身不新增类。唯一涉及代码的是第 6 步给 ModInfo 加依赖列表,那是一段片段,不是完整类。
打包流程
开始动手
动手前先把这三个值定下来,代码、构建脚本、manifest 都要用同一个值:
| 值 | 示例 | 约束 |
|---|---|---|
| 模组 id | hello_mod | 小写 [a-z0-9_.-],不能含 /、:;同时是资源命名空间与 data\mods\<id> 目录名 |
| 入口类全限定名 | com.example.hellomod.HelloMod | 必须与实际 package + 类名逐字一致,且实现 Mod |
| 模组版本 | 0.1.0 | 与 API 版本无关;按「主.次.修订」递增 |
ModInfo 的 id、name、version、authors、description 宿主都要求非空,任一为空会被拒绝注册。
打开模组根目录的 build.bat,只改顶部这 5 行:
set "MOD_ID=hello_mod"
set "MOD_NAME=Hello Mod"
set "MOD_VERSION=0.1.0"
set "MOD_CLASS=com.example.hellomod.HelloMod"
set "API_VERSION=3.2"MOD_CLASS从代码里复制package行再拼类名,避免手打出错。API_VERSION写宿主当前的3.2。模板里默认的3.0也能加载(主版本相同),新工程建议对齐3.2。- 脚本自动查找同级的
mod-api-*.jar,找不到直接报错退出;gson-*.jar先找同级,再找%USERPROFILE%\.m2。
在模组根目录执行(不是游戏根):
build.bat脚本固定做四件事:
javac --release 17 -encoding UTF-8 -cp "mod-api-*.jar;gson-*.jar"把src\main\java编译到target\classes;- 把
src\main\resources整个复制进target\classes,所以资源在 jar 里的路径就是assets\...; - 写
target\manifest.mf,内容来自上一步那 5 个变量; jar cfm "target\<MOD_ID>-<MOD_VERSION>.jar" "target\manifest.mf" -C "target\classes" .
成功时发布物只有一个:
target\hello_mod-0.1.0.jartarget\classes\、target\manifest.mf、target\srcfiles.txt 都是中间产物,不要发出去。
pom.xml 与 build.bat 二选一。先把 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\<artifactId>-<version>.jar。pom.xml 里与发布相关的三处:
<artifactId>与<version>:决定产物文件名;<maven.compiler.release>17</maven.compiler.release>:与javac --release 17等价;maven-jar-plugin的<manifestEntries>:写Seecmd-Mod-Class、Seecmd-Api-Version等字段,改类名或模组 id 时要同步改这里。
两个依赖都必须是 provided:
<dependency>
<groupId>com.seecmd</groupId>
<artifactId>mod-api</artifactId>
<version>3.2</version>
<scope>provided</scope>
</dependency>provided 的含义是「编译期可见、运行期由宿主提供」,也就是「不拷进 jar」的开关。gson 同理。
列出条目:
jar tf target\hello_mod-0.1.0.jar期望看到三类,且都在 jar 根:
META-INF/MANIFEST.MF
com/example/hellomod/HelloMod.class
assets/hello_mod/lang/zh.json
assets/hello_mod/textures/note.png不应该看到:
com/seecmd/mod/api/...
com/google/gson/...出现后者说明 API 或 gson 被打进了 jar。
再直接看 manifest:
jar xf target\hello_mod-0.1.0.jar META-INF/MANIFEST.MF
type META-INF\MANIFEST.MF期望内容:
Manifest-Version: 1.0
Seecmd-Mod-Class: com.example.hellomod.HelloMod
Seecmd-Mod-Id: hello_mod
Seecmd-Mod-Name: Hello Mod
Seecmd-Mod-Version: 0.1.0
Seecmd-Mod-Authors: YourName
Seecmd-Mod-Description: Theseus mod hello_mod .1.0
Seecmd-Api-Version: 3.2宿主真正读取的属性只有两个:
Seecmd-Mod-Class:入口类全限定名,缺失就找不到模组;Seecmd-Api-Version:参与 API 版本协商。
Seecmd-Mod-Id 只在加载失败时作为失败记录的键(API 版本不兼容、入口类找不到、实例化失败),缺失时退化为 jar 文件名。Seecmd-Mod-Name / -Version / -Authors / -Description 当前宿主不读取;模组列表显示的是 ModInfo 里的值。保留这些字段是为了 jar 自描述,改不改都不影响加载。
target\<模组id>-<版本>.jar存在;jar tf能看到META-INF/MANIFEST.MF、入口类.class、assets\<模组id>\...;- manifest 里的
Seecmd-Mod-Class与代码包名、类名逐字一致; - manifest 里
Seecmd-Api-Version的主版本是3。
ModInfo 的 version 是模组自己的版本,建议按语义递增:
- 主版本:有不兼容改动才加,玩家可能需要删旧配置;
- 次版本:兼容地新增功能;
- 修订:只修 bug。
模组之间的依赖写在 info() 里,不在 manifest:
@Override
public ModInfo info() {
return new ModInfo(
"hello_mod", // id
"Hello Mod", // 显示名称
"0.1.0", // 模组版本
List.of("YourName"), // 作者
"依赖 core_lib 的示例", // 描述
List.of("core_lib")); // 第 6 个分量:硬依赖的模组 id
}ModInfo.of(...) 是 5 参工厂,等价于 dependsOn = List.of()。dependsOn 的字段含义:
- 写的是被依赖模组的 id,不是 jar 文件名,也不带版本号;
- 被依赖模组必须已注册且已启用,否则本模组
LOAD_FAILED,原因为Missing dependencies: [core_lib]; - 循环依赖同样失败,原因为
Circular dependency: a -> b -> a; - 只影响本模组,其它模组照常加载。
只想要顺序、不强制目标存在时,用软依赖:
@Override
public ModLoadOrder loadOrder() {
return new ModLoadOrder(LoadStage.INIT, List.of("core_lib"), List.of());
}目标不存在时这条约束自动失效,不会报错。需要「目标一定在,而且先跑完」时,dependsOn 与 loadAfter 一起写。
把 jar 复制到游戏根目录的 mods\:
<游戏根>\
├── mods\
│ └── hello_mod-0.1.0.jar
└── 游戏本体...加载器只扫描 mods\ 的直接子文件,不递归子目录。新放入的 jar 默认未启用:第一次启动看到 REGISTERED 是正常的,在模组菜单里启用后重启,状态才变 ACTIVE。
| 状态 | 含义 | 该做什么 |
|---|---|---|
ACTIVE | 已加载并激活 | 可以发布 |
REGISTERED | 已注册但未启用 | 在模组菜单启用,重启 |
LOAD_FAILED | 启用但加载失败 | 看失败原因:API 版本 / 入口类 / 依赖 / 生命周期异常 |
发布说明至少写清四件事:适用 API 主版本、依赖哪些模组 id、jar 放在 mods\ 根目录、启用后要重启。
为什么不能把 mod-api 与 gson 打进 jar。 宿主给每个模组 jar 配一个独立类加载器,白名单把 com.seecmd.mod.api.* 与 com.google.gson.* 一律交给父加载器。目的是让模组与宿主共享同一份类型:Mod 转型、ctx 传参都依赖「同一个类」。即使你把 API 复制进 jar,加载器仍然用宿主那份,副本不生效,只让 jar 变大;gson 同理。宿主内部类(com.seecmd.* 但不是 mod-api)更不该打进去:不打包时引用它们会被直接拒绝并报「模组不得引用内核内部实现」;一旦打包,类加载器的「自身 jar 优先」会让你用到一份冻结在 jar 里的旧实现,宿主升级后不会跟着变。
加载器怎么发现入口类。 启动时扫描 mods\ 下每个 .jar,读 META-INF/MANIFEST.MF 的主属性:
- 没有 manifest:跳过;
- 没有
Seecmd-Mod-Class:若声明了任何Seecmd-*字段,告警「补全 manifest」;否则当普通 jar 静默跳过; - 有入口类:先校验
Seecmd-Api-Version,再建类加载器,用Class.forName加载入口类; - 校验它是
Mod的子类型,再用无参构造器实例化; - 从
info()取 id 注册。id 非法、字段不全、id 重复都会被拒绝。
所以入口类必须有可用的无参构造器;写成 private 构造器或只提供带参构造器,都会在实例化这一步失败。
API 主版本不匹配会怎样。 校验发生在创建类加载器之前,不会加载到一半才炸。宿主当前 ModApiVersion.CURRENT = "3.2":
Seecmd-Api-Version | 宿主行为 |
|---|---|
| 缺失或空白 | 按宿主当前版本处理,放行 |
| 无法解析 | 拒绝,提示「无法解析…期望形如 3.2」 |
| 主版本不同 | 拒绝,提示「此模组要求 API X,宿主支持 3.2」 |
| 主版本相同、次版本更高 | 告警放行(可能引用了宿主没有的新接口) |
| 只有修订号不同 | 不参与判定 |
版本串容忍 .2、3.2.0、3.2-SNAPSHOT 这类写法;3 视为 3.0。
- 把 API / gson 打进 jar:
provided没写,或用插件打成 fat jar。副本不会被使用,jar 却大了一圈。 - 改了类名只改代码:
build.bat的MOD_CLASS、pom.xml的<Seecmd-Mod-Class>没跟着改,加载时报「入口类/依赖不可用」。 API_VERSION写成模组版本:写0.1.0会让主版本变成0,宿主直接拒绝;这里要写3.2。- 把依赖写进 manifest:宿主不读自定义的依赖键,依赖必须写
ModInfo.dependsOn。 - 以为依赖会自动下载或自动启用:宿主不下载模组;被依赖模组存在但未启用,同样算缺失。
- 只写
loadAfter:目标缺失不报错,但运行期取服务会拿到null;要强约束就补dependsOn。 - 资源目录不是
assets\<模组id>\:资源确实进了 jar,但ResourceLocation找不到,运行期抛ResourceNotFoundException。 - jar 放到
mods\子目录:非递归扫描看不到,列表里也不会出现。 - 看到
REGISTERED就以为打包失败:新 jar 默认未启用,启用并重启才会ACTIVE。 - manifest 里写中文:
build.bat用系统代码页写 manifest,中文可能变乱码;显示名放ModInfo,manifest 保持 ASCII。
target\<模组id>-<版本>.jar在干净环境能加载jar tf里有入口类.class与assets\<模组id>\...jar tf里没有com/seecmd/mod/api/,也没有com/google/gson/Seecmd-Mod-Class与代码包名、类名逐字一致Seecmd-Api-Version的主版本与目标宿主一致(当前3.2)ModInfo.id与Seecmd-Mod-Id、资源命名空间、MOD_ID四处一致- 依赖写在
ModInfo.dependsOn,被依赖模组随包分发并在说明里写明 ModInfo的 id / name / version / authors / description 都非空- 版本号按语义递增,发布说明写清适用 API 版本与安装方式
- 全新安装(删掉
data\mods\<模组id>\)仍能加载