Files
agentmc/plan.md

514 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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. 目标(用户定向,按时间顺序)
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 在真实世界里动,我们在旁边看/记)。
这决定 `操作层` 的边界。
---
## 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. 里程碑(建议顺序)
| # | 内容 | 依赖 | 状态 |
|---|---|---|---|
| M1 | 修 G1(build.sh 编译 CP 加 Fabric 库),1.21.1 构建跑通 | — | 🔄 |
| M2 | **抽象模型骨架**:状态/事件/操作三层(照 mineflayer 形状) | — | ⬜ |
| M2a | └ `rough.py` 常驻粗渲染(World 视图 + DDA 视线 + ASCII/线索) | M2 | ⬜ |
| M2b | └ 事件总线(粗粒度 + 细分层) | M2 | ⬜ |
| M3 | 粗渲染自身判据(构造场景;**不要求预测细渲染**) | M2a | ⬜ |
| M4 | `capture_photo()`(**作为第一个用例**:粗搜机位 → 细出图) | M2a, M2b, M3 | ⬜ |
| M5 | 服务端交互(configuration / registry_data / 物品表) | M1 | ⬜ |
| M6 | 聊天框 → agent → 拍照 全链(§5) | M1, M4, M5 | ⬜ |
**建议先做 M1 + M2**:M1 解阻断;M2 是**主线**(抽象模型骨架),
拍照只是它的第一个用例。M2 做完后 agent 就有常驻感知能力了,
而这正是"日常游玩"的地基。
★ M3 特意**不叫**"一致性判据":按用户口径粗/细服务于不同问题,
不要求粗渲染预测细渲染的像素。M3 只验粗渲染**自己**对日常感知问题的正确性
(构造简单场景,人工可推算)。
---
## 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
```