Commit Graph

30 Commits

Author SHA1 Message Date
c80b06552f plan.md:立起最顶层目的 —— 给 agent 一副能在 MC 里感知与行动的工具包
用户定调:插件目的是「让 agent 可以借助我们提供的工具包去玩我的世界」。

推论三条(改变工程重心):
  ① API 形状要照 LLM 用法设计,不是照程序员用法
     (mineflayer 是给写 bot 的人用的库;我们面向 LLM 直接调用)
  ② 感知 + 行动缺一不可 ⇒ 状态/事件/操作三层是正式骨架
  ③ 渲染是工具之一,不是全部 —— 之前把它当主线是重心错了

新增:
  §1c  空白测试服(不用现役服;现役 world 15G + mods 64,测不出边界)
  §1d  面向 LLM 的 API 形状(自省 / 可读返回 / 限量 / 失败要说清为什么)
  §6   里程碑重排:M0 测试服 → M1 修构建 → M2 协议客户端(真正的门槛)
       细渲染降到 M8(已能跑,只是接不上服务端真数据)
       拍照降为 M9(验证工具包的用例,不是工具包本身)

已核实的关键事实(写入 plan):
  · mineflayer 自述 'create minecraft bots with a stable, high level API'
    41 个 plugins 全是动作与感知,grep 行为树/目标选择 = 0 命中
    ⇒ 它是给 bot 开发者的库;决策在它之外,那是我们该留给 agent 的部分
2026-09-26 10:50:21 +08:00
7c7297ebad plan.md:按用户口径重写 —— 粗/细渲染分工、抽象模型(参考 mineflayer)、机位降为用例 2026-09-26 10:32:22 +08:00
c5560765a3 加 plan.md:工程计划(含 ass 粗渲染与聊天拍照的新方向) 2026-09-26 10:25:11 +08:00
a68497089b 加载器联测加强 + Sodium 诊断探针
## tests/test_loader_render.py

★ 划清本文件的**射程**(诚实标注):
它证明的是「Knot 能被我们驱动,且在加载器下渲染能力照常」。
它**不**证明「真实 mod(带 mixin 的)能工作」—— 因为它用空 mods 目录。

  实测原因:真实 mod 的 mixin 目标是 intermediary 名,而我们跑 named
  ⇒ 同一 JVM 只能一个命名空间 ⇒ 真实 mixin mod 在本路线下不生效。
  ⇒ 「加载器能起动」与「mod 能用」是**两件事**,别把前者读成后者。

这条注释是被自己坑出来的:早先那份 6/6 全绿的报告,
读起来像"加载器路线可用",实际只验证了"Knot 能起来"。

## tools/probe_sodium.py

诊断脚本:在装了 Sodium 的实例上跑一次注入 + debug,
打印它自己的状态(chunkStatus / chunkReady / loadQueue / rebuildQueue /
visibleChunks / chunksDebug),并出一张图。

★ 为什么需要它:vanilla 的 `countRenderedChunks()` 在 Sodium 下
  **永远返回 0**(它数的是 vanilla 的网格),光看这个数会把
  「换了渲染器」误判成「没有数据」。而 blockinfo 能读方块 ⇒ 数据是在的。
  ⇒ 排障必须问**渲染器自己**,不能问 vanilla 的计数器。

(debug 命令里的 sodium* 字段就是这条思路的落地:
  sodiumLoaded / sodiumTracker_chunkStatus / sodiumTracker_chunkReady /
  sodiumRebuildQueue / sodiumChunksDebug …)
2026-09-26 10:18:36 +08:00
51ee57e556 实例创建接口(Python 层)+ 后端透传新参数
## ① mcfold/instances.py —— Python 提供「建实例」接口

用户定向(2026-09-26):
> 这个文件夹内容也是 python 层创建的,而不是标准游戏目录。
> 即 python 提供创建实例的接口(含 mod/插件的实例,内部有对应目录,
> 插件 mod 放在目录中就会被加载)。

