Files
agentmc/tests/test_render_bridge.py
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

185 lines
6.9 KiB
Python
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.

"""渲染桥联测:Python ↔ Java 渲染器。
需要:
- 已构建的 client-named.jar + McfoldRenderServer.class
- 环境变量 MCFOLD_RENDER_CLASSPATH、MCFOLD_NATIVES_DIR
- Xvfb DISPLAY
缺任一项则跳过(不误报失败)。
"""
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from mcfold.render import (
SECTION_VOLUME, _encode_paletted, _varint, parse_save_chunk,
)
from mcfold.render_bridge import RenderBridge, RenderBridgeError
SAVE = Path("/home/mc-test/mc-test")
STORE = Path("/home/newqqagent/mcview/store/vanilla-1.20.1")
def _bridge_available() -> tuple[bool, str]:
cp = os.environ.get("MCFOLD_RENDER_CLASSPATH")
if not cp:
return False, "未设 MCFOLD_RENDER_CLASSPATH"
paths = cp.split(os.pathsep)
if not any("client-named" in p for p in paths):
return False, "classpath 缺 client-named.jar"
if not any("McfoldRenderServer" in p or "classes" in p for p in paths):
return False, "classpath 缺 McfoldRenderServer.class"
if not os.environ.get("DISPLAY"):
return False, "无 DISPLAY"
return True, ""
def test_bridge_protocol():
"""桥的协议层:registry / lookup / resolve —— 不涉及渲染。"""
ok, why = _bridge_available()
if not ok:
print(f" - 跳过({why})")
return
br = RenderBridge(
game_dir=str(STORE / "instances" / "default"),
assets=str(STORE / "assets"), asset_index="1.20.1",
)
try:
br.start()
except RenderBridgeError as e:
print(f" - 跳过(启动失败:{str(e)[:80]})")
return
try:
reg = br.registry_info()
assert reg["ok"] and reg["blockCount"] > 500, reg
assert reg["stateCount"] > 1000, reg
# state id → 方块名(含属性)
st = br.lookup_state(1)
assert st["block"] == "minecraft:stone", st
assert st["inRange"] is True
# 方块名(+属性) → state id
r = br._request({"cmd": "resolve", "blocks": [
"minecraft:stone",
"minecraft:oak_log[axis=y]",
"minecraft:grass_block[snowy=false]",
"minecraft:this_block_does_not_exist",
]})
ids = r["ids"]
assert ids[0] > 0, "stone 应有有效 id"
assert ids[1] > 0, "oak_log[axis=y] 应可解析"
assert ids[2] > 0, "grass_block 应可解析"
assert ids[3] == -1, "不存在的方块应返回 -1"
assert r["unknown"] == 1
print(f" ✓ 桥协议(blocks={reg['blockCount']}, states={reg['stateCount']}, "
f"resolve 3 中 / 1 未知)")
finally:
br.stop()
def test_bridge_render_synthetic():
"""合成数据渲染:验证「注入 → 网格重建 → 出图」全链路。"""
ok, why = _bridge_available()
if not ok:
print(f" - 跳过({why})")
return
br = RenderBridge(
game_dir=str(STORE / "instances" / "default"),
assets=str(STORE / "assets"), asset_index="1.20.1",
)
try:
br.start()
except RenderBridgeError as e:
print(f" - 跳过(启动失败:{str(e)[:80]})")
return
try:
# 先设视野中心(否则区块被丢弃)
br._request({"cmd": "viewport", "cx": 0, "cz": 0, "radius": 12})
# 造:地板 stone(索引 4 = y=0 = 世界 y=64)+ 中间一根原木
stone = br._request({"cmd": "resolve", "blocks": ["minecraft:stone"]})["ids"][0]
log = br._request({"cmd": "resolve", "blocks": ["minecraft:oak_log[axis=y]"]})["ids"][0]
assert stone > 0 and log > 0
payload = bytearray()
for sy in range(24):
ids = [0] * SECTION_VOLUME
if sy == 4:
ids = [stone] * SECTION_VOLUME # 地板(世界 y=64)
elif sy in (5, 6, 7):
for x in range(7, 9): # 中间 2x2 柱子
for z in range(7, 9):
ids[x + z * 16] = log
non_empty = sum(1 for v in ids if v != 0)
payload += (non_empty & 0xFFFF).to_bytes(2, "big")
payload += _encode_paletted(ids, SECTION_VOLUME)
payload += bytes([0]) + _varint(1) + _varint(0)
items = [(cx, cz, bytes(payload)) for cx in range(-3, 4) for cz in range(-3, 4)]
fed = br._request({"cmd": "chunks", "items": [
{"cx": cx, "cz": cz, "data": __import__("base64").b64encode(d).decode()}
for cx, cz, d in items
]}, timeout=180)
assert fed["failed"] == 0, f"注入失败: {fed.get('firstError')}"
assert fed["loadedChunks"] > 0, f"仓库为空: {fed}"
br.set_camera(8.0, 72.0, 8.0, yaw=45.0, pitch=25.0)
res = br.render(frames=80)
assert res.width > 0 and res.height > 0
assert res.chunks_rendered > 0, f"没有区块被渲染: {res.raw}"
assert res.distinct_colors > 400, f"画面过单调: {res.distinct_colors}"
assert res.has_world_content is True
print(f" ✓ 合成数据渲染(chunks={res.chunks_rendered}, "
f"colors={res.distinct_colors}, {len(res.image)}B PNG)")
finally:
br.stop()
def test_bridge_render_real_save():
"""真实存档渲染:最终验收判据。"""
ok, why = _bridge_available()
if not ok:
print(f" - 跳过({why})")
return
if not (SAVE / "level.dat").is_file():
print(" - 跳过(无测试存档)")
return
from mcfold.render import WorldRenderer
wr = WorldRenderer.open(store=str(STORE), java="java")
try:
info = wr.load_save(SAVE, center=(8, -7), radius=4)
assert info["injected"] > 0, info
assert info["unknownBlocks"] == 0, f"有未解析方块: {info}"
wr.camera(x=130.0, y=95.0, z=-110.0, yaw=135.0, pitch=35.0)
dbg = wr.debug()
assert dbg["loadedChunks"] > 0, f"仓库为空: {dbg}"
assert dbg["playerChunk"] == "8,-7", f"相机区块不对: {dbg}"
res = wr.screenshot(frames=100)
assert res.chunks_rendered > 0, res.raw
assert res.distinct_colors > 2000, f"真实地形应色彩丰富: {res.distinct_colors}"
assert res.has_world_content is True
print(f" ✓ 真实存档渲染(injected={info['injected']}, "
f"specs={info['uniqueSpecs']}, chunks={res.chunks_rendered}, "
f"colors={res.distinct_colors})")
finally:
wr.close()
if __name__ == "__main__":
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
failed = 0
for t in tests:
try:
t()
except Exception as e:
failed += 1
import traceback
print(f" ✗ {t.__name__}: {e}")
traceback.print_exc()
print(f"\n渲染桥联测: " + ("全部通过" if failed == 0 else f"{len(tests)-failed}/{len(tests)} 通过"))
sys.exit(1 if failed else 0)