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

5.5 KiB
Raw Permalink Blame History

世代差异清单(实测,非推断)

来源:官方 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 反编译):

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」的原则):

// 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」,不能只看「进程活着」。