版本、依赖与冲突
Copper 强化了依赖与 Mixin 的版本控制:所有版本都是”语义化版本(SemVer)+ 可回退的字符串版本”的双轨制,配合一套完整的版本过滤器语法,可以精确表达”我需要什么版本”。
Copper 的版本控制采用”语义化版本 + 字符串版本”的双轨制:能解析为语义化版本的就按语义比较,解析不了就按字符串精确匹配。
为什么是 SemVer
Section titled “为什么是 SemVer”SemVer 是从 npm 生态舶来并改造的约定。它有一个特别的用意:让被依赖的模组按 SemVer 规范自己的版本号,从而让使用方实现最大程度的自动化。例如一个库模组在只增加新功能、没有破坏性改变时升 minor 版本,那么依赖方写出 >=1.0.0 <2.0.0 或 ^1.0.0 这样的范围,就能自动兼容该库的所有向后兼容版本,无需逐个版本确认。反之,如果大家随意起版本号,使用方就只能退回到精确匹配,维护成本陡增。这套约定同样作用于游戏版本与加载器版本,让”这个模组能跑在哪些环境上”变成一条机器可判定的声明。
模组元数据中的 version 字段、游戏版本、加载器版本都是 SemVer,形如 X.Y.Z,由三个数字分量组成:
| 分量 | 名称 | 语义 | 什么时候升 |
|---|---|---|---|
X | major(主版本) | 破坏性变更 | 不兼容的 API 改动、内容彻底重做 |
Y | minor(次版本) | 向后兼容的新功能 | 新增内容/功能,旧用法仍然有效 |
Z | patch(修订号) | 向后兼容的缺陷修复 | 修 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 表达式,按字符串精确匹配}版本过滤条件写法
Section titled “版本过滤条件写法”在 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}SemVer 过滤器表达式语法
Section titled “SemVer 过滤器表达式语法”字符串形式的过滤条件若能被解析,就是一套类 npm semver 的表达式:
精确匹配与通配符
Section titled “精确匹配与通配符”| 语法 | 示例 | 含义 |
|---|---|---|
| 精确 | 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 | 小于 / 小于等于 |
范围与逻辑组合
Section titled “范围与逻辑组合”| 语法 | 示例 | 含义 |
|---|---|---|
^(脱字符) | ^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 自动识别(见下文) |
loader | Copper 加载器 | 加载器自身版本 |
"dependencies": { "mindustry": ">=8.159", "loader": ">=0.1.0"}游戏版本号是如何生成的
Section titled “游戏版本号是如何生成的”游戏版本由加载器根据游戏 Jar 自动识别,不同发布类型的版本号格式不同:
| 游戏类型 | 版本号形式 | 示例 |
|---|---|---|
| 正式版(Release) | <主版本>.<大版本>.<构建号> | 8.159.7 |
| Bleeding-Edge(BE) | <主版本>.0.<构建号> | 8.0.27179 |
| 自定义构建 | <主版本> | 8 |
加载器支持的游戏版本
Section titled “加载器支持的游戏版本”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 <本模组> : <加载器版本> |
依赖还会带来类可见性:依赖方可以访问被依赖模组导出的类(见类隔离机制)。加载顺序也由依赖关系决定:被依赖的模组先加载。
依赖加载顺序与环检测
Section titled “依赖加载顺序与环检测”- 加载器对依赖图做拓扑排序,依赖者始终在被依赖者之后初始化;
- 如果出现循环依赖(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 <本模组> : <目标> <版本>
- 目标模组不存在,不冲突,正常加载;
- 目标存在但版本不匹配,不冲突。
与依赖的关系
Section titled “与依赖的关系”冲突与依赖是两条独立的声明。同一个模组 id 可以同时出现在 dependencies 和 conflicts 中,二者分别判定。
在 Mixin 配置中使用版本过滤器
Section titled “在 Mixin 配置中使用版本过滤器”版本过滤器的同一套语法也用于 Mixin 配置,用于按目标版本选择要应用的 Mixin 类:
{ "version": 1, "config": { "mixins": { "*": ["CommonPatch"], "<8.0.27179": ["BackportPatch"], ">=8.159": ["ModernPatch"] } }}版本控制最佳实践
Section titled “版本控制最佳实践”- 严格遵守 SemVer:
major.minor.patch。破坏性变更升 major,加功能升 minor,修 bug 升 patch; - 声明所有依赖:包括
mindustry与loader,明确支持的版本范围; - 用
>=而非精确=:给玩家留出升级空间,除非你确实需要精确版本; - 善用
conflicts:与已知不兼容的模组/版本显式冲突,避免玩家遇到玄学问题; - Mixin 针对版本分支:对游戏内部实现有差异的版本,用版本过滤器区分(见 Mixin 配置文档)。