-
Notifications
You must be signed in to change notification settings - Fork 6
Development
仓库结构、构建命令、版本号规矩、映射与许可。
这个仓库不按 Minecraft 版本或加载器分分支。所有目标都在 main 上,一次发版
所有目标各出一个 jar,挂在同一个 Release 上。
按 Minecraft 版本或加载器分支时,非主力分支的更新依赖额外的「追平」提交,其时机 不受任何机制约束,老版本因而长期滞后。全部目标置于同一分支后,某一目标未跟上 在构建中直接暴露。
支持哪些目标写在 versions/targets.json:
| 目标 | Minecraft | 加载器 | 状态 |
|---|---|---|---|
1.20.1-forge |
1.20.1 | Forge | 工程已并入,尚不可构建 |
1.20.1-neoforge |
1.20.1 | NeoForge | 工程尚未建立 |
1.20.1-fabric |
1.20.1 | Fabric | 工程尚未建立 |
1.21.1-forge |
1.21.1 | Forge | 工程尚未建立 |
1.21.1-neoforge |
1.21.1 | NeoForge | 可构建,产物进 Release |
1.21.1-fabric |
1.21.1 | Fabric | 社区 PR 已有可跑实现,尚未并入 |
buildable 决定的是产物去向,不是编不编。 CI 按「工程目录是否存在」枚举要编的
目标,凡目录在就编 —— buildable: false 的目标同样编,构件名带「未验证」后缀,
但不进 Release。发版只取 buildable: true 的目标。
因此一个新目标在「工程已并入、尚未编过」这段时间里也受 CI 保护:编不过当场可见,
不必等它翻成 true。
矩阵是规则,不是清单。 每个 Minecraft 版本必须有 Forge / NeoForge / Fabric
三个条目,新增一个 Minecraft 版本即新增三条。缺失的目标同样要落在 targets.json 里
(buildable: false,note 写明卡在何处)—— 未声明的缺失不可见。CI 的
guard-version 校验这一条。
尚未构建成功的目标,java / gradle / loader_version 填的是推断值,与实测值在形式上
无从区分。推断的字段名列在该条目的 unverified 数组里。buildable 置为 true 时
必须清空 unverified —— 构建成功即意味着这些值已被验证,CI 拒绝两者并存。
shared/ 全部目标共用的代码与资源
layers/version/<层名>/ 某一批 Minecraft 版本的三个加载器共用
layers/loader/<层名>/ 某一加载器的各个 Minecraft 版本共用
layers/both/<层名>/ 某一加载器 × 某一批版本共用(交集层,目前尚无)
platforms/<目标名>/ 各目标自己的工程(含它自己的 gradle wrapper)
versions/targets.json 支持哪些目标,一处声明
versions/layers.json 有哪些共享层,一处声明
versions/datapack-renames.json 1.21 数据包目录改名表,Gradle 与 CI 共用
versions/third-party-apis.json shared/ 里允许出现哪些第三方模组的 API
gradle/ 各平台共用的构建校验
docs/ 带 main() 的断言测试与发版说明(各层另有自己的 docs/)
代码分块共用,按共用范围分层:
| 目录 | 谁用 | 允许什么 |
|---|---|---|
shared/ |
全部目标 | 与 Minecraft 版本、加载器都无关的代码 |
layers/version/<层名>/ |
挂了该层的目标 | 与加载器无关、只在某些 Minecraft 版本上成立的代码 |
layers/loader/<层名>/ |
挂了该层的目标 | 与 Minecraft 版本无关、只在某个加载器上成立的代码 |
layers/both/<层名>/ |
挂了该层的目标 | 该加载器 × 该批版本上成立的代码 |
platforms/<目标名>/ |
单个目标 | 目标专有的一切 |
中间各层的存在理由,以版本层为例:手写编解码与各类网络包只受 Minecraft 版本约束, 与加载器无关。若无此层,同一 Minecraft 版本的三个加载器目标将各持一份副本,而无 任何机制要求三份一致 —— 即按分支划分的同一问题缩小到目录之间,且失去「构建中直接 暴露」这一性质。
since 钉住版本(Minecraft ≥ 该版本的目标才挂),loader 钉住加载器(该加载器的
目标才挂)。层禁哪个轴,取决于哪个轴没被钉住:
| 写了什么 | 种类 | 谁共用 | 层中不得出现 |
|---|---|---|---|
只有 since
|
版本层 | 该批版本的三个加载器 | 绑定加载器的构造 |
只有 loader
|
加载器层 | 该加载器的各个版本 | 绑定版本的构造 |
| 两个都有 | 交集层 | 该加载器 × 该批版本 | —— (两个轴均已钉住) |
| 两个都没有 | —— | 全部目标 | 那是 shared/,不是层 |
前两种对称:各自容纳的正是对方挡下的那一半。交集层容纳既绑加载器又绑版本的代码 —— 它今天一个也没有,因为现有两个目标既不共用加载器也不共用版本档;规则先行放开, 目录待第一个占用者出现时再建。
版本层的层名为「该套 API 自哪个 Minecraft 版本起存在」,而非某一具体版本:
1.21.1 与后续更高版本同挂 1.20.5+,因其内容相同。层可组合,故应按单条边界
划分 —— 数据包目录名的复数改单数发生于 1.21,与 1.20.5+ 那条 codec/组件边界不是
同一条,因此另设 1.21+ 层。
层由配置文件声明,构建脚本不写死:versions/layers.json 定义有哪些层,
versions/targets.json 中每个目标的 layers 字段声明它挂哪几层。新增目标或新增层
只改 JSON,build.gradle 无须改动。
每层还须声明 populated,即该层当前是否应有内容:声明为有却扫不到文件时报错,
声明为无却扫到内容时同样报错。populated: false 的层在检出中可以完全没有目录
(git 不跟踪空目录),这是正常状态。
docs/ 下是带 main() 的断言测试与附属接口文档的可编译副本,由每个目标编译并
运行。分层判据是被测对象住在哪一层,而非测试文件自身有无版本标记:
ChatMessageCodecTest 自身一个标记也没有,但其被测类位于 1.20.5+ 层,1.20.1 不
挂载该层,自然找不到。
因此每层均可有自己的 docs/,按 layers 字段接入;仓库根的 docs/ 只留对全部目标
都成立的那几份。「该层有无内容」的判定亦计入 docs/。
跨层的调用关系会产生覆盖真空。 若某个方法的调用方只在 A 层、而覆盖它的测试住 在 B 层,且没有目标同时挂载 A 与 B,则一边有调用方无测试、一边有测试无调用方。 补测试前先问一句「哪个目标会跑到它」。
四条判据一条都不覆盖第三方模组的包,但也不能一刀切禁掉:Curios 的
top.theillusivec4.curios.api 在 1.20.1 与 1.21.1 上都在,禁掉会把真能共用的联动
代码赶出 shared/。「这个第三方 API 跨版本稳不稳定」是矩阵问题,只能逐条查证。
因此改为一张声明表
versions/third-party-apis.json:
allowed 中的包可以出现在 shared/,未列入的一律拦下,由
verifySharedThirdPartyImports 校验。新增一条之前须确认该包在矩阵中每一个
Minecraft 版本上都存在且签名一致,并把证据写进该条的 evidence。
判据只有一条:每个目标都能原样编译通过。矩阵中 Minecraft 最低一档为 1.20.1,
加载器有三种,因此 shared/ 中不得出现仅在单一目标上成立的构造。
./gradlew build 中的 verifySharedIsTargetNeutral 校验四条:
| 判据 | 形态 |
|---|---|
| 加载器导入 |
net.neoforged / net.minecraftforge / net.fabricmc / cpw.mods
|
| 1.20.5+ 原版类型 |
StreamCodec、ByteBufCodecs、CustomPacketPayload、DataComponent*
|
| 注入到原版类型上的方法 |
player.getData(...)(NeoForge)、getCapability(...)(Forge) |
ItemStack 的组件读写 |
stack.get(ModDataComponents...),1.20.5 起才有 |
第三、四条不以 import 出现:它们是加载器或高版本原版注入到原版类型上的方法,
仅检查导入语句无法发现。
这四条是替身判据。真判据为「以 1.20.1 的类路径编译 shared/」——
platforms/1.20.1-forge/ 已并入且由 CI 编译,该判据自此开始生效;
其 buildable 仍为 false,表示产物尚不进 Release。
platforms/<目标名>/ 下的每一个 java 文件都列在
shared/PLATFORM-SEAMS.md
中,分为两类。写新平台之前先读它:
| 类别 | 处理方式 |
|---|---|
| 有意的接缝 | 每个平台各实现一份,全限定名与签名相同,方法体各异 |
| 尚未迁移 | 不得重新实现,后续将并入 shared/ 或某个共享层 |
前者的典型是 PhonePlayerData:NeoForge 侧以 Data Attachment 存储,1.20.1 仅能退回
裸 NBT;其对应者 PhoneItemData 在本目标上使用数据组件,该机制 1.20.5 起才有。
清单中带 ★ 的条目为共用代码(shared/ 或某个共享层)直接引用者,其全限定名是
硬约束,新平台必须提供。无 ★ 的条目仅供平台内部使用。
清单按目标各成一段:每个平台工程一段,枚举该工程下的全部文件,而非仅被引用者。
引用来源为 shared/ 与该目标所挂的全部共享层 —— 两处若收窄,从未被引用的文件、
以及仅被共享层引用的文件都不会出现在任何一类中,而后者会在第二个挂载该层的目标
并入时才暴露为编译失败。
清单中的两段由构建生成:./gradlew updateSeamsDoc 重写,verifySeamsDocCurrent
校验其与源码一致。分类判据与上述闸共用同一份定义,二者的结论不会分歧。
将归入「有意的接缝」的类型移入 shared/ 会被单独拦下,updateSeamsDoc 同样拒绝执行 ——
若允许重写文档,该次误移将被文档追认,构建随之转绿。
拆分尚未完成。 平台目录中仍有大量应当共用的代码。上述判据可挡下一部分, 但对「方法名不变、签名变更」一类无效 —— 该类差异只有以另一版本实际编译才能发现, 而这正是把 1.20.1 目标并进来的用处。
构建在目标自身的工程目录下执行,不在仓库根 —— 仓库根不含 gradlew。
cd platforms/1.21.1-neoforge
./gradlew build # 产物在 platforms/1.21.1-neoforge/build/libs/
./gradlew runClient # 开发环境启动各目标的 Gradle 与 Java 版本允许不同,由 targets.json 的 gradle 与 java
字段记录。各目标独立构建,CI 逐个执行,无须合并为单一构建。
./gradlew build 附带执行下列校验,定义于 gradle/mcphone-checks.gradle,各平台共用一份。
带「每层一道」的两项按该目标挂载的层数各注册一道:
| 校验 | 内容 |
|---|---|
verifyServiceFiles |
META-INF/services 中声明的类均实际存在(该文件为纯文本,编译器不作检查) |
verifyDistIsolation |
服务端类未引用客户端类型 |
verifyJava17Compatible |
以 --release 17 重新编译一遍:1.20.1 目标运行于 Java 17 |
verifySharedIsTargetNeutral |
shared/ 中无仅在单一目标上成立的构造 |
verifySharedThirdPartyImports |
shared/ 引用的第三方模组 API 均在声明表内 |
verifyLayer<层名> |
层中无「未被该层钉住的那个轴」上的构造;交集层两轴都钉住,不禁(每层一道) |
verifyDatapackNames<层名> |
该层的数据包目录名与其 since 相符(每层一道) |
verifyPlatformDatapackNames |
平台自身资源的数据包目录名与该目标的 minecraft 相符 |
verifySeamsDocCurrent |
接缝清单与源码一致 |
assertTests |
执行仓库根 docs/ 与该目标所挂各层 docs/ 下全部带 main() 的断言测试 |
数据包目录名的改名表在 versions/datapack-renames.json,共 13 条。1.21(数据包格式 48)
将这些目录由复数改为单数,放错不会报错 —— 那条配方或进度只是不存在,构建与游戏
均无提示。CI 另有一道同源的检查,覆盖 buildable: false 的目标:那些目标不会被构建,
Gradle 侧的校验对其无效,而新目标并入初期正是手工搬运资源、最易出错的阶段。
mod_version 与模组身份信息(mod_id / mod_name / mod_license / mod_group_id)
仅存于仓库根的 gradle.properties,各平台不得重复定义。全部目标共用同一版本号:
同一版本号在不同目标上必须对应同一次改动。
该字段只由发版流程修改:Actions → Release → Run workflow,选择 patch / minor / major,
CI 计算新版本号、写入文件、以 github-actions[bot] 身份提交并打 tag,随后逐个目标构建,
将全部 jar 挂载于同一个 Release。
手动修改由 CI 的 guard-version 拦截,其判据为提交人身份:仅 bot 可改。
发版说明写入 docs/release-notes/next.md —— 版本号由 CI 在发版时计算,撰写时无从得知。
发版流程将该文件更名为 v<新版本>.md 一并提交。
- 在
platforms/下建立目录,放置该目标自身的 Gradle 工程与 wrapper; - 在其
build.gradle中先apply fromgradle/mcphone-layers.gradle(解析该目标挂载的层), 再接入shared/与各层的源码目录,最后apply from共用校验; - 在
versions/targets.json中新增一条,填写minecraft/loader/java/gradle/project/layers,buildable置false,推断的字段列入unverified; - 目录一旦存在,CI 即开始编译该目标,构件名带「未验证」后缀;
- 构建稳定后将
buildable置true并清空unverified—— 自此该目标的产物计入每次发版。
只声明而尚无工程目录的目标既不编也不发,不影响其余目标。
Mojang 官方名 + Parchment,其许可见 https://github.com/NeoForged/NeoForm/blob/main/Mojang.md。
MIT。
仓库根目录的 TEMPLATE_LICENSE.txt 另属一事:该文件为 NeoForged 对 MDK 模板文件的 MIT 声明,按其署名要求保留,与本模组自身的许可无关。