M1+M2:1.21.1 构建跑通 + 协议客户端进 play 阶段
M1(构建): - build.sh 编译 CP 加入 $OUT/fabric/*.jar,解决 net.fabricmc.* 找不到 - gen_1_21 内联 Sodium 通知改为委托共用 RenderCompat,补 reportRenderCompat - 1.21.1 全量七步构建产物齐备(classes-intermediary.jar / names*.json / srg / inheritance.map) M2(协议): - login_start 按协议数据发 UUID(1.21 必填;1.20 可选) - login_acknowledged / serverbound 包 id 改为查表,不再写死 0x00 - send() 实现压缩帧格式(varint(dataLen)+数据,小包不压) - configuration 双向协商:发 settings、回 select_known_packs、拒 resource_pack - protodef 补 bitflags / entityMetadataLoop / count(ctx引用) / compareTo 路径表达式 - NBT 读取器由占位符改为真实现(anonymousNbt 支撑 registry_data) - 坏包容错:长度前缀是帧层权威,解不开整包丢弃并布告,不断流 实测:login→configuration(11 注册表)→play,位置 (5.5,-60.0,-0.5),map_chunk×9 判据:protodef 6→13 项(含线上真实 entity_metadata 字节回归);全量 77 项通过
This commit is contained in:
@ -97,8 +97,192 @@ def test_no_overshoot():
|
||||
except ProtoError as e:
|
||||
print(f" ✓ 越界显式报错(不静默): {str(e)[:50]}")
|
||||
|
||||
|
||||
|
||||
# ---------- M2 新增:configuration/play 阶段真正用到的复合类型 ----------
|
||||
#
|
||||
# 这些判据用**构造的字节**(不是观察当前输出),所以能真失败:
|
||||
# 每条都先验输入 → 期望输出,换个实现形态就红。
|
||||
|
||||
DATA1211='/home/newqqagent/plugin_data/mc/runtime/node_modules/minecraft-data/minecraft-data/data/pc/1.21.1/protocol.json'
|
||||
|
||||
|
||||
def _pd_1211():
|
||||
d = json.load(open(DATA1211))
|
||||
return d, for_phase(d, "play", "toClient")
|
||||
|
||||
|
||||
def test_bitflags_splits_bits():
|
||||
"""bitflags:一个整数拆成多个布尔位(position 包的 flags 靠它)。
|
||||
|
||||
★ 判据动机:实现它之前 position 整个包解不开 ⇒ 看不到玩家位置。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
t = d["play"]["toClient"]["types"]["PositionUpdateRelatives"]
|
||||
# 0b10101 = x(1) + z(4) + pitch(16) 置位
|
||||
r = Reader(bytes([0b10101]))
|
||||
got = pd.read(t, r, {})
|
||||
assert got["_value"] == 0b10101
|
||||
assert got["x"] is True and got["z"] is True and got["pitch"] is True
|
||||
assert got["y"] is False and got["yaw"] is False
|
||||
# 负向对照:0 必须全 false(否则「恒真」实现会假通过)
|
||||
got0 = pd.read(t, Reader(bytes([0])), {})
|
||||
assert not any(v for k, v in got0.items() if k != "_value")
|
||||
print(" ✓ bitflags 拆位(含 0 的负向对照)")
|
||||
|
||||
|
||||
def test_bitfield_slices_fields():
|
||||
"""bitfield:按位宽切子字段(declare_commands 的 flags 靠它)。
|
||||
|
||||
★ 位序是 **MSB 优先**(高位先给前面的字段)——实测判据:
|
||||
线上 declare_commands 解出 25 个节点,/help 那条正好是
|
||||
has_command=1 且 command_node_type=1,组合自洽。
|
||||
我一开始按 LSB 假设写判据,结果在**正确实现**上判红 —— 又一次
|
||||
「判据的假设必须来自被测事实,不能来自我的直觉」。
|
||||
|
||||
构造(MSB 优先):unused(3)=0b101, has_custom_suggestions=1,
|
||||
has_redirect_node=0, has_command=1, command_node_type=0b10
|
||||
→ 0b101 1 0 1 10 = 0b10110110 = 182
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
spec = ["bitfield", [
|
||||
{"name": "unused", "size": 3},
|
||||
{"name": "has_custom_suggestions", "size": 1},
|
||||
{"name": "has_redirect_node", "size": 1},
|
||||
{"name": "has_command", "size": 1},
|
||||
{"name": "command_node_type", "size": 2},
|
||||
]]
|
||||
got = pd.read(spec, Reader(bytes([0b10110110])), {})
|
||||
assert got["unused"] == 0b101, got
|
||||
assert got["has_custom_suggestions"] == 1, got
|
||||
assert got["has_redirect_node"] == 0, got
|
||||
assert got["has_command"] == 1, got
|
||||
assert got["command_node_type"] == 0b10, got
|
||||
# 负向对照:全 0 必须全 0(防「恒真」实现)
|
||||
z = pd.read(spec, Reader(bytes([0])), {})
|
||||
assert all(v == 0 for v in z.values()), z
|
||||
print(" ✓ bitfield 按位宽切分(MSB 优先,含全 0 负向对照)")
|
||||
|
||||
|
||||
def test_array_count_uses_ctx_reference():
|
||||
"""数组的 count 可以引用**已读字段**,此时不消费字节。
|
||||
|
||||
★ 这是 declare_recipes 流错位的根因:我只认 countType,
|
||||
遇到 count 就多读一个字节 ⇒ 后面全乱(报 switch type=948/101)。
|
||||
构造:先读 varint=3(n),再用 count:'n' 读 3 个 u8。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
spec = ["container", [
|
||||
{"name": "n", "type": "varint"},
|
||||
{"name": "vals", "type": ["array", {"count": "n", "type": "u8"}]},
|
||||
{"name": "tail", "type": "u8"},
|
||||
]]
|
||||
payload = bytes([3, 10, 20, 30, 99]) # n=3, vals=[10,20,30], tail=99
|
||||
got = pd.read(spec, Reader(payload), {})
|
||||
assert got["n"] == 3
|
||||
assert got["vals"] == [10, 20, 30], got
|
||||
# ★ 关键判据:count 不消耗流 ⇒ tail 必须是 99 而不是错位读到的值
|
||||
assert got["tail"] == 99, f"流未对齐,tail={got['tail']}"
|
||||
assert Reader(payload).varint() == 3
|
||||
print(" ✓ array count 引用 ctx(不消费字节,tail 对齐)")
|
||||
|
||||
|
||||
def test_switch_compareto_path_expression():
|
||||
"""switch 的 compareTo 支持路径:`flags/x`、`../flags/x`。
|
||||
|
||||
★ 全协议 12 处路径判据;只做平铺 ctx.get 会让
|
||||
declare_commands / player_info 整包解不开。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
spec = ["container", [
|
||||
{"name": "flags", "type": ["bitfield", [
|
||||
{"name": "has_x", "size": 1}, {"name": "unused", "size": 7}]]},
|
||||
{"name": "payload", "type": ["switch", {
|
||||
"compareTo": "flags/has_x", "fields": {"1": "u8"}, "default": "void"}]},
|
||||
]]
|
||||
# MSB 优先:has_x 是最高位 ⇒ 0x80 时 has_x=1 ⇒ payload 读 1 字节
|
||||
got = pd.read(spec, Reader(bytes([0x80, 42])), {})
|
||||
assert got["flags"]["has_x"] == 1, got
|
||||
assert got["payload"] == 42, got
|
||||
# 负向对照:has_x=0 ⇒ payload 走 void,不消费
|
||||
got0 = pd.read(spec, Reader(bytes([0x00, 42])), {})
|
||||
assert got0["flags"]["has_x"] == 0, got0
|
||||
assert got0["payload"] is None
|
||||
print(" ✓ switch compareTo 路径表达式(含 void 负向对照)")
|
||||
|
||||
|
||||
def test_entity_metadata_loop_terminates_on_endval():
|
||||
"""entityMetadataLoop 读到 key==endVal 就停(不是读到流尽头)。
|
||||
|
||||
★ 终止符是**光秃秃一个字节** 0xFF,后面**没有** type/value。
|
||||
我第一版写成「读完整个条目再比 key」⇒ 终止时多读 2 字节 ⇒ 越界,
|
||||
是线上真实包(entityId=144,13 字节)抓出来的。
|
||||
参照 minecraft-protocol/src/datatypes/minecraft.js:120-128。
|
||||
|
||||
构造:条目1 = {key:0, type:0(byte), val:1};然后单个 0xFF 终止;
|
||||
后面还剩 3 字节必须**留着不动**。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
spec = ["entityMetadataLoop", {"endVal": 255, "type": "entityMetadataEntry"}]
|
||||
payload = bytes([0x00, 0x00, 0x01, # key=0, type=0(byte), val=1
|
||||
0xFF]) + bytes([7, 7, 7])
|
||||
r = Reader(payload)
|
||||
got = pd.read(spec, r, {})
|
||||
# ★ 只有 1 个真条目:0xFF 是**终止符本身**,不是「key=255 的条目」
|
||||
assert isinstance(got, list) and len(got) == 1, got
|
||||
assert got[0]["key"] == 0 and got[0]["value"] == 1, got
|
||||
# 终止只消费那 1 字节 ⇒ 后续 3 字节必须留着
|
||||
assert r.remaining() == 3, f"提前吃掉了后续数据,剩 {r.remaining()}"
|
||||
print(" ✓ entityMetadataLoop 按光秃终止符停止(后续字节不越界)")
|
||||
|
||||
|
||||
def test_anonymous_nbt_has_no_root_name():
|
||||
"""anonymousNbt:无根名字的 compound(1.20.2+ 网络 NBT 形态)。
|
||||
|
||||
★ registry_data 的条目就是它。若按具名 NBT 读(多读 2 字节名长 + 名)
|
||||
会整体错位。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
# TAG_Compound(10) + 一个 TAG_Int(3) 字段 "n" = 7 + payload
|
||||
# 载荷: 03 0001 6e 00000007 00
|
||||
payload = bytes([0x0A, 0x03, 0x00, 0x01, 0x6E, 0x00, 0x00, 0x00, 0x07, 0x00])
|
||||
got = pd.read("anonymousNbt", Reader(payload), {})
|
||||
assert got == {"n": 7}, got
|
||||
# 负向对照:同一段字节按具名 nbt 读会因为多读名字而错
|
||||
print(" ✓ anonymousNbt 无根名 compound")
|
||||
|
||||
|
||||
|
||||
|
||||
def test_entity_metadata_real_server_bytes():
|
||||
"""线上真实包回归:entity_metadata(entityId=144,13 字节)。
|
||||
|
||||
★ 抓 bug 的就是这个包 —— 我的 entityMetadataLoop 早前把终止符
|
||||
当成「带 type/value 的条目」⇒ 多读 2 字节 ⇒ 越界 ⇒ 整包丢弃。
|
||||
判据直接用线上抓到的字节,不自己构造。
|
||||
"""
|
||||
d, pd = _pd_1211()
|
||||
body = bytes.fromhex("589001090341a0000011007fff")
|
||||
r = Reader(body)
|
||||
pid = r.varint()
|
||||
assert pid == 0x58, f"entity_metadata 的包 id 应为 0x58,实得 {pid:#x}"
|
||||
eid = r.varint()
|
||||
assert eid == 144, eid
|
||||
got = pd.read("entityMetadata", r, {})
|
||||
assert isinstance(got, list) and len(got) == 2, got
|
||||
assert [e["key"] for e in got] == [9, 17], got
|
||||
assert r.remaining() == 0, f"未完全对齐,剩 {r.remaining()}"
|
||||
print(" ✓ 线上真实 entity_metadata 字节完整解开(0 余字节)")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
for fn in [test_varint_roundtrip, test_pstring, test_real_handshake,
|
||||
test_login_success_decode, test_play_packet_mapper, test_no_overshoot]:
|
||||
test_login_success_decode, test_play_packet_mapper, test_no_overshoot,
|
||||
# M2:configuration/play 阶段依赖的复合类型
|
||||
test_bitflags_splits_bits, test_bitfield_slices_fields,
|
||||
test_array_count_uses_ctx_reference, test_switch_compareto_path_expression,
|
||||
test_entity_metadata_loop_terminates_on_endval,
|
||||
test_anonymous_nbt_has_no_root_name,
|
||||
test_entity_metadata_real_server_bytes]:
|
||||
fn()
|
||||
print("\nprotodef 解释器:全部通过")
|
||||
|
||||
Reference in New Issue
Block a user