121 lines
5.3 KiB
Python
121 lines
5.3 KiB
Python
"""世界模型回归:区块聚合 + 世界坐标查询 + 显式降级。"""
|
||
import sys, os, json, subprocess
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from mcfold.world import World, in_world
|
||
from mcfold.chunk import BlockRegistry, MIN_Y, WORLD_HEIGHT
|
||
from mcfold.protodef import Writer
|
||
|
||
RUNTIME = "/home/newqqagent/plugin_data/mc/runtime"
|
||
BLOCKS = f"{RUNTIME}/node_modules/minecraft-data/minecraft-data/data/pc/1.20/blocks.json"
|
||
|
||
def reg(): return BlockRegistry.from_json_file(BLOCKS)
|
||
|
||
def test_negative_coords():
|
||
"""★ 负坐标定位:MC 世界坐标可为负,这是最容易错的地方。"""
|
||
w = World(reg())
|
||
# 在 (-1,-1) 区块放一个方块:(x=-1,z=-1) ⇒ chunk (-1,-1), 内部 (15,15)
|
||
assert (-1) >> 4 == -1 and (-1) & 15 == 15
|
||
# 先确认未加载时报 unloaded(而不是假装空气)
|
||
hit = w.block_at(-1, 64, -1)
|
||
assert not hit.loaded and hit.source == "unloaded", hit
|
||
print(" ✓ 负坐标:未加载区块显式返回 unloaded(非假装空气)")
|
||
|
||
def test_unloaded_vs_air_distinction():
|
||
"""★ 关键语义:「未加载」与「空气」必须可区分。
|
||
|
||
mineflayer 的 bot.blockAt 在未加载时返回 null,但很多调用方当空气处理;
|
||
我们要求显式区分,避免 agent 误判。
|
||
"""
|
||
w = World(reg())
|
||
unloaded = w.block_at(0, 64, 0)
|
||
assert unloaded.source == "unloaded", unloaded
|
||
# 加载一个全空气区块后,同一坐标应变成 air(registry)
|
||
wr = Writer()
|
||
for _ in range(24):
|
||
wr.write((0).to_bytes(2,"big")); wr.write(bytes([0]))
|
||
wr.varint(0); wr.varint(0)
|
||
wr.write(bytes([0])); wr.varint(0); wr.varint(0)
|
||
w.load_chunk_data(0, 0, wr.getvalue())
|
||
air = w.block_at(0, 64, 0)
|
||
assert air.loaded and air.name == "air", (air.loaded, air.name)
|
||
print(" ✓ 未加载(unloaded) 与 空气(air) 语义分明")
|
||
|
||
def test_real_chunk_via_world():
|
||
"""用 prismarine 产出的真实区块走完 World 全路径。"""
|
||
r = subprocess.run(["node", f"{RUNTIME}/chunkgen.js"], capture_output=True,
|
||
text=True, cwd=RUNTIME, timeout=120)
|
||
if r.returncode != 0:
|
||
print(f" - 跳过({r.stderr[:80]})"); return
|
||
d = json.loads(r.stdout)
|
||
w = World(reg())
|
||
w.load_chunk_data(0, 0, bytes.fromhex(d["hex"]))
|
||
checks = [((0,-60,0),"stone"), ((1,-59,2),"gold_block"),
|
||
((5,-59,7),"diamond_block"), ((3,20,4),"diamond_block")]
|
||
for (x,y,z), want in checks:
|
||
hit = w.block_at(x,y,z)
|
||
assert hit.loaded and hit.name == want, (x,y,z,hit)
|
||
print(f" ✓ World 全路径: 4/4 方块命中(解真实区块数据)")
|
||
|
||
def test_world_height_bounds():
|
||
w = World(reg())
|
||
assert w.block_at(0, MIN_Y-1, 0).source == "out-of-world"
|
||
assert w.block_at(0, MIN_Y+WORLD_HEIGHT, 0).source == "out-of-world"
|
||
assert w.block_at(0, MIN_Y, 0).source == "unloaded" # 合法高度但未加载
|
||
assert in_world(MIN_Y) and not in_world(MIN_Y-1)
|
||
print(f" ✓ 世界高度边界: [{MIN_Y}, {MIN_Y+WORLD_HEIGHT}) 之外显式 out-of-world")
|
||
|
||
def test_unknown_state_reported():
|
||
"""★ mod 方块的可见降级:unknown(id) 且计入统计。"""
|
||
r = reg()
|
||
w = World(r)
|
||
wr = Writer()
|
||
# section0: palette=[99999]
|
||
wr.write((1).to_bytes(2,"big")); wr.write(bytes([4]))
|
||
wr.varint(1); wr.varint(99999); wr.varint(256)
|
||
wr.write(b"\x00"*(256*8))
|
||
wr.write(bytes([0])); wr.varint(0); wr.varint(0)
|
||
for _ in range(23):
|
||
wr.write((0).to_bytes(2,"big")); wr.write(bytes([0]))
|
||
wr.varint(0); wr.varint(0)
|
||
wr.write(bytes([0])); wr.varint(0); wr.varint(0)
|
||
w.load_chunk_data(0, 0, wr.getvalue())
|
||
hit = w.block_at(0, MIN_Y, 0) # section 0 的首个方块
|
||
assert hit.source == "unknown-state", hit
|
||
assert "mod" in hit.describe(), hit.describe()
|
||
s = w.summary()
|
||
assert s.get("unknown_state_count") == 1, s
|
||
assert "mod" in s.get("warning",""), s
|
||
print(f" ✓ ★ mod 方块可见降级: {hit.describe()[:40]}… + 世界摘要含告警")
|
||
|
||
def test_multiple_chunks():
|
||
"""多区块聚合:跨区块查询各自命中自己的列。"""
|
||
w = World(reg())
|
||
wr = Writer()
|
||
for _ in range(24):
|
||
wr.write((0).to_bytes(2,"big")); wr.write(bytes([0]))
|
||
wr.varint(0); wr.varint(0)
|
||
wr.write(bytes([0])); wr.varint(0); wr.varint(0)
|
||
data = wr.getvalue()
|
||
for cx, cz in [(0,0), (1,0), (-1,-1), (2,-3)]:
|
||
w.load_chunk_data(cx, cz, data)
|
||
assert w.loaded_count == 4, w.loaded_count
|
||
# (16,64,0) 属于区块 (1,0)
|
||
assert w.block_at(16,64,0).loaded
|
||
# (-1,64,-1) 属于区块 (-1,-1)
|
||
assert w.block_at(-1,64,-1).loaded
|
||
# (32,64,-48) 属于区块 (2,-3)
|
||
assert w.block_at(32,64,-48).loaded
|
||
# (16,64,-1) 属于区块 (1,-1) —— 未加载
|
||
assert w.block_at(16,64,-1).source == "unloaded"
|
||
print(" ✓ 多区块聚合: 4 列,跨区块/负坐标定位均正确,未加载区可辨")
|
||
# 卸载
|
||
w.unload_chunk(1, 0)
|
||
assert w.block_at(16,64,0).source == "unloaded"
|
||
print(" ✓ 区块卸载后查询回到 unloaded")
|
||
|
||
if __name__ == "__main__":
|
||
for fn in [test_negative_coords, test_unloaded_vs_air_distinction, test_real_chunk_via_world,
|
||
test_world_height_bounds, test_unknown_state_reported, test_multiple_chunks]:
|
||
fn()
|
||
print("\n世界模型:全部通过")
|