Files
agentmc/scaffold/README.md
JianFeeeee 34ed70ab53 修复视距不生效 + 接入统一测试
视距(用户报「无法调整视距?」)——根因是三处不同步:
  1. options.renderDistance        本地设置(原先只设了这个)
  2. options.setServerRenderDistance 服务端下发上限(getEffectiveRenderDistance = min(两者))
  3. ClientChunkCache.updateViewRadius storage 半径
第三处最关键:官方只在收到服务端半径包时才调它(ClientPacketListener:2316),
我们不连服 ⇒ Storage.inRange 用旧半径判定,超出区块被**静默丢弃**。

另一个前置:加载半径必须跟随视距。视距 12 只喂 3 圈数据,
画面看起来就像没生效(最容易误判的坑)。

实测(同相机同存档):
  视距 4  → 注入 81   渲染 55   色彩 15226
  视距 12 → 注入 595  渲染 447  色彩 18477
画面差异显著:RD=12 能看到延伸到地平线的森林与完整海岸线。
(sample-render-distance-{4,12}.png)

测试:新增 tests/test_anvil.py(13 项,存档/NBT/编码)
      tests/test_render_bridge.py(3 项,桥协议+合成渲染+真实存档渲染)
      run_tests.sh 改为默认跑单元测试、--with-render 跑联测
      合计 61 项全绿(原 48 + 13)
2026-09-25 17:47:58 +08:00

