tp_probe_pathfind.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. """tp_probe_pathfind.py — 探测传送员真实标识 → 对话传送 → 自动寻路计时。
  2. 前置实证(2026-09-29):
  3. - 走位自愈链路已通(goto 拉回采信循环, 13s 内到传送员旁)
  4. - 静态 aid=10002 + index=2 的 CS1010 全部无响应(9/27 同参数 0.18s 即回 SC643)
  5. - quest_bot3 定案: CS1010 的 UserID=index 来自 NPC 的 SC10 推送(动态, 每角色不同)
  6. → 本脚本: 站到传送员旁收集 SC10 原始帧, 找 body 含"传送"的实体, 用其真实
  7. (info[0], p1) 做对话候选; 全部失败则穷举视野实体 × index 候选。
  8. """
  9. from __future__ import annotations
  10. import importlib.util
  11. import json
  12. import logging
  13. import re
  14. import sys
  15. import time
  16. from collections import deque
  17. from pathlib import Path
  18. ROOT = Path(__file__).resolve().parents[2]
  19. sys.path.insert(0, str(ROOT / "cpp_reversed"))
  20. sys.path.insert(0, str(ROOT / "protocol_bot" / "src"))
  21. import requests # noqa: E402
  22. from pbot.codec.bitbuf import decode6 # noqa: E402
  23. from pbot.config import Account # noqa: E402
  24. from pbot.game.messages import protocol_map_name # noqa: E402
  25. from pbot.game.session import enter_world # noqa: E402
  26. from pbot.login.box_client import enter_game_client, login # noqa: E402
  27. from pbot.transport.gateway import login_gate_with_retry # noqa: E402
  28. from pbot.world.update import WorldUpdater, decode_body_variants # noqa: E402
  29. from map_parser import load as load_map # noqa: E402
  30. from path_find import path_is_valid # noqa: E402
  31. # 复用已验证的 goto/pump/规划 helpers
  32. _spec = importlib.util.spec_from_file_location(
  33. "wt", ROOT / "protocol_bot" / "scripts" / "watch_tp_pathfind.py")
  34. wt = importlib.util.module_from_spec(_spec)
  35. sys.modules["wt"] = wt
  36. _spec.loader.exec_module(wt)
  37. _LOG = logging.getLogger("tp_probe")
  38. TARGET_MAP = "地牢一层东"
  39. LINK_RE = re.compile(r"link=@([^|<>]+)")
  40. def _nearest_walkable(m, pos: tuple[int, int]) -> tuple[int, int] | None:
  41. """pos 周围最近的可走格(螺旋), 供墙格出生点逃逸。"""
  42. if 0 <= pos[0] < m.width and 0 <= pos[1] < m.height \
  43. and m.get_cell(pos[0], pos[1]).is_walkable:
  44. return pos
  45. for rr in range(1, 6):
  46. for dx in range(-rr, rr + 1):
  47. for dy in range(-rr, rr + 1):
  48. if max(abs(dx), abs(dy)) != rr:
  49. continue
  50. x, y = pos[0] + dx, pos[1] + dy
  51. if 0 <= x < m.width and 0 <= y < m.height \
  52. and m.get_cell(x, y).is_walkable:
  53. return (x, y)
  54. return None
  55. def collect_sc10(conn, world, updater, seconds: float) -> list:
  56. """收帧 seconds; 返回 SC10 原始帧列表(同时喂 updater)。"""
  57. out = []
  58. t0 = time.time()
  59. while time.time() - t0 < seconds:
  60. for f in conn.recv_frames(0.2):
  61. if f.msg_id == 10:
  62. out.append(f)
  63. updater.apply(world, f)
  64. world.events.clear()
  65. return out
  66. def sc10_info(f) -> dict | None:
  67. """解析 SC10: 返回 {race, key, x, y, aid, name, raw} 或 None。"""
  68. race = f.recog & 0xFFFF
  69. x, y = f.p1 & 0xFFFF, (f.p1 >> 16) & 0xFFFF
  70. info: list[str] = []
  71. raw = ""
  72. for blob in decode_body_variants(f.body):
  73. try:
  74. txt = blob.decode("utf-8")
  75. except UnicodeDecodeError:
  76. continue
  77. raw = txt
  78. try:
  79. j = json.loads(txt)
  80. except ValueError:
  81. continue
  82. if isinstance(j, dict) and isinstance(j.get("info"), list):
  83. info = [str(v) for v in j["info"]]
  84. break
  85. return {"race": race, "key": str(f.p1), "x": x, "y": y,
  86. "aid": info[0] if info else "", "name": info[1] if len(info) > 1
  87. else "", "raw": raw, "p2": f.p2, "p3": f.p3}
  88. def main() -> int:
  89. http = requests.Session()
  90. http.trust_env = False
  91. http.verify = False
  92. jwt, uid = login(Account(username="longwu988", password="Longwu1988"))
  93. _ = enter_game_client(http, jwt, 14558825)
  94. key32, sid, rname, tkey, gport = login_gate_with_retry(
  95. 14558825, jwt, uid, "凌风踏雪", wt.PORTS)
  96. stop = [False]
  97. conn, world, updater = enter_world(14558825, rname, tkey, sid, gport, uid,
  98. stop=stop)
  99. _LOG.info("[出生] %s @(%d,%d)", world.me.map_name, world.me.x, world.me.y)
  100. try:
  101. if world.me.map_name != TARGET_MAP:
  102. time.sleep(8.0)
  103. wt.pump(conn, world, updater, 1.0)
  104. # 回城落地必须 ACK 盟重(5012/5027, README 铁律), 否则服务器
  105. # 不下发本图视野推送(SC10) — 拿不到传送员动态 aid/index
  106. proto3 = protocol_map_name(world.me.map_id, world.me.map_name)
  107. for _ in range(2):
  108. _ = conn.send_frame(5012, 0, 0, 0, sid, proto3.encode())
  109. _ = conn.send_frame(5027, 0, 0, 0, sid, proto3.encode())
  110. _ = conn.send_frame(5012, 0, 0, 0, 32611848, b"cTh")
  111. _LOG.info("[盟重ACK] body=%r", proto3)
  112. t0 = time.time()
  113. while time.time() - t0 < 6:
  114. wt.pump(conn, world, updater, 0.3)
  115. m3 = load_map(str(ROOT / "protocol_bot" / "assets" / "maps"
  116. / "3.map"))
  117. # 出生点可能落在墙格(服务器随机落点): 先逃到最近可走格再规划
  118. me0 = (world.me.x, world.me.y)
  119. if not m3.get_cell(me0[0], me0[1]).is_walkable:
  120. near = _nearest_walkable(m3, me0)
  121. if near and wt.goto(conn, sid, world, updater, m3, near):
  122. _LOG.info("[出墙] %s(墙) → %s", me0, near)
  123. if not wt.goto(conn, sid, world, updater, m3, wt.TELEPORTER_POS):
  124. _LOG.error("[走位] 失败")
  125. return 1
  126. _LOG.info("[到位] (%d,%d)", world.me.x, world.me.y)
  127. # ---- 探测: 收集 SC10, 找传送员 ----
  128. sc10s = collect_sc10(conn, world, updater, 12.0)
  129. _LOG.info("[SC10] 共 %d 帧", len(sc10s))
  130. parsed = [p for p in (sc10_info(f) for f in sc10s) if p]
  131. for p in parsed[:20]:
  132. _LOG.info(" SC10 race=%d key=%s pos=(%d,%d) aid=%s name=%s",
  133. p["race"], p["key"], p["x"], p["y"], p["aid"][:16],
  134. p["name"][:16])
  135. tp = [p for p in parsed
  136. if "传送" in p["name"] or "九" in p["name"]
  137. or "传送" in p["raw"]]
  138. _LOG.info("[探测] 含'传送'实体: %d 个", len(tp))
  139. # ---- 构造对话候选 ----
  140. cands: list[tuple[str, int]] = []
  141. for p in tp:
  142. cands.append((p["aid"], int(p["key"]) if p["key"].isdigit()
  143. else 2))
  144. cands.append((p["aid"], 2))
  145. if not cands:
  146. cands = [(wt.TELEPORTER_AID, 2)]
  147. # 兜底穷举: 视野实体 aid × 常见 index
  148. extra = [(p["aid"], idx) for p in parsed
  149. if p["aid"].isdigit() for idx in (2, 35, 34)]
  150. for c in extra:
  151. if c not in cands:
  152. cands.append(c)
  153. _LOG.info("[候选] %s", cands[:12])
  154. # ---- 逐候选点对话 ----
  155. dlg = None
  156. for i, (aid_c, idx_c) in enumerate(cands[:10]):
  157. body = json.dumps({"UserID": aid_c, "index": idx_c},
  158. separators=(",", ":")).encode()
  159. _ = conn.send_frame(1010, 0, 0, 0, sid, body)
  160. _LOG.info("[对话] 候选%d aid=%s index=%d", i + 1, aid_c[:16],
  161. idx_c)
  162. t0 = time.time()
  163. while time.time() - t0 < 4:
  164. wt.pump(conn, world, updater, 0.2)
  165. if world.npc_dialog:
  166. dlg = world.npc_dialog
  167. break
  168. if dlg is not None:
  169. _LOG.info("[弹窗✓] 候选%d 命中", i + 1)
  170. break
  171. if dlg is None:
  172. _LOG.error("[对话] 全部候选无弹窗 — dump 终态")
  173. return 1
  174. smsg = str(dlg.get("sMsg", ""))
  175. links = LINK_RE.findall(smsg)
  176. _LOG.info("[SC643] UserID=%s index=%s 按钮: %s",
  177. dlg.get("UserID"), dlg.get("index"), links[:24])
  178. # 连点全部非 exit 按钮(9/27 模式: QFunction 一次给全, 顺序点)
  179. for act in [a for a in links if a not in ("exit", "close")]:
  180. _ = conn.send_frame(
  181. 1011, 0, 0, 0, sid,
  182. json.dumps({"UserID": str(dlg.get("UserID", "999999999")),
  183. "Act": f"@{act}",
  184. "index": int(dlg.get("index", 999999999))},
  185. separators=(",", ":")).encode())
  186. _LOG.info("[点] @%s", act)
  187. t0 = time.time()
  188. while time.time() - t0 < 5:
  189. wt.pump(conn, world, updater, 0.2)
  190. if world.me.map_name == TARGET_MAP:
  191. break
  192. if world.me.map_name == TARGET_MAP:
  193. break
  194. if world.me.map_name != TARGET_MAP:
  195. _LOG.error("[传送] 未落地 — 终态 %s", world.me.map_name)
  196. return 1
  197. # ---- 地牢: ACK + 自动寻路计时 ----
  198. proto = protocol_map_name(world.me.map_id, world.me.map_name)
  199. for _ in range(2):
  200. _ = conn.send_frame(5012, 0, 0, 0, sid, proto.encode())
  201. _ = conn.send_frame(5027, 0, 0, 0, sid, proto.encode())
  202. _ = conn.send_frame(5012, 0, 0, 0, 32611848, b"cTh")
  203. t0 = time.time()
  204. while time.time() - t0 < 4:
  205. wt.pump(conn, world, updater, 0.2)
  206. _LOG.info("[落地] %s @(%d,%d)", world.me.map_name, world.me.x,
  207. world.me.y)
  208. start = (world.me.x, world.me.y)
  209. md = load_map(str(ROOT / "protocol_bot" / "assets" / "maps"
  210. / wt.MAP_FILES[TARGET_MAP]))
  211. if not (0 <= start[0] < md.width and 0 <= start[1] < md.height):
  212. _LOG.error("[地图] 落脚点越界 %s", start)
  213. return 1
  214. dist = wt.bfs_walkable(md, start)
  215. goal = max(dist, key=lambda p: ((p[0] - start[0]) ** 2
  216. + (p[1] - start[1]) ** 2, dist[p]))
  217. _LOG.info("[目标] (%d,%d) 可达域 %d 格 曼哈顿 %d", goal[0], goal[1],
  218. len(dist), abs(goal[0] - start[0]) + abs(goal[1] - start[1]))
  219. t1 = time.time()
  220. path = wt.plan_multiseg(md, start, goal)
  221. if path is None or not path_is_valid(md, path):
  222. _LOG.error("[规划] 失败")
  223. return 1
  224. legs = wt._compress(path)
  225. _LOG.info("[规划] %d 步 → %d 腿, %.2fs", len(path), len(legs),
  226. time.time() - t1)
  227. _ = conn.send_frame(5013, goal[0], goal[1], 1, sid, b"")
  228. _LOG.info("[5013] START → (%d,%d)", *goal)
  229. t_start = time.time()
  230. confirmed = rejected = silent = replan = 0
  231. li = 0
  232. cur_legs = legs
  233. while li < len(cur_legs) and replan < 40:
  234. me = (world.me.x, world.me.y)
  235. while li < len(cur_legs) and max(abs(cur_legs[li][0] - me[0]),
  236. abs(cur_legs[li][1] - me[1])) <= 2:
  237. li += 1
  238. if li >= len(cur_legs):
  239. break
  240. if max(abs(cur_legs[li][0] - me[0]),
  241. abs(cur_legs[li][1] - me[1])) > 4:
  242. rest = wt.plan_multiseg(md, me, goal)
  243. if rest is None:
  244. _LOG.error("[重规划] 无路 — ABORT")
  245. break
  246. cur_legs = wt._compress(rest)
  247. li = 0
  248. replan += 1
  249. continue
  250. nxt = cur_legs[li]
  251. dx = max(-1, min(1, nxt[0] - me[0]))
  252. dy = max(-1, min(1, nxt[1] - me[1]))
  253. heading = next((i for i, (ox, oy) in enumerate(wt.DIRS)
  254. if ox == dx and oy == dy), 4)
  255. d2 = 2 if (abs(nxt[0] - me[0]) == 2
  256. or abs(nxt[1] - me[1]) == 2) else 1
  257. r = wt.walk_one_leg(conn, sid, world, updater, heading, d2, proto)
  258. if r == "ok":
  259. confirmed += 1
  260. li += 1
  261. elif r == "push":
  262. rejected += 1
  263. rest = wt.plan_multiseg(md, (world.me.x, world.me.y), goal)
  264. if rest is None:
  265. break
  266. cur_legs = wt._compress(rest)
  267. li = 0
  268. replan += 1
  269. else:
  270. silent += 1
  271. li += 1
  272. n = confirmed + rejected
  273. if n and n % 40 == 0:
  274. _LOG.info("[进度] #%d @(%d,%d) ✓%d ✗%d 静默%d",
  275. n, world.me.x, world.me.y, goal[0], goal[1],
  276. confirmed, rejected, silent)
  277. elapsed = time.time() - t_start
  278. _ = conn.send_frame(5013, 0, 0, 3, sid, b"")
  279. final = (world.me.x, world.me.y)
  280. ok = max(abs(final[0] - goal[0]), abs(final[1] - goal[1])) <= 3
  281. _LOG.info("=" * 60)
  282. _LOG.info("[结果] %s 起点(%d,%d)→目标(%d,%d) 终点(%d,%d)",
  283. "✓到达" if ok else "✗未达", start[0], start[1],
  284. goal[0], goal[1], final[0], final[1])
  285. _LOG.info("[计时] %.1fs (%.1f 分钟)", elapsed, elapsed / 60)
  286. _LOG.info("[帧统计] SC1=%d SC28=%d 静默=%d 重规划=%d",
  287. confirmed, rejected, silent, replan)
  288. return 0 if ok else 2
  289. finally:
  290. stop[0] = True
  291. conn.close()
  292. if __name__ == "__main__":
  293. sys.exit(main())