跳转到内容

版本、依赖与冲突

Copper 强化了依赖与 Mixin 的版本控制:所有版本都是”语义化版本(SemVer)+ 可回退的字符串版本”的双轨制,配合一套完整的版本过滤器语法,可以精确表达”我需要什么版本”。

Copper 的版本控制采用”语义化版本 + 字符串版本”的双轨制:能解析为语义化版本的就按语义比较,解析不了就按字符串精确匹配。

SemVer 是从 npm 生态舶来并改造的约定。它有一个特别的用意:让被依赖的模组按 SemVer 规范自己的版本号,从而让使用方实现最大程度的自动化。例如一个库模组在只增加新功能、没有破坏性改变时升 minor 版本,那么依赖方写出 >=1.0.0 <2.0.0 或 ^1.0.0 这样的范围,就能自动兼容该库的所有向后兼容版本,无需逐个版本确认。反之,如果大家随意起版本号,使用方就只能退回到精确匹配,维护成本陡增。这套约定同样作用于游戏版本与加载器版本,让”这个模组能跑在哪些环境上”变成一条机器可判定的声明。

模组元数据中的 version 字段、游戏版本、加载器版本都是 SemVer,形如 X.Y.Z,由三个数字分量组成:

分量名称语义什么时候升
Xmajor(主版本)破坏性变更不兼容的 API 改动、内容彻底重做
Yminor(次版本)向后兼容的新功能新增内容/功能,旧用法仍然有效
Zpatch(修订号)向后兼容的缺陷修复修 bug、性能优化,行为不变

例如模组从 1.2.0 升到 1.3.0,意味着它加了新东西但没破坏任何旧用法——依赖方写 >=1.2.0 <2.0.0 就能继续用;而升到 2.0.0 则意味着可能有破坏性变化,依赖方需要重新确认兼容性。

  • 解析规则:按点拆分,缺省分量补 0("1.2" 解析为 1.2.0,"1" 解析为 1.0.0);
  • 每个分量必须是整数,否则解析失败;
  • 比较:逐分量依次比较(major、minor、patch),先比 major,相同再比 minor,以此类推。
"version": "1.0.0"

以实际环境为例:游戏正式版 8.159.7 表示主版本 8、次版本 159、修订号 7;BE 版本 8.0.27179 的次版本恒为 0、修订号为构建号。版本过滤表达式(见下文)正是按这三个分量逐级比较的。

当版本无法解析为 SemVer 时(例如原版 Mindustry 模组的 "version": "1.0" 可以解析,但形如 "v1.0-beta" 之类的不行),Copper 自动回退为字符串版本:按精确字符串参与匹配。

什么时候会用到字符串版本?

  • 读取原版 Mindustry 模组(mod.json / mod.hjson)时,如果其版本号不是合法的 SemVer;
  • 在依赖/冲突/Mixin 配置中,你写的过滤条件如果无法被解析为 SemVer 表达式,也会回退为字符串精确匹配。
// 原版模组 version 字段不是合法 SemVer 时的兜底
"dependencies": {
"mindustry:legacy-mod": "1.0-beta2" // 无法解析为 SemVer 表达式,按字符串精确匹配
}

在 dependencies、conflicts、以及 Mixin 配置的版本键中,同一个字段的值可以是字符串或字符串数组:

形式示例行为
单个字符串">=1.0.0"先尝试解析为 SemVer 表达式;失败则回退为字符串精确匹配
字符串数组["3.2.1", "4.0.0"]总是精确匹配列表(字符串版本过滤器)
"dependencies": {
"some:mod": ">=1.0.0", // SemVer 表达式
"other:mod": ["3.2.1", "4.0.0"] // 精确匹配:3.2.1 或 4.0.0
}

字符串形式的过滤条件若能被解析,就是一套类 npm semver 的表达式:

语法示例含义
精确1.0.0恰好等于 1.0.0
通配符1.*.0、1.x.0* 或 x(不区分大小写)匹配该分量的任意数字
语法示例含义
==1.0.0等于
!=!=1.0.0不等于
> / >=>1.0.0、>=1.0.0大于 / 大于等于
< / <=<2.0.0、<=2.0.0小于 / 小于等于
语法示例含义
^(脱字符)^1.2.3允许不修改最左非零数字的升级(semver 约定,^1.2.3 等价于 >=1.2.3 <2.0.0)
~(波浪号)~1.2.3允许同 minor 内的 patch 级变更(>=1.2.3 <1.3.0)
区间1.0.0 - 2.0.0等价 >=1.0.0 && <=2.0.0
与>=1.0.0 && <2.0.0两个条件都满足
或1.0.0 || 1.2.0任一条件满足
分组(>=1.0.0 && <2.0.0) || 3.0.0括号控制优先级
隐式与>=1.0.0 <2.0.0相邻条件间省略运算符视为 &&
"dependencies": {
"lib:a": ">=1.0.0 <2.0.0", // [1.0.0, 2.0.0)
"lib:b": "^1.2.3", // >=1.2.3 <2.0.0
"lib:c": "~1.2.3", // >=1.2.3 <1.3.0
"lib:d": "1.0.0 - 2.0.0", // [1.0.0, 2.0.0]
"lib:e": "1.x", // 1.x 任意
"lib:f": "(>=1.0.0 && <1.5.0) || 2.0.0"
}
  • 空字符串 / 缺失:视为”任意版本”(总是匹配);
  • *:在 SemVer 表达式中等价于”任意版本”。

在 dependencies 与 conflicts 中,有两个特殊 id:

id匹配对象版本来源
mindustry游戏本体从游戏 Jar 自动识别(见下文)
loaderCopper 加载器加载器自身版本
"dependencies": {
"mindustry": ">=8.159",
"loader": ">=0.1.0"
}

游戏版本由加载器根据游戏 Jar 自动识别,不同发布类型的版本号格式不同:

游戏类型版本号形式示例
正式版(Release)<主版本>.<大版本>.<构建号>8.159.7
Bleeding-Edge(BE)<主版本>.0.<构建号>8.0.27179
自定义构建<主版本>8

Copper 只支持官方发行版 146.0 及以上与 BE 构建号 24369 及以上(两套编号各自独立,互不可比)。这个范围写在核心模组的 mindustry 依赖里,所以在更低的游戏版本上启动会在依赖检查阶段直接失败:

game version is rejected by copper:core : <游戏版本>

同一份范围还以机器可读的形式发布在 loader 仓库根目录的 support.json:键是加载器版本范围、值是游戏版本范围。它供启动器判断「某个加载器版本能不能配这个游戏版本」,加载器自己并不读它。

dependencies 声明本模组依赖的其他模组及版本要求。解析发生在启动阶段,不满足即报错:

情况报错
依赖模组未安装failed to find dependency for <本模组> : <依赖>
依赖版本不满足dependency is not supported by <本模组> : <依赖> <版本>
游戏版本不满足game version is rejected by <本模组> : <游戏版本>
加载器版本不满足loader version is rejected by <本模组> : <加载器版本>

依赖还会带来类可见性:依赖方可以访问被依赖模组导出的类(见类隔离机制)。加载顺序也由依赖关系决定:被依赖的模组先加载。

  • 加载器对依赖图做拓扑排序,依赖者始终在被依赖者之后初始化;
  • 如果出现循环依赖(A 依赖 B、B 依赖 A),启动报错:
    one or more rings were found in mod dependency path, related mods: A B

conflicts 是 Copper 新增的显式冲突机制:声明本模组与哪些模组(的哪些版本)不兼容。

"conflicts": {
"bad:mod": "*", // 与 bad:mod 的任何版本冲突
"old:mod": "<2.0.0" // 与 old:mod 的 2.0.0 以下版本冲突
}
  • 目标模组存在且版本匹配过滤条件,加载被拒绝:
    mod is conflict with <本模组> : <目标> <版本>
  • 目标模组不存在,不冲突,正常加载;
  • 目标存在但版本不匹配,不冲突。

冲突与依赖是两条独立的声明。同一个模组 id 可以同时出现在 dependencies 和 conflicts 中,二者分别判定。

版本过滤器的同一套语法也用于 Mixin 配置,用于按目标版本选择要应用的 Mixin 类:

{
"version": 1,
"config": {
"mixins": {
"*": ["CommonPatch"],
"<8.0.27179": ["BackportPatch"],
">=8.159": ["ModernPatch"]
}
}
}

详见Mixin 配置 JSON 详解。

  1. 严格遵守 SemVer:major.minor.patch。破坏性变更升 major,加功能升 minor,修 bug 升 patch;
  2. 声明所有依赖:包括 mindustry 与 loader,明确支持的版本范围;
  3. 用 >= 而非精确 =:给玩家留出升级空间,除非你确实需要精确版本;
  4. 善用 conflicts:与已知不兼容的模组/版本显式冲突,避免玩家遇到玄学问题;
  5. Mixin 针对版本分支:对游戏内部实现有差异的版本,用版本过滤器区分(见 Mixin 配置文档)。