140 lines
5.9 KiB
Markdown
Raw Permalink 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 scaffold —— 用官方客户端渲染,但不跑客户端
**目标**(用户 2026-09-25 定向):不起完整客户端,把 MC 的渲染能力抽出来,
用我们自己的世界数据驱动它出图。
## 实测结论(2026-09-25 全绿)
客户端 jar 是 **ProGuard 全量混淆**的(`LevelRenderer` 叫 `fjv`),
所以「抽渲染器」的第一步必须是**反混淆**。全部关卡已验证:
| 关卡 | 证据 | 结果 |
|---|---|---|
| 反混淆 | `client-named.jar`;`javap` 读出 `LevelRenderer` | ✅ |
| GL 离屏 | Xvfb + llvmpipe → OpenGL 4.5 core | ✅ |
| 渲染栈初始化 | `new Minecraft(cfg)` 完成初始化,**不进主循环** | ✅ |
| 资源重载 | 22 个 listener;ModelManager/FontManager/Dispatcher 全 ok | ✅ |
| 注册表 | 客户端自带 `data/` → dimension_type=4 | ✅ |
| 区块注入 | 81 chunks → `countRenderedChunks=29` | ✅ |
| **真实世界渲染** | 草地+原木+树叶+天空+HUD 文字,`distinct colors=1450` | ✅ |
见 `sample-world-render.png`。
## 为什么渲染器不能独立 new
反编译(Vineflower)实测签名:
```java
LevelRenderer(Minecraft, EntityRenderDispatcher, BlockEntityRenderDispatcher, RenderBuffers)
renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f)
```
全挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。
正确形态是「初始化 MC 渲染栈,但不跑主循环」。
## 权威序列
初始化(反编译 `net.minecraft.client.main.Main`):
```java
SharedConstants.tryDetectVersion(); // 漏了 → "Game version not set"
SharedConstants.enableDataFixerOptimizations();
Bootstrap.bootStrap();
RenderSystem.initRenderThread();
RenderSystem.beginInitialization();
Minecraft mc = new Minecraft(gameConfig);
RenderSystem.finishInitialization();
```
渲染一帧(反编译 `Minecraft.render()`):
```java
RenderSystem.clear(16640, ON_OSX);
mainRenderTarget.bindWrite(true);
gameRenderer.render(partialTick, nanos, true);
mainRenderTarget.unbindWrite();
mainRenderTarget.blitToScreen(w, h);
window.updateDisplay();
```
## 踩过的坑(全部有实测证据)
1. **漏 `tryDetectVersion()`** → `DataFixers.<clinit>` 抛 `Game version not set`
2. **`FolderData` 参数顺序** = `(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)`
—— 写错则 `icons/icon_16x16.png` 404
3. **assetIndex 要用落盘名**(`1.20.1`)而非 `version.json` 的 `assetIndex.id`(`5`)
—— MCLC 按 `version.number` 落盘
4. **`LoadingOverlay` 必须摘** → 否则永远渲出 Mojang 加载屏
5. **`ReceivingLevelScreen`("Joining world...")必须清** → 否则客户端认为未进世界
6. **必须跑 `mc.tick()`** → 区块网格重建队列靠 tick 推进(不跑则 `countRenderedChunks=0`)
7. **`reloadResourcePacks()` 不可 join**(等主循环 → 死锁);
正解是**直接调各 listener 的 public `reload()`**,barrier 同步返回、
apply 用 `Runnable::run`(主线程即渲染线程)
8. **`GameRenderer.reloadShaders` 包私有** → 同包 bridge 调用
9. **读像素必须绑 FBO**(`glBindFramebuffer(target.frameBufferId)` + `GL_COLOR_ATTACHMENT0`);
`bindRead()` 路径实测读到的是残留缓冲
10. **判据要锐利**:`non-black=100%` 会被红底加载屏骗过;
`distinct colors` 才区分得出「加载屏(199) vs 世界(1450)」
## 注入世界数据
```java
LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz));
chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false);
// ★ 放进 ClientChunkCache.Storage.chunks(反射 getIndex)
// ★ 光照:LightEngine.queueSectionData(SKY/BLOCK, SectionPos, DataLayer 全15)
// ★ 触发重建:levelRenderer.allChanged() + 每帧 mc.tick()
```
**与 mcfold Python 客户端的接口**:`ClientChunkCache.replaceWithPacketData(...)`
接收的正是 `map_chunk` 包原始字节 —— **Python 客户端收到的就是它**,
可零转换直通;或用 Python 解码后的方块写入 `LevelChunk`。
## 注册表:state id ↔ 方块名(mod 支持的关键)
```java
BuiltInRegistries.BLOCK.size() // 1003
Block.BLOCK_STATE_REGISTRY.size() // 24135
Block.stateById(1003) // note_block[instrument=xylophone,note=7,powered=false]
BuiltInRegistries.BLOCK.getKey(state.getBlock())
```
★ **更正附录 D 的一个错误结论**:`1003 → note_block` **不是**静默误映射
—— note_block 有 800 个状态,1003 真的落在其范围内。真客户端给的是**完整状态**
(`instrument/note/powered`),这正是 1.20.1 上 mod 方块语义的唯一权威来源。
## 构建
```bash
./build.sh /path/to/store/vanilla-1.20.1 1.20.1
```
## 视距(render distance)
★ **三处都要设**,缺一则视距不生效(全部实测):
```java
mc.options.renderDistance().set(rd); // 本地设置
mc.options.setServerRenderDistance(rd); // 模拟服务端下发的上限
level.getChunkSource().updateViewRadius(rd); // ClientChunkCache 的 storage 半径
```
为什么第三处必需:官方只在**收到服务端半径包**时才调 `updateViewRadius`
(`ClientPacketListener.java:2316`)。我们不连服,所以必须自己调 ——
否则 `Storage.inRange` 用旧半径判定,超出的区块被**静默丢弃**。
★ **加载半径必须跟随视距**:视距设 12 但只喂 3 圈数据,
画面看起来就像视距没生效(这是最容易误判「设了没用」的坑)。
实测对比(同一相机、同一存档):
| 视距 | 注入区块 | 渲染区块 | distinctColors |
|---|---|---|---|
| 4 | 81 | 55 | 15226 |
| 12 | 595 | **447** | 18477 |
见 `sample-render-distance-4.png` / `sample-render-distance-12.png`。
雾(fog)也由同一处驱动:`GameRenderer.renderLevel` 每帧取
`options.getEffectiveRenderDistance() * 16` 传给 `FogRenderer.setupFog`。