Files
agentmc/tests/test_chunk.py

156 lines
7.1 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.

"""chunk 解析回归。
核心验证方式:用 **prismarine-chunk(独立实现)产出的真实格式数据**
交给 Python 解 —— 两边独立,对得上才算格式对。
样本由 /home/newqqagent/plugin_data/mc/runtime/chunkgen*.js 生成。
"""
import sys, os, json, subprocess, tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mcfold.chunk import (decode_chunk, BlockRegistry, MIN_Y, NUM_SECTIONS,
BitArray, Reader, ChunkError)
RUNTIME = "/home/newqqagent/plugin_data/mc/runtime"
BLOCKS = f"{RUNTIME}/node_modules/minecraft-data/minecraft-data/data/pc/1.20/blocks.json"
def gen(script):
"""跑 Node 生成独立实现产出的样本。"""
r = subprocess.run(["node", f"{RUNTIME}/{script}"], capture_output=True, text=True,
cwd=RUNTIME, timeout=120)
if r.returncode != 0:
return None, r.stderr[:300]
return json.loads(r.stdout), None
def reg():
return BlockRegistry.from_json_file(BLOCKS)
def test_bitarray():
"""位打包:值不跨 long 边界(这是最容易写错的地方)。"""
# 4 位/值,一个 long 放 16 个值
ba = BitArray(data=[0x3210], capacity=16, bits_per_value=4)
assert [ba.get(i) for i in range(4)] == [0, 1, 2, 3], [ba.get(i) for i in range(4)]
print(" ✓ 位打包:值不跨 long 边界(4位/值 → 16 值/long)")
# 5 位/值 → floor(64/5)=12 值/long
ba2 = BitArray(data=[0x1], capacity=12, bits_per_value=5)
assert ba2.values_per_long == 12, ba2.values_per_long
assert ba2.get(0) == 1
print(" ✓ 位打包:5位/值 → 12 值/long(余位丢弃)")
def test_real_chunk_from_prismarine():
"""核心回归:解独立实现产出的区块。"""
d, err = gen("chunkgen.js")
if d is None:
print(f" - 跳过(Node 生成失败: {err})"); return
exp = d["expect"]
ch = decode_chunk(0, 0, bytes.fromhex(d["hex"]))
assert len(ch.sections) == NUM_SECTIONS, len(ch.sections)
r = reg()
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:
sid = ch.state_id_at(x,y,z)
got = r.name_or_unknown(sid)
assert got == want, f"({x},{y},{z}) {sid} -> {got}, 期望 {want}"
nonempty = [s.y_index for s in ch.sections if s.solid_block_count > 0]
assert nonempty == [0,5], nonempty
print(f" ✓ 解 prismarine 真实区块: 4/4 方块命中, 非空 section={nonempty}")
def test_all_encoding_branches():
"""覆盖单值/本地调色板/高 section 三种编码分支。"""
d, err = gen("chunkgen2.js")
if d is None:
print(f" - 跳过(Node 生成失败: {err})"); return
r = reg()
# A: 全空气 → 应无实心方块
chA = decode_chunk(0,0,bytes.fromhex(d["A"]["hex"]))
assert all(s.solid_block_count == 0 for s in chA.sections), "全空气却有实心方块"
print(" ✓ 分支 A(全空气): solidBlockCount 全 0")
# B: 一行石头
chB = decode_chunk(0,0,bytes.fromhex(d["B"]["hex"]))
names = {r.name_or_unknown(chB.state_id_at(i,0,0)) for i in range(16)}
assert names == {"stone"}, names
print(f" ✓ 分支 B(一行石头): y=0 的 16 个方块全是 stone")
# C: 四种混合 → 验证本地调色板
chC = decode_chunk(0,0,bytes.fromhex(d["C"]["hex"]))
got = [r.name_or_unknown(chC.state_id_at(i,60,0)) for i in range(4)]
assert got == ["stone","dirt","gold_block","diamond_block"], got
print(f" ✓ 分支 C(本地调色板): {got}")
# D: 高 section
chD = decode_chunk(0,0,bytes.fromhex(d["D"]["hex"]))
assert r.name_or_unknown(chD.state_id_at(3,20,4)) == "diamond_block"
print(" ✓ 分支 D(高 section y=20)")
def test_mod_block_explicit_unknown():
"""★ 关键行为:本机注册表没有的 state id 必须显式 unknown,**不可静默错配**。
这正是 mineflayer 的缺陷(越界 id 静默变成 note_block),我们要把它变成可见降级。
说明:格式正确性已由 test_real_chunk_from_prismarine(独立实现数据,4/4)证明;
这里聚焦**注册表行为**,故用 Python 侧的 writer 构造样本(自洽即可)。
"""
from mcfold.protodef import Writer
w = Writer()
# section 0: solidCount=1, bits=4, palette=[99999], longs=256, 全 0 位
w.write((1).to_bytes(2, "big"))
w.write(bytes([4]))
w.varint(1); w.varint(99999)
w.varint(256)
w.write(b"\x00" * (256 * 8))
# biome section(单值)
w.write(bytes([0])); w.varint(0); w.varint(0)
# 其余 23 个 section:单值空气 + biome
for _ in range(23):
w.write((0).to_bytes(2, "big")); w.write(bytes([0]))
w.varint(0); w.varint(0)
w.write(bytes([0])); w.varint(0); w.varint(0)
ch = decode_chunk(0, 0, w.getvalue())
# 末尾 section 属半写状态,取已完整解出的 section 0(y=-64..-49)
sid = ch.state_id_at(0, -64, 0)
assert sid == 99999, f"state id 解码错: {sid}"
r = reg()
got = r.name_or_unknown(sid)
assert got.startswith("unknown("), f"越界 id 竟解析出了名字: {got}"
assert r.is_out_of_range(sid), "越界判定失效"
# 对照:mineflayer 的行为是静默错配 —— 我们明确不同
assert r.name_of(sid) is None, "越界 id 不应能查到名字"
print(f" ✓ ★ mod 方块显式降级: state {sid} -> {got}(而非静默错配)")
def test_registry_degradation_direct():
"""直接验证注册表降级语义(不依赖线格式)。"""
r = reg()
assert r.name_or_unknown(0) == "air"
assert r.name_or_unknown(24134) is not None # 原版上界内
assert r.name_or_unknown(24135).startswith("unknown(") # 超出上界
assert r.is_out_of_range(50000)
print(" ✓ 注册表降级语义:原版内可查、越界显式 unknown")
def test_out_of_bounds_safe():
"""世界边界外的查询必须安全返回空气,而不是崩。"""
from mcfold.protodef import Writer
w = Writer()
for _ in range(NUM_SECTIONS):
w.write((0).to_bytes(2, "big")); w.write(bytes([0]))
w.varint(0); w.varint(0) # 方块 section(单值空气)
w.write(bytes([0])); w.varint(0); w.varint(0) # biome section
ch = decode_chunk(0, 0, w.getvalue())
assert ch.state_id_at(0, -1000, 0) == 0
assert ch.state_id_at(0, 1000, 0) == 0
assert ch.state_id_at(0, MIN_Y, 0) == 0
print(" ✓ 世界边界外查询安全返回空气")
def test_bad_data_raises():
"""损坏数据必须报错,不能静默产出错数据。"""
try:
decode_chunk(0,0,b"\x00\x01\xff") # bitsPerBlock=255
assert False, "应当报错"
except (ChunkError, Exception) as e:
print(f" ✓ 损坏数据显式报错: {str(e)[:45]}")
if __name__ == "__main__":
for fn in [test_bitarray, test_real_chunk_from_prismarine, test_all_encoding_branches,
test_mod_block_explicit_unknown, test_registry_degradation_direct,
test_out_of_bounds_safe, test_bad_data_raises]:
fn()
print("\nchunk 解析:全部通过")