Files
agentmc/plan.md
JianFeeeee 4980d6e81f M8/M9:细渲染接服务端真状态 + capture_photo 拍照用例
新增 mcfold/fine.py:把 M3 状态层的区块灌进细渲染器
- chunks_to_sections:铺平顺序 = (y<<8)|(z<<4)|x(与 Java PalettedContainer 一致)
  ★ 错一位不报错、只是地形静默错位 ⇒ 判据用构造数据打这个点
- 缺失的 section **省略**而非填 0(0=空气,把「没数据」渲染成空洞)
- render_from_state:单次调用,失败全部返回 ok:false + why + how_to

新增 mcfold/photo.py:capture_photo
- propose_shots 用**粗渲染观察**生成机位候选(正面/斜侧/俯视/远景/当前位置),
  每个都带「为什么选它」的依据;候选阶段不出图(省时省钱)
- 构图好坏是审美判断=决策,工具不替 agent 选

★ 关键发现:桥接比预想的干净
原以为要把 state id 反算成 name[props],但 Java 侧 set_blocks 收的
**本来就是 state id**(走官方 LevelChunk API),而 state id 本身含属性
⇒ 无损、无需转换。

修的真 bug:
1. ★★ HUD 没隐藏:桥层 RenderBackend.set_view 早有 hide_gui
   (→ 官方 options.hideGui),但 WorldRenderer **没有这个入口**
   ⇒ AttributeError 被 except: pass 吞掉 ⇒ 出图带生命条/饥饿/物品栏,
   而返回值仍写 hide_gui: True(自相矛盾)
   教训:桥层有能力 ≠ 上层有入口;except: pass 让「以为设了」与
   「实际没设」无法区分。现在 hide_gui 设不上就显式失败。
2. 俯视机位朝向不自洽(水平 yaw 退化却配写死的 pitch)→ 3D dot=0.82。
   改为由位置向量反算 yaw/pitch;正面/斜侧/远景同样修正。
3. inject_world 的 ok 自相矛盾(无区块时 ok=True 却说「没有可渲染的区块」)
4. 缺失 section 被填 0 冒充空气

实测出图:854×480、1902 种颜色,地形/天空/月亮/草地纹理正确。

判据 17 项;2 项变异全被抓。
★ 「机位朝向」判据我第一版只比水平分量 ⇒ 俯视机位恒为 0 会判红,
  改为完整 3D 点积(dot > 0.99)。

200 项全通过。
2026-09-26 13:52:33 +08:00

