新增 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 项全通过。
924 lines
46 KiB
Markdown
924 lines
46 KiB
Markdown
# 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
|
||
```
|