8 Commits

Author SHA1 Message Date
547f4beb7e M1+M2:1.21.1 构建跑通 + 协议客户端进 play 阶段
M1(构建):
- build.sh 编译 CP 加入 $OUT/fabric/*.jar,解决 net.fabricmc.* 找不到
- gen_1_21 内联 Sodium 通知改为委托共用 RenderCompat,补 reportRenderCompat
- 1.21.1 全量七步构建产物齐备(classes-intermediary.jar / names*.json / srg / inheritance.map)

M2(协议):
- login_start 按协议数据发 UUID(1.21 必填;1.20 可选)
- login_acknowledged / serverbound 包 id 改为查表,不再写死 0x00
- send() 实现压缩帧格式(varint(dataLen)+数据,小包不压)
- configuration 双向协商:发 settings、回 select_known_packs、拒 resource_pack
- protodef 补 bitflags / entityMetadataLoop / count(ctx引用) / compareTo 路径表达式
- NBT 读取器由占位符改为真实现(anonymousNbt 支撑 registry_data)
- 坏包容错:长度前缀是帧层权威,解不开整包丢弃并布告,不断流

实测:login→configuration(11 注册表)→play,位置 (5.5,-60.0,-0.5),map_chunk×9
判据:protodef 6→13 项(含线上真实 entity_metadata 字节回归);全量 77 项通过
2026-09-26 12:15:48 +08:00
c3a57256ac McfoldBridge 改反射 + 新增 Sodium tracker 通知与 hideGui
三件事,其中后两件是实测查出来的。

## ① McfoldBridge:「同包直调」在 intermediary 下失效 ⇒ 改反射

原来把桥类放进 `net.minecraft.client.renderer` 包,靠**同包**访问
GameRenderer 的 package-private 方法。这个技巧只在 **named** 下成立:

    named        桥与 GameRenderer 同在 net.minecraft.client.renderer  ⇒ 同包 ✅
    intermediary GameRenderer → net.minecraft.class_757(包变成 net.minecraft)
                 桥仍在 net.minecraft.client.renderer                ⇒ 异包 ❌
    ⇒ IllegalAccessError: McfoldBridge tried to access method
       'void net.minecraft.class_757.method_34538(...)'

★ **包名本身也被 remap**,所以「靠包名对齐」切换命名空间就断了。
  反射 + 语义名(McfoldNames)与命名空间无关,是唯一两态都通的形态。
  (gen_1_21 本来就为 loadEffect 用了反射,现在两处统一。)

## ② Sodium 通知:装载地形渲染的 mod 现在能看见了(根因三层,逐层实测)

**症状**:装了 Sodium 后画面只有天空、chunks=0,而 blockinfo 能正确读出
方块 ⇒ 数据在,只是它不知道。

**三层根因(每层都有实测数据)**:

┌ ① mixin 永不命中
│   Sodium 挂在 replaceWithPacketData 内 @At(INVOKE onChunkLoaded, AFTER)
│   而 1.20.1 该方法**内部不调** onChunkLoaded(在 getChunk/drop 里各有一处)
│   ⇒ 它的 ChunkTracker 从未被告知任何区块
│   【修】notifyRenderTracker() 补通知
│
├ ② 反射签名错
│   ChunkTrackerHolder.get 的参数是 **ClientLevel** 不是 Level
│   第三参数是**位掩码 int** 不是 ChunkStatus 对象(签名 (III)V)
│
└ ③ ★ status 值错(真正的那道坎)
    Sodium 的语义:
        FLAG_HAS_BLOCK_DATA = 1   FLAG_HAS_LIGHT_DATA = 2   FLAG_ALL = 3
    而 ChunkTracker.updateMerged 的 2×2 邻域判定是
        `四邻 status 按位与 == 3`   (字节码 :54 iand / :71 iconst_3)
    ⇒ 传 1:1&1&1&1=1 ≠ 3 ⇒ 全部判成"凑不齐 2×2"
            ⇒ chunkReady 恒 0、loadQueue 恒空、画面无地形
      传 3:3&3&3&3=3 == 3 ⇒ 25 个 2×2 组就绪 ⇒ 重建队列有 25 项
    (我们确实有光照数据:注入时跑了 light.runLightUpdates())

**实测对比**(同一场景 7×7=49 区块):
    传 1:  chunkStatus=49  chunkReady=0   loadQueue=0   colors=4748
    传 3:  chunkStatus=49  chunkReady=25  loadQueue=25  colors=113546

★ 走反射 + 类不存在时静默跳过 ⇒ 不装 Sodium 时零影响。
  这不是"给某个 mod 打补丁":注入路径缺了"区块已就绪"广播,
  补上它对所有依赖该事件的 mod 都有效。

**同时广播官方的 level.onChunkLoaded(pos)** —— 与网络包路径等价的那一步。

## ③ hideGui:截图场景关掉 HUD

走**官方选项** `options.hideGui`(= 游戏里 F1),不改渲染代码 ——
GameRenderer.render 里就是 `if (!options.hideGui) gui.render(...)`。
为什么要关:截图只关心画面,HUD 是噪声,而且它给 distinctColors
掺一大块固定像素(实测污染判据)。

两个世代后端都加了 hideGui;Python 侧 set_view(hide_gui=...)。
2026-09-26 10:17:48 +08:00
35534b1564 feat: get_block_info —— 方块信息查询(Java 侧回答,Python 只路由)
新增 cmd=blockinfo(两代后端编译通过、实测):
  返回 block 名(含 mod 方块)、stateId、属性、isAir/isSolid、
  光照/硬度、贴图缺失的程序化判据、是否来自 mod。

★ 修掉一个真实缺陷(附录 D 的语义区分落到了实处):
  未加载区块原先返回 void_air(known=true),实测发现两个 API 都不可用:
    - ClientLevel.hasChunk()      反编译实测**恒返回 true**
    - ChunkSource.hasChunk()      走 getChunk(...,false),而 ClientChunkCache
      覆写后在该参数下返回 emptyChunk(占位,非 null)⇒ 也恒 true
  正解:getChunk(..., requireChunk=**false**) 判 null
    (反编译:return requireChunk ? emptyChunk : null;)
  ⇒ 现在未加载明确回 known=false + reason=chunk_not_loaded

实测(1.20.1):
  草地 → minecraft:grass_block props={'snowy':'false'} missing=False
  原木 → minecraft:oak_log       props={'axis':'y'}
  空气 → minecraft:air
  远处 → known=false reason=chunk_not_loaded        ← 与空气区分

实测(1.21.1 + mod):
  mts:mtsofficialpack.fancydirt_block   mod=True tex=mtsofficialpack:block/fancydirt
  createbigcannons:steel_block          mod=True tex=createbigcannons:block/steel_block
  ⇒ mod 方块同样能查到正确贴图名与 fromMod 标记

顺带修 gen_1_21 两处世代差异(ChunkStatus 换包、bridge 编译顺序)。
测试:69 项全绿(新增 test_get_block_info 6 条判据)
2026-09-26 00:24:29 +08:00
6827f4d0e1 实测 Fabric 客户端可启动 + 加载器后端设计文档
用户判断:mod 支持分 fabric/forge,且不同版本还有差异 ⇒ 各有独立后端。
实测支持此结论,且差异比方法签名更深一层。

实测成功(关键):
  Fabric Loader 0.19.5 完整启动 1.20.1 客户端
  'Loading Minecraft 1.20.1 with Fabric Loader 0.19.5' + Mixin 子系统 + 12 个资源图集
  ★ 必须剔除 1.20.1 自带的 asm 9.2/9.6,改用 Fabric 的 9.10.1
    (否则 Knot.<clinit> 抛 duplicate ASM classes)

架构结论:后端身份是 (加载器, API 世代) 二元组
  因为 Fabric/Forge 下**我们不拥有启动流程**(Loader 起游戏+跑主循环),
  与我们「不跑主循环+手动喂数据」的模型根本冲突。
  两条可行路线已写入 scaffold/LOADERS.md(A: 作为 mod 被加载;
  B: 只借 Fabric 的注册表与资源包,保留我们的渲染循环)——均待实测。

已实现(零加载器,两个 vanilla 后端编译通过、待实测):
  cmd=mods:Java 侧自己扫 jar/注册占位方块/挂资源包
  覆盖:外观 ✅ state id ✅ 变体 ⚠️ 行为 ❌ Iris ❌
2026-09-25 19:55:13 +08:00
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
16f2e04fff 架构重构:差异全部下移到 Java,Python 只做路由
用户口径:差异放在 Java 层,Python 相当于路由。

下移到 Java(新增 cmd=setblocks):
  Python 只发 RLE([[stateId,count],...]),Java 用官方 API 完成
  调色板组装 / 位打包 / 官方序列化 / 入 cache / 渲染。

★ 意外收益:走 ClientboundLevelChunkPacketData(chunk).getReadBuffer()
  + replaceWithPacketData(...) 这条官方路径,两代签名完全一致
  ⇒ 原先的 Storage.chunks 反射注入**整段删掉**。

新增 cmd=capacity:世界尺寸(sectionsCount/minSection/defaultBiome)
  由运行时回答,Python 不再硬编码 24/-4(1.16 时代是 16/0)。

Python 侧删除(防回归,见 test_import_surface):
  render_bridge.py(旧桥,无世代概念)
  render._pack/_long_array/_encode_paletted/_encode_chunk/_varint
  anvil.encode_block_section/encode_chunk_payload/save_chunk_to_payload
  合计 454→374 行(render.py)、300→177 行(anvil.py)

保留在 Python(都是「不随版本变」的知识):
  RLE 压缩(rle_encode/rle_decode,具名可测)
  存档读取(region/NBT)
  方块名→id 的路由(查询在 Java)
  NDJSON 协议

测试:62 项单元 + 6 项渲染联测全绿
  新测试反映新架构:RLE 边界、运行时容量、世代分派、新路径渲染
  test_anvil 里原有 8 项协议位打包测试已删(那已下移到 Java,
  由两代后端各自的编译验证覆盖)

新增 scaffold/ARCHITECTURE.md(分层原则 + 加新世代步骤 + 防回归清单)
2026-09-25 19:21:45 +08:00
253146cffe gen_1_21 实测启动成功 + provision 工具
决定性验证:gen_1_21 后端在 1.21.1 上收到 {"event":"ready"}
  ⇒ 世代分派架构成立:同一套 Python 层,两个独立编译的 Java 后端

运行时发现第 12 处世代差异(编译期看不出):
  1.20.x  registryAccess 是可变 LayeredRegistryAccess → 事后反射替换
  1.21.x  registryAccess 是 final RegistryAccess$Frozen → 反射抛
          "Can not set final field"
  处理:gen_1_21 改为先载注册表 → 经 CommonListenerCookie.receivedRegistries
        传入 → 再构造 listener。比 1.20 的反射做法更干净。

新增 mcfold/provision.py(从官方 manifest 装任意版本,版本无关):
  - 跳过音频资源:1.21.1 实测 3911 条里跳过 3744 条
  - natives 两种形态都认(1.21 起是独立库条目,非 classifiers)
  - 实测 1.21.1:274 文件 / 200MB / 32s / 0 失败
2026-09-25 18:35:28 +08:00
8c7d5b9d38 世代分派体系:Python 层 switch + 各世代独立编译的 Java 后端
用户定向:在 python 层写 switch,调用不同 java 后端(而非一个文件里 switch)。

新增:
  mcfold/backends/{__init__,base,gen_1_20,gen_1_21}.py  后端体系与分派
  scaffold/java/gen_1_20/  对 1.20.x client jar 编译
  scaffold/java/gen_1_21/  对 1.21.1 client jar 编译(✅ 实测编译通过)
  scaffold/java/VERSION-DIFFS.md  实测差异全表

判据:DeltaTracker 类是否存在(1.21 引入,正是 render 签名变化的根源)
  ⇒ 比硬编码版本号稳

实测差异共 11 处(21 个检查点里 13 个跨世代完全一致):
  setLevel / GameRenderer.render / countRendered* / createPackRepository /
  ResourceLocation 构造 / enableDataFixerOptimizations(移除) / User 构造 /
  ChunkStatus 包路径 / ClientPacketListener 构造(参数个数语义都变) /
  loadEffect(变 private)

为什么必须分后端:DeltaTracker 在 1.20.x jar 中不存在,
两代调用无法共存于一份源码(实测编译报 cannot find symbol)。

1.21.1 反混淆已验证(29MB named jar,DeltaTracker 3 处)
2026-09-25 18:21:08 +08:00