924 lines
46 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# mcfold 工程计划
> 最后更新:2026-09-26
> 状态标记:✅ 已完成 · 🔄 进行中 · ⬜ 未开始 · ⚠️ 有已知缺口
---
## 0. 目的(最顶层,2026-09-26 用户定调)
> 因为我们的这个插件目的就是为了**让 agent 可以借助我们提供的工具包去玩我的世界**。
★ **这是最顶层的目的,下面所有东西都是为它服务的手段。**
```
我们提供:一副「能在 MC 世界里感知 + 行动」的**工具包**
agent 提供:决策(该干什么、去哪、为什么)—— 我们**不替它决定**
```
推论(三条,改变工程重心):
1. **工具形状要照 LLM 的用法设计,不是照程序员的用法。**
mineflayer 是 `create minecraft bots with a stable, high level API` ——
它是给**写 bot 的人**用的库;我们面向的是 **LLM 直接调用**。
差别很实:参数要自解释、返回要可读、错误要说清"为什么不行",
而不是抛个异常让调用者去读栈。
2. **「能感知 + 能行动」两者缺一不可。**
只有感知(渲染)⇒ agent 是个观众;
只有行动(操作)⇒ agent 是瞎子。
所以 §1b 的「状态 / 事件 / 操作」三层是**工具包的正式骨架**,
不是"以后再补"。
3. **渲染是工具之一,不是全部。**
细渲染(出图)是工具;粗渲染(常驻感知)是工具;
`move` / `dig` / `place` / `chat` 也是工具。
之前把它当主线,是重心错了。
---
## 0b. 目标(用户定向,按时间顺序)
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,本计划的新方向)
★ **这是抽象模型的一个用例,不是全部**(用户 2026-09-26:
「机位这个只是一个具体应用场景,而不是只有这一个场景」)。
它用来验证 §1b 那套「状态 + 事件 + 操作」是否立得住 ——
若拍照能跑通,agent 的日常感知/交互也就能跑通。
> 我设想的场景是玩家可以在聊天框给 agent 发消息,然后比如让 agent 给他**拍照**,
> 如果是他和某个景物的**合照**,agent 可以先通过 **ass 粗渲染**,找好角度
> 再去**渲染实图**。
拆成可执行的三段:
```
① 玩家在聊天框发消息 → agent 通过统一接口收到
② agent 要"给玩家拍合照" → 需要:玩家位置/朝向 + 景物位置 + 相机可摆放范围
③ 选角度 → 用 **ass 粗渲染**(低成本、快)试若干机位,
挑一个好的,再用**细渲染**出图
```
★ 注意:这只是粗渲染的**一个用例**。它的本职是 **agent 的常驻感知层**
(日常游玩),见 §4。
### ★ 粗渲染与细渲染的分工(用户 2026-09-26 明确,关键)
> **粗渲染是 agent 日常游玩用的**;**细渲染是出图和 agent 判断精细状态用的**;
> **细渲染是单次调用的而非常驻的**。
| | 粗渲染(ass) | 细渲染 |
|---|---|---|
| 用途 | **agent 日常游玩** | **出图 + 判断精细状态** |
| 生命周期 | **常驻**(随 agent 会话) | **单次调用**(起 → 渲染 → 销毁) |
| 频率 | 高(agent 每一步都可能问) | 低(要图 / 需要精细判断时) |
| 延迟预算 | 毫秒级 | 20-40s 可接受 |
| 依赖 | 纯 Python(**无 JVM**) | JVM + 完整 MC 栈 + 加载器/光影 |
| 数据源 | 轻量世界快照 | 完整渲染管线 |
★ **这条分工决定了架构**:
- 粗渲染**不是**细渲染的降级,而是 agent 的**常驻感知层** ——
它要回答 agent 平时的所有感知问题(周围有什么、挡不挡视线、
前面走得通吗、那边是什么),这些都必须廉价。
- 细渲染**按需起、用完走**(JVM 启停成本不敏感)⇒ 慢一点没关系。
- ⇒ 所以**「粗渲染↔细渲染一致性」不是刚需**:两者服务于不同问题,
不要求粗渲染预测细渲染的像素。真正要测的是
**各自对各自用途的正确性**(见 4.5)。
---
## 1b. 抽象模型:参考 mineflayer 的「状态 + 事件 + 操作」三层
> 至于事件更新通报与操作抽象,我觉得可以参考 mineflayer,
> 使用它的抽象模型调用我们的后端。(用户 2026-09-26)
★ **注意**:机位/拍照只是这个抽象的**一个用例**,不是目的。
下面这套模型要覆盖 agent 的日常:感知、移动、交互、观察、问答。
### 1b.1 mineflayer 的实际形态(已读源码核实)
```
① 状态(挂在 bot 上的对象,agent 直接读)
world 方块/生物群系/光照 entities {id: Entity}
players {username: Player} time / game / controlState
inventory / experience / tablist / teamMap / spawnPoint / settings
② 事件(bot.emit(name, ...),细分层用 ':')
消息类 message / messagestr / chat / chat:<pattern> / actionBar
世界类 blockUpdate / chunkColumnLoad / weatherUpdate
实体类 entitySpawn / entityMoved / playerJoined / playerLeft
玩家类 moved / heldItemChanged / lookingAt / physicsTick
流程类 login / spawn / health / death / kicked / error
③ 操作(bot.<verb>(...))
查询 blockAt / findBlocks / raycast / nearestEntity
移动 move / look / setControlState / simpleClick
交互 dig / placeBlock / activateBlock / attackEntity / equip
社交 chat / whisper / tell
观察 saveScreenshot
容器 openContainer / clickWindow / closeWindow / …
```
**★ 三个值得直接照搬的设计**:
1. **状态是「对象」而不是「方法调用」** —— agent 读 `bot.entities` /
`bot.world` 就拿到快照,不必每次查询。
⇒ 我们的粗渲染(常驻)就该是这个角色。
2. **事件分「粗粒度 + 细分层」**(`message` 与 `chat:<pattern>` 并存)
—— agent 可以只订阅粗粒度(省 token),需要时再挂细分。
⇒ 避免「每个方块变化都推给 agent」把上下文淹掉。
3. **操作是动词,参数是结构化对象**
—— `bot.findBlocks({ matching, maxDistance, count })`
⇒ 不是「一条命令 + 位置参数」,而是「意图 + 可选约束」。
### 1b.2 映射到我们的后端
```
mineflayer 概念 我们的实现 备注
─────────────────────────────────────────────────────────────────
bot.world mcfold 的 World(已有 block_at) ★ 粗渲染的底座
bot.entities 待加:EntityStore 从 chunk/实体数据来
bot.on(...) 事件总线(待加) 粗/细分层同 mineflayer
bot.findBlocks({...}) rough.find_blocks(matching=…, …) 纯 Python,毫秒级
bot.raycast rough.cast_ray(…) DDA
bot.move / bot.look 待定:见 1b.4(我们不做移动)
bot.chat 走 agentmail / 服务端聊天 已有通路
bot.saveScreenshot WorldRenderer.screenshot() 细渲染(单次调用)
```
★ **最重要的一条**:`bot` 那些方法最终落到 `bot._client`(协议层),
而我们落到 **jar 端**(用户定调:「jar 端接受操作意图,输出包」)。
所以**抽象形状照抄 mineflayer,落地换到 jar 端** —— 这正是用户那句话的含义。
### 1b.3 我们的三层(与 mineflayer 同形)
```
状态层 常驻、无 JVM、毫秒级
World / EntityStore / 玩家与相机状态 / 时间天气
★ 这层就是「粗渲染」
操作层 意图式动词
find_blocks / cast_ray / look / say / place / dig …
多数落粗渲染(快);需精细视觉的转细渲染(慢,单次)
事件层 粗粒度 + 细分层
moved / block_changed / entity_appeared / message / chat:<pattern>
★ 分层是为省 token,不是为性能
```
### 1b.4 一个必须先定的边界
⚠️ 我们**不做移动/战斗**(用户:范围是「渲染 + 与服务端 mod 物品交互」)。
所以 mineflayer 的 `bot.move` / `dig` / `attackEntity` **不照抄** ——
它们要么不需要,要么该由「服务端交互」那条线以别的方式提供。
⇒ 待定:`bot.move` 这类「改变世界状态」的操作,在我们的定位里
究竟**不做**、还是**转交服务端**(agent 在真实世界里动,我们在旁边看/记)。
这决定 `操作层` 的边界。
---
## 1c. 空白测试服(用户 2026-09-26 定向)
> 我觉得可以**起一个空白新服**用来测试功能,而不是用现在的服。
**理由(不只是"干净")**:
- 现有服 `/home/minecraft`:world 15G、mods 64 个、neoforge 1.21.1 ——
任何协议层改动都可能污染它,且**测不出边界**(模组太多、状态太杂)。
- 我们要验的是**协议与行为**,需要**可预期**的环境:已知方块集、已知出生点、
可重置的世界。
**形态(待定,需你定)**:
| 项 | 选项 |
|---|---|
| 版本 | vanilla 1.21.1(最简,协议最干净) / neoforge 1.21.1(与现有一致) |
| 模组 | 先 0 个(验协议)→ 再逐个加(验 mod 差异) |
| 端口 | 25565(避开现役) / 25566+ |
| 目录 | 与 `/home/minecraft` **同级**(版本隔离:内容根 / 实例 / 运行期) |
| 世界 | 全新生成,固定种子(可预期) |
★ 磁盘:/home 尚有 136G ⇒ 放得下(现役 world 已 15G,空白服初期只占几十 M)。
**它同时是「版本隔离」策略的第二个实例**:
`store/vanilla-1.21.1/server/` + `runtime/test-server/` —— 与我们给
agent 建的实例同一套口径。
### ✅ M0 已完成(2026-09-26,提交 `480fe8e`)
```
形态 vanilla 1.21.1 / 0 mods / 超平坦 / 固定种子 mcfold-test-2026
creative · peaceful · 无怪物 · 无结构 · spawn-protection=0
位置 store/vanilla-1.21.1/server/ (内容根)
runtime/test-server/ (运行期:log + pid)
端口 25566(避开现役 25565)
接口 python3 -m mcfold.server {start,stop,status,reset}
探活 协议级 Server List Ping ⇒ 1.21.1 / protocol 767 / players 0/4
判据 tests/test_server.py(4 项,默认套件)+ 变异验证过(能红)
```
★ 两个踩过的坑(已写进判据守护):
1. **properties 行内注释**会被解析进值里(`k=v # c`)⇒ 生成器不写行内注释。
2. **端口要看清楚** —— 我第一版以为 25566,写完启动日志才发现跑在 25565
(配置被自己写坏后回落到默认)。**端口占用要 `ss` 实测,不能凭配置推断**。
---
## 1d. 面向 LLM 的 API 形状(§0 推论 1 的落点)
mineflayer 的 API 面向**程序员**;我们面向 **LLM**。具体差别与对策:
| 维度 | 程序员友好的做法 | LLM 需要的 | 我们的对策 |
|---|---|---|---|
| 参数 | 位置参数 + 选项对象 | **全命名、少量必填** | `find_blocks(matching=..., max_distance=...)` |
| 返回 | 原始对象/布尔 | **可读摘要 + 关键字段** | 附 `describe()`(`BlockHit` 已有) |
| 失败 | 抛异常 | **说清"为什么不行 + 怎么办"** | `source="unloaded"` / `"unknown-state"` 那种自述 |
| 规模 | 返回全部 | **限量 + 计数**(省 token) | 默认 `count=10`,另给 `total` |
| 发现 | 读文档 | **可自省** | `bot.describe()` 列出可用操作与必需参数 |
★ 已有可借鉴的样本:`BlockHit.describe()` —— 明确区分
「未加载」/「超出世界高度」/「unknown(id),本机注册表无此 state id,
可能来自 mod 方块」/ 正常。**这正是 LLM 需要的说法**:
不是报错,而是告诉它「这个方块你暂时看不到,为什么」。
⚠️ `unknown(id)` 这一点尤其关键(§0 推论):mod 方块的 state id **只有服务端知道**,
客户端本机注册表里没有 ⇒ 粗渲染遇到它必须**如实说"我不认识"**,不能瞎猜一个。
这与「与服务端 mod 物品交互」那条线直接相关。
---
## 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.<clinit>` 抛 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/<loader>-<version>/ 内容根 共享只读:jar/libraries/assets/.mcfold 产物
BASE/instances/<name>/ 实例 独占:mods/shaderpacks/config/saves
BASE/runtime/<name>/ 运行期 独占: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):agent 的常驻感知层(§1、§4)
见 §4 设计(**常驻**,与细渲染的**单次调用**互补)。
### ⬜ G5. 聊天框 → agent → 拍照 的行为链(§1,新方向)
见 §5 设计。
---
## 4. 粗渲染(ass)设计(⬜ 未开始)
### 4.1 定位:agent 的**常驻感知层**
按 §1 的分工,粗渲染服务于**日常游玩**,所以它不是一个"拍照辅助",
而是一个**常驻的、无 JVM 的世界感知接口**。
```
细渲染: JVM + 完整 MC 栈 + Iris 单次调用(起→渲→销) ~20-40s
粗渲染: 纯 Python + 世界快照 常驻 <100ms
```
### 4.2 它要回答的问题(agent 游玩时会问的)
| 问法 | 需要的能力 |
|---|---|
| 「我面前是什么?」 | 视线求交(DDA)|
| 「这堵墙后面有没有通路?」 | 射线 + 邻域查询 |
| 「我站的地方是空的吗?」 | `block_at`(已有)|
| 「那边那座山是什么?」 | 远距离扫描 / 俯瞰索引 |
| 「周围一圈都有什么?」 | 局部切面(ASCII 最有用的场景)|
| 「从这里能不能看到它?」 | 可见性测试(遮挡判定)|
**其中「ASCII 网格」只是其中一种展示形态** —— 它的价值在于
让 LLM 能直接读,而 PNG 需要多模态。
### 4.3 核心原语(借鉴 mineflayer / prismarine 的做法)
mineflayer 的 `raycast` / `blockAt` 是纯数据的视线求交 —— 我们不连服务端,
但**同样的计算**可以跑在存档/注入的世界快照上。
```python
# 基础已有(mcfold/world.py):
# World.block_at(x,y,z) -> BlockHit(name, state_id, loaded, source)
# ★ 它已能区分 unloaded / unknown-state ⇒ mod 方块不会被当成空气
# 待加(mcfold/rough.py):
def cast_ray(world, origin, direction, max_dist) -> RayHit
"""沿方向步进(DDA,与 MC 方块网格对齐),返回首个非空气方块"""
def visibility(world, eye, target) -> Visibility
"""两点之间是否通视(被几个方块挡住)"""
def slice_view(world, eye, yaw, pitch, w, h) -> AsciiGrid
"""把视野切成 w×h 的 ASCII 网格(agent 直接读)"""
def scene_summary(world, center, radius) -> dict
"""结构化线索:方块分布 / 开阔度 / 遮挡率(LLM 读数字比读 ASCII 准)"""
```
### 4.4 「ass」的三种展示形态(服务于不同用途)
```
① ASCII 网格 视野切面 —— 人看 + LLM 直接读
② 结构化线索 开阔度/遮挡率/最近景物/可通行方向 —— LLM 推理用(比①准)
③ 可选:俯视 ASCII 局部地图(w×h 格,每格一个地标字符)—— 定位用
```
### 4.5 验证口径(★ 改成"各自对各自用途",不是互相预测)
```
粗渲染的判据(不需要细渲染参与):
· block_at 已区分 unloaded / unknown-state ⇒ 不能把未加载当空气
· cast_ray 的结果与人工推算一致(可构造的简单场景:单方块、空走廊、斜坡)
· 局部切面的 ASCII 与直接读方块数据一致
细渲染的判据(不需要粗渲染参与):
· 现有:注入地形 → distinctColors / chunksRendered 达阈
· 现有:Sodium tracker 通知后 chunkReady > 0
★ **不再要求**「粗渲染能预测细渲染」—— 两者服务不同问题
(用户口径:粗=日常游玩,细=出图与精细判断)
唯一的交集是「拍照选机位」这一个用例,那里是**用法**上的配合,
不是**正确性**上的依赖。
```
### 4.6 实现落点
`mcfold/rough.py`(新模块,纯 Python、**不依赖 JVM**)
`tests/test_rough.py`(默认套件里跑,秒级)
---
## 5. 聊天框 → 拍照 行为链设计(⬜ 未开始)
### 5.1 事件流(借鉴 mineflayer 的**事件触发**形态)
```
玩家在聊天框发消息
↓
① 收消息 ClientboundSystemChatPacket(或玩家聊天)
↓
② 意图识别 "给我拍张照" / "和那座山合个影"
↓
③ 需要什么 玩家位置/朝向 + 景物位置 + 可用机位
↓
④ 规划 粗渲染(常驻)试 N 个机位 → 挑最好的
↓
⑤ 出图 细渲染(单次调用:起 JVM → 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. 里程碑(按 §0 目的重排)
★ 重心:**「让 agent 能玩」** = 工具包(感知 + 行动)。
  渲染是工具之一,不是主线;机位/拍照是用例,不是目的。
| # | 内容 | 依赖 | 状态 |
|---|---|---|---|
| **M0** | 起**空白测试服**(§1c),版本隔离三件套 | — | ✅ |
| **M1** | 修 build.sh 编译 CP(加 Fabric 库)⇒ 1.21.1 构建跑通 | — | ✅ |
| **M2** | **协议客户端**:连测试服 → configuration → registry_data → 进 play | M0 | ✅ |
| | (这是最大缺口:现有只到 `login_start` 冒烟级) | | |
| **M3** | **状态层**:World / entities / inventory 从**服务端真状态**来 | M2 | ✅ |
| **M4** | **事件层**:粗粒度 + 细分层(照 mineflayer,省 token) | M2 | ✅ |
| **M5** | **操作层**:chat / look / move / dig / place(能改世界) | M2, M3 | ✅ |
| **M6** | **粗渲染**(常驻感知):基于 M3 的真状态 | M3 | ✅ |
| **M7** | **LLM 友好的 API 形状**(§1d):自省 / 可读返回 / 限量 | M3–M6 | ✅ |
| **M8** | **细渲染**(单次调用):出图,带光影 | M2 | ✅ |
| **M9** | 第一个用例:`capture_photo()`(粗搜机位 → 细出图) | M6, M8 | ✅ |
### M2 实测结论(2026-09-26)
**已达成的可复现事实**(连 M0 测试服 127.0.0.1:25566):
```
login phase=login → success(服务端回 UUID 与离线算法一致)
configuration phase=play 注册表=11(biome/damage_type/enchantment/…)
play 28 种包 map_chunk×9、position、declare_commands、entity_metadata
position (5.5, -60.0, -0.5)
```
**踩过的 7 个真实坑**(都是「世代差异」或协议数据理解错误):
| 症状 | 根因 |
|---|---|
| `Failed to decode packet 'hello'` | 1.21.1 `login_start.playerUUID` 是**必填 UUID**;1.20 是 `['option','UUID']` |
| configuration 收不到包 | `login_acknowledged` 包 id 写死 `0x00`,实际 **`0x03`** |
| `Badly compressed packet` | 写侧没实现压缩帧:`varint(dataLen)+数据`,小包 `dataLen=0` |
| 服务端发完 `select_known_packs` 不动 | configuration 是**双向协商**,必须回 `select_known_packs`/发 `settings` |
| `registry_data` 解不开 | 缺 `anonymousNbt`;旧 NBT 只是占位符 |
| `declare_recipes` 流错位 | 数组计数有 `countType`(103处) 与 **`count`(引用 ctx,6处)** 两种 |
| `player_info`/`declare_commands` 解不开 | `compareTo` 可用**路径**:`flags/x`、`../flags/x`(12 处) |
| `entity_metadata` 越界 | 终止符是**光秃 0xFF 一个字节**,不含 type/value |
**关键架构发现(修正项目前提)**:vanilla 1.21.1 只在线同步 **11 个动态注册表**,
**不含 `minecraft:block` / `minecraft:item`**。静态注册表不走线同步 ——
客户端用自己 jar 里的。⇒ Java 侧读 client jar 的路线对 vanilla 正确;
但 **mod 新增方块 vanilla 不会告诉客户端**,那条要靠加载器自身的同步机制。
**已知上游数据缺口(非本仓 bug,已容错)**:`declare_recipes`(108KB)
在**官方 minecraft-data 3.117 + minecraft-protocol** 上同样解不开
(PartialReadError: found 108271 / expected 114153)。
处理:长度前缀是帧层权威 ⇒ **坏包整包丢弃不断流**(`decode_failures` 布告)。
### M3 实测结论(2026-09-26)
新增 `mcfold/state.py`(状态层)+ `mcfold/session.py`(包流 → 状态)。
**实测(连 M0 测试服)**:
```
connect registries=11 phase=play geometry=minecraft:overworld y∈[-64,320)
pump 9 区块 / 生命 20 / 饥饿 20 / entityId / gamemode 全落进状态
block_at (x,y-1,z) → grass_block state_id=9 source=registry
列剖面 y=-64 bedrock → -63 dirt → -62 dirt → -61 grass_block
nearby 半径 6 → 3 种(dirt 338 / bedrock 169 / grass_block 169)
视野外 source=unloaded(与 out-of-world、unknown-state 可区分)
```
**本轮挖出的真 bug**:`unload_chunk` 协议字段顺序是 **`chunkZ` 在前、`chunkX` 在后**;
我按「x 先」读 ⇒ 坐标互换 ⇒ 卸载永不命中 ⇒ 区块只增不减(状态在撒谎)。
**设计立场(重要,防越界)**:
- 只陈述观察,**不做决策**:`nearby_blocks` 给种类统计而不给「该挖哪个」。
- **未知 ≠ 没看见**:认不出的 state id 记 `unknown_state_ids` 并显式暴露,
**绝不**静默映射成别的方块(静默映射会让 agent 基于虚构世界决策)。
- **超范围要拒绝并说明**,不静默缩小(否则 agent 以为扫全了)。
- 几何从服务端 `dimension_type` 注册表吸收,不写死 24/384。
**判据**:`tests/test_state.py` 14 项;4 项变异全部被抓
(相对位移改绝对 / 未知方块静默映射 / unload 坐标交换 / keep_alive 不回应)。
★ 其中「未知方块静默映射」第一次变异**是空操作**(改错了文件)⇒ 假绿;
核对变异是否真命中后重做,才确认判据有效。**变异必须先验它落地了**。
### M4 实测结论(2026-09-26)
新增 `mcfold/events.py`(事件日志)+ session 的事件接线。
**关键设计:不是回调,而是「环形缓冲 + 游标」**
mineflayer 用回调(`bot.on('physicsTick')`),适合长跑机器人;
但 LLM agent 是**回合制**的 —— 回调对它有三个坏处:事件在两次观察之间
静默丢失、状态散在闭包里不可查、粒度不可控(physicsTick 20/s 烧穿 token)。
⇒ 用可按需 `drain()` 的日志 + 单调 `seq`,agent 能问「我看过 seq=37 之后又发生了什么」。
**实测(连测试服,含真实世界改动)**:
```
/setblock -2 -61 -5 minecraft:stone
#27 方块变化 @(-2,-61,-5) grass_block → stone
#26 系统: commands.setblock.success
以后 setblock ... air
#33 方块变化 @(-2,-61,-5) stone → air
状态层: stone(state_id=1) → air(state_id=0) ✓ 一致
细分层可查: block_changed:-2:-61:-5 ✓
```
**本轮修的真 bug(3 个)**:
1. **`dropped` 恒为 0**:`deque(maxlen=)` 丢元素**完全无声**(实测 maxlen=4 塞 10 只剩
[6,7,8,9])。这正是本层最不能犯的错 —— 事件丢了却不说,agent 以为世界没变。
⇒ 改手动判满 + 显式计数。
2. **事件键与渲染器不一致**:我发 `to_state_id`、渲染器读 `to` ⇒ 显示
「grass_block → None」(看着有事件、其实没信息)。改为解出**方块名**,
且 `from` 区分「未加载/界外」而不是裸 None(把 unloaded 读成 air 会让 agent 误判脚下)。
3. **run_tests.sh 少报判据数**:只数 `^ ✓`,我特意用 `★` 标的回归判据没被计入
(test_events 16 条只报 7 条)⇒ 少报也是撒谎。修后总数 98 → **110**。
**判据**:`tests/test_events.py` 16 项;4 项变异全部被抓
(drain 不推游标 / limit 变丢弃 / 细分层混入粗流 / 实体增量不除 4096)。
### M5 实测结论(2026-09-26)
新增 `mcfold/actions.py`(16 个操作)+ `mcfold/textcomp.py`(聊天文本组件)。
**实测(真服务器,真正改变了世界)**:
```
dig(7,-60,10) → 方块变化 grass_block → air ✓
place(7,-61,10, face=up) → air → stone ✓
look(90,45) / select_slot / jump / sneak / sprint / swing 均被服务端接受、连接不崩
```
**★★ 本轮最重要的发现:`teleport_confirm` 是必需前置包**
1.19+ 的 `position` 包带 `teleportId`,客户端**必须**回 `teleport_confirm`,
**否则服务端默默忽略所有方块操作**(dig/place/use 全部无效)。
- 症状:dig 一开始就失败、place 永远放不上去,**没有任何报错**
- 我为此绕了一大圈:改了包名、改了字段顺序、改了 face 类型、
测了 sequence 递增、试了 5 种位置与 6 种面 —— 全都不对
- **决定性一步是拿成熟实现 minecraft-protocol 做对照**:
它发同样的包**也放不上去** ⇒ 排除了「我的包写错了」
- 顺带用 `data get entity` 排除了权限/物品/位置问题
(`playerGameType=1` 创造、`SelectedItem={count:64,id:stone}`、Pos 正确)
- 补发 `teleport_confirm` 后**立刻成功**(air → stone)
⇒ **教训:静默失败要靠对照物定位,不要靠猜。** 猜了 5 轮都不如
一次「同样的事别人怎么做」。
**其他真 bug(判据抓到的)**:
1. `packet_interact` / `packet_swing` / `packet_animation` 在 1.21.1 **都不存在**
(正确是 `packet_use_entity` / `packet_arm_animation`)⇒ 我一次写了三个不存在的包名
2. dig 原写成 `packet_block_place` + 2 个多余 varint;正确是**独立的 `packet_block_dig`**
⇒ 多写字节把服务端解码器弄崩、**连接被断**(BrokenPipe),表面只是「挖了没反应」
3. `use_entity`(mouse=0) **也带 hand 字段**(我以为只有 mouse=2 带)
⇒ 包少 1 字节。**这条是「把构造的包用官方 protodef 解回来」判据抓到的**
4. 1.21.1 背包字段是 **`itemId`**(驼峰),我写 `item_id` ⇒ 永远判「手上是空的」
5. window0 的 slot **36..44 才是热键栏**(0..8 是创作物品栏),索引搞错就看错物品
6. 聊天是 NBT 文本组件,三重漏:只取 `text`→空串;translate 原样吐→键名不是人话;
`with` 里数字存成 `{"":1}` 空键没读→「Gave Dirt to 」。
现在查 language.json 正确渲染成 `Gave 1 Dirt to mcfold_act`
**判据**:`tests/test_actions.py` 16 项;5 项变异全部被抓。
核心判据是「**16 个操作的包都能被官方 protodef 精确解回(0 余字节)**」——
这是唯一能**离线**证明「包写对了」的办法(本层缺陷几乎全是静默的)。
### M6 实测结论(2026-09-26)
新增 `mcfold/rough.py`(纯 Python、**不依赖 JVM**):agent 的常驻感知层。
**回答的五类日常问题**:
`raycast`(挡不挡视线/前面是什么)、`can_see`、`can_stand`、`walkable`(走得通吗)、
`slice_view`(ASCII 切面)、`surroundings`(一次总览,省往返)。
**实测(真服务器)**:
```
朝向 南(+Z) 脚下 grass_block 可站立 True 前面 clear
可行走网格 11×11:120 可站 / 1 障碍 / 0 未知
南北切面(宽 24 高 9):
| | ← 8 行空气
|gggggggggggggggggggggggg| ← grass 层
|dddddddddddddddddddddddd| ← dirt
|dddddddddddddddddddddddd|
```
**判据口径(plan §4.5)**:粗渲染不需要细渲染参与 —— 用**构造场景**判
(线上 flat world 太平凡,测不出射线对不对):单格薄墙、空走廊、斜坡、
未加载区、缺残注册表。
**本轮修的真 bug(3 个)**:
1. **`can_see` 越过目标撞身后的墙**:我射到 `max_distance` 再看首个命中
是不是目标格 ⇒ 目标 (1.5,2,0.5) 在墙前,射线越过它撞到 x=3 的墙,
把「看得见」误报成「被挡」。改为**射程只到目标距离**。
2. **切面从脚下往上取 ⇒ 全是空气,看不出地形**:改为基准下移,
让地面线落进画面(实测从「9 行全空」变成能看见 g/d 分层)。
3. **`walkable` 网格的 `?` 覆盖不足**:原来只统计「未加载」,
已加载但**认不出名字**的方块会被漏算。
**判据**:`tests/test_rough.py` 20 项;6 项变异全部被抓。
★ 其中变异 F(认不出的方块改画 `.`)**第一次没抓到** —— 因为我的
「未知格」判据只构造了**未加载**路径,覆盖面不足 ⇒ 补上「已加载但缺残注册表」
的场景后才抓住。**变异抓不到时要先怀疑判据覆盖面,而不是以为实现没问题。**
### M7 实测结论(2026-09-26)
新增 `mcfold/api.py`:**23 个工具**(观察 14 / 事件 1 / 操作 7 / 会话 2),
外加 `call(name, **kwargs)` 派发入口。
| 维度 | 做法 | 判据 |
|---|---|---|
| 参数 | **全部 keyword-only**(坐标必须按名字传) | 反射检查参数种类 |
| 返回 | 每条结果带 `describe`(人类可读摘要) | 12 个观察工具逐个查 |
| 失败 | **永不抛异常**:`ok:false` + `why` + `how_to` | 10 组垃圾输入全要结构化答复 |
| 规模 | 限量 + `total`/`remaining`/`truncated` | 事件限量报剩余、超限拒绝并说明 |
| 发现 | `describe()` 一次列全工具 + 约定 + 4 条提示 | 目录与实现**双向一致** |
**实测(真服务器)**:
```
status() phase=play protocol=767 chunks=9 entities=4 decode_failures=1
where_am_i() 在 (-2.5,-60.0,5.5) 朝向南(+Z),踩着 grass_block
block_at(9999,60,9999) 未加载(该区块还没收到) ← 不谎称空气
inventory() 手上(第 0 格): item#1(1.20.5+ 不传物品名)
dig(x=..,y=..,z=..) ok=True → events 收到 block_changed
```
**判据抓到的真问题**:
1. **坐标工具允许位置传参**(`block_at(0,0,0)`)⇒ LLM 看到 `0,0,0`
不知道哪个是 x/z。已把**全部工具改成 keyword-only**。
2. 我第一版判据写成「不能有必填参数」,把「坐标必需」误判成违规
⇒ 判据改为检查**参数种类**(关键字专用)—— 这两件事不是一回事。
3. 5 项变异全部被抓(异常外泄 / 未知工具抛 KeyError / 物品编名字 /
不报剩余 / 自省少列工具)。
**★ 一条操作纪律(我自己的失误)**:变异测试时用 `/tmp/state.bak` 恢复文件,
那份备份是**过期**的(少了 M6 新增的 113 行)⇒ 把好代码覆盖坏了,
且让其中一个变异「看起来被抓了」——其实是坏代码造成的。
⇒ **恢复文件必须用 git(已知正确版本)**,且**每个变异单独确认基线是绿的**。
修好基线后重做该变异,才确认判据真的有效。
### 寻路(用户 2026-09-26 追加裁定,编号外的独立一节)
用户裁定要点:
- **完整版**寻路 + **可达性判断**,且这两个是**两个独立工具**
- 执行:两者都给(逐步 / 整段)
- 未加载:**走到边界为止 + 如实报告**(绝不乐观推断)
新增 `mcfold/pathfind.py` + API 的 `reachable` / `find_path`(工具数 23→25)。
**实测(构造场景 + 真服务器)**:
```
平地 reachable=True find_path 10 步纯 forward 改世界 0
1 格障碍 reachable=True step_up/jump 跨过 改世界 0
2/3/4 格墙 reachable=False 挖穿一列 2/3/4 格,无空气 改世界 N
深坑 3 格 reachable=False place 搭桥 1 块
岩浆沟 reachable=False place 搭桥
未加载边界 reachable=False stopped_because=hit_unloaded
停在 (15,-60,8),走了 19 步 +「再往前没数据」
```
**本轮修的真 bug(6 个)**:
1. **`is_ground` 与 `is_standable` 语义混淆**(差一格):
混用导致平地上找不到落脚点,寻路只肯搭桥。
2. **`can_already_pass` 判据用错对象**(拿「更上方格子 standable」
当「已能通过」,而那是挖完才成立的)⇒ dig 永不生成,2 格墙走不通。
3. **dig 只支持挖 1 格** ⇒ 4 格墙永远走不通。改为「挖穿一列」,
代价按格数线性增长(挖 4 格远贵于挖 1 格 ⇒ A* 先试绕)。
4. **代价权重失衡**:1 格台阶被挖而不是 step_up
(dig 6.0 vs 上下一级 2.7)⇒ 荒谬地挖穿一排墙。
调高 dig/place 代价 + 加「背离目标的额外代价」后修正。
5. **未加载被报成 `exhausted`**(听起来像「地形不通」):
根因是 `neighbors()` 在未加载格上不生成移动 ⇒ A* 里
「if not known(...)」**永远执行不到**。改为主动探测边界。
6. `Terrain.known` 调用签名错、挖清单会包含空气(挖到墙顶之上)。
**判据**:`tests/test_pathfind.py` 19 项;5 项变异全部被抓。
★ 其中「岩浆当地面」这个变异**第一次没被抓** —— 查下去发现我的判据
测的是**冗余代码**:`lava`/`water` 本来就 `solid=False`,
去掉 hazard 检查也不会红。
真正需要 hazard 检查的是 **solid=True 但致命**的
(`cactus`/`magma_block`/`campfire`/`scaffolding`)⇒ 改用这些方块才抓到。
**判据必须打在「真正承载语义的那段代码」上,否则测的是冗余。**
### M8 / M9 实测结论(2026-09-26)
新增 `mcfold/fine.py`(细渲染接真状态)+ `mcfold/photo.py`(拍照用例)。
**关键发现:桥接比预想的干净。**
我原以为要把 M3 的 state id 反算成 `name[props]`,但查 Java 侧
`set_blocks` 发现它收的**本来就是 state id**(走官方 `LevelChunk` API),
而 state id 本身已含属性 ⇒ **无损、无需转换**。
**M9 真实出图**(854×480,1902 种颜色,地形/天空/月亮/草地纹理都对):
```
机位候选(纯粗渲染观察,0 出图成本):
[0] 正面 依据:站在目标正前方 3 格(0,-61,10 有 grass_block 支撑)
[1] 斜侧 依据:侧 45°、距离 3 格(6,-61,12 有支撑)
[2] 俯视 依据:目标正上方 4 格(朝向由位置反算)
[3] 远景 依据:8 格外(近处视线:blocked)
[4] 当前位置 依据:就用你现在的视角
出图:columns_rendered=9 hide_gui=True
```
**本轮修的真 bug(4 个)**:
1. **HUD 没隐藏**(plan 明确要求截图隐藏 HUD):
桥层 `RenderBackend.set_view` 早就实现了 `hide_gui` → 官方 `options.hideGui`,
但 **`WorldRenderer` 根本没有这个入口** ⇒ 我调它时撞 AttributeError,
又被 `except: pass` 吞掉 ⇒ **出图带着生命条/饥饿/物品栏**,
而返回值还写着 `hide_gui: True`(自相矛盾,没人发现)。
教训:**桥层有能力 ≠ 上层有入口**;且 `except: pass` 会把「以为设了」
与「实际没设」变成无法区分。现在 hide_gui 设不上就**显式失败**。
2. **俯视机位的朝向不自洽**:机位在目标正上方 4 格,水平 yaw 已退化
(dx,dz≈0),我却写死 `pitch=-35` 配水平算出的 yaw
⇒ 实测 3D dot=0.82(相机没真对着目标)。改为**由位置向量反算** yaw/pitch。
顺带修了正面/斜侧/远景(高度不同但 pitch 写死 0)。
3. `inject_world` 的 `ok` 自相矛盾:一个区块都没有时返回 `ok=True`,
而 note 却在说「没有可渲染的区块」。统一为「有成功注入」。
4. 缺失的 section 被填 0(=空气)⇒ 把「没数据」渲染成空洞。
改为**省略**(Java 侧留空)。
**判据**:`tests/test_photo.py` 17 项;2 项变异全被抓
(删掉 set_view / hide_gui 又被静默吞)。
★ 其中「机位朝向」那条判据我第一版**只比水平分量** ⇒ 对俯视机位恒为 0,
会把正确实现判红。改为**完整 3D 点积**(dot > 0.99)。
**建议顺序:M0 → M1 → M2**。
- M0/M1 是解阻断(M1 是我已经查到根因的那个 classpath bug)
- **M2 是真正的门槛**:从「能渲染」到「能玩」,缺的不是渲染,是**完整协议层**。
mineflayer 的 41 个 plugins 全建立在它之上,我们目前只到 `login_start`。
★ M8(细渲染)**不是**早期优先级 —— 它已经能跑(Iris 光影 + 地形,
`colors=113546` 是实测),只是**接不上服务端真数据**。等 M2/M3 通了,
它自然就在链路上。
★ M9(拍照)放最后 —— 它是**验证工具包立不立得住**的用例,
不是工具包本身。
## 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/<name>/
构建产物 内容根/.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
```