Files
agentmc/scaffold/java/VERSION-DIFFS.md
JianFeeeee d0fb5517a0 修复 gen_1_21 渲染:1.21 的 gameLoadFinished 门禁(第 13 处世代差异)
症状:gen_1_21 启动成功、数据注入成功(loadedChunks=49),
      但 chunksRendered=0、画面停在 Mojang 加载屏(distinctColors≈199)。

根因(1.21 GameRenderer.render 内部,编译期看不出):
    boolean ok = minecraft.isGameLoadFinished();
    if (ok && renderLevel && level != null) renderLevel(...);
  该标志只在 reloadResourcePacks 回调(onResourceLoadFinished)里置 true。
  我们不跑异步 reload(会死锁)⇒ 恒 false ⇒ 世界永不渲染。
  1.20.x 无此门禁 ⇒ gen_1_20 从未受影响。

修法:gen_1_21 的 McfoldBridge 加 markGameLoadFinished(反射置位),
      在 boot 里 shader 编译后调用 —— 差异留在 Java 侧。

★ 教训:这类差异不崩不报错,只是画面不对 ⇒ 判据必须用
  chunksRendered/distinctColors,不能只看进程活着。

顺带修:tick 异常不再静默吞掉(try{tick()}catch(ignore){} → 打印第一帧异常)
新增诊断字段:screenNow/overlayNow/levelNow/playerNow/noRender

实测:gen_1_21 现在 chunksRendered=25、has_world_content=True
      (草地/天空/太阳/HUD 正常)
2026-09-25 19:45:24 +08:00

114 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 世代差异清单(实测,非推断)
来源:官方 `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_<ver>/` 建目录,拷上一代源码
2. 按上表改差异点(对着 **javap 输出**改,不要凭印象写 API 名)
3. `mcfold/backends/gen_<ver>.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`」,不能只看「进程活着」。