From c5560765a30127436b55f1c1a31778163f2560eb Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 10:25:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=20plan.md=EF=BC=9A=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E8=AE=A1=E5=88=92=EF=BC=88=E5=90=AB=20ass=20=E7=B2=97=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=B8=8E=E8=81=8A=E5=A4=A9=E6=8B=8D=E7=85=A7=E7=9A=84?= =?UTF-8?q?=E6=96=B0=E6=96=B9=E5=90=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plan.md | 394 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..a1645a6 --- /dev/null +++ b/plan.md @@ -0,0 +1,394 @@ +# mcfold 工程计划 + +> 最后更新:2026-09-26 +> 状态标记:✅ 已完成 · 🔄 进行中 · ⬜ 未开始 · ⚠️ 有已知缺口 + +--- + +## 0. 目标(用户定向,按时间顺序) + +1. **照相机式渲染**:给一个世界(存档数据 / 服务端),渲染出图。 + - 用户原话(2026-09-25):「不起完整客户端」——抽出 MC 的**渲染能力**, + 喂我们自己的数据。 + - 用户纠正:「**不写 mod**,而不是**不用 mod 加载器**」⇒ 用加载器( + 让真实 mod / Iris 光影能工作),但不写自己的 mod。 +2. **架构分层**(用户 2026-09-26 明确定调): + > jar 部分封装为统一的接口,非通用数据通过 extern 参数传入。 + > jar 端接受**操作意图**,输出**包**,python 再将包发送到服务端。 + > python 相当于只是包装,让 agent 可以通过统一的接口调用。 + > 同时不要忘了**版本隔离**。同一个版本应当可以存在**多个实例**(因为要装不同的 mod)。 +3. **范围**(用户 2026-09-26 明确限定): + > 我们只是为了**渲染**以及**与服务端的 mod 物品交互**。 + ⇒ 不做完整客户端、不做玩法、不做移动/战斗。 +4. **HUD 不显示**(用户 2026-09-26):截图只关心画面,HUD 是噪声, + 而且它给 `distinctColors` 掺固定像素(污染判据)。 + ⇒ 走官方选项 `options.hideGui`(= 游戏里 F1)。 + +--- + +## 1. 用户设想的场景(2026-09-26,本计划的新方向) + +> 我设想的场景是玩家可以在聊天框给 agent 发消息,然后比如让 agent 给他**拍照**, +> 如果是他和某个景物的**合照**,agent 可以先通过 **ass 粗渲染**,找好角度 +> 再去**渲染实图**。 + +拆成可执行的三段: + +``` +① 玩家在聊天框发消息 → agent 通过统一接口收到 +② agent 要"给玩家拍合照" → 需要:玩家位置/朝向 + 景物位置 + 相机可摆放范围 +③ 选角度 → 用 **ass 粗渲染**(低成本、快)试若干机位, + 挑一个好的,再用**真渲染**出图 +``` + +### ★ ass 粗渲染是什么(这里是关键设计点) + +「ass」= **ASCII / 低保真近似**(用户 2026-09-26 提出)。 +它要能回答「这个角度拍出来大概是什么样」,但**不启动 MC**: + +- **不是**把真渲染结果降采样 —— 那还是要跑 MC,没省。 +- **是**用一个**纯数据的近似模型**:从方块数据直接推 + 「视线方向上有哪些方块、遮挡关系、大致明暗」, + 在文本/低分辨率网格上画出来。 + +**为什么它值得做**(三个具体收益): + +1. **试错便宜**:一次真渲染要起 JVM(~20-40s),粗渲染是毫秒级 + ⇒ agent 可以试几十个机位,只把最好的那一个送进真渲染。 +2. **给 agent 可读的中间结果**:ASCII 图能被 LLM 直接「看」和理解, + 而 PNG 需要多模态输入。agent 可以在文本空间里推理"该往哪挪"。 +3. **失败模式友好**:机位选错时,粗渲染已经暴露(比如"镜头埋在地里"、 + "景物被树挡住"),不用浪费一次完整渲染。 + +### mineflayer 的参考价值(用户 2026-09-26 点名) + +> 记得参考 mineflayer 的实现,我觉得它对于**行为触发条件**与**展示方式** +> 以及 **ass 粗略绘图**的形式很优秀。 + +mineflayer 相关能力(本机已有:`/home/newqqagent/mcbridge/node/node_modules/mineflayer`): + +| mineflayer 的做法 | 对我们的启发 | +|---|---| +| **事件驱动**:`bot.on('chat')` / `'entitySpawn'` / `'physicsTick'` | 行为触发条件:聊天消息 → 意图识别 → 动作(拍照)| +| **`bot.chat()` / `bot.entity.position`** | agent 拿到玩家位置/朝向;回复走聊天 | +| **`prismarine-viewer`**:三种视角(first_person / third_person / fixed)| ★ 相机摆放的**现成语义**:合照 = third_person + 朝向景物 | +| **`.saveScreenshot()`** | 截图接口的形态 | +| **blockAt / findBlock / raycast** | ★ **粗渲染的核心原语**:从方块数据做视线求交,算遮挡 | +| **minecraft-data**:权威方块/物品数据 | 服务端交互的注册表来源之一 | + +**⚠️ 但要注意 mineflayer 与我们的根本差别**: +mineflayer 是**真连服务端**、有完整世界状态; +我们是**离屏渲染 + 自喂数据**。所以它的**行为触发条件**能直接借鉴, +而**世界访问**要走我们自己的数据通路(存档 / 服务端注入)。 + +--- + +## 2. 已完成(可复用,有提交) + +### 2.1 渲染桥(P8/P9) + +- `pg2srg.py`:ProGuard → SRG 映射转换 +- `client-named.jar`:官方 jar 反混淆产物 +- 官方初始化序列(反编译确认):`tryDetectVersion` → `enableDataFixerOptimizations` + → `Bootstrap.bootStrap()` → `initRenderThread` → `beginInitialization` + → `new Minecraft(cfg)` → `finishInitialization`(**不跑主循环**) +- 单帧渲染:`clear` → `bindWrite` → `gameRenderer.render` → `unbindWrite` + → `blitToScreen` → `updateDisplay` + +### 2.2 架构(用户定调的落地) + +- **Python 只做路由**:0 处 `net.minecraft` 引用(实测) +- **差异全在 Java**:`cmd=setblocks`(Python 发 RLE,Java 组装协议)、`cmd=capacity`(问运行时) +- **世代分派**:`DeltaTracker` 类存在 ⇔ 1.21+;每代独立编译的 Java 后端 +- **加载器 × 世代正交**:动态子类组合,不写 N 个类 + +### 2.3 加载器路线 + 光影 ✅(2026-09-26 打通) + +**关键突破**:`Knot.init()` + `loadIntoTarget()` —— 拿到 mod 生态,但不起官方主循环。 + +三个必需条件(缺一失败,全部实测): +1. classpath 放 `client-named.jar`(不是官方 obf jar) +2. `-Dfabric.gameMappingNamespace=named` +3. **asm 只留一份**(1.20.1 自带 9.2+9.6 两版 ⇒ `Knot.` 抛 duplicate ASM classes) + +**B 方案(用户选定)**:统一接口 + 非通用数据走 extern +- `McfoldNames`:源码写**语义路径**(`net/minecraft/client/Minecraft/resourceManager`), + 运行时按 `names.json` 查物理名 +- 19 处字符串反射 → 0(remap 改不了字符串字面量,这是唯一两态都通的形态) +- `gen_names.py`:从两份映射复合生成表(11/11 解析成功) + +**remap 链**(`build.sh` 第 7 步,含两个实测坑): +- 坑①:SRG 的 desc 两侧都是 official ⇒ 左必须转 named、右转 intermediary + (只改类名不改 desc ⇒ 类对了、方法没改 ⇒ `NoSuchMethodError`) +- 坑②:SpecialSource 按 owner 精确匹配、**不查继承链** ⇒ 必须 `-h inheritance.map` + (不带漏 ~60 处) + +**结果**:Iris 光影 + 地形**同时**渲染 ✅ +``` +before: colors=4748 (只有天空) +after: colors=113546 (地形 + 光影 + 阴影) +``` + +### 2.4 Sodium tracker 通知(`notifyRenderTracker`)✅ + +**症状**:装 Sodium 后只有天空、`chunks=0`,而 `blockinfo` 能正确读方块 +⇒ 数据在,渲染器不知道。 + +**三层根因**(逐层实测): +1. Sodium 的 mixin 挂 `replaceWithPacketData` 内 `@At(INVOKE onChunkLoaded)`, + 而 1.20.1 该方法**内部不调** `onChunkLoaded` ⇒ 注入点永不命中 +2. 反射签名:参数是 `ClientLevel` 不是 `Level`;第三参是**位掩码 int** +3. ★ **status 值**:`FLAG_HAS_BLOCK_DATA=1` / `FLAG_HAS_LIGHT_DATA=2` / `FLAG_ALL=3`, + 而 `updateMerged` 的 2×2 判定要 `四邻 & == 3` + ⇒ 传 1:`1&1&1&1=1 ≠ 3` ⇒ 全部判成"凑不齐" ⇒ 队列恒空 + +``` +传 1: chunkStatus=49 chunkReady=0 loadQueue=0 colors=4748 +传 3: chunkStatus=49 chunkReady=25 loadQueue=25 colors=113546 +``` + +### 2.5 版本隔离 + 实例层 ✅ + +- **三层**(权威口径 `mcview/lib/versions.cjs:59/61`): + ``` + BASE/store/-/ 内容根 共享只读:jar/libraries/assets/.mcfold 产物 + BASE/instances// 实例 独占:mods/shaderpacks/config/saves + BASE/runtime// 运行期 独占:X display/screenshots/logs + ``` +- **`mcfold/instances.py`**(Python 接口,用户定调): + ```python + 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 + - **不在实例里建 versions/libraries/assets**(属内容根,只读共享) + - 实例名白名单 `^[A-Za-z0-9._-]{1,64}$`;已存在则报错而非默默复用 +- **版本隔离守卫**(`tests/test_version_isolation.py`,默认套件里跑) + - 首次运行就抓出**两处**真实回流(`store/vanilla-1.20.1/instances/` 与 + `store/vanilla-1.21.1/instances/`) + - ★ 教训:「我检查过了」≠「有一条会自己跑的检查」 + +### 2.6 HUD 开关 ✅ + +`hide_gui`(两世代后端 + Python `set_view(hide_gui=...)`),走官方 `options.hideGui`。 + +### 2.7 工程卫生 ✅ + +- `.gitignore`(运行期产物 / 实例配置 / 构建缓存) +- `git rm --cached` 28 个误入库的文件 +- Sodium 往**进程 cwd** 写 config/ 的漏洞已修(`cwd=self.cwd or self.game_dir`) + +**测试**:`./run_tests.sh` 66 项 · `--with-render` 80 项(实测全绿) + +--- + +## 3. 当前缺口(按依赖顺序) + +### ⚠️ G1. 1.21.1 的构建没跑通(**阻断服务端那条线**) + +`build.sh` 的路径 bug 已修(`$HERE/common` → `$HERE/java/common`), +但 1.21.1 编译仍有错: + +``` +LoaderBoot.java:1: error: package net.fabricmc.api does not exist +LoaderBoot.java:2: error: package net.fabricmc.loader.impl.launch.knot does not exist +LoaderBoot.java:53: error: package net.fabricmc.loader.api does not exist +gen_1_21/McfoldRenderServer.java:761: error: cannot find symbol +``` + +**根因**:`build.sh` 第 5 步的编译 classpath(`CP`)**没有包含 Fabric 库**, +而 `LoaderBoot` 需要 `net.fabricmc.*`。1.20.1 那次是侥幸(Fabric 库在 +hand-composed classpath 里),build.sh 本身没把它加进编译 CP。 + +**修法**:第 5 步编译时若有 `$OUT/fabric/`,把它加进 `CP`。 + +### ⬜ G2. 服务端交互没实现(目标里明写的那半) + +现状:`client.py` 只有 `handshake` / `login_start` / `run_smoke`(冒烟级)。 + +缺: +- **configuration 阶段**(1.20.2+,登录后、play 前):这里有 `registry_data` +- **服务端注册表**:方块 id / 物品 id 的**权威来源** +- **物品 ↔ 方块映射** + +★ **技术核心**:mod 方块的 state id **只有服务端知道** +(客户端只知道自己 jar 里那套)。这正是「与服务端 mod 物品交互」的难点。 + +已实测确认:`ServerboundChatPacket` / `ClientboundSystemChatPacket` 的 +protodef 描述**不随版本变** ⇒ `ProtocolVersion` 只影响少数结构。 + +### ⬜ G3. 统一接口门面(用户口径:agent 调 Python) + +已有 `WorldRenderer` / `Instance` / `create_instance`,缺一个把 +「渲染」+「服务端交互」串起来的门面。 + +### ⬜ G4. ass 粗渲染(§1,新方向) + +见 §4 设计。 + +### ⬜ G5. 聊天框 → agent → 拍照 的行为链(§1,新方向) + +见 §5 设计。 + +--- + +## 4. ass 粗渲染设计(⬜ 未开始) + +### 4.1 定位 + +``` +真渲染: JVM + 完整 MC 渲染栈 + (可选)Iris 光影 ~20-40s ~1MB PNG +粗渲染: 纯 Python,从方块数据做视线近似 <100ms 文本网格 +``` + +**两者不是精度不同,是用途不同**: +- 粗渲染用来**搜索机位**(试几十个) +- 真渲染用来**出最终图**(只跑选中的那个) + +### 4.2 核心原语(借鉴 mineflayer / prismarine 的做法) + +mineflayer 的 `raycast` / `blockAt` 是纯数据的视线求交 —— 我们不连服务端, +但**同样的计算**可以跑在存档数据上: + +```python +# 输入:世界的方块数据(已能拿到,见 render.py 的 SaveChunk) +# 输出:ASCII 网格 + 结构化线索 + +for 每条视线(从相机出发,过每个网格像素): + 沿射线步进(DDA,与 MC 的方块网格对齐) + 记录第一个命中的非空气方块 + → 得到 depth / block / 明暗(按法线与光方向) +``` + +### 4.3 输出形态(三件,服务于"让 agent 能推理") + +``` +① ASCII 网格 相机看到的大致形状("镜头埋地里"一眼可见) +② 结构化线索 视野内方块分布 / 主景物的屏幕占比 / 遮挡率 +③ 建议 "往左挪 3 格可让景物居中"(可选,规则驱动) +``` + +**②比①更重要** —— LLM 读数字比读 ASCII 准; +ASCII 主要是给人看和做快速否决。 + +### 4.4 与真渲染的一致性(关键风险) + +⚠️ 粗渲染的结论必须**能预测**真渲染。要测: +- 同一机位,粗渲染说"看得见主景物" ⇒ 真渲染确实看得见 +- 建立**一致性判据**(不是"看起来差不多",而是可判的: + 「主景物的屏幕包围盒重叠率 > 阈值」) + +### 4.5 实现落点 + +`mcfold/rough.py`(新模块)+ `tests/test_rough.py` +- 输入:`WorldRenderer` 的方块数据(或直接读存档) +- 不依赖 JVM ⇒ 可在默认测试套件里跑(秒级) + +--- + +## 5. 聊天框 → 拍照 行为链设计(⬜ 未开始) + +### 5.1 事件流(借鉴 mineflayer 的**事件触发**形态) + +``` +玩家在聊天框发消息 + ↓ +① 收消息 ClientboundSystemChatPacket(或玩家聊天) + ↓ +② 意图识别 "给我拍张照" / "和那座山合个影" + ↓ +③ 需要什么 玩家位置/朝向 + 景物位置 + 可用机位 + ↓ +④ 规划 粗渲染试 N 个机位 → 挑最好的 + ↓ +⑤ 出图 真渲染(Iris + 光影) + ↓ +⑥ 送达 回聊天 / 发邮件(走现有 agentmail 通路) +``` + +**mineflayer 的参考点**:它的 `bot.on('chat', ...)` + `bot.chat()` 就是 +这个闭环的极简形态。我们要加的是 ③④⑤(拍照特有的)。 + +### 5.2 三个必须先确认的事实(否则设计是空中楼阁) + +1. **玩家位置从哪来?** 我们是离屏渲染,没有真玩家实体。 + - 若接服务端:从 `ClientboundPlayerPositionPacket` 拿真玩家位置 + - 若不接:由调用方**指定**一个"玩家"坐标(合成场景) +2. **"景物"怎么定位?** agent 怎么知道"那座山"在哪? + - 自然语言 → 坐标,这是**另一个问题**(可能需要俯瞰图 + 索引) +3. **相机可摆范围?** 不能在方块里、必须在世界高度内、不能超出视距 + +### 5.3 最小可用版本(建议先做这个) + +不接服务端,用**调用方指定坐标**打通全链: + +```python +# agent 调统一接口 +shot = capture_photo( + world=<存档或注入的场景>, + subject=(x, y, z), # 景物 + subject_scale=20, # 景物半径 + with_player=True, # 是否要合照 + player_pos=(x, y, z), # 合照时的"玩家"位置 + samples=20, # 粗渲染试几个机位 +) +shot.png → /path/out.png +shot.rough → ASCII + 线索(给 agent 看它为什么选了这个角度) +``` + +--- + +## 6. 里程碑(建议顺序) + +| # | 内容 | 依赖 | 状态 | +|---|---|---|---| +| M1 | 修 G1(build.sh 编译 CP 加 Fabric 库),1.21.1 构建跑通 | — | 🔄 | +| M2 | `rough.py` 粗渲染(纯 Python,DDA 视线) | — | ⬜ | +| M3 | 粗渲染 ↔ 真渲染一致性判据 | M2 | ⬜ | +| M4 | `capture_photo()` 机位搜索 + 统一接口门面 | M2, M3 | ⬜ | +| M5 | 服务端交互(configuration / registry_data / 物品表) | M1 | ⬜ | +| M6 | 聊天框 → agent → 拍照 全链 | M1, M4, M5 | ⬜ | + +**建议先做 M1 + M2**:M1 解阻断,M2 是用户新设想里最有独立价值的一块 +(纯 Python、不依赖 JVM、可秒级验证)。 + +--- + +## 7. 工程纪律(本项目已确立的口径) + +1. **Python 只路由,差异进 Java** —— Python 侧 0 处 MC 类名引用 +2. **验证要能失败** —— 判据必须**构造触发**,不靠观察现状 + (「今天有反例」与「今天没反例」都可能抓不到缺陷) +3. **extern 参数只传路径** —— 命名空间知识留在 jar 侧 +4. **报数必须带**:来源 + 取数时刻 + 谓词/域 +5. **门禁要放默认套件** —— 放 `--with-render` 才跑的守卫等于没守卫 +6. **实例与内容根同级** —— 绝不嵌套;同版本可多实例 +7. **诚实标注射程** —— 「加载器能起动」≠「mod 能用」 + +--- + +## 8. 环境备忘 + +``` +仓库 /home/newqqagent/plugindev/mcfold (远端 ssh://git@jianfgit.xyz:220/jianf/agentmc.git) +内容根 /home/newqqagent/mcview/store/vanilla-{1.20.1,1.21.1}/ +实例 /home/newqqagent/mcview/instances// +构建产物 内容根/.mcfold/{classes,classes-intermediary.jar,client-named.jar, + names*.json,named2intermediary.srg,inheritance.map,classpath.txt, + natives/,fabric/} +服务端 /home/minecraft (neoforge 1.21.1,mods 若干,端口 25565) +mineflayer /home/newqqagent/mcbridge/node/node_modules/mineflayer ← 参考实现 +Xvfb :98 + +测试 ./run_tests.sh 66 项 + ./run_tests.sh --with-render 80 项(需 MCFOLD_RENDER_CLASSPATH 等) +``` + +**渲染环境变量**: +```bash +export MCFOLD_RENDER_CLASSPATH="$(paste -sd: $STORE/.mcfold/classpath.txt):$STORE/.mcfold/client-named.jar:$STORE/.mcfold/classes" +export MCFOLD_NATIVES_DIR=$STORE/.mcfold/natives +export DISPLAY=:98 +```