156 lines
7.1 KiB
Python
156 lines
7.1 KiB
Python
"""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 解析:全部通过")
|