加 plan.md:工程计划(含 ass 粗渲染与聊天拍照的新方向)
This commit is contained in:
394
plan.md
Normal file
394
plan.md
Normal file
@ -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.<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 粗渲染(§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/<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
|
||||
```
|
||||
Reference in New Issue
Block a user