| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- """守望+传送+自动寻路 一条龙 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())
|