# 世代差异清单(实测,非推断) 来源:官方 `client_mappings.txt` 逐版本对比(1.20.1 / 1.20.2 / 1.21.1), 以及两侧源码对各自 client jar 的实际编译结果。 ## 判据 **`net.minecraft.client.DeltaTracker` 类是否存在** ⇒ 1.21+。 这是 1.21 引入的时间抽象,也正是 `GameRenderer.render` 签名变化的根源。 比硬编码版本号稳(官方改版本号不影响判据)。 实现:`mcfold/backends/__init__.py:detect_generation()` ## 差异全表(8 处,覆盖 21 个检查点中的 13 个「完全一致」之外的部分) | # | API | 1.20.x | 1.21+ | 处理 | |---|---|---|---|---| | 1 | `Minecraft.setLevel` | `(ClientLevel)` | `(ClientLevel, ReceivingLevelScreen$Reason)` | gen_1_21 传 `Reason.OTHER` | | 2 | `GameRenderer.render` | `(float,long,boolean)` | `(DeltaTracker, boolean)` | gen_1_21 用 `mc.getTimer()` | | 3 | 区块计数 | `countRenderedChunks()` | `countRenderedSections()` | 各世代用各自名字 | | 4 | 同理 | `hasRenderedAllChunks()` | `hasRenderedAllSections()` | 同上 | | 5 | `ServerPacksSource.createPackRepository` | `(Path)` | `(Path, DirectoryValidator)` | gen_1_21 构造 validator | | 6 | `ResourceLocation` 构造 | `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | 静态工厂 | | 7 | `SharedConstants.enableDataFixerOptimizations` | 有 | **已移除** | gen_1_21 不调 | | 8 | `User` 构造 | `(String,String sessionId,...)` | `(String,UUID,...)` | 传 UUID | | 9 | `ChunkStatus` | `world.level.chunk.ChunkStatus` | `world.level.chunk.status.ChunkStatus` | 包路径变了 | | 10 | `ClientPacketListener` 构造 | `(Minecraft,Screen,Connection,ServerData,GameProfile,Telemetry)` | `(Minecraft,Connection,CommonListenerCookie)` | ★ 参数个数与语义都变 | | 11 | `GameRenderer.loadEffect` | package-private(同包可调) | **private**(同包也调不到) | gen_1_21 用反射 | ★ 关键结论:**差异集中在 11 处,而不是「每个版本一套」**。 21 个检查点里 13 个(如 `tick`/`allChanged`/`replaceWithPacketData`/ `updateViewCenter`/`getChunkSource`/`PackRepository.*`)跨世代**完全一致**。 ## 为什么必须分后端(而不是一个文件里 switch) Java 编译期绑定签名。实测: ``` 世代 B 的调用对 1.20.1 jar 编译: error: cannot find symbol class DeltaTracker ``` `DeltaTracker` 在 1.20.x 的 jar 里不存在 ⇒ 两代的调用**无法共存于一份源码**。 所以每个世代独立编译,Python 层做分派。 ## 加新世代的做法(用户 2026-09-25 定向) 1. `scaffold/java/gen_/` 建目录,拷上一代源码 2. 按上表改差异点(对着 **javap 输出**改,不要凭印象写 API 名) 3. `mcfold/backends/gen_.py` 声明 `markers`(判据类) 4. `mcfold/backends/__init__.py` 的 `GENERATIONS` 加一条 5. 各自对**对应版本**的 named jar 编译 ⚠️ 教训(本轮踩过):`Reason.LEVEL_LOAD`、`DirectoryValidator.NONE` 都是凭印象写出来的、实际不存在的 API;`loadEffect` 的可见性也没验证。 **改之前先 `javap`。** ## 补充:1.21.1 实际运行验证(比编译更强的证据) `gen_1_21` 已实测启动成功(stdout 收到 `{"event":"ready"}`)。 运行期又暴露 1 处「编译期看不出」的差异: | # | API | 1.20.x | 1.21+ | 处理 | |---|---|---|---|---| | 12 | `ClientPacketListener.registryAccess` | 可变 `LayeredRegistryAccess`,可事后反射替换 | **`final RegistryAccess$Frozen`**,反射抛 `Can not set final field` | gen_1_21 改为**先载注册表 → 经 `CommonListenerCookie.receivedRegistries` 传入 → 再构造 listener** | ★ 这条是本轮最有价值的发现:1.21 的 cookie 机制让注册表注入**比 1.20 的反射更干净**。 两代的启动顺序因此不同: ``` 1.20.x: 构造 listener → 反射替换 registryAccess 字段 1.21.x: 先加载注册表 → 构造 cookie(含注册表) → 构造 listener(final 字段一次到位) ``` ## provision 的实测发现 - natives 在 1.21.1 是**独立库条目**(`name` 含 `:natives-linux`), 不走老版 `downloads.classifiers` 结构 ⇒ 两种形态都要认。 - 跳过音频资源可省 **3744/3911 条**(仅 167 条实际下载), 实测 1.21.1 全量 274 文件 / 200MB / 32s,0 失败。 ## 补充 2:1.21 的 gameLoadFinished 门禁(第 13 处,编译期看不出) gen_1_21 曾经「启动成功但渲染 0 区块、画面停在 Mojang 加载屏」。 根因在 `GameRenderer.render(DeltaTracker, boolean)` 内部(1.21 反编译): ```java boolean ok = this.minecraft.isGameLoadFinished(); if (ok && renderLevel && this.minecraft.level != null) { this.renderLevel(deltaTracker); // ← 只有 ok 才渲染世界 } ``` 而该标志只在 **`reloadResourcePacks()` 的回调**里被置 true (`Minecraft.onResourceLoadFinished` 第 628 行)。 ⇒ 我们不跑异步 reload(会死锁,见前文)⇒ 该位恒 false ⇒ 世界永不渲染。 **1.20.x 没有这个门禁**,所以 gen_1_20 一直正常。 修法(在 Java 侧,符合「差异留在 Java」的原则): ```java // gen_1_21/net/minecraft/client/renderer/McfoldBridge.java public static void markGameLoadFinished(Minecraft mc) { Field f = Minecraft.class.getDeclaredField("gameLoadFinished"); f.setAccessible(true); f.setBoolean(mc, true); } ``` ★ 这类差异**只在上层失败**,不会崩、不会报错,只是画面停在加载屏 —— 所以判据要用「`chunksRendered` / `distinctColors`」,不能只看「进程活着」。