run_juece13_mine_farthest.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. """run_juece13_mine_farthest.py — 角色【新角色】→ 比奇矿区 → 走到最远点 → 静止5分钟。
  2. 流程:
  3. [1-4] 登录 → 选角 → 进游戏(与 kill30 系列同链)
  4. [5] 喝药保护确认
  5. [6] 进矿区:
  6. 出生=盟重土城 → KillBehavior 传送链复用
  7. (tp_goto: A* 走到传送员(342,338) → CS1010{UserID:10002,index:2}
  8. → SC643 菜单 → 连点 @jiuxian_move_1,1 → @jiuxian_move_1,1,1(比奇矿区)
  9. → @act_move,1(进入地图) — 2026-09-26 tp 实测)
  10. 出生=比奇矿区 → 直接进入下一阶段
  11. [7] 可达域 BFS: 从落点出发, 找地图上最远可达点(d401.map 权威墙格)
  12. [8] AutoMoveBehavior: A* 路径 + CS5013 START + 3013 跑链走到最远点
  13. [9] 静止不动(仅心跳+收帧), 5 分钟后自动退出
  14. 用法:
  15. uv run python scripts/run_juece13_mine_farthest.py -u longwu988 -p Longwu1988 \
  16. --zone 14570166 --name 新角色 [--hold-seconds 300]
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import logging
  21. import math
  22. import sys
  23. import time
  24. from collections import deque
  25. from typing import override
  26. import requests
  27. from pbot.bot.behavior import Behavior, TickResult
  28. from pbot.bot.kill import KillBehavior
  29. from pbot.bot.pathfind import LearnedGrid, astar, load_walls_from_map
  30. from pbot.bot.runner import pump, run
  31. from pbot.config import Account
  32. from pbot.errors import PbotError
  33. from pbot.game.session import enter_world
  34. from pbot.login.box_client import enter_game_client, login
  35. from pbot.transport.gateway import login_gate_with_retry
  36. from pbot.types import ZoneId
  37. from pbot.world.mirror import WorldMirror
  38. _LOG = logging.getLogger("juece13_far")
  39. _PORTS: tuple[int, ...] = (8021, 8061, 8001, 8041)
  40. MINE_MAP = "比奇矿区"
  41. TOWN_MAP = "盟重土城"
  42. RENWU_PHASES = frozenset({"renwu_3031", "renwu_1011", "wait_map"})
  43. # 矿区 = d401.map 200x200(2026-09-29 定案; 旧 621/400x102 判定作废)
  44. MINE_W, MINE_H = 200, 200
  45. _MINE_MAP_FILE = "assets/maps/d401.map"
  46. def setup_logging() -> None:
  47. """全量 INFO 日志。"""
  48. logging.basicConfig(
  49. level=logging.INFO,
  50. format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
  51. stream=sys.stdout,
  52. )
  53. def banner(msg: str, *args: object) -> None:
  54. """阶段分隔条日志。"""
  55. _LOG.info("======== %s ========", msg % args if args else msg)
  56. class MineEnterBehavior(KillBehavior):
  57. """盟重→矿区传送 + 矿内不打怪不游走; 落地矿区即 DONE。
  58. 复用父类 tp_goto/tp_dialog 相位(走到传送员+三连点), 但:
  59. - 矿内(意外提前落地): 永不 renwu, 直接 DONE
  60. - renwu 链在盟重失败 → 父类自动走 tp_goto 兜底(v1/v3 实测可达)
  61. """
  62. @override
  63. def on_enter(self, conn: GatewayConnection, world: WorldMirror) -> None:
  64. self._world = world
  65. self._last_step_t = time.time()
  66. if self._bound is None:
  67. self._bound = ((world.me.x, world.me.y), 25)
  68. self._load_map_if_needed(world)
  69. if world.me.map_name == MINE_MAP:
  70. _LOG.info("[矿区] 出生已在 %s @(%d,%d)", MINE_MAP,
  71. world.me.x, world.me.y)
  72. self._phase = "done" # 不进入任何行动相位
  73. return
  74. # 挂机声明对传送无害(真客户端进图也发), 保留以最大化会话合法性
  75. self._declare_guaji(conn)
  76. if world.me.map_name == TOWN_MAP or "土城" in world.me.map_name:
  77. # 盟重: 传送员就在 7 格外, 不走父类 A* 路径腿(_run_toward 在
  78. # 盟重实测对直线路径反复弹回死循环) — 改用 1 格步进 + 滑行
  79. # (Crystal 直走逻辑), 到达后父类 tp_goto 自然接管点对话
  80. self._allow_reroll = False
  81. self._enter("tp_goto")
  82. self._slip_dir: int | None = None # 滑行后固定的方向
  83. _LOG.info("[盟重] @(%d,%d) — 1格步进+滑行走向传送员(任务链已失效)",
  84. world.me.x, world.me.y)
  85. else:
  86. self._force_act = False
  87. self._allow_reroll = True
  88. _LOG.info("[%s] @(%d,%d) — 走默认任务链/传送员兜底",
  89. world.me.map_name, world.me.x, world.me.y)
  90. self._enter("renwu_3031")
  91. @override
  92. def tick(self, conn: GatewayConnection, world: WorldMirror) -> object:
  93. if world.me.known() and world.me.map_name == MINE_MAP:
  94. _LOG.info("[矿区✓] 落地 @(%d,%d) — 传送阶段完成",
  95. world.me.x, world.me.y)
  96. return TickResult.DONE
  97. # 盟重 tp_goto: 1 格步进 + 撞墙滑行(Crystal 直走逻辑), 替代父类
  98. # A* 路径腿(实测直线路径反复弹回死循环)
  99. if self._phase == "tp_goto" and world.me.known() \
  100. and not world.me.map_name == MINE_MAP:
  101. return self._tick_tp_goto_slip(conn, world)
  102. result = super().tick(conn, world)
  103. if world.me.known() and world.me.map_name == MINE_MAP:
  104. _LOG.info("[矿区✓] 落地 @(%d,%d)(传送中检测) — 完成",
  105. world.me.x, world.me.y)
  106. return TickResult.DONE
  107. _ = conn
  108. return result
  109. def _tick_tp_goto_slip(self, conn: GatewayConnection,
  110. world: WorldMirror) -> TickResult:
  111. """1 格步进走向传送员(342,338); 撞墙顺/逆 45° 滑行; 到达转 tp_dialog。"""
  112. from pbot.bot.wander import DIRS as WDIRS
  113. from pbot.bot.kill import TELEPORTER_POS, TELEPORTER_AID
  114. me = world.me
  115. now = time.time()
  116. tx, ty = TELEPORTER_POS
  117. if max(abs(tx - me.x), abs(ty - me.y)) <= 2:
  118. _LOG.info("[盟重✓] 已到传送员旁 @(%d,%d) — 交回父类点对话",
  119. me.x, me.y)
  120. self._enter("tp_goto")
  121. self._slip_dir = None
  122. return super().tick(conn, world) # 父类到达分支: 发 1010 开菜单
  123. if self._pending_rej:
  124. if now - self._pending_rej_t < 0.9:
  125. return TickResult.RUNNING
  126. self._pending_rej = False
  127. if now - self._last_step_t < 0.9:
  128. return TickResult.RUNNING
  129. # 事件消费: SC1 确认清滑行锁; 仅 pull_back=真弹回才触发滑行
  130. # (anchor_echo 是锚点申报回显=位置确认, 非拒绝)
  131. for e in world.events:
  132. if e.kind == "self_move_confirmed":
  133. self._slip_dir = None
  134. elif e.kind == "pull_back":
  135. self._slip_blocked = True
  136. world.events.clear()
  137. want = self._dir8(me.x, me.y, tx, ty)
  138. d = want
  139. if self._slip_dir is not None:
  140. d = self._slip_dir # 滑行中锁定方向直到 SC1 确认
  141. elif self._slip_blocked:
  142. # 原版滑行: 顺 45° → 逆 45°, 轮流试
  143. cand = next_dir(want) if self._slip_try % 2 == 0 \
  144. else prev_dir(want)
  145. self._slip_try += 1
  146. d = cand
  147. _LOG.info("[盟重·滑行] want=%d blocked → dir=%d", want, d)
  148. self._slip_blocked = False
  149. dx, dy = WDIRS[d]
  150. fx, fy = me.x + dx, me.y + dy
  151. from pbot.game.messages import protocol_map_name
  152. mn = protocol_map_name(me.map_id, me.map_name)
  153. _ = conn.send_frame(3011, 0, d, (me.y << 16) | me.x, self._sid,
  154. mn.encode(), pre=(fy << 16) | fx)
  155. self._last_step_t = time.time()
  156. self._pending_rej = True
  157. self._pending_rej_t = self._last_step_t
  158. _LOG.info("[盟重·走] (%d,%d) dir=%d → (%d,%d)", me.x, me.y, d, fx, fy)
  159. return TickResult.RUNNING
  160. def _dir8(self, x: int, y: int, tx: int, ty: int) -> int:
  161. dx = max(-1, min(1, tx - x))
  162. dy = max(-1, min(1, ty - y))
  163. from pbot.bot.wander import DIRS as WDIRS
  164. return next((i for i, (ox, oy) in enumerate(WDIRS)
  165. if ox == dx and oy == dy), 4)
  166. def __init__(self, *args: object, **kwargs: object) -> None:
  167. super().__init__(*args, **kwargs) # type: ignore[arg-type]
  168. self._pending_rej = False
  169. self._pending_rej_t = 0.0
  170. self._slip_blocked = False
  171. self._slip_try = 0
  172. self._slip_dir = None
  173. @override
  174. def _pick_target(self, world: WorldMirror) -> object:
  175. """永不打怪。"""
  176. return None
  177. @override
  178. def _credit_kill(self, world: WorldMirror, aid: int, name: str,
  179. signal: str) -> None:
  180. """永不计杀(打怪被禁)。"""
  181. _ = world, aid, name, signal
  182. _D401_WALLS: set[tuple[int, int]] | None = None
  183. def _d401_walls() -> set[tuple[int, int]]:
  184. """加载 d401.map 墙格(权威地形, 2026-09-29 定案)。"""
  185. global _D401_WALLS
  186. if _D401_WALLS is None:
  187. _D401_WALLS = load_walls_from_map(_MINE_MAP_FILE)
  188. return _D401_WALLS
  189. def find_farthest(start: tuple[int, int]) -> tuple[tuple[int, int], int, bool]:
  190. """矿区可达域 BFS(d401 墙): 返回(最远可达点, 路径步数, 是否飞地落点)。
  191. d401 定案后恢复真 BFS — 墙数据 100% 吻合实测, 最远点即主连通块
  192. 内 BFS 深度最大的格子, 不再需要"全图开阔区任意远点"的放水兜底。
  193. """
  194. walls = _d401_walls()
  195. visited: set[tuple[int, int]] = {start}
  196. dq = deque([(start, 0)])
  197. far, far_d = start, 0
  198. while dq:
  199. (x, y), d = dq.popleft()
  200. if d > far_d:
  201. far, far_d = (x, y), d
  202. for dx, dy in DIR_VECS:
  203. nxt = (x + dx, y + dy)
  204. if nxt in visited or not (0 <= nxt[0] < MINE_W and 0 <= nxt[1] < MINE_H):
  205. continue
  206. if nxt in walls:
  207. continue
  208. visited.add(nxt)
  209. dq.append((nxt, d + 1))
  210. return far, far_d, len(visited) < 50
  211. _SC28_PATH = "assets/mine_sc28_walls.json"
  212. def _load_sc28_walls() -> set[tuple[int, int]]:
  213. """加载跨会话累积的 SC28 实测墙格(服务器权威地形)。"""
  214. import json
  215. from pathlib import Path
  216. try:
  217. data = json.loads(Path(_SC28_PATH).read_text())
  218. if data.get("map") == MINE_MAP:
  219. return {(int(x), int(y)) for x, y in data.get("walls", [])}
  220. except (OSError, ValueError):
  221. pass
  222. return set()
  223. def _save_sc28_walls(walls: set[tuple[int, int]]) -> None:
  224. """回写 SC28 实测墙格(与旧数据合并)。"""
  225. import json
  226. from pathlib import Path
  227. old = _load_sc28_walls()
  228. merged = sorted(old | walls)
  229. Path(_SC28_PATH).write_text(json.dumps(
  230. {"map": MINE_MAP,
  231. "updated": time.strftime("%Y-%m-%dT%H:%M"),
  232. "count": len(merged),
  233. "walls": merged}, ensure_ascii=False, indent=0))
  234. _LOG.info("[MirWalk] SC28 实测墙回写 %d 格(累计 %d) → %s",
  235. len(walls), len(merged), _SC28_PATH)
  236. def next_dir(d: int) -> int:
  237. """顺时针 45°(0上1右上2右3右下4下5左下6左7左上)。"""
  238. return (d + 1) % 8
  239. def prev_dir(d: int) -> int:
  240. """逆时针 45°。"""
  241. return (d - 1) % 8
  242. DIR_VECS: tuple[tuple[int, int], ...] = (
  243. (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1))
  244. class MirWalkExecutor(Behavior):
  245. """传奇原版 AutoPath 忠实复刻(Suprcode/Crystal@0e315fe GameScene.cs:11715):
  246. 每 tick:
  247. 1. 消费事件: SC28 弹回格 → grid.mark_blocked(唯一的"学习"机制)
  248. 2. A* 重算 当前位置 → 终点(200×200 网格实测 0-5ms, 非负担)
  249. 3. path 为空 → 终点不可达, 等待(可能弹回学习后恢复)
  250. 4. 取 path[0] 方向: CanRun(方向 2 格全可走)→ 跑; CanWalk → 走
  251. 无路径游标/滑行锁/振荡计数 — 原版就是每帧重算, 墙数据保证 A* 每次给出
  252. 当前最优路, 弹回一次下次自动绕开(此前打转 = 游标卡死 + 补丁状态机)。
  253. """
  254. name = "mir_walk"
  255. def __init__(self, sid: int, dest: tuple[int, int],
  256. start_pt: tuple[int, int], allow_replan: bool = True,
  257. walls: set[tuple[int, int]] | None = None,
  258. learned_walls: set[tuple[int, int]] | None = None,
  259. map_name: str = "") -> None:
  260. del allow_replan, start_pt
  261. self._sid = sid
  262. self._dest = dest
  263. self._map_name = map_name
  264. self._died = False
  265. self.steps = 0
  266. self.rejected = 0
  267. self.slips = 0
  268. self._pending = False
  269. self._pending_t = 0.0
  270. self._last_t = 0.0
  271. self._no_path_n = 0
  272. self._replans = 0
  273. self._last_target: tuple[int, int] | None = None
  274. self._learned: set[tuple[int, int]] = set()
  275. self._grid = LearnedGrid()
  276. # 墙源参数化: 默认 d401(矿区); 其他图传自己的墙集(如 d601 蜈蚣洞穴)
  277. # — .map 定案后墙数据即权威地形, MirWalkExecutor 本身地图无关
  278. map_walls = walls if walls is not None else _d401_walls()
  279. self._map_walls = map_walls
  280. self._grid.mark_from_map(map_walls)
  281. _LOG.info("[MirWalk] 权威墙格 %d(硬边界)", len(map_walls))
  282. # SC28 实测墙(动态补充): 仅采信权威墙内的格(滤超速弹回误标) —
  283. # 服务器真新增动态墙会经实时弹回重新学到
  284. learned = (learned_walls or set()) & map_walls
  285. if learned:
  286. self._grid.mark_from_map(learned)
  287. self._learned = set(learned)
  288. _LOG.info("[MirWalk] SC28 实测墙 %d 格(已滤误标)", len(learned))
  289. def on_enter(self, conn: GatewayConnection, world: WorldMirror) -> None:
  290. _ = conn
  291. self._last_t = time.time()
  292. _LOG.info("[MirWalk] 启动 (%d,%d) → (%d,%d) — 原版 AutoPath 模式",
  293. world.me.x, world.me.y, self._dest[0], self._dest[1])
  294. def tick(self, conn: GatewayConnection, world: WorldMirror) -> TickResult:
  295. me = world.me
  296. if not me.known():
  297. return TickResult.RUNNING
  298. # 换图守卫: 死亡回城/意外传送(蜈蚣洞穴 run3 实证: 25级被怪围殴致死
  299. # → 回盟重, executor 拿盟重坐标在 d601 网格上乱标墙) — 立即退出
  300. if self._map_name and me.map_name != self._map_name:
  301. _LOG.warning("[MirWalk✗] 换图 %s → %s(死亡回城/意外传送) — 退出 "
  302. "(发腿=%d 弹回=%d 滑行=%d)",
  303. self._map_name, me.map_name, self.steps,
  304. self.rejected, self.slips)
  305. return TickResult.DONE
  306. # 到达判定: 距终点 ≤2(跑步 2 格步长容差)
  307. if max(abs(self._dest[0] - me.x), abs(self._dest[1] - me.y)) <= 2:
  308. _LOG.info("[MirWalk✓] 到达 (%d,%d) 共 %d 腿 弹回 %d 滑行 %d",
  309. me.x, me.y, self.steps, self.rejected, self.slips)
  310. return TickResult.DONE
  311. # 事件消费: 仅真弹回(pull_back)标墙 — anchor_echo(p3=CS1 魔数)是
  312. # 锚点申报回显=位置确认, 标墙会产生误标(2026-09-29 定案的 19 格
  313. # 误标即部分来源于此)
  314. for e in world.events:
  315. if e.kind == "self_move_confirmed":
  316. self._pending = False
  317. elif e.kind == "pull_back":
  318. self.rejected += 1
  319. self._pending = False
  320. if self._last_target is not None:
  321. self._grid.mark_blocked(self._last_target)
  322. self._learned.add(self._last_target)
  323. if self.rejected % 20 == 0:
  324. _save_sc28_walls(self._learned)
  325. _LOG.info("[MirWalk] 弹回 → 标墙 %s(累计学 %d)",
  326. self._last_target, len(self._learned))
  327. world.events.clear()
  328. now = time.time()
  329. if self._pending:
  330. if now - self._pending_t < 1.0:
  331. return TickResult.RUNNING
  332. self._pending = False
  333. if now - self._last_t < 0.6:
  334. return TickResult.RUNNING
  335. # ★ 原版核心: 每 tick 重算 A*(当前位置 → 终点)
  336. path = astar((me.x, me.y), self._dest, self._grid)
  337. if not path:
  338. self._no_path_n += 1
  339. # 无路自救: 只清 SC28 学习增量, 绝不动 d401 权威墙(清真墙=必撞)
  340. # (学习数据若含飞地误标, 豁免半径渐进扩大; d401 与服务器 98% 吻合,
  341. # 落点必在可达域 — 无路的根因几乎只剩学习误标)
  342. if self._no_path_n % 10 == 0:
  343. r = 15 + self._no_path_n // 10 * 10
  344. cleared = 0
  345. for dx in range(-r, r + 1):
  346. for dy in range(-r, r + 1):
  347. c = (me.x + dx, me.y + dy)
  348. if c in self._grid.blocked and c not in self._map_walls:
  349. self._grid.blocked.discard(c)
  350. cleared += 1
  351. _LOG.info("[MirWalk] 无路#%d — 豁免半径 %d(清学习墙 %d, d401 权威墙保留)",
  352. self._no_path_n, r, cleared)
  353. if self._no_path_n >= 60:
  354. _LOG.warning("[MirWalk] 连续无路 60 次 — 放弃")
  355. return TickResult.DONE
  356. self._last_t = now
  357. return TickResult.RUNNING
  358. self._no_path_n = 0
  359. # path[0] = 下一步落点(2 格步长 A*)
  360. nxt = path[1] if len(path) > 1 else path[0]
  361. want = self._dir_to(me.x, me.y, nxt[0], nxt[1])
  362. cand = self._pick_dir(me.x, me.y, want)
  363. if cand is None:
  364. # 三向全墙(数据矛盾) — 把路径首格标墙, 下 tick A* 自动绕
  365. self._grid.mark_blocked(nxt)
  366. self.slips += 1
  367. _LOG.info("[MirWalk] 三向堵 → 标墙 %s 下 tick 绕行", nxt)
  368. self._last_t = now
  369. return TickResult.RUNNING
  370. run2 = self._can_run(me.x, me.y, cand)
  371. self._last_target = (me.x + DIR_VECS[cand][0] * (2 if run2 else 1),
  372. me.y + DIR_VECS[cand][1] * (2 if run2 else 1))
  373. self._send_leg(conn, world, cand, run2)
  374. self.steps += 1
  375. if cand != want:
  376. self.slips += 1
  377. _LOG.info("[MirWalk] 滑行: 期望dir=%d 实际dir=%d", want, cand)
  378. return TickResult.RUNNING
  379. def _pick_dir(self, x: int, y: int, want: int) -> int | None:
  380. """原版 CanWalk(dir,out): 直向 → 顺 45° → 逆 45°。"""
  381. if self._can_run(x, y, want) or self._can_walk(x, y, want):
  382. return want
  383. for nd in (next_dir(want), prev_dir(want)):
  384. if self._can_walk(x, y, nd):
  385. return nd
  386. return None
  387. def _can_walk(self, x: int, y: int, d: int) -> bool:
  388. dx, dy = DIR_VECS[d]
  389. return (x + dx, y + dy) not in self._grid.blocked
  390. def _can_run(self, x: int, y: int, d: int) -> bool:
  391. """原版 CanRun: 方向上 2 格全部可走才跑。"""
  392. dx, dy = DIR_VECS[d]
  393. return all((x + dx * i, y + dy * i) not in self._grid.blocked
  394. for i in (1, 2))
  395. def _send_leg(self, conn: GatewayConnection, world: WorldMirror,
  396. d: int, run: bool) -> None:
  397. me = world.me
  398. dx, dy = DIR_VECS[d]
  399. dist2 = 2 if run else 1
  400. fx, fy = me.x + dx * dist2, me.y + dy * dist2
  401. from pbot.game.messages import protocol_map_name
  402. mn = protocol_map_name(me.map_id, me.map_name)
  403. _ = conn.send_frame(3013 if run else 3011, 0, d,
  404. (me.y << 16) | me.x, self._sid, mn.encode(),
  405. pre=(fy << 16) | fx)
  406. self._pending = True
  407. self._pending_t = time.time()
  408. self._last_t = self._pending_t
  409. _LOG.info("[%s] (%d,%d) dir=%d → (%d,%d) #%d",
  410. "跑" if run else "走", me.x, me.y, d, fx, fy, self.steps + 1)
  411. def _dir_to(self, x: int, y: int, tx: int, ty: int) -> int:
  412. dx = max(-1, min(1, tx - x))
  413. dy = max(-1, min(1, ty - y))
  414. return next((i for i, (ox, oy) in enumerate(DIR_VECS)
  415. if ox == dx and oy == dy), 4)
  416. def parse_args() -> argparse.Namespace:
  417. """命令行。"""
  418. ap = argparse.ArgumentParser()
  419. _ = ap.add_argument("-u", "--user", required=True)
  420. _ = ap.add_argument("-p", "--password", required=True)
  421. _ = ap.add_argument("--zone", type=int, required=True)
  422. _ = ap.add_argument("--name", required=True)
  423. _ = ap.add_argument("--hold-seconds", type=float, default=300.0,
  424. help="到达最远点后静止时长(秒)")
  425. _ = ap.add_argument("--pre-wait", type=float, default=60.0,
  426. help="落地矿区后静置时长(秒), 0=立即出发")
  427. _ = ap.add_argument("--login-port", type=int, default=0)
  428. return ap.parse_args()
  429. def main() -> int:
  430. """登录 → 进矿区 → 最远点规划 → 自动走过去 → 静止5分钟退出。"""
  431. a = parse_args()
  432. setup_logging()
  433. banner("[0] 启动: 区=%d 角色=%s 静止时长=%.0fs",
  434. a.zone, a.name, a.hold_seconds)
  435. http = requests.Session()
  436. http.trust_env = False
  437. http.verify = False
  438. banner("[1] 盒子登录")
  439. jwt, uid = login(Account(username=a.user, password=a.password))
  440. _LOG.info("[✓] uid=%d", uid)
  441. banner("[2] EnterGameClient zone=%d", a.zone)
  442. for attempt in range(4):
  443. try:
  444. _ = enter_game_client(http, jwt, ZoneId(a.zone))
  445. _LOG.info("[✓] 上报成功")
  446. break
  447. except Exception as e:
  448. if attempt == 3:
  449. raise
  450. _LOG.warning("[!] 第%d次失败(%s), 12s 后重试", attempt + 1, e)
  451. time.sleep(12)
  452. banner("[3] 登录服选角: %s", a.name)
  453. ports = (a.login_port,) if a.login_port else _PORTS
  454. key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
  455. ZoneId(a.zone), jwt, uid, a.name, ports)
  456. _LOG.info("[票据] key32=%s sid=%d 角色=%s 游戏端口=%d",
  457. key32.decode(errors="replace"), sid, real_name, game_port)
  458. banner("[4] 进游戏")
  459. stop: list[bool] = [False]
  460. conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
  461. sid, game_port, uid, stop=stop)
  462. _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
  463. world.me.y)
  464. try:
  465. banner("[5] 喝药保护确认")
  466. from pbot.game.heal import enable_auto_heal
  467. state = enable_auto_heal(conn, sid, want_on=True, world=world)
  468. _LOG.info("[%s] 喝药状态=%s", "✓" if state == "on" else "!", state)
  469. # ---- [6] 进矿区(如需要) ----
  470. if world.me.map_name == MINE_MAP:
  471. banner("[6] 出生已在 %s @(%d,%d) — 跳过传送",
  472. MINE_MAP, world.me.x, world.me.y)
  473. else:
  474. banner("[6] 进矿区: 当前 %s @(%d,%d)",
  475. world.me.map_name, world.me.x, world.me.y)
  476. enter_bh = MineEnterBehavior(sid=sid, kill_target=0)
  477. r = run(conn, world, updater, enter_bh, duration_s=300.0)
  478. _LOG.info("[传送结果] result=%s map=%s @(%d,%d)",
  479. r.value, world.me.map_name, world.me.x, world.me.y)
  480. if world.me.map_name != MINE_MAP:
  481. _LOG.error("[✗] 未能进入矿区(看上方日志), 退出")
  482. return 1
  483. # 落地静置(ACK 后服务器建视野)
  484. time.sleep(3.0)
  485. _ = pump(conn, world, updater, 2.0)
  486. # ---- [6.5] 落地静置(仅心跳+收帧) ----
  487. if a.pre_wait > 0:
  488. banner("[6.5] 落地静置 %.0f 秒再出发(洗超速计数/服务器建视野)",
  489. a.pre_wait)
  490. deadline = time.time() + a.pre_wait
  491. while time.time() < deadline:
  492. _ = pump(conn, world, updater, 0.5)
  493. start_pt = (world.me.x, world.me.y)
  494. banner("[7] 可达域分析: 起点(%d,%d)", *start_pt)
  495. far, d_steps, is_enclave = find_farthest(start_pt)
  496. _LOG.info("[最远点] (%d,%d) 路径距离 %d 步(直线 %.0f)",
  497. far[0], far[1], d_steps,
  498. math.hypot(far[0] - start_pt[0], far[1] - start_pt[1]))
  499. # ---- [8] 传奇式走到最远点(A* 一次规划 + 撞墙滑行) ----
  500. banner("[8] 传奇式寻路 → (%d,%d)", far[0], far[1])
  501. executor = MirWalkExecutor(
  502. sid=sid, dest=far, start_pt=start_pt,
  503. allow_replan=not is_enclave,
  504. )
  505. # 时间预算按实测弹回率放宽: 理想 0.75s/腿, 弹回重试约 2.5s/有效腿
  506. est_s = d_steps * 0.75 * 4 + 180.0
  507. _LOG.info("预计跑动上限 %.0fs(弹回冗余×4)", est_s)
  508. r = run(conn, world, updater, executor, duration_s=est_s)
  509. if executor._learned:
  510. _save_sc28_walls(executor._learned)
  511. final_d = max(abs(far[0] - world.me.x), abs(far[1] - world.me.y))
  512. _LOG.info("[寻路结束] result=%s 位置=(%d,%d) 距目标 %d 格 map=%s "
  513. "(发腿=%d 弹回=%d 滑行=%d)",
  514. r.value, world.me.x, world.me.y, final_d,
  515. world.me.map_name, executor.steps, executor.rejected,
  516. executor.slips)
  517. # ---- [9] 静止不动 ----
  518. banner("[9] 静止 %.0f 秒(仅心跳+收帧)", a.hold_seconds)
  519. deadline = time.time() + a.hold_seconds
  520. while time.time() < deadline:
  521. _ = pump(conn, world, updater, 0.5)
  522. remain = world.me.map_name
  523. _LOG.info("[静止结束] 最终 map=%s @(%d,%d)", remain, world.me.x,
  524. world.me.y)
  525. top = sorted(world.frame_counts.items(), key=lambda kv: -kv[1])[:15]
  526. _LOG.info("收帧Top15: %s", top)
  527. return 0
  528. finally:
  529. stop[0] = True
  530. conn.close()
  531. _LOG.info("连接已关闭")
  532. if __name__ == "__main__":
  533. try:
  534. sys.exit(main())
  535. except PbotError as e:
  536. _LOG.error("失败: %s", e.detail)
  537. sys.exit(1)