Files
agentmc/plan.md
JianFeeeee 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

656 lines
31 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` 布告)。
**建议顺序: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
```