run_juece13_wugong_farthest.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. #!/usr/bin/env python3
  2. """run_juece13_wugong_farthest.py — 角色【新角色】→ 蜈蚣洞穴(地牢一层东) → 走到最远点 → 静止。
  3. 验证 2026-09-30 凌晨映射修正(地牢一层东=d601)的实跑效果:
  4. 登录 → 盟重传送员(342,338) → @jiuxian_move_1,1,2(蜈蚣洞穴) + @act_move,1
  5. → ACK 5012+5027(body='地牢一层东') → 落地 → d601 墙 BFS 最远点
  6. → MirWalkExecutor 跑图 → 静止 → 退出
  7. 用法:
  8. uv run python scripts/run_juece13_wugong_farthest.py -u longwu988 -p Longwu1988 \
  9. --zone 14570166 --name 新角色 [--pre-wait 60] [--hold-seconds 300]
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import logging
  14. import math
  15. import sys
  16. import time
  17. from collections import deque
  18. from typing import override
  19. import requests
  20. from pbot.bot.behavior import Behavior, TickResult
  21. from pbot.bot.kill import KillBehavior
  22. from pbot.bot.pathfind import LearnedGrid, astar, load_walls_from_map
  23. from pbot.bot.runner import pump, run
  24. from pbot.config import Account
  25. from pbot.errors import PbotError
  26. from pbot.game.session import enter_world
  27. from pbot.login.box_client import enter_game_client, login
  28. from pbot.transport.gateway import GatewayConnection, login_gate_with_retry
  29. from pbot.types import ZoneId
  30. from pbot.world.mirror import WorldMirror
  31. sys.path.insert(0, "scripts")
  32. from run_juece13_mine_farthest import ( # noqa: E402
  33. DIR_VECS, MirWalkExecutor, next_dir, prev_dir, setup_logging, banner,
  34. )
  35. _LOG = logging.getLogger("juece13_wugong")
  36. _PORTS: tuple[int, ...] = (8021, 8061, 8001, 8041)
  37. CAVE_MAP = "地牢一层东"
  38. TOWN_MAP = "盟重土城"
  39. CAVE_W, CAVE_H = 250, 250
  40. _CAVE_MAP_FILE = "assets/maps/d601.map"
  41. # 真机抓包(cap_live2, 21040 帧): 地牢一层东移动帧 body = 显示名(文件ID=中文名)
  42. _ACK_BODY = "地牢一层东"
  43. _TELEPORT_ACTS: tuple[str, ...] = ("@jiuxian_move_1,1,2", "@act_move,1")
  44. SC28_PATH = "assets/cave_sc28_walls.json"
  45. _CAVE_WALLS: set[tuple[int, int]] | None = None
  46. def _cave_walls() -> set[tuple[int, int]]:
  47. """加载 d601.map 墙格(权威地形, sourid 实抓定案)."""
  48. global _CAVE_WALLS
  49. if _CAVE_WALLS is None:
  50. _CAVE_WALLS = load_walls_from_map(_CAVE_MAP_FILE)
  51. return _CAVE_WALLS
  52. def find_farthest(start: tuple[int, int]) -> tuple[tuple[int, int], int, bool]:
  53. """d601 墙 BFS 最远可达点."""
  54. walls = _cave_walls()
  55. visited: set[tuple[int, int]] = {start}
  56. dq = deque([(start, 0)])
  57. far, far_d = start, 0
  58. while dq:
  59. (x, y), d = dq.popleft()
  60. if d > far_d:
  61. far, far_d = (x, y), d
  62. for dx, dy in DIR_VECS:
  63. nxt = (x + dx, y + dy)
  64. if nxt in visited or not (0 <= nxt[0] < CAVE_W and 0 <= nxt[1] < CAVE_H):
  65. continue
  66. if nxt in walls:
  67. continue
  68. visited.add(nxt)
  69. dq.append((nxt, d + 1))
  70. return far, far_d, len(visited) < 50
  71. class CaveEnterBehavior(KillBehavior):
  72. """盟重→蜈蚣洞穴传送(菜单第 2 项); 落地地牢一层东即 DONE.
  73. 复用 mine_farthest.MineEnterBehavior 的盟重 1 格步进+滑行相位,
  74. 仅替换传送 Act 序列与落地判定图名/ACK body(父类 tp_dialog 为硬编码
  75. 比奇矿区序列, 这里接管 _tick_tp_dialog 相位自点).
  76. """
  77. def __init__(self, *args: object, **kwargs: object) -> None:
  78. super().__init__(*args, **kwargs)
  79. self._cave_act_i = 0
  80. self._cave_phase_t = 0.0
  81. self._pending_rej = False
  82. self._pending_rej_t = 0.0
  83. self._slip_dir: int | None = None
  84. self._slip_try = 0
  85. @override
  86. def on_enter(self, conn: GatewayConnection, world: WorldMirror) -> None:
  87. self._world = world
  88. self._last_step_t = time.time()
  89. if self._bound is None:
  90. self._bound = ((world.me.x, world.me.y), 25)
  91. self._load_map_if_needed(world)
  92. if world.me.map_name == CAVE_MAP:
  93. _LOG.info("[洞穴] 出生已在 %s @(%d,%d)", CAVE_MAP,
  94. world.me.x, world.me.y)
  95. self._phase = "done"
  96. return
  97. self._declare_guaji(conn)
  98. if world.me.map_name == TOWN_MAP or "土城" in world.me.map_name:
  99. self._allow_reroll = False
  100. self._enter("tp_goto")
  101. self._slip_dir = None
  102. _LOG.info("[盟重] @(%d,%d) — 1格步进+滑行走向传送员", world.me.x, world.me.y)
  103. else:
  104. self._force_act = False
  105. self._allow_reroll = True
  106. _LOG.info("[%s] @(%d,%d) — 走默认任务链/传送员兜底",
  107. world.me.map_name, world.me.x, world.me.y)
  108. self._enter("renwu_3031")
  109. @override
  110. def tick(self, conn: GatewayConnection, world: WorldMirror) -> object:
  111. if world.me.known() and world.me.map_name == CAVE_MAP:
  112. _LOG.info("[洞穴✓] 落地 %s @(%d,%d)", CAVE_MAP, world.me.x, world.me.y)
  113. return TickResult.DONE
  114. if self._phase == "tp_goto" and world.me.known():
  115. return self._tick_tp_goto_cave(conn, world)
  116. # run16b 定案: _enter("tp_dialog") 后父类主 tick 的 tp_dialog 分支
  117. # 会抢先接管(读 kill.py 矿区 TELEPORT_ACTS+ACK='比奇矿区') — 菜单
  118. # 点击其实已定目的地, 但洞穴/沃玛第 1 步不同, 必须全程自管该相位
  119. if self._phase == "tp_dialog":
  120. return self._tick_tp_dialog_cave(conn, world, time.time())
  121. result = super().tick(conn, world)
  122. if world.me.known() and world.me.map_name == CAVE_MAP:
  123. _LOG.info("[洞穴✓] 落地 %s @(%d,%d)(传送中检测)", CAVE_MAP,
  124. world.me.x, world.me.y)
  125. return TickResult.DONE
  126. _ = conn
  127. return result
  128. def _tick_tp_goto_cave(self, conn: GatewayConnection,
  129. world: WorldMirror) -> TickResult:
  130. """盟重 1 格步进+滑行到传送员 → 到达转自点洞穴菜单."""
  131. from pbot.bot.wander import DIRS as WDIRS
  132. from pbot.bot.kill import TELEPORTER_POS
  133. me = world.me
  134. now = time.time()
  135. tx, ty = TELEPORTER_POS
  136. if max(abs(tx - me.x), abs(ty - me.y)) <= 2:
  137. _LOG.info("[盟重✓] 已到传送员旁 @(%d,%d) — 开菜单点蜈蚣洞穴",
  138. me.x, me.y)
  139. self._enter("tp_dialog")
  140. self._cave_act_i = 0
  141. self._cave_phase_t = now
  142. return self._tick_tp_dialog_cave(conn, world, now)
  143. if getattr(self, "_pending_rej", False):
  144. if now - self._pending_rej_t < 0.9:
  145. return TickResult.RUNNING
  146. self._pending_rej = False
  147. if now - self._last_step_t < 0.9:
  148. return TickResult.RUNNING
  149. for e in world.events:
  150. if e.kind == "self_move_confirmed":
  151. self._slip_dir = None
  152. elif e.kind == "pull_back":
  153. self._pending_rej = True
  154. world.events.clear()
  155. want = self._dir8(me.x, me.y, tx, ty)
  156. d = want
  157. if self._slip_dir is not None:
  158. d = self._slip_dir
  159. elif self._pending_rej:
  160. cand = next_dir(want) if getattr(self, "_slip_try", 0) % 2 == 0 \
  161. else prev_dir(want)
  162. self._slip_try = getattr(self, "_slip_try", 0) + 1
  163. d = cand
  164. _LOG.info("[盟重·滑行] want=%d → dir=%d", want, d)
  165. self._pending_rej = False
  166. dx, dy = WDIRS[d]
  167. from pbot.game.messages import protocol_map_name
  168. mn = protocol_map_name(me.map_id, me.map_name)
  169. _ = conn.send_frame(3011, 0, d, (me.y << 16) | me.x, self._sid,
  170. mn.encode(), pre=((me.y + dy) << 16) | (me.x + dx))
  171. self._pending_rej = True
  172. self._pending_rej_t = time.time()
  173. self._last_step_t = self._pending_rej_t
  174. return TickResult.RUNNING
  175. def _tick_tp_dialog_cave(self, conn: GatewayConnection,
  176. world: WorldMirror, now: float) -> TickResult:
  177. """点 @jiuxian_move_1,1,2(蜈蚣洞穴) → @act_move,1 → ACK(中文图名)."""
  178. dlg = world.npc_dialog
  179. if dlg is None:
  180. if now - self._cave_phase_t > 15.0:
  181. self._cave_phase_t = now
  182. import json as _json
  183. from pbot.bot.kill import TELEPORTER_AID
  184. body = _json.dumps({"UserID": TELEPORTER_AID, "index": 2},
  185. separators=(",", ":")).encode()
  186. _ = conn.send_frame(1010, 0, 0, 0, self._sid, body)
  187. return TickResult.RUNNING
  188. if self._cave_act_i < len(_TELEPORT_ACTS):
  189. if now - self._cave_phase_t >= (1.2 if self._cave_act_i else 0.8):
  190. act = _TELEPORT_ACTS[self._cave_act_i]
  191. self._cave_act_i += 1
  192. import json as _json
  193. body = _json.dumps({"UserID": dlg.get("UserID", "999999999"),
  194. "index": dlg.get("index", 999999999),
  195. "Act": act},
  196. separators=(",", ":")).encode()
  197. _ = conn.send_frame(1011, 0, 0, 0, self._sid, body)
  198. _LOG.info("[传送] 点 %s (%d/%d)", act, self._cave_act_i,
  199. len(_TELEPORT_ACTS))
  200. self._cave_phase_t = now
  201. if self._cave_act_i == len(_TELEPORT_ACTS):
  202. _ = conn.send_frame(5012, 0, 0, 0, self._sid,
  203. _ACK_BODY.encode())
  204. _ = conn.send_frame(5027, 0, 0, 0, self._sid,
  205. _ACK_BODY.encode())
  206. _LOG.info("[传送] ACK 5012+5027('%s')已发 — 等落地", _ACK_BODY)
  207. return TickResult.RUNNING
  208. return TickResult.RUNNING
  209. def _dir8(self, x: int, y: int, tx: int, ty: int) -> int:
  210. dx = max(-1, min(1, tx - x))
  211. dy = max(-1, min(1, ty - y))
  212. return next((i for i, (ox, oy) in enumerate(DIR_VECS)
  213. if ox == dx and oy == dy), 4)
  214. def parse_args() -> argparse.Namespace:
  215. """命令行."""
  216. ap = argparse.ArgumentParser()
  217. _ = ap.add_argument("-u", "--user", required=True)
  218. _ = ap.add_argument("-p", "--password", required=True)
  219. _ = ap.add_argument("--zone", type=int, required=True)
  220. _ = ap.add_argument("--name", required=True)
  221. _ = ap.add_argument("--hold-seconds", type=float, default=300.0)
  222. _ = ap.add_argument("--pre-wait", type=float, default=60.0)
  223. _ = ap.add_argument("--login-port", type=int, default=0)
  224. return ap.parse_args()
  225. def main() -> int:
  226. """登录 → 进蜈蚣洞穴 → 最远点跑图 → 静止 → 退出."""
  227. a = parse_args()
  228. setup_logging()
  229. banner("[0] 启动: 区=%d 角色=%s 静止=%.0fs 预跑=%.0fs",
  230. a.zone, a.name, a.hold_seconds, a.pre_wait)
  231. http = requests.Session()
  232. http.trust_env = False
  233. http.verify = False
  234. banner("[1] 盒子登录")
  235. jwt, uid = login(Account(username=a.user, password=a.password))
  236. _LOG.info("[✓] uid=%d", uid)
  237. banner("[2] EnterGameClient zone=%d", a.zone)
  238. for attempt in range(4):
  239. try:
  240. _ = enter_game_client(http, jwt, ZoneId(a.zone))
  241. _LOG.info("[✓] 上报成功")
  242. break
  243. except Exception as e:
  244. if attempt == 3:
  245. raise
  246. _LOG.warning("[!] 第%d次失败(%s), 12s 后重试", attempt + 1, e)
  247. time.sleep(12)
  248. banner("[3] 登录服选角: %s", a.name)
  249. ports = (a.login_port,) if a.login_port else _PORTS
  250. key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
  251. ZoneId(a.zone), jwt, uid, a.name, ports)
  252. _LOG.info("[票据] key32=%s sid=%d 角色=%s 游戏端口=%d",
  253. key32.decode(errors="replace"), sid, real_name, game_port)
  254. banner("[4] 进游戏")
  255. stop: list[bool] = [False]
  256. conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
  257. sid, game_port, uid, stop=stop)
  258. _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
  259. world.me.y)
  260. try:
  261. banner("[5] 喝药保护确认")
  262. from pbot.game.heal import enable_auto_heal
  263. state = enable_auto_heal(conn, sid, want_on=True, world=world)
  264. _LOG.info("[%s] 喝药状态=%s", "✓" if state == "on" else "!", state)
  265. if world.me.map_name == CAVE_MAP:
  266. banner("[6] 出生已在 %s @(%d,%d) — 跳过传送",
  267. CAVE_MAP, world.me.x, world.me.y)
  268. else:
  269. banner("[6] 进蜈蚣洞穴: 当前 %s @(%d,%d)",
  270. world.me.map_name, world.me.x, world.me.y)
  271. enter_bh = CaveEnterBehavior(sid=sid, kill_target=0)
  272. r = run(conn, world, updater, enter_bh, duration_s=300.0)
  273. _LOG.info("[传送结果] result=%s map=%s @(%d,%d)",
  274. r.value, world.me.map_name, world.me.x, world.me.y)
  275. if world.me.map_name != CAVE_MAP:
  276. _LOG.error("[✗] 未能进入蜈蚣洞穴(看上方日志), 退出")
  277. return 1
  278. time.sleep(3.0)
  279. _ = pump(conn, world, updater, 2.0)
  280. if a.pre_wait > 0:
  281. banner("[6.5] 落地静置 %.0f 秒再出发", a.pre_wait)
  282. deadline = time.time() + a.pre_wait
  283. while time.time() < deadline:
  284. _ = pump(conn, world, updater, 0.5)
  285. start_pt = (world.me.x, world.me.y)
  286. banner("[7] d601 可达域分析: 起点(%d,%d)", *start_pt)
  287. far, d_steps, is_enclave = find_farthest(start_pt)
  288. _LOG.info("[最远点] (%d,%d) 路径距离 %d 步(直线 %.0f)",
  289. far[0], far[1], d_steps,
  290. math.hypot(far[0] - start_pt[0], far[1] - start_pt[1]))
  291. banner("[8] MirWalk 跑图 → (%d,%d)", far[0], far[1])
  292. cave_walls = _cave_walls()
  293. executor = MirWalkExecutor(sid=sid, dest=far, start_pt=start_pt,
  294. walls=cave_walls, map_name=CAVE_MAP)
  295. est_s = d_steps * 0.75 * 4 + 180.0
  296. _LOG.info("预计跑动上限 %.0fs(弹回冗余×4)", est_s)
  297. r = run(conn, world, updater, executor, duration_s=est_s)
  298. final_d = max(abs(far[0] - world.me.x), abs(far[1] - world.me.y))
  299. _LOG.info("[寻路结束] result=%s 位置=(%d,%d) 距目标 %d 格 map=%s "
  300. "(发腿=%d 弹回=%d 滑行=%d)",
  301. r.value, world.me.x, world.me.y, final_d,
  302. world.me.map_name, executor.steps, executor.rejected,
  303. executor.slips)
  304. banner("[9] 静止 %.0f 秒", a.hold_seconds)
  305. deadline = time.time() + a.hold_seconds
  306. while time.time() < deadline:
  307. _ = pump(conn, world, updater, 0.5)
  308. _LOG.info("[静止结束] 最终 map=%s @(%d,%d)", world.me.map_name,
  309. world.me.x, world.me.y)
  310. return 0
  311. finally:
  312. stop[0] = True
  313. conn.close()
  314. _LOG.info("连接已关闭")
  315. if __name__ == "__main__":
  316. try:
  317. sys.exit(main())
  318. except PbotError as e:
  319. _LOG.error("失败: %s", e.detail)
  320. sys.exit(1)