所以实例是**我们定义的目录**,不是一份 MC 客户端安装:

    create_instance(name, version=, loader=, mods=[], force=False)  → Path
    list_instances()                                    → [实例摘要]
    find_content_root(version, loader)                  → 共享只读那层

- 实例内部:`instance.json`(自述)+ **mods/ shaderpacks/ resourcepacks/
  datapacks/**(★ agent 的接口面,放进去即生效)+ config/ saves/ logs/
  options.txt(MC/加载器自己写的运行时产物)
- **不在实例里建 versions/libraries/assets** —— 那些属于内容根(只读共享)。
  放进实例会让「同一版本多实例」变成多份副本,也把「实例私有/版本公有」
  的边界糊掉。
- 实例名白名单 `^[A-Za-z0-9._-]{1,64}$`(它要进路径)
- 已存在时**报错而非默默复用**,并提示「换名字 = 装不同 mod 的方式」

★ 这与已有的三层口径一致(mcview/lib/versions.cjs:59/61):
  store/<loader>-<version>/ · instances/<name>/ · runtime/<name>/

## ② base.py 三个修正

**(a) cwd 缺省改为 game_dir** —— 版本隔离漏洞:
    渲染进程的 cwd 原是仓库根,而 Sodium 等会往 cwd 写 config/
    ⇒ 实测 config/sodium-mixins.properties 落进了 mcfold 仓库根。
    这些产物属于**实例**。已改为 `cwd=self.cwd or self.game_dir`。

**(b) 命名空间按 client jar 形态自动判** —— 原先硬编码 `named`:
    官方 obf jar + 加载器 ⇒ intermediary(mod 的 mixin 能解析)
    client-named.jar      ⇒ named(仅裸启动可用)
    实测:named 下 42 个 mod 的 464 处 mixin 全部找不到目标。

**(c) 新增 names_file 参数** —— 把 names.json 路径透给后端
    (B 方案的 extern 参数在 Python 侧的可入口)。
2026-09-26 10:18:26 +08:00
e6f8c0e596 build.sh 七步:加 remap 与名字表(加载器路线所需)
## 新增两步(原五步 → 七步)

**[6/7] 反射名次表** —— 调 gen_names.py 生成
    names.json(named 形态)与 names-intermediary.json(intermediary 形态)

**[7/7] remap named → intermediary** —— 三步:
    7a. 复合出 named→intermediary 的 SRG(gen_intermediary_srg.py)
    7b. 造一个 **intermediary 形态的 client jar** ← 继承查找的输入,缺它不行
    7c. 从它生成继承映射(-H),再用 -h 把我们的 classes remap 过去

## 为什么必须 remap

实 mod(Iris/Sodium 等)的 mixin 注解写的是 **intermediary** 名且不带 refmap。
跑 named 时 42 个 mod 的 464 处 mixin 全部找不到目标(实测)。
⇒「mod 要 intermediary」与「我们的产物是 named」在同一 JVM 互斥,
  解法是把**我们的产物** remap 过去(非通用数据由 extern 名次表解决)。

## gen_intermediary_srg.py 的两个实测坑(不处理会出错且症状误导)

① **描述符必须转**。mappings.srg 的 MD 行两侧 desc **都是 official 形式**:
       MD: enn/Z ()Laki; net/.../Minecraft/getResourcePackRepository ()Laki;
   左 desc 必须转成 named、右转成 intermediary。
   只改类名不改 desc 的症状:**类改名成功了、方法却没改** → NoSuchMethodError。

② **必须生成继承映射**。SpecialSource 按 owner+name+desc 精确匹配、
   **不查继承链**;而 MC 里大量方法继承自父类/接口
   (如 ClientPacketCache.registryAccess 实际声明在 RegistryAccess)。
   不带它漏 ~60 处(实测:60 → 12,且剩余 12 全是 JDK 继承方法、
   record component、和我们自己那个不该改名的桥类)。

## 另修三处

- **SS_CP 提到全局**:原来在第 3 步的 if 里定义,而第 7 步也要用它 ——
  若 client-named.jar 已是最新,第 3 步不进入 if ⇒ 变量为空。
- **LoaderBoot 纳入编译**:launcher_fabric 从没被 build.sh 编译过,
  运行期报 ClassNotFoundException: LoaderBoot。
- **common/ 纳入编译**:mcfold/McfoldNames.java 在 common 下,
  漏了它 ⇒ remap 后产物里没有这个类 ⇒ NoSuchFieldException(实测踩过)。
- **natives 平铺**(见上一批提交):jar 内是 linux/x64/org/lwjgl/<mod>/lib*.so,
  而 LWJGL 按**文件名**找库 ⇒ 保留包路径会 UnsatisfiedLinkError。

## 产物清单(落在内容根 .mcfold/)

    classes/                    裸启动 / named
    classes-intermediary.jar    加载器 / intermediary
    client-named.jar · client-intermediary.jar
    names.json · names-intermediary.json
    named2intermediary.srg · inheritance.map
    classpath.txt · natives/(平铺)· fabric/(8 库)
2026-09-26 10:18:08 +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
d4ba586ac7 反射名的 extern 机制(B 方案):19 处字符串反射 → 0
## 问题

Java 后端要访问 MC 的**私有字段/方法**(Minecraft.resourceManager 等),
只能走反射。而反射参数是**字符串**:

    Minecraft.class.getDeclaredField("resourceManager")

remap 工具只改常量池里的**符号引用**,**改不了字符串字面量** ⇒
把产物从 named remap 到 intermediary 时,`Xxx.class` 都对了,
`"resourceManager"` 原样留着 ⇒ 运行期 NoSuchFieldException(实测撞到过)。

## 做法:语义名 → 物理名,表由 extern 参数传入

源码写**语义路径**(跨命名空间稳定):
    McfoldNames.field(Minecraft.class, "net/minecraft/client/Minecraft/resourceManager")

- named 模式(无表):退化为取末段 ⇒ "resourceManager" ✅
- intermediary 模式:表里查 ⇒ "field_1745" ✅

⇒ **同一份 class 在两种命名空间下都能跑**,切换不必重编。
   这是「mod 要 intermediary」与「我们的产物本来是 named」能共存的**前提**。

## 表的来源(scaffold/gen_names.py)

扫源码里的语义引用 → 经两份映射复合解析:
    named ──(mappings.srg 反查)──> official ──(intermediary tiny)──> intermediary

运行时按此序找表:
  1. -Dmcfold.names=<path>          ★ 显式 extern 参数(调用方只传路径)
  2. <class 同级>/names.json
  3. 都无 ⇒ named 模式

★ 调用方(Python)**不需要知道任何 MC 名字**,只传一个路径 ——
  命名空间知识全部留在 jar 与构建产物里。这就是「接口统一、非通用数据走 extern」。

## 实测踩坑(都在 gen_names.py 里记了原因)

① 查表必须**带描述符**:同一个 obf 名(如 fjq/b)有十几个重载,
   只按 (kind, owner, name) 会取到第一个重载 ——
   症状:reloadShaders(Lala;)V 被解成 method_3207()V(无参),运行期 NoSuchMethodException。
② 字段要单建一张 (owner,name) 表:tiny 的 FIELD 行带字段类型描述符,
   而 srg 的 FD 行没有 ⇒ 拿 None 去查永远不命中(7 个字段全漏)。
③ **不能靠首字母大小猜字段/方法**,必须在 srg 里实查 kind。
④ MC 里存在**同名字段与方法**(如 ClientPacketListener.registryAccess
   既有 FD fex/B 又有 MD fex/r)⇒ 用 McfoldNames.field/method 的**调用形式**
   判定 kind,与运行期用法一致。

## 验证

- 名字表 11/11 解析成功(gen_1_20)
- 两世代后端字符串反射归零(gen_1_20 10 处、gen_1_21 9 处)
- 加载器路线端到端跑通(见后续提交)
2026-09-26 10:17:29 +08:00
652a460405 加 .gitignore,并把 28 个运行期产物移出版本库
## 为什么

35534b1 把 logs/*.log.gz(10 个)与 __pycache__/*.pyc(18 个)
一起提交了,而仓库**根本没有 .gitignore** —— 那不是手误,是缺一道门。

已 `git rm --cached` 那 28 个(磁盘文件保留,只是不再跟踪)。

## 覆盖三类

① 运行期产物:logs/ *.log.gz __pycache__/ .pytest_cache/ …
② **加载器/mod 的运行时配置**:config/ shaderpacks/ options.txt saves/
   datapacks/ resourcepacks/
   ★ 这类属于**实例**,不属于仓库。实测踩过:Sodium 往**进程 cwd**
     写了 config/sodium-mixins.properties,落进了仓库根。
     根因已修(base.py:cwd=self.cwd or self.game_dir,让运行时产物
     落进实例目录),这里再兜一层。
③ 构建缓存:.mcfold-tools/ build/ dist/ *.egg-info/
2026-09-26 10:17:14 +08:00
330867c6df 版本隔离守卫(新套件立即抓出两处真实回流)
## 修掉渲染联测的嵌套实例目录

`test_render_bridge.py` 把实例写成 `<store>/instances/test-mcfold`。
而权威口径(mcview/lib/versions.cjs:59/61)是:

    contentRoot = BASE/store/<key>       共享只读(jar/libraries/assets)
    instanceDir = BASE/instances/<name>  实例独占(mods/config/options/saves)

⇒ 那是「一个版本的内容根里长出了实例态」。而版本隔离要防的崩因正是
「多实例共用 mods」—— 内层目录会把它重新引回来,而那条路径看起来完全合法。
改为 `BASE/instances/test-mcfold`。

## 新增 tests/test_version_isolation.py(4 项,默认套件里跑)

★ 它**不需要 JVM**(纯路径断言),所以必须落在默认测试里 ——
放进 `--with-render` 才跑的套件等于没守卫(大多数人不会加那个参数)。

  ✓ 三层同级(store/ · instances/ · runtime/ 是 BASE 下的兄弟)
  ✓ 内容根干净(无 instances/ 嵌套、无 mods/config/options.txt/saves)
  ✓ 测试实例归位(落在 instances/ 层,不在任何内容根下)
  ✓ 内容根无实例配置(查内容,不只查存在)

## ★ 守卫首次运行就抓出第二处(我此前漏掉的)

```
  ✗ 内容根被实例化:
    - store/vanilla-1.21.1/instances/(内容根里嵌套了实例层)
```

内容是 `instances/default/{options.txt,downloads/log.json}`(生成态,
正式 `instances/default` 已存在)⇒ 已删。两个内容根现已干净。

⇒ 这条说明:**「我检查过了」不等于「有一条会自己跑的检查」** ——
  我上一轮手工核实过 1.20.1 那处并修掉,却没发现 1.21.1 那处;
  守卫一运行,两处一次全暴露。

## 验证

  ./run_tests.sh               → 66 项
  ./run_tests.sh --with-render → **80 项全绿**
2026-09-26 07:23:54 +08:00
8b1fc9289d Fabric 加载器路线跑通并落地验证(launcher="fabric",6/6 绿)
上个会话把 `launcher="fabric"` 写进了后端,却**没有留下任何可重跑的验证** ——
「经加载器能起来」这件事只活在对话里。本次把它变成可复算的。

## 机制(路线 C,不是 LOADERS.md 原来写的 A/B)

A/B 都假设「游戏由 Loader 起」。但 `Knot.init()` 是 public 的:它做完
「发现 mod → 建 KnotClassLoader → 应用 mixin → invokeEntrypoints(preLaunch)」
后**返回**,不进入官方主循环(那在 GameProvider.launch())。
⇒ `knot.loadIntoTarget("McfoldRenderServer")` 把我们的服务端注入同一个
KnotClassLoader —— **mod 生态与我们的控制权同时拿到,不必把服务改写成 mod**。

## 主要改动

### ① `RenderBackend._fabric_classpath()` —— 组装从用户手里移到后端

两个变换是实测必需、且用户不该自己拼的机械操作:
  - 剔除 1.20.1 自带的 asm(9.2 与 9.6 两版并存)—— 否则 Knot.<clinit> 抛
    "duplicate ASM classes found on classpath"
  - 追加 Fabric 的 8 个库(asm×5 / sponge-mixin / intermediary / fabric-loader)

库来源改为内容根 `.mcfold/fabric/`(原在 /tmp/fab,tmpfs 且违反版本隔离)。
实测 67 条 → 去 7 asm + 加 8 fabric = 68 条。

### ② `tests/test_loader_render.py`(新增,6 项)

  ✓ 启动架构与世代正交(动态子类,不写 N 个类)
  ✓ 两个架构可枚举
  ✓ classpath 组装(67→68:去 7 asm,加 8 fabric)
  ✓ 非 asm 库原样保留(去 asm 是外科手术,不是重写)
  ✓ **Fabric 加载器真启动并渲染**(KnotClassLoader / chunks=10..25 / colors=4391..4547)
  ✓ **Fabric 拒绝不兼容 mod** ← 反向对照:缺 fabric-api 的 Explorify 被拒

最后一条是**两条路线的可判差异**:零加载器是「无脑注册」,不知道 mod 要
fabric-api;Fabric 会。没有它,"加载器真的介入了"这句话就没有反面证据。

### ③ `run_tests.sh` —— 慢套件改为列表驱动 + 全跳过不再报绿

- SLOW 列表含 test_render_bridge / test_loader_render(默认不跑)
- ★ 全部跳过的套件**不算绿**:报"跳过 N 项"而非计入通过数 ——
  否则"没测"会被读成"测过了"。

## 验证

  ./run_tests.sh               → 62 项(2 个慢套件标 ⊘)
  ./run_tests.sh --with-render → **76 项全绿**
    (62 单元 + test_render_bridge 8 + test_loader_render 6)

## 仍未做

  Iris 光影真实加载(加载器已通,未验过一次 shader pack)
  forge / neoforge 后端(gen_1_20 / gen_1_21 均待设计)
2026-09-26 07:12:41 +08:00
a1ce560c05 修复渲染联测的两个阻断缺陷(之前 5/7 → 现 7/7)
## ① natives 必须平铺,不能保留包路径

natives-linux jar 里的路径是 `linux/x64/org/lwjgl/<module>/lib*.so`,
而 LWJGL 经 java.library.path / LD_LIBRARY_PATH 按**文件名**找库 ——
嵌套一层的目录它找不到。实测:

  UnsatisfiedLinkError: Failed to locate library: liblwjgl.so
    at RenderSystem.<clinit> → Tesselator.<clinit>

build.sh 原来直接 `unzip -d $OUT/natives`(保留包路径)⇒ 产物不可用。
改为解到临时目录再 `find -name '*.so' -exec cp` 平铺。
(已用新逻辑重铺 store/vanilla-1.20.1/.mcfold/natives,7 个 .so 在根下。)

## ② api_notes 应为 staticmethod

35534b1 把世代分派改成类级调用后,`api_notes` 仍是实例方法,
测试里 `Gen_1_20.api_notes()` 直接 TypeError。它不读 self ⇒ 一律 staticmethod。
这也是它能"不启动后端就做世代对照"的前提。

## ③ classpath.txt 的语义写进注释

它**只含第三方 libraries**;client-named.jar 与 classes/ 由使用方拼接
(build.sh:88 自己拼,测试/后端从各自路径取)。
在 build.sh 里注明,避免下一个人以为它是"完整运行 classpath"。

## 验证

  ./run_tests.sh                    → 62 项全绿
  .mcfold classpath + 平铺 natives  → 渲染联测 **7/7 全绿**
    (injected=49 chunks=31 colors=11714 / placed=1003716 chunks=25 colors=4540)
2026-09-26 06:53:30 +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
bea69b2556 mod 方块渲染(零加载器):注册链路实测打通
关键洞察:方块**渲染**只需要「注册表里有名字」+「资源包有模型」,
与方块行为无关 ⇒ 不必等 Fabric 加载器。

实测(已跑通):
  frozen 置 false + unregisteredIntrusiveHolders 恢复非 null
  注册占位方块 → BLOCK 1003→1004
  BlockState 手动 add 进 BLOCK_STATE_REGISTRY → 24135→24136
  state id = 24135,parseForBlock 可解析(渲染路径通)

★ 两个必须都处理的字段:只解冻 frozen 会抛
  "This registry can't create intrusive holders"
★ BlockState 不会自动注册(state id 会是 0),需手动 IdMapper.add

边界已诚实标注(scaffold/MOD-BLOCKS.md):
  外观 ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌(需加载器)
  渲染类型(半透明)⚠️ 未验证 / 方块实体 ⚠️ 未验证
  Iris 光影必须加载器,此路不覆盖
2026-09-25 19:33:42 +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
34ed70ab53 修复视距不生效 + 接入统一测试
视距(用户报「无法调整视距?」)——根因是三处不同步:
  1. options.renderDistance        本地设置(原先只设了这个)
  2. options.setServerRenderDistance 服务端下发上限(getEffectiveRenderDistance = min(两者))
  3. ClientChunkCache.updateViewRadius storage 半径
第三处最关键:官方只在收到服务端半径包时才调它(ClientPacketListener:2316),
我们不连服 ⇒ Storage.inRange 用旧半径判定,超出区块被**静默丢弃**。

另一个前置:加载半径必须跟随视距。视距 12 只喂 3 圈数据,
画面看起来就像没生效(最容易误判的坑)。

实测(同相机同存档):
  视距 4  → 注入 81   渲染 55   色彩 15226
  视距 12 → 注入 595  渲染 447  色彩 18477
画面差异显著:RD=12 能看到延伸到地平线的森林与完整海岸线。
(sample-render-distance-{4,12}.png)

测试:新增 tests/test_anvil.py(13 项,存档/NBT/编码)
      tests/test_render_bridge.py(3 项,桥协议+合成渲染+真实存档渲染)
      run_tests.sh 改为默认跑单元测试、--with-render 跑联测
      合计 61 项全绿(原 48 + 13)
2026-09-25 17:47:58 +08:00
391d452c85 P9:Python 包装层 —— 真实存档 → 官方渲染器 → 出图
新增两模块:
  mcfold/anvil.py        存档读取(region/NBT)+ 区块编解码工具
  mcfold/render_bridge.py 桥:常驻 Java 渲染器进程,NDJSON 协议
  mcfold/render.py       对外入口:WorldRenderer(存档→画面)
  scaffold/McfoldRenderServer.java  Java 侧服务端(常驻,NDJSON)

实测(真实存档 mc-test,DataVersion 3465=1.20.1):
  injected=81  unknownBlocks=0  uniqueSpecs=111
  loadedChunks=81  chunksRendered=60  distinctColors=11585
  画面:草地/沙滩/树/水/天空/太阳/HUD(sample-real-save-render.png)

★ 分工落在实处(附录 D 的结论变成代码):
  存档 palette 是**方块名**(minecraft:oak_log[axis=y]),网络载荷要 **state id**,
  名字→id 的权威映射只有客户端注册表有 ⇒ 新增 resolve 命令,
  Java 侧用 BlockStateParser.parseForBlock 解析(实测 111 种全中,0 unknown)

踩坑(本轮新发现):
  1. level.dat 有两种布局(8字节头+zlib / 直接 gzip)—— 实测本机是后者
  2. section 索引 ≠ 绝对 Y:存档用 -4..19,载荷用 0..23(少这步 → readerIndex 越界)
  3. 注入前必须 set_viewport,否则 ClientChunkCache.inRange 丢弃区块
  4. setPos 不会更新实体区块归属,要 setPosRaw + updateViewCenter
  5. 生物群系只影响着色,用统一 plains 不影响「能渲染出世界」
  6. camera 回包要用 JsonArray,字符串会被 Python tuple() 按字符拆开

性能:启动 66s(GL+资源重载),之后每帧渲染 ~1s(含 100 帧 tick)
2026-09-25 17:33:41 +08:00
541fe232a8 P8 完成:世界数据 → 官方渲染器 → 真实出图
不起完整客户端、不连服、不登录,只初始化 MC 渲染栈并喂入我们自己造的世界数据。

已验证(全绿):
  countRenderedChunks=29  distinct colors=1450
  画面:草地/原木/树叶/天空/HUD/英文文字(sample-world-render.png)

打通的关卡:
  - 反混淆(client-named.jar)+ GL 离屏 + new Minecraft(cfg) 不进主循环
  - 资源同步重载:22 listener(ModelManager/FontManager/Dispatcher)
    ★ 不能 join reloadResourcePacks(等主循环→死锁),直接调各 listener reload()
  - 注册表从客户端自带 data/ 加载(dimension_type=4)
  - 区块注入:LevelChunk + setBlockState → ClientChunkCache.Storage.chunks
    + LightEngine.queueSectionData(全亮)→ allChanged()
  - 每帧 mc.tick() 推进网格重建队列(不跑则 countRenderedChunks=0)

踩坑记录(10 条,README 详列):
  LoadingOverlay/ReceivingLevelScreen 遮挡、FolderData 参数序、
  assetIndex 用落盘名、reloadShaders 包私有、读像素必须绑 FBO、
  判据用 distinct colors 而非 non-black

更正附录 D:state_id=1003 → note_block 不是误映射(note_block 有 800 状态)
  —— 真客户端给完整状态,是 1.20.1 上 mod 方块语义的唯一权威来源
2026-09-25 17:01:49 +08:00
e223be2571 P8: scaffold —— 反混淆 + MC 渲染栈离屏初始化 + 真渲染出图
实测链路(全绿):
  1. 官方 ProGuard 映射 → SRG → SpecialSource 重映射 → client-named.jar
  2. 反编译确认 MC 渲染器挂在 Minecraft 单例上,不能独立 new
  3. 权威序列:tryDetectVersion → Bootstrap → RenderSystem → new Minecraft(cfg)
  4. 渲 3 帧 + glReadPixels → PNG,画面是 Mojang 加载屏(真渲染,非清屏)

三个坑(实测证据):
  - 漏 tryDetectVersion → Game version not set
  - FolderData 参数顺序错 → icons/ 404
  - assetIndex 用 id(5) 而非落盘名(1.20.1) → 资源全找不到

下一步:ClientLevel 构造只需 listener.registryAccess(),
ClientChunkCache.replaceWithPacketData 接收 map_chunk 原始字节
(与 chunk.py 同格式)⇒ 这就是数据喂渲染器的落点。
2026-09-25 15:39:41 +08:00
pi
5038c7f679 feat(mcfold): P7 实例层(传入文件夹→拉起实例→读mod注册方块表→连服,含端到端验证)+ 统一测试入口 2026-09-25 14:36:21 +08:00
pi
b0182de8c7 feat(mcfold): P6 世界模型(多区块聚合/负坐标定位/unloaded与air语义分明/mod降级统计) 2026-09-25 14:33:26 +08:00
pi
85ad7e580e feat(mcfold): P5 mod jar 解析(三种加载器+多加载器诊断+方块名提取,含诚实边界标注) 2026-09-25 14:32:39 +08:00
pi
40c5739cae feat(mcfold): P4 区块解析(位打包/三种调色板分支/注册表显式降级)+ prismarine 真实数据交叉验证 2026-09-25 14:25:34 +08:00
pi
a4914b932e feat(mcfold): 最小客户端 + 跨实现互操作测试(Python 客户端握手 Node 服务端成功) 2026-09-25 14:12:06 +08:00
pi
fc5a1aae91 feat(mcfold): 声明式 protodef 解释器(原生类型+阶段作用域+switch/mapper/bitfield),真实握手字节验证通过 2026-09-25 14:10:40 +08:00
pi
22cdee35e3 feat(mcfold): P1 按实例文件夹解析版本(inheritsFrom 归一化 + 加载器判定)+ 回归测试 2026-09-25 14:06:55 +08:00