wear_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. """wear_test.py — CS1003 穿装备最小变量实验。
  2. 假设(cap42/43 离线分析定案): bot 旧会话"冷登录链+CS20 platform=2+引导对"
  3. 是缝合特征, 服务器判会话异常 → 装备动作恒 616。
  4. 对照: 真客户端冷登录 = platform=1 + 无引导对 + init18→心跳→openmianban→1003。
  5. 实验矩阵(默认 A; --variant 切换):
  6. A: platform=1 + 无引导 + openmianban,7→1003 (真客户端黄金时序复刻)
  7. B: platform=2 + 无引导 + openmianban,7→1003 (分离 platform 变量)
  8. C: platform=1 + 无引导 + 裸穿(无 3199 前置) (分离 openmianban 变量)
  9. 全程逐帧日志(含 SC-1 子帧展开), 判定: SC-1 含 615/203=成功, 616=失败。
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import json
  14. import logging
  15. import sys
  16. import time
  17. import zlib
  18. from pathlib import Path
  19. from typing import Final
  20. ROOT: Final[Path] = Path(__file__).resolve().parents[1]
  21. if str(ROOT) not in sys.path:
  22. sys.path.insert(0, str(ROOT))
  23. from pbot.codec.bitbuf import decode6 # noqa: E402
  24. from pbot.codec.frame import Frame, FrameSplitter # noqa: E402
  25. from pbot.config import Account # noqa: E402
  26. from pbot.game.session import apply_batch, enter_world # noqa: E402
  27. from pbot.login.box_client import enter_game_client, login # noqa: E402
  28. from pbot.login.zone_ports import fetch_zone_ports # noqa: E402
  29. from pbot.transport.gateway import login_gate_with_retry # noqa: E402
  30. from pbot.types import ZoneId # noqa: E402
  31. from pbot.world.mirror import WorldMirror # noqa: E402
  32. from pbot.world.update import WorldUpdater # noqa: E402
  33. _LOG = logging.getLogger("wear_test")
  34. ZONE_40: Final[int] = 14606328
  35. CHAR_NAME: Final[str] = "俞丁兰"
  36. USER: Final[str] = "longwu988"
  37. PASSWORD: Final[str] = "Longwu1988"
  38. PORTS_FALLBACK: Final[tuple[int, ...]] = (8041, 8043, 8081)
  39. _WOODEN_SWORD_INDEX: Final[int] = 50000 # SC201 定案: 木剑 Index=50000
  40. def split_sub(raw: bytes) -> list[Frame]:
  41. frames = FrameSplitter().feed(raw)
  42. if frames:
  43. return frames
  44. try:
  45. return FrameSplitter().feed(decode6(raw))
  46. except Exception: # noqa: BLE001
  47. return []
  48. def describe_frame(f: Frame, direction: str) -> str:
  49. body_preview = f.body[:60].decode("utf-8", "replace") if f.body else ""
  50. line = (f"{direction}{f.msg_id:<6} tail={f.tail:<4} recog={f.recog:<11} "
  51. f"p1={f.p1} p2={f.p2} p3={f.p3} {body_preview!r}")
  52. if f.msg_id == -1 and f.body:
  53. try:
  54. subs = split_sub(zlib.decompress(f.body))
  55. ids = [s.msg_id for s in subs]
  56. line += f" 子帧={ids}"
  57. for s in subs:
  58. if s.msg_id in (200, 201, 202, 203, 52, 615, 616, 619, 622):
  59. try:
  60. txt = s.body.decode("utf-8", "replace")
  61. except UnicodeDecodeError:
  62. try:
  63. txt = decode6(s.body).decode("utf-8", "replace")
  64. except Exception: # noqa: BLE001
  65. txt = repr(s.body[:60])
  66. line += f"\n └ SC{s.msg_id}: {txt[:200]}"
  67. except zlib.error:
  68. pass
  69. return line
  70. def pump_logged(conn, world: WorldMirror, updater: WorldUpdater,
  71. seconds: float, tag: str = "") -> list[Frame]:
  72. """收帧 + 逐帧日志 + 应用镜像。返回原始帧(含 -1)。"""
  73. deadline = time.time() + seconds
  74. collected: list[Frame] = []
  75. while time.time() < deadline:
  76. frames = conn.recv_frames(max(0.1, deadline - time.time()))
  77. for f in frames:
  78. _LOG.info("[%s] ← %s", tag, describe_frame(f, "SC"))
  79. collected += frames
  80. apply_batch(world, updater, frames)
  81. return collected
  82. def wear_and_check(conn, world: WorldMirror, updater: WorldUpdater, sid: int,
  83. item_name: str, tail_hint: str, makeindex: int = 0) -> str:
  84. """发 CS1003 并监听 3s, 返回 SUCCESS / FAIL_616 / NO_RESP。
  85. pre=makeindex — cap_hoe_success2 pcap 9字段头定案: 真客户端把
  86. MakeIndex 放在头部 pre 字段(CS20 的 pre=900000 同位), recog=1 是
  87. 常量标志。pre=0 → 服务器 GetItem 查无 → 恒 616(此前全部失败的根因)。
  88. """
  89. used = conn.send_frame(1003, 1, 0, 0, sid, item_name.encode("utf-8"),
  90. pre=makeindex)
  91. _LOG.info("[%s] → CS1003 %r tail=%d pre=%d(recog=1 p1=0 p2=0 p3=%d)",
  92. tail_hint, item_name, used, makeindex, sid)
  93. frames = pump_logged(conn, world, updater, 3.0, tag=f"{tail_hint}:resp")
  94. result = "NO_RESP"
  95. for f in frames:
  96. if f.msg_id == 616:
  97. result = "FAIL_616"
  98. break
  99. if f.msg_id == -1 and f.body:
  100. try:
  101. subs = split_sub(zlib.decompress(f.body))
  102. except zlib.error:
  103. continue
  104. ids = [s.msg_id for s in subs]
  105. if 203 in ids or 615 in ids:
  106. result = "SUCCESS"
  107. break
  108. if 616 in ids:
  109. result = "FAIL_616(batch)"
  110. break
  111. _LOG.info("[%s] 判定: %s", tail_hint, result)
  112. return result
  113. def fetch_bag(conn, world: WorldMirror, updater: WorldUpdater, sid: int) -> list:
  114. """CS81 拉背包 → SC-1 子帧 SC201 JSON。"""
  115. _ = conn.send_frame(81, 0, 0, 0, sid, b"")
  116. _LOG.info("[bag] → CS81 拉背包")
  117. deadline = time.time() + 3.0
  118. while time.time() < deadline:
  119. frames = conn.recv_frames(max(0.1, deadline - time.time()))
  120. for f in frames:
  121. _LOG.info("[bag] ← %s", describe_frame(f, "SC"))
  122. apply_batch(world, updater, frames)
  123. for f in frames:
  124. if f.msg_id == -1 and f.body:
  125. try:
  126. subs = split_sub(zlib.decompress(f.body))
  127. except zlib.error:
  128. continue
  129. for s in subs:
  130. if s.msg_id == 201:
  131. # SC201 body 为 6bit 文本态(JSON), 双层解码
  132. for cand in (s.body,):
  133. try:
  134. return json.loads(cand.decode("utf-8"))
  135. except (UnicodeDecodeError, json.JSONDecodeError):
  136. pass
  137. try:
  138. return json.loads(decode6(s.body).decode("utf-8", "replace"))
  139. except json.JSONDecodeError:
  140. _LOG.warning("[bag] SC201 解码失败")
  141. return []
  142. def main() -> int:
  143. ap = argparse.ArgumentParser()
  144. _ = ap.add_argument("--variant", choices=["A", "B", "C"], default="A")
  145. _ = ap.add_argument("--item", default="木剑")
  146. _ = ap.add_argument("--takeoff-mk", type=int, default=0,
  147. help="卸载分支用的 makeindex(装备栏 SC200 未接入前的手动通道)")
  148. _ = ap.add_argument("--zone", type=int, default=ZONE_40)
  149. _ = ap.add_argument("--name", default=CHAR_NAME)
  150. _ = ap.add_argument("-u", "--user", default=USER)
  151. _ = ap.add_argument("-p", "--password", default=PASSWORD)
  152. a = ap.parse_args()
  153. logging.basicConfig(level=logging.INFO,
  154. format="%(asctime)s %(levelname)s %(message)s",
  155. stream=sys.stdout)
  156. _LOG.info("=== 实验变体 %s: item=%s zone=%d char=%s ===",
  157. a.variant, a.item, a.zone, a.name)
  158. jwt, uid = login(Account(username=a.user, password=a.password))
  159. _LOG.info("[1] uid=%d", uid)
  160. _ = enter_game_client(_http_session(), jwt, ZoneId(a.zone))
  161. _LOG.info("[2] 选区上报 OK")
  162. ports = fetch_zone_ports().get(a.zone)
  163. port_tuple = (ports["port"],) if ports and ports["port"] else PORTS_FALLBACK
  164. _LOG.info("[3] 登录端口: %s", port_tuple)
  165. key32, sid, real, ticket, gport = login_gate_with_retry(
  166. ZoneId(a.zone), jwt, uid, a.name, port_tuple)
  167. _LOG.info("[4] sid=%d 角色=%s 票据OK 游戏端口=%d", sid, real, gport)
  168. platform = 1 if a.variant in ("A", "C") else 2
  169. stop: list[bool] = [False]
  170. conn, world, updater = enter_world(
  171. ZoneId(a.zone), real, ticket, sid, gport, uid,
  172. stop=stop, platform=platform, with_guide=False)
  173. _LOG.info("[5] 进世界 OK platform=%d 出生 map=%s @(%d,%d)",
  174. platform, world.me.map_name, world.me.x, world.me.y)
  175. try:
  176. bag = fetch_bag(conn, world, updater, sid)
  177. if bag:
  178. brief = [{"mk": it.get("makeindex"), "Index": it.get("Index"),
  179. "where": it.get("where")} for it in bag]
  180. _LOG.info("[6] 背包 %d 件: %s", len(bag), json.dumps(brief, ensure_ascii=False))
  181. else:
  182. _LOG.warning("[6] 背包拉取失败(空)")
  183. if a.variant in ("A", "B"):
  184. _LOG.info("[6] 节奏: +2.2s → 3199(自动tail) → +2.5s → 1003")
  185. pump_logged(conn, world, updater, 2.2, tag="gap1")
  186. used = conn.send_frame(
  187. 3199, 0, 0, 0, sid,
  188. b'{"act":"@openmianban,7","subid":1,"index":104}')
  189. _LOG.info("[7] → CS3199 @openmianban,7 tail=%d", used)
  190. got = pump_logged(conn, world, updater, 2.5, tag="panel")
  191. sc1109 = [f for f in got if f.msg_id == 1109]
  192. _LOG.info("[7.1] SC1109(面板确认) %s",
  193. "收到✓ 会话正常" if sc1109 else "未收(会话异常)")
  194. else:
  195. _LOG.info("[6] 变体C: 裸穿, 仅静置")
  196. pump_logged(conn, world, updater, 3.0, tag="gap1")
  197. mk = 0
  198. for it in bag:
  199. if it.get("Index") == _WOODEN_SWORD_INDEX:
  200. mk = int(it.get("makeindex", 0))
  201. _LOG.info("[6.5] %s 在包中=%s makeindex=%d",
  202. a.item, "是" if mk else "否(已穿上或不在)", mk)
  203. if mk:
  204. result = wear_and_check(conn, world, updater, sid, a.item,
  205. tail_hint=f"variant{a.variant}", makeindex=mk)
  206. _LOG.info("=== 最终结果: %s ===", result)
  207. if result.startswith("SUCCESS"):
  208. _LOG.info("[8] 等 2s 观察装备栏/属性推送")
  209. _ = pump_logged(conn, world, updater, 2.0, tag="after")
  210. _LOG.info("[9] 3s 后卸下(验证 CS1004 同构: pre=makeindex)")
  211. time.sleep(3.0)
  212. used = conn.send_frame(1004, 1, 0, 0, sid,
  213. a.item.encode("utf-8"), pre=mk)
  214. _LOG.info("[9] → CS1004 %r tail=%d pre=%d", a.item, used, mk)
  215. frames = pump_logged(conn, world, updater, 3.0, tag="takeoff")
  216. off = "NO_RESP"
  217. for f in frames:
  218. if f.msg_id == -1 and f.body:
  219. try:
  220. subs = split_sub(zlib.decompress(f.body))
  221. except zlib.error:
  222. continue
  223. ids = [s.msg_id for s in subs]
  224. if 619 in ids or 622 in ids:
  225. off = "TAKEOFF_SUCCESS"
  226. break
  227. if 620 in ids:
  228. off = "TAKEOFF_FAIL_620"
  229. break
  230. if f.msg_id in (619, 622):
  231. off = "TAKEOFF_SUCCESS"
  232. break
  233. if f.msg_id == 620:
  234. off = "TAKEOFF_FAIL_620"
  235. break
  236. _LOG.info("[9] 卸下判定: %s", off)
  237. else:
  238. _LOG.info("=== 跳过穿戴(物品不在包) — 直接验证卸载 ===")
  239. used = conn.send_frame(1004, 1, 0, 0, sid,
  240. a.item.encode("utf-8"), pre=a.takeoff_mk)
  241. _LOG.info("[9] → CS1004 %r tail=%d pre=%d", a.item, used, a.takeoff_mk)
  242. frames = pump_logged(conn, world, updater, 3.0, tag="takeoff0")
  243. for f in frames:
  244. if f.msg_id == -1 and f.body:
  245. try:
  246. ids = [s.msg_id for s in split_sub(zlib.decompress(f.body))]
  247. except zlib.error:
  248. continue
  249. if 619 in ids or 620 in ids or 622 in ids:
  250. _LOG.info("[9] 卸载应答子帧: %s", ids)
  251. break
  252. elif f.msg_id in (619, 620, 622):
  253. _LOG.info("[9] 卸载应答: SC%d", f.msg_id)
  254. break
  255. finally:
  256. stop[0] = True
  257. time.sleep(0.3)
  258. conn.close()
  259. return 0
  260. def _http_session():
  261. import requests
  262. s = requests.Session()
  263. s.trust_env = False
  264. s.verify = False
  265. return s
  266. if __name__ == "__main__":
  267. raise SystemExit(main())