run_juece13_cave30.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """run_juece13_cave30.py — 角色【新角色】→ 天然洞穴一层 → 杀 30 怪(全部在洞内)。
  2. 与 run_juece13_kill30.py 的差异:
  3. - 硬约束: 30 杀必须全部发生在【天然洞穴一层】地图内
  4. - CaveOnlyKillBehavior 继承 KillBehavior, 三道闸门:
  5. 1) on_enter: 非洞穴 → 只走 renwu_mov,2 任务链传送进洞, 绝不异图开战
  6. 2) _pick_target/_current_target: 非洞穴 → 永不选怪(异图只触发换落点)
  7. 3) _credit_kill: 异图击杀 → 记日志但不计数(只有洞内击杀进 30/30)
  8. - 无怪应对(框架既有 + 本约束): CS5284 问服务器怪位 → 新颖度锚点跑图
  9. → 落点无怪 45s 重走 renwu 链换洞内落点
  10. 用法:
  11. uv run python scripts/run_juece13_cave30.py -u longwu988 -p Longwu1988 \
  12. --zone 14570166 --name 新角色 --kill 30
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import logging
  17. import sys
  18. import time
  19. import requests
  20. from typing import override
  21. from pbot.bot.kill import LAND_QUIET_S, KillBehavior
  22. from pbot.bot.runner import run
  23. from pbot.config import Account
  24. from pbot.errors import PbotError
  25. from pbot.game.session import enter_world
  26. from pbot.login.box_client import enter_game_client, login
  27. from pbot.transport.gateway import login_gate_with_retry
  28. from pbot.types import ZoneId
  29. from pbot.world.mirror import WorldMirror
  30. _LOG = logging.getLogger("juece13_cave")
  31. _PORTS: tuple[int, ...] = (8021, 8061, 8001, 8041)
  32. CAVE_MAP: str = "天然洞穴一层"
  33. def setup_logging() -> None:
  34. """全量 INFO 日志, 框架每步/每刀/每杀均有日志, 便于事后分析。"""
  35. logging.basicConfig(
  36. level=logging.INFO,
  37. format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
  38. stream=sys.stdout,
  39. )
  40. def banner(msg: str, *args: object) -> None:
  41. """阶段分隔条日志(支持 %-style 格式化参数)。"""
  42. _LOG.info("======== %s ========", msg % args if args else msg)
  43. class CaveOnlyKillBehavior(KillBehavior):
  44. """只在 天然洞穴一层 选怪/开战/计杀; 异图一律重走 renwu 链回洞。"""
  45. def _in_cave(self, world: WorldMirror) -> bool:
  46. """当前是否在目标洞穴图。"""
  47. return world.me.map_name == CAVE_MAP
  48. @override
  49. def on_enter(self, conn: GatewayConnection, world: WorldMirror) -> None:
  50. """入口闸门: 洞内直接开打; 异图(比奇/等待图/其他)→ renwu 链进洞。"""
  51. self._world = world
  52. self._last_step_t = time.time()
  53. self._declare_guaji(conn)
  54. if self._bound is None:
  55. self._bound = ((world.me.x, world.me.y), 25)
  56. self._load_map_if_needed(world)
  57. if self._in_cave(world):
  58. _LOG.info("[洞穴] 出生即在 %s @(%d,%d) — 静置%.1fs后开打",
  59. CAVE_MAP, world.me.x, world.me.y, LAND_QUIET_S)
  60. self._enter("fight")
  61. self._phase_t = time.time() + LAND_QUIET_S
  62. else:
  63. _LOG.info("[洞穴] 当前 %s @(%d,%d) 非目标图 — 走 renwu_mov,2 传送进洞",
  64. world.me.map_name, world.me.x, world.me.y)
  65. self._enter("renwu_3031")
  66. @override
  67. def _current_target(self, world: WorldMirror): # noqa: ANN201
  68. """非洞穴 → 永不锁定目标(防切图瞬间打残影目标)。"""
  69. if not self._in_cave(world):
  70. return None
  71. return super()._current_target(world)
  72. @override
  73. def _pick_target(self, world: WorldMirror): # noqa: ANN201
  74. """非洞穴 → 永不选怪: tick 走无怪分支 → 45s 后 renwu 换落点回洞。"""
  75. if not self._in_cave(world):
  76. return None
  77. return super()._pick_target(world)
  78. @override
  79. def _credit_kill(self, world: WorldMirror, aid: int, name: str,
  80. signal: str) -> None:
  81. """计杀闸门: 只有洞内击杀计入 30/30; 异图击杀仅记日志。"""
  82. if not self._in_cave(world):
  83. _LOG.warning("[异图击杀不计] aid=%d %s 信号=%s @%s — 目标图=%s",
  84. aid, name, signal, world.me.map_name, CAVE_MAP)
  85. _ = self._my_hits.pop(aid, None)
  86. return
  87. super()._credit_kill(world, aid, name, signal)
  88. def parse_args() -> argparse.Namespace:
  89. """命令行: 账号/区/角色/目标击杀数。"""
  90. ap = argparse.ArgumentParser()
  91. _ = ap.add_argument("-u", "--user", required=True)
  92. _ = ap.add_argument("-p", "--password", required=True)
  93. _ = ap.add_argument("--zone", type=int, required=True)
  94. _ = ap.add_argument("--name", required=True, help="已有角色名")
  95. _ = ap.add_argument("--kill", type=int, default=30)
  96. _ = ap.add_argument("--login-port", type=int, default=0)
  97. return ap.parse_args()
  98. def main() -> int:
  99. """登录 → 进游戏 → 喝药确认 → 洞穴限定的 30 杀。"""
  100. a = parse_args()
  101. setup_logging()
  102. banner("[0] 启动: 区=%d 角色=%s 目标=%d 杀(仅限 %s)",
  103. a.zone, a.name, a.kill, CAVE_MAP)
  104. http = requests.Session()
  105. http.trust_env = False
  106. http.verify = False
  107. banner("[1] 盒子登录")
  108. jwt, uid = login(Account(username=a.user, password=a.password))
  109. _LOG.info("[✓] uid=%d", uid)
  110. banner("[2] EnterGameClient zone=%d", a.zone)
  111. for attempt in range(4):
  112. try:
  113. _ = enter_game_client(http, jwt, ZoneId(a.zone))
  114. _LOG.info("[✓] 上报成功")
  115. break
  116. except Exception as e:
  117. if attempt == 3:
  118. raise
  119. _LOG.warning("[!] 第%d次失败(%s), 12s 后重试", attempt + 1, e)
  120. time.sleep(12)
  121. banner("[3] 登录服选角: %s", a.name)
  122. ports = (a.login_port,) if a.login_port else _PORTS
  123. key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
  124. ZoneId(a.zone), jwt, uid, a.name, ports)
  125. _LOG.info("[票据] key32=%s sid=%d 角色=%s 游戏端口=%d",
  126. key32.decode(errors="replace"), sid, real_name, game_port)
  127. banner("[4] 进游戏(CS20 → SC658 → 17帧初始化 → 出生)")
  128. stop: list[bool] = [False]
  129. conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
  130. sid, game_port, uid, stop=stop)
  131. _LOG.info("[✓] 出生 map=%s @(%d,%d) actor=%s",
  132. world.me.map_name, world.me.x, world.me.y, world.me.actor_key)
  133. if world.me.map_name != CAVE_MAP:
  134. _LOG.info("[i] 出生不在洞穴(在 %s) — 行为入口会自动走 renwu 链传送进洞",
  135. world.me.map_name)
  136. try:
  137. banner("[5] 喝药保护确认")
  138. from pbot.game.heal import enable_auto_heal
  139. state = enable_auto_heal(conn, sid, want_on=True, world=world)
  140. if state == "on":
  141. _LOG.info("[✓] 喝药保护已开启(服务端 but_1 确认)")
  142. else:
  143. _LOG.warning("[!] 喝药状态=%s(不阻断, 继续任务链)", state)
  144. banner("[6] 洞穴杀怪启动: 目标 %d 杀 @%s", a.kill, CAVE_MAP)
  145. kill = CaveOnlyKillBehavior(
  146. sid=sid,
  147. kill_target=a.kill,
  148. renwu_act="renwu_mov,2", # 任务二 = 天然洞穴一层(SC643 弹窗实测)
  149. allow_reroll=True, # 落点无怪 → 重走 renwu 链换洞内落点
  150. )
  151. duration = max(900.0, a.kill * 150.0)
  152. _LOG.info("行为超时上限: %.0fs(达标即提前退出)", duration)
  153. result = run(conn, world, updater, kill, duration_s=duration)
  154. banner("[7] 结果")
  155. _LOG.info("result=%s 洞内击杀=%d/%d 最终 map=%s @(%d,%d)",
  156. result.value, world.kills, a.kill,
  157. world.me.map_name, world.me.x, world.me.y)
  158. top = sorted(world.frame_counts.items(), key=lambda kv: -kv[1])[:15]
  159. _LOG.info("收帧Top15(问题分析用): %s", top)
  160. ok = world.kills >= a.kill and world.me.map_name == CAVE_MAP
  161. _LOG.info("[%s] %s", "✓" if ok else "✗",
  162. "任务完成: 30 杀全部在 天然洞穴一层"
  163. if ok else "未达标(看上方日志定位)")
  164. return 0 if ok else 1
  165. finally:
  166. stop[0] = True
  167. conn.close()
  168. _LOG.info("连接已关闭")
  169. if __name__ == "__main__":
  170. try:
  171. sys.exit(main())
  172. except PbotError as e:
  173. _LOG.error("失败: %s", e.detail)
  174. sys.exit(1)