test_tp_minimal.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """test_tp_minimal.py — 传送最小变量实验: 字节级复刻真客户端传送链。
  2. 2026-09-29 抓包对照分析(cap_tp.jsonl)得出的 3 个差异, 本脚本全部修正:
  3. 1. 1011 body 键序: {"UserID":..,"index":..,"Act":..}(真) ≠ bot 旧序(UserID,Act,index)
  4. 2. 传送窗口零噪音: 不发 3199/9102/5286(真客户端窗口内只有 1010/1011×2/ACK+心跳)
  5. 3. 时序对齐: 1010 后 2.0s 点矿区, 再 1.15s 点进入地图, 再 20ms 发 ACK
  6. ACK body='比奇矿区'(中文, 与真客户端一致)
  7. 成功判据: 发完 ACK 后 10s 内 world.me.map_name 变为 比奇矿区。
  8. """
  9. from __future__ import annotations
  10. import json
  11. import logging
  12. import sys
  13. import time
  14. import requests
  15. from pbot.config import Account
  16. from pbot.game.messages import protocol_map_name
  17. from pbot.game.session import enter_world, apply_batch
  18. from pbot.login.box_client import enter_game_client, login
  19. from pbot.transport.gateway import login_gate_with_retry
  20. from pbot.types import ZoneId
  21. from pbot.bot.wander import DIRS
  22. _LOG = logging.getLogger("tp_min")
  23. PORTS = (8021, 8061, 8001, 8041)
  24. TP_AID = "10002"
  25. TP_POS = (342, 338)
  26. def js(objects: dict) -> bytes:
  27. """按传入键序序列化(真客户端字节级复刻的关键)。"""
  28. return json.dumps(objects, separators=(",", ":")).encode()
  29. def main() -> int:
  30. logging.basicConfig(level=logging.INFO,
  31. format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
  32. stream=sys.stdout)
  33. http = requests.Session()
  34. http.trust_env = False
  35. http.verify = False
  36. jwt, uid = login(Account(username="longwu988", password="Longwu1988"))
  37. _ = enter_game_client(http, jwt, ZoneId(14570166))
  38. _k, sid, name, ticket, gport = login_gate_with_retry(
  39. ZoneId(14570166), jwt, uid, "新角色", PORTS)
  40. stop = [False]
  41. conn, world, upd = enter_world(ZoneId(14570166), name, ticket, sid,
  42. gport, uid, stop=stop)
  43. _LOG.info("出生 %s @(%d,%d)", world.me.map_name, world.me.x, world.me.y)
  44. if world.me.map_name == "比奇矿区":
  45. _LOG.info("[i] 已在矿区, 无需传送(实验目的达成?)")
  46. stop[0] = True
  47. conn.close()
  48. return 0
  49. if world.me.map_name != "盟重土城":
  50. _LOG.error("[✗] 出生 %s 非盟重, 实验终止", world.me.map_name)
  51. stop[0] = True
  52. conn.close()
  53. return 1
  54. try:
  55. # ---- 走到传送员(真客户端: 9 步 3013 后点菜单) ----
  56. _LOG.info("[走] → 传送员%s", TP_POS)
  57. for _ in range(60):
  58. me = world.me
  59. if max(abs(TP_POS[0] - me.x), abs(TP_POS[1] - me.y)) <= 1:
  60. break
  61. dx = max(-1, min(1, TP_POS[0] - me.x))
  62. dy = max(-1, min(1, TP_POS[1] - me.y))
  63. h = next((i for i, (ox, oy) in enumerate(DIRS)
  64. if ox == dx and oy == dy), 4)
  65. mn = protocol_map_name(me.map_id, me.map_name)
  66. conn.send_frame(3013, 0, h, (me.y << 16) | me.x, sid,
  67. mn.encode(), pre=((me.y + dy * 2) << 16) | (me.x + dx * 2))
  68. time.sleep(0.55)
  69. apply_batch(world, upd, conn.recv_frames(0.4))
  70. _LOG.info("[走✓] @(%d,%d) — 静置1s(对齐真客户端节奏)",
  71. world.me.x, world.me.y)
  72. time.sleep(1.0)
  73. apply_batch(world, upd, conn.recv_frames(0.8))
  74. # ---- [帧1] CS1010 开菜单(字节级同真客户端) ----
  75. conn.send_frame(1010, 0, 0, 0, sid, js({"UserID": TP_AID, "index": 2}))
  76. _LOG.info("[帧1] CS1010 开菜单 — 等 2.0s(真客户端含一次心跳)")
  77. time.sleep(2.0)
  78. apply_batch(world, upd, conn.recv_frames(1.5))
  79. _LOG.info("菜单到达: %s", world.npc_dialog is not None)
  80. # ---- [帧2] CS1011 点【比奇矿区】(键序: UserID,index,Act) ----
  81. body2 = js({"UserID": "999999999", "index": 999999999,
  82. "Act": "@jiuxian_move_1,1,1"})
  83. conn.send_frame(1011, 0, 0, 0, sid, body2)
  84. _LOG.info("[帧2] CS1011 @jiuxian_move_1,1,1 (blen=%d, 键序=UserID,index,Act)",
  85. len(body2))
  86. time.sleep(1.15)
  87. apply_batch(world, upd, conn.recv_frames(0.8))
  88. # ---- [帧3] CS1011 点【进入地图】 ----
  89. body3 = js({"UserID": "999999999", "index": 999999999,
  90. "Act": "@act_move,1"})
  91. conn.send_frame(1011, 0, 0, 0, sid, body3)
  92. _LOG.info("[帧3] CS1011 @act_move,1 (blen=%d)", len(body3))
  93. # ---- [帧4/5] ACK 5012+5027('比奇矿区') — 20ms 内 ----
  94. time.sleep(0.02)
  95. conn.send_frame(5012, 0, 0, 0, sid, "比奇矿区".encode())
  96. conn.send_frame(5027, 0, 0, 0, sid, "比奇矿区".encode())
  97. _LOG.info("[帧4/5] ACK 5012+5027('比奇矿区') — 等落地 12s")
  98. # ---- 判定 ----
  99. for i in range(24):
  100. time.sleep(0.5)
  101. apply_batch(world, upd, conn.recv_frames(0.4))
  102. if world.me.map_name == "比奇矿区":
  103. _LOG.info("[✓✓✓] 传送成功! %s @(%d,%d) (耗时 %.1fs)",
  104. world.me.map_name, world.me.x, world.me.y,
  105. (i + 1) * 0.5)
  106. stop[0] = True
  107. conn.close()
  108. return 0
  109. if (i + 1) % 4 == 0:
  110. _LOG.info(" ... %.1fs 仍在 %s", (i + 1) * 0.5,
  111. world.me.map_name)
  112. _LOG.error("[✗] 12s 未落地 — 字节级复刻仍静默, 剩余变量: 会话冷却")
  113. stop[0] = True
  114. conn.close()
  115. return 1
  116. except KeyboardInterrupt:
  117. stop[0] = True
  118. conn.close()
  119. return 130
  120. if __name__ == "__main__":
  121. sys.exit(main())