watch_tp_pathfind.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. """守望+传送+自动寻路 一条龙 v2(位置对齐驱动):
  2. 拉回根因(2026-09-29 diag 实证):
  3. - 登录回城过渡态位置错位(SC51/出生帧/SC634 坐标互相矛盾) → 起点错 → SC28 拒
  4. - 落点被占 → 服务器推挤到邻近空格(SC28 p2=实际放向)
  5. - SC28 拉回后 update.py 采信服务器位 = 天然对齐手段, 下一步即通
  6. → 策略: 连续走位, 每步读回帧; SC1 继续, SC28 采信新位立即重发; 不再误判
  7. "会话限制", 不再长时间静置。
  8. """
  9. from __future__ import annotations
  10. import json
  11. import logging
  12. import re
  13. import sys
  14. import time
  15. from collections import deque
  16. from pathlib import Path
  17. ROOT = Path(__file__).resolve().parents[2]
  18. sys.path.insert(0, str(ROOT / "cpp_reversed"))
  19. sys.path.insert(0, str(ROOT / "protocol_bot" / "src"))
  20. import requests # noqa: E402
  21. from pbot.config import Account # noqa: E402
  22. from pbot.game.messages import protocol_map_name # noqa: E402
  23. from pbot.game.session import enter_world # noqa: E402
  24. from pbot.login.box_client import enter_game_client, login # noqa: E402
  25. from pbot.transport.gateway import GatewayConnection, login_gate_with_retry # noqa: E402
  26. from pbot.world.update import WorldUpdater # noqa: E402
  27. from map_parser import MapData, load as load_map # noqa: E402
  28. from path_find import find_path, path_is_valid # noqa: E402
  29. logging.basicConfig(level=logging.INFO,
  30. format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
  31. stream=sys.stdout)
  32. _LOG = logging.getLogger("watch_tp")
  33. DIRS = ((0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1))
  34. DIS_X, DIS_Y = 24, 16
  35. RUN_STEP_S = 0.85 # 步频(kill.py 0.7 同级, 留封速余量)
  36. STEP_TIMEOUT_S = 2.0 # 单步无回帧超时 → 以当前位重发
  37. MAX_ALIGN_ROUNDS = 40 # 走位总轮数上限
  38. PORTS = (8021, 8061, 8001, 8041)
  39. TELEPORTER_POS = (343, 339) # 用户实证(2026-09-29): 传送员实体位; 靠近才能点对话
  40. TELEPORTER_AID = "10002"
  41. TARGET_MAP = "地牢一层东"
  42. MAP_FILES = {"地牢一层东": "d401.map"}
  43. _LINK_RE = re.compile(r"link=@([^|<>]+)")
  44. def pump(conn: GatewayConnection, world, updater, dt: float) -> list[str]:
  45. for f in conn.recv_frames(dt):
  46. updater.apply(world, f)
  47. kinds = [e.kind for e in world.events]
  48. world.events.clear()
  49. return kinds
  50. def walk_one_leg(conn: GatewayConnection, sid: int, world, updater,
  51. heading: int, dist: int, proto: str) -> str:
  52. """发一腿并等回帧; 返回 'ok'(SC1) / 'push'(SC28) / 'silent'。"""
  53. me = (world.me.x, world.me.y)
  54. dx, dy = DIRS[heading]
  55. _ = conn.send_frame(3013, 0, heading, (me[1] << 16) | me[0], sid,
  56. proto.encode(),
  57. pre=((me[1] + dy * dist) << 16) | (me[0] + dx * dist))
  58. t0 = time.time()
  59. kinds: list[str] = []
  60. while time.time() - t0 < STEP_TIMEOUT_S:
  61. kinds = pump(conn, world, updater, 0.05)
  62. if "self_move_confirmed" in kinds:
  63. return "ok"
  64. if "pull_back" in kinds or "anchor_echo" in kinds:
  65. return "push"
  66. return "silent"
  67. def goto(conn: GatewayConnection, sid: int, world, updater, m: MapData,
  68. target: tuple[int, int]) -> bool:
  69. """连续自愈走位: 规划→逐腿; SC28 采信新位立即重规划续走。"""
  70. proto = protocol_map_name(world.me.map_id, world.me.map_name)
  71. for _round in range(MAX_ALIGN_ROUNDS):
  72. me = (world.me.x, world.me.y)
  73. if max(abs(me[0] - target[0]), abs(me[1] - target[1])) <= 2:
  74. return True
  75. path = find_path(m, me[0], me[1], target[0], target[1],
  76. dis_x=80, dis_y=80)
  77. if not path:
  78. path = _plan_multiseg(m, me, target)
  79. if not path:
  80. _LOG.warning("[走位] 无路 %s → %s", me, target)
  81. time.sleep(3.0)
  82. continue
  83. legs = _compress(path)
  84. _LOG.info("[走位] (%d,%d)→(%d,%d) %d步%d腿", me[0], me[1],
  85. target[0], target[1], len(path), len(legs))
  86. li = 0
  87. while li < len(legs):
  88. me = (world.me.x, world.me.y)
  89. while li < len(legs) and max(abs(legs[li][0] - me[0]),
  90. abs(legs[li][1] - me[1])) <= 2:
  91. li += 1
  92. if li >= len(legs):
  93. break
  94. if max(abs(legs[li][0] - me[0]), abs(legs[li][1] - me[1])) > 4:
  95. break # 偏航 → 外层重规划
  96. nxt = legs[li]
  97. dx = max(-1, min(1, nxt[0] - me[0]))
  98. dy = max(-1, min(1, nxt[1] - me[1]))
  99. heading = next((i for i, (ox, oy) in enumerate(DIRS)
  100. if ox == dx and oy == dy), 4)
  101. dist = 2 if (abs(nxt[0] - me[0]) == 2
  102. or abs(nxt[1] - me[1]) == 2) else 1
  103. r = walk_one_leg(conn, sid, world, updater, heading, dist, proto)
  104. if r == "ok":
  105. li += 1
  106. elif r == "push":
  107. # 采信服务器位(已由 updater 更新), 立即重规划续走
  108. _LOG.info("[拉回→采信] (%d,%d) 重规划", world.me.x, world.me.y)
  109. break
  110. else:
  111. _LOG.warning("[静默] 重发")
  112. if max(abs(world.me.x - target[0]),
  113. abs(world.me.y - target[1])) <= 2:
  114. return True
  115. return max(abs(world.me.x - target[0]),
  116. abs(world.me.y - target[1])) <= 3
  117. def _compress(path: list[tuple[int, int]]) -> list[tuple[int, int]]:
  118. legs: list[tuple[int, int]] = []
  119. i = 0
  120. while i < len(path) - 1:
  121. if i + 2 < len(path):
  122. d1 = (path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1])
  123. d2 = (path[i + 2][0] - path[i + 1][0],
  124. path[i + 2][1] - path[i + 1][1])
  125. if d1 == d2 and d1 in DIRS:
  126. legs.append(path[i + 2])
  127. i += 2
  128. continue
  129. legs.append(path[i + 1])
  130. i += 1
  131. return legs
  132. def _plan_multiseg(m: MapData, beg: tuple[int, int],
  133. end: tuple[int, int]) -> list[tuple[int, int]] | None:
  134. full: list[tuple[int, int]] = [beg]
  135. cur = beg
  136. while cur != end:
  137. dx, dy = end[0] - cur[0], end[1] - cur[1]
  138. d = max(abs(dx), abs(dy))
  139. if d == 0:
  140. break
  141. step = min(20, d)
  142. wx = max(cur[0] - DIS_X, min(cur[0] + DIS_X, cur[0] + round(dx * step / d)))
  143. wy = max(cur[1] - DIS_Y, min(cur[1] + DIS_Y, cur[1] + round(dy * step / d)))
  144. got = None
  145. for r in range(9):
  146. for ox in range(-r, r + 1):
  147. for oy in range(-r, r + 1):
  148. if max(abs(ox), abs(oy)) != r:
  149. continue
  150. c = (wx + ox, wy + oy)
  151. if not (0 <= c[0] < m.width and 0 <= c[1] < m.height):
  152. continue
  153. s = find_path(m, cur[0], cur[1], c[0], c[1])
  154. if s:
  155. got = s
  156. break
  157. if got:
  158. break
  159. if got:
  160. break
  161. if not got:
  162. return None
  163. full.extend(got[1:])
  164. cur = got[-1]
  165. if len(full) > 3000:
  166. return None
  167. return full
  168. def bfs_walkable(m: MapData, start: tuple[int, int]) -> dict[tuple[int, int], int]:
  169. walk = [[m.get_cell(x, y).is_walkable
  170. for x in range(m.width)] for y in range(m.height)]
  171. dist: dict[tuple[int, int], int] = {start: 0}
  172. q: deque[tuple[int, int]] = deque([start])
  173. while q:
  174. x, y = q.popleft()
  175. for dx, dy in DIRS:
  176. nx, ny = x + dx, y + dy
  177. if not (0 <= nx < m.width and 0 <= ny < m.height):
  178. continue
  179. if not walk[ny][nx] or (nx, ny) in dist:
  180. continue
  181. if dx and dy and (not walk[y][x + dx] or not walk[y + dy][x]):
  182. continue
  183. dist[(nx, ny)] = dist[(x, y)] + 1
  184. q.append((nx, ny))
  185. return dist
  186. def main() -> int:
  187. http = requests.Session()
  188. http.trust_env = False
  189. http.verify = False
  190. jwt, uid = login(Account(username="longwu988", password="Longwu1988"))
  191. _ = enter_game_client(http, jwt, 14558825)
  192. key32, sid, rname, tkey, gport = login_gate_with_retry(
  193. 14558825, jwt, uid, "凌风踏雪", PORTS)
  194. stop = [False]
  195. conn, world, updater = enter_world(14558825, rname, tkey, sid, gport, uid,
  196. stop=stop)
  197. _LOG.info("[出生] %s @(%d,%d)", world.me.map_name, world.me.x, world.me.y)
  198. try:
  199. # 回城过渡态静置: 等 SC51/SC634 序列稳定(位置真源对齐窗口)
  200. time.sleep(8.0)
  201. pump(conn, world, updater, 1.0)
  202. _LOG.info("[对齐] 当前采信位 (%d,%d)", world.me.x, world.me.y)
  203. if world.me.map_name == "盟重土城":
  204. m3 = load_map(str(ROOT / "protocol_bot" / "assets" / "maps" / "3.map"))
  205. if not goto(conn, sid, world, updater, m3, TELEPORTER_POS):
  206. _LOG.error("[走位] 到不了传送员")
  207. return 1
  208. _LOG.info("[到位] 传送员旁 @(%d,%d)", world.me.x, world.me.y)
  209. # NPC 必须先进视野(SC10)服务器才受理对话; 优先用视野内名字含
  210. # "传送/九"的 NPC 真实 aid, 无则用静态 aid=10002 兜底(kill.py 同源)
  211. t0 = time.time()
  212. while time.time() - t0 < 8:
  213. pump(conn, world, updater, 0.2)
  214. if any("传送" in n.name or "九" in n.name
  215. for n in world.npcs.values()):
  216. break
  217. npcs = [(n.aid, n.name, n.x, n.y) for n in world.npcs.values()]
  218. _LOG.info("[视野] NPC %d 个: %s", len(npcs),
  219. [(a[:8], nm[:10], x, y) for a, nm, x, y in npcs[:6]])
  220. aid = next((n.aid for n in world.npcs.values()
  221. if "传送" in n.name or "九" in n.name), TELEPORTER_AID)
  222. _LOG.info("[对话] 用 aid=%s", aid[:16])
  223. # 会话前置: 9/27 成功轮点传送员前发过 CS3031(任务链激活);
  224. # 今天跳过任务链直接 1010 全部无弹窗 — 补 CS3031 前置
  225. _ = conn.send_frame(
  226. 3031, 0, 0, 0, sid,
  227. (ROOT / "protocol_bot" / "assets" / "cs3031_full.bin").read_bytes())
  228. _LOG.info("[前置] CS3031 已发, 2s 后点对话")
  229. time.sleep(2.0)
  230. pump(conn, world, updater, 1.0)
  231. body = json.dumps({"UserID": aid, "index": 2},
  232. separators=(",", ":")).encode()
  233. clicked: set[str] = set()
  234. arrived = False
  235. for round_i in range(4):
  236. _ = conn.send_frame(1010, 0, 0, 0, sid, body)
  237. _LOG.info("[对话] CS1010 传送员 第%d次", round_i + 1)
  238. dlg = None
  239. t0 = time.time()
  240. while time.time() - t0 < 12:
  241. pump(conn, world, updater, 0.2)
  242. if world.npc_dialog:
  243. dlg = world.npc_dialog
  244. break
  245. if dlg is None:
  246. _LOG.warning("[对话] 无弹窗, 重试")
  247. continue
  248. links = _LINK_RE.findall(str(dlg.get("sMsg", "")))
  249. _LOG.info("[SC643] 按钮: %s", links[:20])
  250. acts = [a for a in links if a not in ("exit", "close")
  251. and a not in clicked]
  252. if not acts:
  253. break
  254. for act in acts:
  255. _ = conn.send_frame(
  256. 1011, 0, 0, 0, sid,
  257. json.dumps({"UserID": str(dlg.get("UserID", "999999999")),
  258. "Act": f"@{act}",
  259. "index": int(dlg.get("index", 999999999))},
  260. separators=(",", ":")).encode())
  261. clicked.add(act)
  262. _LOG.info("[对话] 点 @%s", act)
  263. t0 = time.time()
  264. while time.time() - t0 < 8:
  265. pump(conn, world, updater, 0.2)
  266. if world.me.map_name == TARGET_MAP:
  267. arrived = True
  268. break
  269. if arrived:
  270. break
  271. if arrived:
  272. break
  273. if not arrived:
  274. _LOG.error("[传送] 未落地地牢 — 终态 %s", world.me.map_name)
  275. return 1
  276. if world.me.map_name != TARGET_MAP:
  277. _LOG.error("[地图] 不在地牢: %s", world.me.map_name)
  278. return 1
  279. # ---- 落地 ACK + 自动寻路 ----
  280. proto = protocol_map_name(world.me.map_id, world.me.map_name)
  281. for _ in range(2):
  282. _ = conn.send_frame(5012, 0, 0, 0, sid, proto.encode())
  283. _ = conn.send_frame(5027, 0, 0, 0, sid, proto.encode())
  284. _ = conn.send_frame(5012, 0, 0, 0, 32611848, b"cTh")
  285. t0 = time.time()
  286. while time.time() - t0 < 4:
  287. pump(conn, world, updater, 0.2)
  288. _LOG.info("[落地ACK] %s @(%d,%d)", world.me.map_name, world.me.x,
  289. world.me.y)
  290. start = (world.me.x, world.me.y)
  291. md = load_map(str(ROOT / "protocol_bot" / "assets" / "maps"
  292. / MAP_FILES[TARGET_MAP]))
  293. if not (0 <= start[0] < md.width and 0 <= start[1] < md.height):
  294. _LOG.error("[地图] 落脚点越界 %s", start)
  295. return 1
  296. dist = bfs_walkable(md, start)
  297. goal = max(dist, key=lambda p: ((p[0] - start[0]) ** 2
  298. + (p[1] - start[1]) ** 2, dist[p]))
  299. _LOG.info("[目标] (%d,%d) 可达域 %d 格 曼哈顿 %d", goal[0], goal[1],
  300. len(dist), abs(goal[0] - start[0]) + abs(goal[1] - start[1]))
  301. t1 = time.time()
  302. path = _plan_multiseg(md, start, goal)
  303. if path is None or not path_is_valid(md, path):
  304. _LOG.error("[规划] 失败")
  305. return 1
  306. legs = _compress(path)
  307. _LOG.info("[规划] %d 步 → %d 腿, %.2fs", len(path), len(legs),
  308. time.time() - t1)
  309. _ = conn.send_frame(5013, goal[0], goal[1], 1, sid, b"")
  310. _LOG.info("[5013] START → (%d,%d)", *goal)
  311. t_start = time.time()
  312. confirmed = rejected = silent = 0
  313. li = 0
  314. replan = 0
  315. cur_legs = legs
  316. while li < len(cur_legs) and replan < 30:
  317. me = (world.me.x, world.me.y)
  318. while li < len(cur_legs) and max(abs(cur_legs[li][0] - me[0]),
  319. abs(cur_legs[li][1] - me[1])) <= 2:
  320. li += 1
  321. if li >= len(cur_legs):
  322. break
  323. if max(abs(cur_legs[li][0] - me[0]),
  324. abs(cur_legs[li][1] - me[1])) > 4:
  325. rest = _plan_multiseg(md, me, goal)
  326. if rest is None:
  327. _LOG.error("[重规划] 无路 — ABORT")
  328. break
  329. cur_legs = _compress(rest)
  330. li = 0
  331. replan += 1
  332. _LOG.info("[偏航重规划] #%d @(%d,%d)", replan, me[0], me[1])
  333. continue
  334. nxt = cur_legs[li]
  335. dx = max(-1, min(1, nxt[0] - me[0]))
  336. dy = max(-1, min(1, nxt[1] - me[1]))
  337. heading = next((i for i, (ox, oy) in enumerate(DIRS)
  338. if ox == dx and oy == dy), 4)
  339. d2 = 2 if (abs(nxt[0] - me[0]) == 2
  340. or abs(nxt[1] - me[1]) == 2) else 1
  341. r = walk_one_leg(conn, sid, world, updater, heading, d2, proto)
  342. if r == "ok":
  343. confirmed += 1
  344. li += 1
  345. elif r == "push":
  346. rejected += 1
  347. rest = _plan_multiseg(md, (world.me.x, world.me.y), goal)
  348. if rest is None:
  349. _LOG.error("[重规划] 无路 — ABORT")
  350. break
  351. cur_legs = _compress(rest)
  352. li = 0
  353. replan += 1
  354. else:
  355. silent += 1
  356. li += 1 # 静默: 位置可能仍对, 下一循环校正
  357. if (confirmed + rejected) % 30 == 0 and confirmed + rejected:
  358. _LOG.info("[进度] #%d @(%d,%d) → (%d,%d) ✓%d ✗%d 静默%d",
  359. confirmed + rejected, world.me.x, world.me.y,
  360. goal[0], goal[1], confirmed, rejected, silent)
  361. elapsed = time.time() - t_start
  362. _ = conn.send_frame(5013, 0, 0, 3, sid, b"")
  363. final = (world.me.x, world.me.y)
  364. ok = max(abs(final[0] - goal[0]), abs(final[1] - goal[1])) <= 3
  365. _LOG.info("=" * 60)
  366. _LOG.info("[结果] %s 起点(%d,%d)→目标(%d,%d) 终点(%d,%d)",
  367. "✓到达" if ok else "✗未达", start[0], start[1],
  368. goal[0], goal[1], final[0], final[1])
  369. _LOG.info("[计时] %.1fs (%.1f 分钟)", elapsed, elapsed / 60)
  370. _LOG.info("[帧统计] SC1=%d SC28=%d 静默=%d 重规划=%d",
  371. confirmed, rejected, silent, replan)
  372. return 0 if ok else 2
  373. finally:
  374. stop[0] = True
  375. conn.close()
  376. if __name__ == "__main__":
  377. sys.exit(main())