frame.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. #!/usr/bin/env python3
  2. """
  3. 996传奇客户端 协议线层帧装拆
  4. 来源: netClient::SendNetMessage @0x10003d408 + encode_header @0x10004b4e4 反汇编还原
  5. 校核: apilot/wire.py + protocol_bot/encode6bitbuf_final.py (已实网验证)
  6. 验证: 与已知心跳帧逐字节一致 (2026-09-23)
  7. 线格式 (从 encode6bitbuf_final.build_frame + apilot/wire.HDR 反推):
  8. ┌─────────────────────────── 36B 明文头(9×i32, little-endian) ──────────────────────────┐
  9. │ [0] zero [1] msg_id [2] recog [3] p1 [4] p2 [5] p3 [6] zero [7] body_len [8] tail │
  10. └──────────────────────────────────────────────────────────────────────────────────────────┘
  11. │ Encode6BitBuf (hdr_codec.encode6)
  12. ▼
  13. ┌────── 48B 编码头 (全可打印 0x3C..0x7B, 即 "<=?@A-Z[\\]^_`a-z{") ──────┐ ┌──── N 字节 body (明文, 不加密) ────┐
  14. │ ↑ server→client 时, body 可能是 zlib 压缩 JSON
  15. 备注:
  16. - 24B→32B (hdr_codec.py) 是 36B→48B 的前 24 字节子集, 即 netMessage 类的内部 header 表示
  17. - 实际 socket 上跑的是 36B→48B + body
  18. - [0][6] 恒为 0 (无业务含义, 推测是协议层预留)
  19. - [8] tail 字段在不同消息上有不同含义, 常见值: 0=普通, 1=批量(-1)的分片, 9=握手串
  20. 依赖: hdr_codec.py (同目录, ENC_TABLE + encode6/decode6)
  21. """
  22. from __future__ import annotations
  23. import os
  24. import struct
  25. import sys
  26. from dataclasses import dataclass
  27. from typing import Optional
  28. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  29. from hdr_codec import ENC_TABLE, decode_header, encode_header
  30. # wire format 常量
  31. HEADER_PLAIN_SIZE = 36 # 9 × i32 明文头
  32. HEADER_ENC_SIZE = 48 # 编码后头大小 (36B → 48B, 4/3 比例)
  33. MIN_FRAME_SIZE = HEADER_ENC_SIZE # 最小帧 = 48B 头 + 0B body
  34. # 头布局 (9×i32, little-endian)
  35. HDR_STRUCT = struct.Struct("<9i")
  36. @dataclass
  37. class Frame:
  38. """一条协议消息的完整结构"""
  39. msg_id: int
  40. recog: int = 0 # 业务识别符, 创角时 900000=水印
  41. p1: int = 0 # param1
  42. p2: int = 0 # param2
  43. p3: int = 0 # param3
  44. tail: int = 0 # 末尾字段, 见文档说明
  45. body: bytes = b""
  46. def pack(self) -> bytes:
  47. """组帧: 36B 明文头 → 48B 编码头 + body"""
  48. return pack_frame(self.msg_id, self.recog, self.p1, self.p2, self.p3,
  49. self.tail, self.body)
  50. def _encode_full(plain36: bytes) -> bytes:
  51. """36B 明文 → 48B 编码 (与 hdr_codec.encode_header 24B→32B 同一算法, 输入更长)
  52. 算法 (来自 netClient::encode_header @0x10004b4e4):
  53. 对每个明文字节 b:
  54. v = (ENC_TABLE[b] ^ 9) & 0xFFFF
  55. bitcnt 当前累积位 (起始 0)
  56. out_byte = (acc | (v >> (bitcnt+2))) & 0x3F + 0x3C
  57. leftover = ((v << (6-bitcnt)) >> 2) & 0x3F
  58. 若 bitcnt < 4: acc = leftover; bitcnt += 2
  59. 否则: 输出 leftover+0x3C; acc=0, bitcnt=0
  60. 末尾若 bitcnt>0: 输出 acc+0x3C
  61. """
  62. if len(plain36) % 3 != 0:
  63. raise ValueError(f"plain 长度必须为 3 倍数 (encode_full 是 24B→32B 的扩展), 实际={len(plain36)}")
  64. out = bytearray()
  65. acc = 0
  66. bitcnt = 0
  67. for ch in plain36:
  68. v = (ENC_TABLE[ch] ^ 9) & 0xFFFF
  69. new_bitcnt = bitcnt + 2
  70. acc = (acc | (v >> new_bitcnt)) & 0x3F
  71. out.append((acc + 0x3C) & 0xFF)
  72. leftover = ((v << ((6 - bitcnt) & 0xFF)) & 0xFFFFFFFF) >> 2 & 0x3F
  73. if bitcnt < 4:
  74. acc = leftover
  75. bitcnt = new_bitcnt
  76. else:
  77. out.append((leftover + 0x3C) & 0xFF)
  78. acc = 0
  79. bitcnt = 0
  80. if bitcnt > 0:
  81. out.append((acc + 0x3C) & 0xFF)
  82. return bytes(out)
  83. def _decode_full(enc_bytes: bytes, plain_size: int = HEADER_PLAIN_SIZE) -> bytes:
  84. """编码字节流 → 明文 (与 hdr_codec.decode_header 32B→24B 同一算法, 输出更长)"""
  85. bits = 0
  86. nbits = 0
  87. raw = bytearray()
  88. for ch in enc_bytes:
  89. bits = (bits << 6) | (ch - 0x3C)
  90. nbits += 6
  91. while nbits >= 8:
  92. nbits -= 8
  93. raw.append((bits >> nbits) & 0xFF)
  94. # 还原: raw 字节过 INV_TABLE[ENC_TABLE[i] ^ 9] = i
  95. inv = [0] * 256
  96. for i in range(256):
  97. inv[ENC_TABLE[i] ^ 9] = i
  98. plain = bytes(inv[b] for b in raw[:plain_size])
  99. return plain
  100. def pack_frame(msg_id: int, recog: int, p1: int, p2: int, p3: int,
  101. tail: int, body: bytes = b"") -> bytes:
  102. """
  103. 构造一条线协议消息
  104. :return: 48B 编码头 + body
  105. """
  106. # 9 字段: zero, msg_id, recog, p1, p2, p3, zero, body_len, tail
  107. head36 = HDR_STRUCT.pack(0, msg_id, recog, p1, p2, p3, 0, len(body), tail)
  108. return _encode_full(head36) + body
  109. def unpack_stream(buf: bytes) -> tuple[list[Frame], bytes]:
  110. """
  111. 从累积的字节流中切出所有完整帧
  112. :return: (frames, rest) — frames 是切出的完整帧, rest 是不足一帧的尾部残留
  113. """
  114. out: list[Frame] = []
  115. pos = 0
  116. while pos + HEADER_ENC_SIZE <= len(buf):
  117. # 解头
  118. head36 = _decode_full(buf[pos:pos + HEADER_ENC_SIZE], HEADER_PLAIN_SIZE)
  119. try:
  120. zero, msg_id, recog, p1, p2, p3, zero2, body_len, tail = HDR_STRUCT.unpack(head36)
  121. except struct.error:
  122. break
  123. # 检查 body 是否完整
  124. if pos + HEADER_ENC_SIZE + body_len > len(buf):
  125. break
  126. body = buf[pos + HEADER_ENC_SIZE:pos + HEADER_ENC_SIZE + body_len]
  127. out.append(Frame(msg_id=msg_id, recog=recog, p1=p1, p2=p2, p3=p3,
  128. tail=tail, body=body))
  129. pos += HEADER_ENC_SIZE + body_len
  130. return out, buf[pos:]
  131. # ============== 已知消息类型 (来自 netMessageDef.lua) ==============
  132. # 常用 msg_id 速查
  133. MSG_CS_LOGIN = 20
  134. MSG_CS_CREATE_ROLE = 101
  135. MSG_CS_SELECT_ROLE = 103
  136. MSG_SC_ROLE_LIST = 520
  137. MSG_SC_CREATE_ROLE_OK = 521
  138. MSG_SC_CREATE_ROLE_FAIL = 522
  139. MSG_SC_GAME_SERVER_ADDR = 525
  140. MSG_CS_HEARTBEAT = 60000
  141. MSG_SC_DISCONNECTED = 0x1FFFFFFE
  142. MSG_ILLEGAL = 0x2FFFFFFE
  143. MSG_BATCH = -1
  144. if __name__ == "__main__":
  145. import sys
  146. import os
  147. _PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  148. if _PARENT not in sys.path:
  149. sys.path.insert(0, _PARENT)
  150. from protocol_bot.encode6bitbuf_final import encode6 as ref_encode6, build_frame as ref_build_frame
  151. head36_test = HDR_STRUCT.pack(0, 60000, 1789709744, 0, 0, 149, 0, 0, 0x70000000)
  152. enc_mine = _encode_full(head36_test)
  153. enc_ref = ref_encode6(head36_test)
  154. print(f"encoded heartbeat: {enc_mine.hex()}")
  155. print(f"reference (encode6): {enc_ref.hex()}")
  156. assert enc_mine == enc_ref, f"心跳帧编码与 reference 不一致"
  157. print("✓ 心跳帧 48B 编码与 protocol_bot/encode6bitbuf_final 字节级一致\n")
  158. # 自测 2: roundtrip
  159. f = Frame(msg_id=101, recog=900000, p1=12345, p2=37504, p3=0, tail=0,
  160. body=b'{"chr":"\xe8\x90\x8c\xe6\x96\xb0","job":0,"sex":1}')
  161. packed = f.pack()
  162. print(f"packed: {len(packed)}B = 48 header + {len(f.body)} body")
  163. frames, rest = unpack_stream(packed)
  164. assert rest == b""
  165. assert len(frames) == 1
  166. g = frames[0]
  167. assert g.msg_id == f.msg_id
  168. assert g.recog == f.recog
  169. assert g.p1 == f.p1
  170. assert g.p2 == f.p2
  171. assert g.p3 == f.p3
  172. assert g.tail == f.tail
  173. assert g.body == f.body
  174. print(f"✓ Frame roundtrip: msg_id={g.msg_id}, recog={g.recog}, body={g.body!r}\n")
  175. # 自测 3: 部分包(模拟半包/粘包)
  176. partial = packed[:-10] # 砍掉最后 10 字节
  177. frames, rest = unpack_stream(partial)
  178. assert frames == []
  179. assert rest == partial
  180. print(f"✓ Partial packet: 0 frames, rest len={len(rest)} == {len(partial)}\n")
  181. # 自测 4: 多包粘包
  182. f1 = Frame(msg_id=20, recog=0, p1=0, p2=0, p3=0, tail=9,
  183. body=b"*#acct/role/12345/9/1/0/110585/0/1/1/1/1")
  184. f2 = Frame(msg_id=529, recog=0, p1=0, p2=0, p3=0, tail=1, body=b"key32abc")
  185. multi = f1.pack() + f2.pack()
  186. frames, rest = unpack_stream(multi)
  187. assert len(frames) == 2
  188. assert frames[0].msg_id == 20
  189. assert frames[1].msg_id == 529
  190. assert frames[1].body == b"key32abc"
  191. print(f"✓ Multi-packet: 2 frames [{frames[0].msg_id}, {frames[1].msg_id}], rest={rest!r}\n")
  192. other = ref_build_frame(101, 900000, 12345, 37504, 0)
  193. mine = pack_frame(101, 900000, 12345, 37504, 0, 0, b"")
  194. assert other == mine, f"interop fail:\n other: {other.hex()}\n mine: {mine.hex()}"
  195. print(f"✓ 与 protocol_bot/encode6bitbuf_final.build_frame 字节级一致\n")
  196. sample = Frame(msg_id=12345, recog=0x12345678, p1=0x0A0B0C0D, p2=0x12345678, p3=0, tail=0, body=b"x"*100)
  197. enc_header = sample.pack()[:48]
  198. printable = all(0x3C <= b <= 0x7B for b in enc_header)
  199. assert printable, f"非可打印字符出现: {enc_header.hex()}"
  200. print(f"✓ 编码头全可打印 (0x3C..0x7B): {enc_header[:8].hex()}...{enc_header[-4:].hex()}\n")
  201. print("All frame.py self-tests passed.")