Theseus 模组文档
教程

打包与发布

一个能被游戏加载、能分享给别人的模组 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 会被直接拒绝。

前置条件

本篇没有完整 Java 类

打包本身不新增类。唯一涉及代码的是第 6 步给 ModInfo 加依赖列表,那是一段片段,不是完整类。

打包流程

源码 src\main\java 编译 --release 17 资源 src\main\ resources manifest 入口类 API 版本 打包 jar jar cfm 安装 游戏根\mods\ 加载器校验 入口类 API 版本 前 5 步都在模组根目录完成,最终产物只有一个 jar。 第 7 步发生在游戏启动时:读 manifest → 校验 API 主版本 → 建类加载器 → 找入口类。 任何一步失败,列表里就是 LOAD_FAILED,原因看模组列表或日志。
从源码到加载:构建期五步在本地完成,校验全部由宿主在启动时做

开始动手

第 1 步:先定三个名字

动手前先把这三个值定下来,代码、构建脚本、manifest 都要用同一个值:

值示例约束
模组 idhello_mod小写 [a-z0-9_.-],不能含 /、:;同时是资源命名空间与 data\mods\<id> 目录名
入口类全限定名com.example.hellomod.HelloMod必须与实际 package + 类名逐字一致,且实现 Mod
模组版本0.1.0与 API 版本无关;按「主.次.修订」递增

ModInfo 的 id、name、version、authors、description 宿主都要求非空,任一为空会被拒绝注册。

第 2 步:路线一,改 build.bat

打开模组根目录的 build.bat,只改顶部这 5 行:

bat
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。
第 3 步:跑构建,确认产物

在模组根目录执行(不是游戏根):

bat
build.bat

脚本固定做四件事:

  1. javac --release 17 -encoding UTF-8 -cp "mod-api-*.jar;gson-*.jar" 把 src\main\java 编译到 target\classes;
  2. 把 src\main\resources 整个复制进 target\classes,所以资源在 jar 里的路径就是 assets\...;
  3. 写 target\manifest.mf,内容来自上一步那 5 个变量;
  4. jar cfm "target\<MOD_ID>-<MOD_VERSION>.jar" "target\manifest.mf" -C "target\classes" .

成功时发布物只有一个:

text
target\hello_mod-0.1.0.jar

target\classes\、target\manifest.mf、target\srcfiles.txt 都是中间产物,不要发出去。

第 4 步:路线二,Maven

pom.xml 与 build.bat 二选一。先把 API jar 安装进本地仓库(只需一次),在模组根执行:

bat
mvn install:install-file -Dfile=mod-api-3.2.jar -DgroupId=com.seecmd -DartifactId=mod-api -Dversion=3.2 -Dpackaging=jar

然后:

bat
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:

xml
<dependency>
    <groupId>com.seecmd</groupId>
    <artifactId>mod-api</artifactId>
    <version>3.2</version>
    <scope>provided</scope>
</dependency>

provided 的含义是「编译期可见、运行期由宿主提供」,也就是「不拷进 jar」的开关。gson 同理。

第 5 步:检查 jar 里到底有什么

列出条目:

bat
jar tf target\hello_mod-0.1.0.jar

期望看到三类,且都在 jar 根:

text
META-INF/MANIFEST.MF
com/example/hellomod/HelloMod.class
assets/hello_mod/lang/zh.json
assets/hello_mod/textures/note.png

不应该看到:

text
com/seecmd/mod/api/...
com/google/gson/...

出现后者说明 API 或 gson 被打进了 jar。

再直接看 manifest:

bat
jar xf target\hello_mod-0.1.0.jar META-INF/MANIFEST.MF
type META-INF\MANIFEST.MF

期望内容:

text
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。
第 6 步:声明版本与依赖

ModInfo 的 version 是模组自己的版本,建议按语义递增:

  • 主版本:有不兼容改动才加,玩家可能需要删旧配置;
  • 次版本:兼容地新增功能;
  • 修订:只修 bug。

模组之间的依赖写在 info() 里,不在 manifest:

java
@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;
  • 只影响本模组,其它模组照常加载。

只想要顺序、不强制目标存在时,用软依赖:

java
@Override
public ModLoadOrder loadOrder() {
    return new ModLoadOrder(LoadStage.INIT, List.of("core_lib"), List.of());
}

目标不存在时这条约束自动失效,不会报错。需要「目标一定在,而且先跑完」时,dependsOn 与 loadAfter 一起写。

第 7 步:安装给别人

把 jar 复制到游戏根目录的 mods\:

text
<游戏根>\
├── mods\
│   └── hello_mod-0.1.0.jar
└── 游戏本体...

加载器只扫描 mods\ 的直接子文件,不递归子目录。新放入的 jar 默认未启用:第一次启动看到 REGISTERED 是正常的,在模组菜单里启用后重启,状态才变 ACTIVE。

状态含义该做什么
ACTIVE已加载并激活可以发布
REGISTERED已注册但未启用在模组菜单启用,重启
LOAD_FAILED启用但加载失败看失败原因:API 版本 / 入口类 / 依赖 / 生命周期异常

发布说明至少写清四件事:适用 API 主版本、依赖哪些模组 id、jar 放在 mods\ 根目录、启用后要重启。

原理:manifest、类加载器与版本校验

为什么不能把 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 的主属性:

  1. 没有 manifest:跳过;
  2. 没有 Seecmd-Mod-Class:若声明了任何 Seecmd-* 字段,告警「补全 manifest」;否则当普通 jar 静默跳过;
  3. 有入口类:先校验 Seecmd-Api-Version,再建类加载器,用 Class.forName 加载入口类;
  4. 校验它是 Mod 的子类型,再用无参构造器实例化;
  5. 从 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>\)仍能加载

接下来