"""守望+传送+自动寻路 一条龙 v2(位置对齐驱动): 拉回根因(2026-09-29 diag 实证): - 登录回城过渡态位置错位(SC51/出生帧/SC634 坐标互相矛盾) → 起点错 → SC28 拒 - 落点被占 → 服务器推挤到邻近空格(SC28 p2=实际放向) - SC28 拉回后 update.py 采信服务器位 = 天然对齐手段, 下一步即通 → 策略: 连续走位, 每步读回帧; SC1 继续, SC28 采信新位立即重发; 不再误判 "会话限制", 不再长时间静置。 """ from __future__ import annotations import json import logging import re import sys import time from collections import deque from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "cpp_reversed")) sys.path.insert(0, str(ROOT / "protocol_bot" / "src")) import requests # noqa: E402 from pbot.config import Account # noqa: E402 from pbot.game.messages import protocol_map_name # noqa: E402 from pbot.game.session import enter_world # noqa: E402 from pbot.login.box_client import enter_game_client, login # noqa: E402 from pbot.transport.gateway import GatewayConnection, login_gate_with_retry # noqa: E402 from pbot.world.update import WorldUpdater # noqa: E402 from map_parser import MapData, load as load_map # noqa: E402 from path_find import find_path, path_is_valid # noqa: E402 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", stream=sys.stdout) _LOG = logging.getLogger("watch_tp") DIRS = ((0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)) DIS_X, DIS_Y = 24, 16 RUN_STEP_S = 0.85 # 步频(kill.py 0.7 同级, 留封速余量) STEP_TIMEOUT_S = 2.0 # 单步无回帧超时 → 以当前位重发 MAX_ALIGN_ROUNDS = 40 # 走位总轮数上限 PORTS = (8021, 8061, 8001, 8041) TELEPORTER_POS = (343, 339) # 用户实证(2026-09-29): 传送员实体位; 靠近才能点对话 TELEPORTER_AID = "10002" TARGET_MAP = "地牢一层东" MAP_FILES = {"地牢一层东": "d401.map"} _LINK_RE = re.compile(r"link=@([^|<>]+)") def pump(conn: GatewayConnection, world, updater, dt: float) -> list[str]: for f in conn.recv_frames(dt): updater.apply(world, f) kinds = [e.kind for e in world.events] world.events.clear() return kinds def walk_one_leg(conn: GatewayConnection, sid: int, world, updater, heading: int, dist: int, proto: str) -> str: """发一腿并等回帧; 返回 'ok'(SC1) / 'push'(SC28) / 'silent'。""" me = (world.me.x, world.me.y) dx, dy = DIRS[heading] _ = conn.send_frame(3013, 0, heading, (me[1] << 16) | me[0], sid, proto.encode(), pre=((me[1] + dy * dist) << 16) | (me[0] + dx * dist)) t0 = time.time() kinds: list[str] = [] while time.time() - t0 < STEP_TIMEOUT_S: kinds = pump(conn, world, updater, 0.05) if "self_move_confirmed" in kinds: return "ok" if "pull_back" in kinds or "anchor_echo" in kinds: return "push" return "silent" def goto(conn: GatewayConnection, sid: int, world, updater, m: MapData, target: tuple[int, int]) -> bool: """连续自愈走位: 规划→逐腿; SC28 采信新位立即重规划续走。""" proto = protocol_map_name(world.me.map_id, world.me.map_name) for _round in range(MAX_ALIGN_ROUNDS): me = (world.me.x, world.me.y) if max(abs(me[0] - target[0]), abs(me[1] - target[1])) <= 2: return True path = find_path(m, me[0], me[1], target[0], target[1], dis_x=80, dis_y=80) if not path: path = _plan_multiseg(m, me, target) if not path: _LOG.warning("[走位] 无路 %s → %s", me, target) time.sleep(3.0) continue legs = _compress(path) _LOG.info("[走位] (%d,%d)→(%d,%d) %d步%d腿", me[0], me[1], target[0], target[1], len(path), len(legs)) li = 0 while li < len(legs): me = (world.me.x, world.me.y) while li < len(legs) and max(abs(legs[li][0] - me[0]), abs(legs[li][1] - me[1])) <= 2: li += 1 if li >= len(legs): break if max(abs(legs[li][0] - me[0]), abs(legs[li][1] - me[1])) > 4: break # 偏航 → 外层重规划 nxt = legs[li] dx = max(-1, min(1, nxt[0] - me[0])) dy = max(-1, min(1, nxt[1] - me[1])) heading = next((i for i, (ox, oy) in enumerate(DIRS) if ox == dx and oy == dy), 4) dist = 2 if (abs(nxt[0] - me[0]) == 2 or abs(nxt[1] - me[1]) == 2) else 1 r = walk_one_leg(conn, sid, world, updater, heading, dist, proto) if r == "ok": li += 1 elif r == "push": # 采信服务器位(已由 updater 更新), 立即重规划续走 _LOG.info("[拉回→采信] (%d,%d) 重规划", world.me.x, world.me.y) break else: _LOG.warning("[静默] 重发") if max(abs(world.me.x - target[0]), abs(world.me.y - target[1])) <= 2: return True return max(abs(world.me.x - target[0]), abs(world.me.y - target[1])) <= 3 def _compress(path: list[tuple[int, int]]) -> list[tuple[int, int]]: legs: list[tuple[int, int]] = [] i = 0 while i < len(path) - 1: if i + 2 < len(path): d1 = (path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1]) d2 = (path[i + 2][0] - path[i + 1][0], path[i + 2][1] - path[i + 1][1]) if d1 == d2 and d1 in DIRS: legs.append(path[i + 2]) i += 2 continue legs.append(path[i + 1]) i += 1 return legs def _plan_multiseg(m: MapData, beg: tuple[int, int], end: tuple[int, int]) -> list[tuple[int, int]] | None: full: list[tuple[int, int]] = [beg] cur = beg while cur != end: dx, dy = end[0] - cur[0], end[1] - cur[1] d = max(abs(dx), abs(dy)) if d == 0: break step = min(20, d) wx = max(cur[0] - DIS_X, min(cur[0] + DIS_X, cur[0] + round(dx * step / d))) wy = max(cur[1] - DIS_Y, min(cur[1] + DIS_Y, cur[1] + round(dy * step / d))) got = None for r in range(9): for ox in range(-r, r + 1): for oy in range(-r, r + 1): if max(abs(ox), abs(oy)) != r: continue c = (wx + ox, wy + oy) if not (0 <= c[0] < m.width and 0 <= c[1] < m.height): continue s = find_path(m, cur[0], cur[1], c[0], c[1]) if s: got = s break if got: break if got: break if not got: return None full.extend(got[1:]) cur = got[-1] if len(full) > 3000: return None return full def bfs_walkable(m: MapData, start: tuple[int, int]) -> dict[tuple[int, int], int]: walk = [[m.get_cell(x, y).is_walkable for x in range(m.width)] for y in range(m.height)] dist: dict[tuple[int, int], int] = {start: 0} q: deque[tuple[int, int]] = deque([start]) while q: x, y = q.popleft() for dx, dy in DIRS: nx, ny = x + dx, y + dy if not (0 <= nx < m.width and 0 <= ny < m.height): continue if not walk[ny][nx] or (nx, ny) in dist: continue if dx and dy and (not walk[y][x + dx] or not walk[y + dy][x]): continue dist[(nx, ny)] = dist[(x, y)] + 1 q.append((nx, ny)) return dist def main() -> int: http = requests.Session() http.trust_env = False http.verify = False jwt, uid = login(Account(username="longwu988", password="Longwu1988")) _ = enter_game_client(http, jwt, 14558825) key32, sid, rname, tkey, gport = login_gate_with_retry( 14558825, jwt, uid, "凌风踏雪", PORTS) stop = [False] conn, world, updater = enter_world(14558825, rname, tkey, sid, gport, uid, stop=stop) _LOG.info("[出生] %s @(%d,%d)", world.me.map_name, world.me.x, world.me.y) try: # 回城过渡态静置: 等 SC51/SC634 序列稳定(位置真源对齐窗口) time.sleep(8.0) pump(conn, world, updater, 1.0) _LOG.info("[对齐] 当前采信位 (%d,%d)", world.me.x, world.me.y) if world.me.map_name == "盟重土城": m3 = load_map(str(ROOT / "protocol_bot" / "assets" / "maps" / "3.map")) if not goto(conn, sid, world, updater, m3, TELEPORTER_POS): _LOG.error("[走位] 到不了传送员") return 1 _LOG.info("[到位] 传送员旁 @(%d,%d)", world.me.x, world.me.y) # NPC 必须先进视野(SC10)服务器才受理对话; 优先用视野内名字含 # "传送/九"的 NPC 真实 aid, 无则用静态 aid=10002 兜底(kill.py 同源) t0 = time.time() while time.time() - t0 < 8: pump(conn, world, updater, 0.2) if any("传送" in n.name or "九" in n.name for n in world.npcs.values()): break npcs = [(n.aid, n.name, n.x, n.y) for n in world.npcs.values()] _LOG.info("[视野] NPC %d 个: %s", len(npcs), [(a[:8], nm[:10], x, y) for a, nm, x, y in npcs[:6]]) aid = next((n.aid for n in world.npcs.values() if "传送" in n.name or "九" in n.name), TELEPORTER_AID) _LOG.info("[对话] 用 aid=%s", aid[:16]) # 会话前置: 9/27 成功轮点传送员前发过 CS3031(任务链激活); # 今天跳过任务链直接 1010 全部无弹窗 — 补 CS3031 前置 _ = conn.send_frame( 3031, 0, 0, 0, sid, (ROOT / "protocol_bot" / "assets" / "cs3031_full.bin").read_bytes()) _LOG.info("[前置] CS3031 已发, 2s 后点对话") time.sleep(2.0) pump(conn, world, updater, 1.0) body = json.dumps({"UserID": aid, "index": 2}, separators=(",", ":")).encode() clicked: set[str] = set() arrived = False for round_i in range(4): _ = conn.send_frame(1010, 0, 0, 0, sid, body) _LOG.info("[对话] CS1010 传送员 第%d次", round_i + 1) dlg = None t0 = time.time() while time.time() - t0 < 12: pump(conn, world, updater, 0.2) if world.npc_dialog: dlg = world.npc_dialog break if dlg is None: _LOG.warning("[对话] 无弹窗, 重试") continue links = _LINK_RE.findall(str(dlg.get("sMsg", ""))) _LOG.info("[SC643] 按钮: %s", links[:20]) acts = [a for a in links if a not in ("exit", "close") and a not in clicked] if not acts: break for act in acts: _ = conn.send_frame( 1011, 0, 0, 0, sid, json.dumps({"UserID": str(dlg.get("UserID", "999999999")), "Act": f"@{act}", "index": int(dlg.get("index", 999999999))}, separators=(",", ":")).encode()) clicked.add(act) _LOG.info("[对话] 点 @%s", act) t0 = time.time() while time.time() - t0 < 8: pump(conn, world, updater, 0.2) if world.me.map_name == TARGET_MAP: arrived = True break if arrived: break if arrived: break if not arrived: _LOG.error("[传送] 未落地地牢 — 终态 %s", world.me.map_name) return 1 if world.me.map_name != TARGET_MAP: _LOG.error("[地图] 不在地牢: %s", world.me.map_name) return 1 # ---- 落地 ACK + 自动寻路 ---- proto = protocol_map_name(world.me.map_id, world.me.map_name) for _ in range(2): _ = conn.send_frame(5012, 0, 0, 0, sid, proto.encode()) _ = conn.send_frame(5027, 0, 0, 0, sid, proto.encode()) _ = conn.send_frame(5012, 0, 0, 0, 32611848, b"cTh") t0 = time.time() while time.time() - t0 < 4: pump(conn, world, updater, 0.2) _LOG.info("[落地ACK] %s @(%d,%d)", world.me.map_name, world.me.x, world.me.y) start = (world.me.x, world.me.y) md = load_map(str(ROOT / "protocol_bot" / "assets" / "maps" / MAP_FILES[TARGET_MAP])) if not (0 <= start[0] < md.width and 0 <= start[1] < md.height): _LOG.error("[地图] 落脚点越界 %s", start) return 1 dist = bfs_walkable(md, start) goal = max(dist, key=lambda p: ((p[0] - start[0]) ** 2 + (p[1] - start[1]) ** 2, dist[p])) _LOG.info("[目标] (%d,%d) 可达域 %d 格 曼哈顿 %d", goal[0], goal[1], len(dist), abs(goal[0] - start[0]) + abs(goal[1] - start[1])) t1 = time.time() path = _plan_multiseg(md, start, goal) if path is None or not path_is_valid(md, path): _LOG.error("[规划] 失败") return 1 legs = _compress(path) _LOG.info("[规划] %d 步 → %d 腿, %.2fs", len(path), len(legs), time.time() - t1) _ = conn.send_frame(5013, goal[0], goal[1], 1, sid, b"") _LOG.info("[5013] START → (%d,%d)", *goal) t_start = time.time() confirmed = rejected = silent = 0 li = 0 replan = 0 cur_legs = legs while li < len(cur_legs) and replan < 30: me = (world.me.x, world.me.y) while li < len(cur_legs) and max(abs(cur_legs[li][0] - me[0]), abs(cur_legs[li][1] - me[1])) <= 2: li += 1 if li >= len(cur_legs): break if max(abs(cur_legs[li][0] - me[0]), abs(cur_legs[li][1] - me[1])) > 4: rest = _plan_multiseg(md, me, goal) if rest is None: _LOG.error("[重规划] 无路 — ABORT") break cur_legs = _compress(rest) li = 0 replan += 1 _LOG.info("[偏航重规划] #%d @(%d,%d)", replan, me[0], me[1]) continue nxt = cur_legs[li] dx = max(-1, min(1, nxt[0] - me[0])) dy = max(-1, min(1, nxt[1] - me[1])) heading = next((i for i, (ox, oy) in enumerate(DIRS) if ox == dx and oy == dy), 4) d2 = 2 if (abs(nxt[0] - me[0]) == 2 or abs(nxt[1] - me[1]) == 2) else 1 r = walk_one_leg(conn, sid, world, updater, heading, d2, proto) if r == "ok": confirmed += 1 li += 1 elif r == "push": rejected += 1 rest = _plan_multiseg(md, (world.me.x, world.me.y), goal) if rest is None: _LOG.error("[重规划] 无路 — ABORT") break cur_legs = _compress(rest) li = 0 replan += 1 else: silent += 1 li += 1 # 静默: 位置可能仍对, 下一循环校正 if (confirmed + rejected) % 30 == 0 and confirmed + rejected: _LOG.info("[进度] #%d @(%d,%d) → (%d,%d) ✓%d ✗%d 静默%d", confirmed + rejected, world.me.x, world.me.y, goal[0], goal[1], confirmed, rejected, silent) elapsed = time.time() - t_start _ = conn.send_frame(5013, 0, 0, 3, sid, b"") final = (world.me.x, world.me.y) ok = max(abs(final[0] - goal[0]), abs(final[1] - goal[1])) <= 3 _LOG.info("=" * 60) _LOG.info("[结果] %s 起点(%d,%d)→目标(%d,%d) 终点(%d,%d)", "✓到达" if ok else "✗未达", start[0], start[1], goal[0], goal[1], final[0], final[1]) _LOG.info("[计时] %.1fs (%.1f 分钟)", elapsed, elapsed / 60) _LOG.info("[帧统计] SC1=%d SC28=%d 静默=%d 重规划=%d", confirmed, rejected, silent, replan) return 0 if ok else 2 finally: stop[0] = True conn.close() if __name__ == "__main__": sys.exit(main())