wait_wander4.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # -*- coding: utf-8 -*-
  2. """
  3. wait_wander4.py — 等待图走动修复版: 绕开滞留带, 从可走区内走。
  4. 根因(2026-09-23 定案):
  5. 0139.map 客户端文件: y=0-2 整行不可走(墙), 可走区 y>=3(x25-35 一带 y>=8);
  6. 服务器把登录角色放在滞留带 y=0 (31,0), 该带内一切 CS3011/3013 全被拒
  7. (SC28 recog=24, 新旧角色/走跑帧/5013握手均同样 — 三轮对照实验)。
  8. 实证可走路径: 07:59 轮, 角色从比奇 @renwu_mov → SC634 落 (24,27)[可走区],
  9. 随后 20+ 步无拉回。
  10. 修复流程:
  11. 1. 在滞留带(y<=4) → CS1010 点新手接待NPC(idx候选) → 传比奇 (07:47 实证)
  12. 2. 比奇 → CS3031 + @renwu_mov,1 → SC634 传回等待图可走区 (07:59 实证)
  13. 3. 可走区内自由走动(0139 网格 A* 随机游走), 全帧日志
  14. 用法:
  15. .venv/bin/python wait_wander4.py --zone 14504562 --name 萌新8195 \
  16. -u longwu988 -p Longwu1988 --minutes 2
  17. """
  18. import argparse
  19. import json
  20. import os
  21. import random
  22. import time
  23. import requests as rq
  24. import urllib3
  25. urllib3.disable_warnings()
  26. import login_996pc as L
  27. import quest_bot3 as Q
  28. from combat_bot import log, pack_point, lo16, hi16, DIRS, proto_map_name
  29. from mir_map import load as map_load
  30. class WalkBot(Q.PushBot):
  31. """全帧日志 + SC20102→CS5013 寻路握手 + 网格游走。"""
  32. def __init__(self, *a, **kw):
  33. super().__init__(*a, **kw)
  34. self.sent_log = {}
  35. self.rail_target = None # SC20102 {mapID,x,y,targetType,...}
  36. self.rail_on = False # CS5013 START 已发
  37. def auto_move_notify(self, act, x=0, y=0):
  38. """CS5013(Lua: LuaSendMsg(5013, act, destX, destY)): 1=START 2=ABORT 3=FINISH。"""
  39. self.send(5013, act, x, y, 0, b"")
  40. self.rail_on = (act == 1)
  41. log(f" >> CS5013 act={act} ({x},{y})")
  42. def send(self, mid, recog, p1, p2, p3, body=b"", tail=None):
  43. if mid in (3011, 3013):
  44. self.sent_log[mid] = self.sent_log.get(mid, 0) + 1
  45. if self.sent_log[mid] <= 40:
  46. log(f" >> CS{mid} 起点=({lo16(recog)},{hi16(recog)}) dir={p2} "
  47. f"last=({lo16(p3)},{hi16(p3)}) #{self.sent_log[mid]}")
  48. return super().send(mid, recog, p1, p2, p3, body, tail)
  49. def _dispatch(self, mid, recog, p1, p2, p3, body):
  50. ev = super()._dispatch(mid, recog, p1, p2, p3, body)
  51. if mid in (28, 11, 1, 5013, 800, 634, 51, 20102):
  52. y2 = p2 & 0x7FF if (p2 & 0x800) else p2
  53. if mid == 28:
  54. if getattr(self, "_sc28_n", 0) < 40:
  55. self._sc28_n = getattr(self, "_sc28_n", 0) + 1
  56. log(f" << SC28 recog={recog} 拉回→({p1},{y2})")
  57. elif mid in (11, 13):
  58. j = self._jbody(body) or {}
  59. uid = str(j.get("UserID", ""))
  60. if uid in ((self.me.get("uid") or ""),
  61. (self.me.get("sid_short") or ""),
  62. str(self.me.get("id") or "")):
  63. log(f" << SC{mid} ★移动确认 → ({p1},{y2})")
  64. elif mid in (1, 5013, 20102):
  65. log(f" << SC{mid} recog={recog} p=({p1},{y2},{p3}) "
  66. f"body={body[:60]!r}")
  67. elif mid in (800, 634, 51):
  68. log(f" << SC{mid} p=({p1},{y2}) body={body[:50]!r}")
  69. return ev
  70. def step_dir(self, d, dist=1):
  71. dx, dy = DIRS[d]
  72. x, y = self.me["x"], self.me["y"]
  73. lx, ly = self.me.get("last_x", x), self.me.get("last_y", y)
  74. mid = 3013 if dist == 2 else 3011
  75. self.send(mid, pack_point(x, y), 0, d, pack_point(lx, ly),
  76. proto_map_name(self.me).encode())
  77. self.me["last_x"], self.me["last_y"] = x, y
  78. self.me["x"], self.me["y"], self.me["dir"] = x + dx * dist, y + dy * dist, d
  79. self._step_pending_ack = (dx * dist, dy * dist, time.time() + Q.STEP_MS / 1000.0)
  80. self._last_step_t = time.time()
  81. def enter(args):
  82. j = None
  83. for a in range(5):
  84. try:
  85. s = rq.Session()
  86. j, _ = L.do_login(s, args.user, args.password)
  87. if j.get("code") == 200:
  88. break
  89. except Exception as e:
  90. log(f"登录重试{a + 1}: {str(e)[:60]}")
  91. time.sleep(12)
  92. if not j or j.get("code") != 200:
  93. raise SystemExit(f"[!] 登录失败: {(j or {}).get('msg', '网络异常')}")
  94. jwt = j["data"]["token"]
  95. uid = str(j["data"]["userInfo"]["id"])
  96. import cq2_pure_enter as P
  97. from combat_bot import send_init_frames
  98. for a in range(4):
  99. try:
  100. P.enter_game(s, jwt, args.zone)
  101. break
  102. except Exception:
  103. if a == 3:
  104. raise
  105. time.sleep(12)
  106. got = None
  107. for pt in ((args.port,) if args.port else (8021, 8061, 8001, 8041)):
  108. try:
  109. r = P.login_gate(args.zone, jwt, args.name, pt, uid)
  110. if r[0]:
  111. got = r
  112. break
  113. except SystemExit as e:
  114. log(f" 端口{pt}: {e}")
  115. except Exception as e:
  116. log(f" 端口{pt}: {str(e)[:60]}")
  117. if not got:
  118. raise SystemExit("[!] 登录服失败")
  119. key32, sid, rname, tkey, gp = got
  120. g = P.enter_game_gate(args.zone, rname, tkey, sid, gp, uid)
  121. bot = WalkBot(g, sid, rname)
  122. P.heartbeat_loop(g, sid, bot.lock)
  123. t0 = time.time()
  124. while time.time() - t0 < 10:
  125. bot.pump(0.3)
  126. if 658 in bot.frame_log:
  127. break
  128. if 658 not in bot.frame_log:
  129. g.close()
  130. raise SystemExit("[!] SC658 超时")
  131. send_init_frames(bot, uid)
  132. for _ in range(24):
  133. bot.pump(0.5)
  134. if bot.me["id"] and bot.me.get("mapname"):
  135. break
  136. log(f"进世界: map={bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']} "
  137. f"uid={bot.me.get('uid')}")
  138. return g, bot
  139. def npc_talk_to_biqi(bot):
  140. """滞留带 → 点NPC → 传比奇(quest_bot3 stage[3] 同款: idx候选+对话按钮)。"""
  141. t0 = time.time()
  142. while time.time() - t0 < 8 and not bot.npcs:
  143. bot.pump(0.4)
  144. npc = bot.find_guide_npc()
  145. if not npc:
  146. log("[!] 视野内无NPC")
  147. return False
  148. aid = npc["aid"]
  149. idx_cands = [npc["index"]] + [i for i in (35, 34) if i != npc["index"]]
  150. base_tp = bot.teleports
  151. for idx in idx_cands:
  152. bot.pending_map_ack = "比奇大城"
  153. bot.send(1010, 0, 0, 0, bot.sid, json.dumps(
  154. {"UserID": aid, "index": idx}, separators=(",", ":")).encode())
  155. bot.send(5012, 0, 0, 0, bot.sid, b"0")
  156. bot.send(5027, 0, 0, 0, bot.sid, b"0")
  157. log(f"[NPC] CS1010 idx={idx} ...")
  158. t1 = time.time()
  159. clicks = 0
  160. while time.time() - t1 < 10 and bot.alive:
  161. evs = bot.pump(0.3)
  162. bot.tick_step_ack()
  163. if bot.me.get("mapname") == "比奇大城" or bot.teleports > base_tp:
  164. bot.pending_map_ack = None
  165. log(f"[NPC] ★已传比奇 @{bot.me['x']},{bot.me['y']}")
  166. return True
  167. talk = next((e[1] for e in evs if e[0] == "npc_talk"), None)
  168. if talk is None:
  169. continue
  170. acts = bot.parse_sui_acts(str(talk.get("sMsg", "")))
  171. log(f"[NPC对话] acts={acts}")
  172. if clicks < 3:
  173. act = next((a for a in acts
  174. if a not in ("exit", "close", "guanbi")), None)
  175. if act:
  176. bot.npc_click_act(talk, act)
  177. clicks += 1
  178. log(f"[NPC对话] 点 @{act}")
  179. bot.pending_map_ack = None
  180. return bot.me.get("mapname") == "比奇大城"
  181. def renwu_back_to_wait(bot):
  182. """比奇 → @renwu_mov → 传回等待图可走区(07:59 实证落点 ~ (24,27))。"""
  183. if 1 in bot.mission_bodies:
  184. body3031 = json.dumps(bot.mission_bodies[1], separators=(",", ":")).encode()
  185. else:
  186. body3031 = open(os.path.join(Q.HERE, "docs", "cs3031_template.txt"), "rb").read()
  187. bot.send(3031, 0, 0, 0, bot.sid, body3031)
  188. bot.pump(0.5)
  189. for act in ("@renwu_mov,1", "@renwu_mov,2"):
  190. bot.send(1011, 0, 0, 0, bot.sid, json.dumps(
  191. {"Act": act, "UserID": "999999999", "index": 999999999},
  192. separators=(",", ":")).encode())
  193. t0 = time.time()
  194. while time.time() - t0 < 10 and bot.alive:
  195. evs = bot.pump(0.3)
  196. bot.tick_step_ack()
  197. if bot.me.get("mapname") == Q.WAIT_MAP:
  198. log(f"★回到等待图 @{bot.me['x']},{bot.me['y']} (act={act})")
  199. break
  200. if bot.me.get("mapname") == Q.WAIT_MAP:
  201. # 传送动画期服务器仍按旧位置结算走路帧 → 静置 2.5s 吃掉过期 SC28,
  202. # 再以服务器坐标为锚重置 lastPoint(09:03 竞态实证: 0秒发步被拉回)
  203. time.sleep(0.5)
  204. t1 = time.time()
  205. while time.time() - t1 < 2.0 and bot.alive:
  206. bot.pump(0.2)
  207. bot.tick_step_ack()
  208. bot.me["last_x"] = bot.me["x"]
  209. bot.me["last_y"] = bot.me["y"]
  210. bot._step_pending_ack = None
  211. if bot.me["y"] > 4:
  212. return True
  213. log(" 仍在滞留带 y<=4")
  214. break
  215. return bot.me.get("mapname") == Q.WAIT_MAP and bot.me["y"] > 4
  216. def wander_interior(bot, seconds, m):
  217. """可走区内随机游走。文件网格仅作方向微调(服务器网格与0139有偏差,
  218. 09:12 实证: 按文件网格硬拦截会在(19,29)停摆); 拒绝靠近距SC28校正+换向。"""
  219. t_end = time.time() + seconds
  220. last_step = 0.0
  221. rep = time.time()
  222. accepted = 0
  223. rejected = 0
  224. consec_rej = 0
  225. dest = None
  226. while time.time() < t_end and bot.alive:
  227. evs = bot.pump(0.05)
  228. bot.tick_step_ack()
  229. for e in evs:
  230. if e[0] == "self_move":
  231. accepted += 1
  232. consec_rej = 0
  233. elif e[0] in ("pull_back", "pull_back_bogus_dropped"):
  234. rejected += 1
  235. now = time.time()
  236. x, y = bot.me["x"], bot.me["y"]
  237. if dest is None or (abs(dest[0] - x) <= 1 and abs(dest[1] - y) <= 1):
  238. for _ in range(20):
  239. nx = x + random.randint(-12, 12)
  240. ny = y + random.randint(-12, 12)
  241. if (m.in_bounds(nx, ny) and m.walkable(nx, ny)
  242. and abs(nx - x) + abs(ny - y) > 3):
  243. dest = (nx, ny)
  244. log(f"[游走] 新目标 ({nx},{ny})")
  245. break
  246. else:
  247. dest = None
  248. if dest and now - last_step >= Q.STEP_MS / 1000.0 + 0.05:
  249. dx = max(-1, min(1, dest[0] - x))
  250. dy = max(-1, min(1, dest[1] - y))
  251. if dx and dy and not (m.walkable(x + dx, y) or m.walkable(x, y + dy)):
  252. if m.walkable(x + dx, y):
  253. dy = 0
  254. elif m.walkable(x, y + dy):
  255. dx = 0
  256. if consec_rej >= 3:
  257. d = random.randrange(8)
  258. consec_rej = 0
  259. else:
  260. d = next(i for i, (ox, oy) in enumerate(DIRS)
  261. if ox == dx and oy == dy)
  262. bot.step_dir(d, 1)
  263. last_step = now
  264. if now - rep >= 15:
  265. rep = now
  266. log(f"[STAT] pos=({x},{y}) 目标={dest} 发帧={bot.sent_log} "
  267. f"SC11确认={accepted} SC28拒绝={rejected}")
  268. log(f"[游走结束] pos=({bot.me['x']},{bot.me['y']}) 发帧={bot.sent_log} "
  269. f"确认={accepted} 拒绝={rejected}")
  270. def main():
  271. ap = argparse.ArgumentParser()
  272. ap.add_argument("-u", "--user", default="longwu988")
  273. ap.add_argument("-p", "--password", default="Longwu1988")
  274. ap.add_argument("--zone", type=int, default=14504562)
  275. ap.add_argument("--name", default="萌新8195")
  276. ap.add_argument("--port", type=int, default=0)
  277. ap.add_argument("--minutes", type=float, default=2)
  278. args = ap.parse_args()
  279. args.minutes = min(args.minutes, 2)
  280. m = map_load("0139")
  281. g, bot = enter(args)
  282. try:
  283. state = Q.P.enable_auto_heal(g, bot.sid, want_on=True)
  284. log(f"喝药保护: {state}")
  285. mname = bot.me.get("mapname")
  286. if mname == Q.WAIT_MAP and bot.me["y"] <= 4:
  287. log("[阶段1] 滞留带 → NPC对话传比奇")
  288. if not npc_talk_to_biqi(bot):
  289. log("[!] 未传比奇, 中止")
  290. return
  291. mname = bot.me.get("mapname")
  292. if mname != Q.WAIT_MAP:
  293. log("[阶段2] 比奇 → @renwu_mov 传回等待图可走区")
  294. if not renwu_back_to_wait(bot):
  295. log("[!] 未回到等待图可走区, 中止")
  296. return
  297. log(f"[阶段3] 等待图可走区走动 @{bot.me['x']},{bot.me['y']}")
  298. wander_interior(bot, args.minutes * 60, m)
  299. finally:
  300. g.close()
  301. if __name__ == "__main__":
  302. main()