Skip to content

Development

HoshinoYumeka edited this page Sep 9, 2026 · 6 revisions

从源码构建

首页使用手册

仓库结构、构建命令、版本号规矩、映射与许可。

一个分支,多个版本

这个仓库不按 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: falsenote 写明卡在何处)—— 未声明的缺失不可见。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/ 同样分层

docs/ 下是带 main() 的断言测试与附属接口文档的可编译副本,由每个目标编译并 运行。分层判据是被测对象住在哪一层,而非测试文件自身有无版本标记: ChatMessageCodecTest 自身一个标记也没有,但其被测类位于 1.20.5+ 层,1.20.1 不 挂载该层,自然找不到。

因此每层均可有自己的 docs/,按 layers 字段接入;仓库根的 docs/ 只留对全部目标 都成立的那几份。「该层有无内容」的判定亦计入 docs/

跨层的调用关系会产生覆盖真空。 若某个方法的调用方只在 A 层、而覆盖它的测试住 在 B 层,且没有目标同时挂载 A 与 B,则一边有调用方无测试、一边有测试无调用方。 补测试前先问一句「哪个目标会跑到它」。

shared/ 里的第三方模组 API

四条判据一条都不覆盖第三方模组的包,但也不能一刀切禁掉:Curios 的 top.theillusivec4.curios.api 在 1.20.1 与 1.21.1 上都在,禁掉会把真能共用的联动 代码赶出 shared/。「这个第三方 API 跨版本稳不稳定」是矩阵问题,只能逐条查证。

因此改为一张声明表 versions/third-party-apis.jsonallowed 中的包可以出现在 shared/,未列入的一律拦下,由 verifySharedThirdPartyImports 校验。新增一条之前须确认该包在矩阵中每一个 Minecraft 版本上都存在且签名一致,并把证据写进该条的 evidence

什么该进 shared/

判据只有一条:每个目标都能原样编译通过。矩阵中 Minecraft 最低一档为 1.20.1, 加载器有三种,因此 shared/ 中不得出现仅在单一目标上成立的构造。 ./gradlew build 中的 verifySharedIsTargetNeutral 校验四条:

判据 形态
加载器导入 net.neoforged / net.minecraftforge / net.fabricmc / cpw.mods
1.20.5+ 原版类型 StreamCodecByteBufCodecsCustomPacketPayloadDataComponent*
注入到原版类型上的方法 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.jsongradlejava 字段记录。各目标独立构建,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 一并提交。

加一个新目标

  1. platforms/ 下建立目录,放置该目标自身的 Gradle 工程与 wrapper;
  2. 在其 build.gradle 中先 apply from gradle/mcphone-layers.gradle(解析该目标挂载的层), 再接入 shared/ 与各层的源码目录,最后 apply from 共用校验;
  3. versions/targets.json 中新增一条,填写 minecraft / loader / java / gradle / project / layersbuildablefalse,推断的字段列入 unverified
  4. 目录一旦存在,CI 即开始编译该目标,构件名带「未验证」后缀;
  5. 构建稳定后将 buildabletrue 并清空 unverified —— 自此该目标的产物计入每次发版。

只声明而尚无工程目录的目标既不编也不发,不影响其余目标。

映射

Mojang 官方名 + Parchment,其许可见 https://github.com/NeoForged/NeoForm/blob/main/Mojang.md


许可

MIT

仓库根目录的 TEMPLATE_LICENSE.txt 另属一事:该文件为 NeoForged 对 MDK 模板文件的 MIT 声明,按其署名要求保留,与本模组自身的许可无关。


首页使用手册 | 上一页:服主须知

Clone this wiki locally