# -*- coding: utf-8 -*- """ wait_wander5.py — 终极对齐实验: 完整复刻客户端移动链路。 Lua 真源链(解密源码/mod_fgcq/stab/scripts): SC20102(T=1) → AutoMoveBeginCommand: autoProxy:SetAutoTarget(data) + PushMove(dest) + InputMove(dest) → 若 autoMoveType==SERVER/MINIMAP 且 GetPathPointSize()>0: RequestAutoMoveNotify(START, destX, destY) ← CS5013(recog=1, p1=X, p2=Y) InputMoveCommand: A* pathPoints → PlayerInputProxy BehaviorMove(每帧): checkNextMovePos(下一格, 直线可并1-3格) → SetMoveTo+SetAction(WALK/RUN) → 动画步时(~550ms) 到点 → handleActionBegin: LuaSendMsg(3011/3013, point, 0, dir, lastPoint, mapID) → 等 SC11 自回显/SC1 确认(-5s超时兜底) → 下一格 到达: AutoMoveEndCommand → RequestAutoMoveNotify(FINISH=3) 本实验: 滞留带→NPC传比奇→@renwu_mov回等待图→[SC20102到达]→CS5013 START → 严格550ms节拍逐格走向 SC20102 目标 → 每步等确认 → 到达发 FINISH。 全帧日志。若 SC11 出现自回显 → 走动打通。 """ import argparse import json import os import time import requests as rq import urllib3 urllib3.disable_warnings() import login_996pc as L import quest_bot3 as Q from combat_bot import log, pack_point, lo16, hi16, DIRS, proto_map_name from mir_map import load as map_load class RailBot(Q.PushBot): def __init__(self, *a, **kw): super().__init__(*a, **kw) self.sent_log = {} self.rail = None # SC20102 目标 self.sc28_dropped = 0 def send(self, mid, recog, p1, p2, p3, body=b"", tail=None): if mid in (3011, 3013): self.sent_log[mid] = self.sent_log.get(mid, 0) + 1 return super().send(mid, recog, p1, p2, p3, body, tail) def _dispatch(self, mid, recog, p1, p2, p3, body): ev = super()._dispatch(mid, recog, p1, p2, p3, body) if mid == 20102: j = self._jbody(body) or {} if j.get("mapID"): self.rail = j log(f" << SC20102 ★ {j}") elif mid in (11, 13): j = self._jbody(body) or {} uid = str(j.get("UserID", "")) if uid in ((self.me.get("uid") or ""), (self.me.get("sid_short") or ""), str(self.me.get("id") or "")): log(f" << SC{mid} ★★自己移动确认 → ({p1},{p2 & 0x7FF})") elif mid == 28: self.sc28_dropped += 0 if self.frame_log.get(28, 0) <= self.sc28_dropped else 0 return ev def step_dir(self, d, dist=1): dx, dy = DIRS[d] x, y = self.me["x"], self.me["y"] lx, ly = self.me.get("last_x", x), self.me.get("last_y", y) mid = 3013 if dist >= 2 else 3011 self.send(mid, pack_point(x, y), 0, d, pack_point(lx, ly), proto_map_name(self.me).encode()) self.me["last_x"], self.me["last_y"] = x, y self.me["x"], self.me["y"], self.me["dir"] = x + dx * dist, y + dy * dist, d self._step_pending_ack = (dx * dist, dy * dist, time.time() + Q.STEP_MS / 1000.0) self._last_step_t = time.time() def enter(args): j = None for a in range(5): try: s = rq.Session() j, _ = L.do_login(s, args.user, args.password) if j.get("code") == 200: break except Exception as e: log(f"登录重试{a + 1}: {str(e)[:60]}") time.sleep(12) if not j or j.get("code") != 200: raise SystemExit(f"[!] 登录失败: {(j or {}).get('msg', '网络异常')}") jwt = j["data"]["token"] uid = str(j["data"]["userInfo"]["id"]) import cq2_pure_enter as P from combat_bot import send_init_frames for a in range(4): try: P.enter_game(s, jwt, args.zone) break except Exception: if a == 3: raise time.sleep(12) got = None for pt in ((args.port,) if args.port else (8021, 8061, 8001, 8041)): try: r = P.login_gate(args.zone, jwt, args.name, pt, uid) if r[0]: got = r break except SystemExit as e: log(f" 端口{pt}: {e}") except Exception as e: log(f" 端口{pt}: {str(e)[:60]}") if not got: raise SystemExit("[!] 登录服失败") key32, sid, rname, tkey, gp = got g = P.enter_game_gate(args.zone, rname, tkey, sid, gp, uid) bot = RailBot(g, sid, rname) P.heartbeat_loop(g, sid, bot.lock) t0 = time.time() while time.time() - t0 < 10: bot.pump(0.3) if 658 in bot.frame_log: break if 658 not in bot.frame_log: g.close() raise SystemExit("[!] SC658 超时") send_init_frames(bot, uid) for _ in range(24): bot.pump(0.5) if bot.me["id"] and bot.me.get("mapname"): break log(f"进世界: map={bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']}") return g, bot def npc_talk_to_biqi(bot): t0 = time.time() while time.time() - t0 < 8 and not bot.npcs: bot.pump(0.4) npc = bot.find_guide_npc() if not npc: return bot.me.get("mapname") == "比奇大城" aid = npc["aid"] for idx in [npc["index"], 35, 34]: if idx == npc["index"] and 35 == npc["index"]: idxs = [35] bot.pending_map_ack = "比奇大城" bot.send(1010, 0, 0, 0, bot.sid, json.dumps( {"UserID": aid, "index": idx}, separators=(",", ":")).encode()) bot.send(5012, 0, 0, 0, bot.sid, b"0") bot.send(5027, 0, 0, 0, bot.sid, b"0") t1 = time.time() while time.time() - t1 < 8 and bot.alive: evs = bot.pump(0.3) if bot.me.get("mapname") == "比奇大城": bot.pending_map_ack = None return True talk = next((e[1] for e in evs if e[0] == "npc_talk"), None) if talk: acts = bot.parse_sui_acts(str(talk.get("sMsg", ""))) act = next((a for a in acts if a not in ("exit", "close")), None) if act: bot.npc_click_act(talk, act) bot.pending_map_ack = None if bot.me.get("mapname") == "比奇大城": return True return bot.me.get("mapname") == "比奇大城" def renwu_back_to_wait(bot): if 1 in bot.mission_bodies: body3031 = json.dumps(bot.mission_bodies[1], separators=(",", ":")).encode() else: body3031 = open(os.path.join(Q.HERE, "docs", "cs3031_template.txt"), "rb").read() bot.send(3031, 0, 0, 0, bot.sid, body3031) bot.pump(0.5) bot.send(1011, 0, 0, 0, bot.sid, json.dumps( {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999}, separators=(",", ":")).encode()) t0 = time.time() while time.time() - t0 < 12 and bot.alive: bot.pump(0.3) if bot.me.get("mapname") == Q.WAIT_MAP: break # 静置吃掉传送期过期 SC28, 重置锚点 time.sleep(0.5) t1 = time.time() while time.time() - t1 < 2.0: bot.pump(0.2) bot.me["last_x"], bot.me["last_y"] = bot.me["x"], bot.me["y"] bot._step_pending_ack = None log(f"回到等待图 @{bot.me['x']},{bot.me['y']} " f"SC20102目标={bot.rail}") return bot.me.get("mapname") == Q.WAIT_MAP and bot.me["y"] > 4 def rail_walk(bot, seconds, m): """SC20102→CS5013 START→逐格550ms走向目标(完整复刻客户端链)。""" t_end = time.time() + seconds started = False last_step = 0.0 rep = time.time() rejected = 0 confirmed = 0 while time.time() < t_end and bot.alive: evs = bot.pump(0.05) bot.tick_step_ack() for e in evs: if e[0] == "self_move": confirmed += 1 elif e[0] in ("pull_back", "pull_back_bogus_dropped"): rejected += 1 if not started and bot.rail and bot.rail.get("x") is not None: tx, ty = int(bot.rail["x"]), int(bot.rail["y"]) bot.send(5013, 1, tx, ty, 0, b"") started = True last_step = time.time() # 握手后第一个550ms才开始走 log(f" >> CS5013 START → ({tx},{ty}) [复刻AutoMoveBegin]") now = time.time() if started and bot.rail and now - last_step >= Q.STEP_MS / 1000.0: tx, ty = int(bot.rail["x"]), int(bot.rail["y"]) x, y = bot.me["x"], bot.me["y"] if abs(tx - x) <= 1 and abs(ty - y) <= 1: bot.send(5013, 3, 0, 0, 0, b"") log(f" >> CS5013 FINISH @到达 ({x},{y})") # 到达后重新触发寻路(保持会话活跃, 循环走动) bot.send(1011, 0, 0, 0, bot.sid, json.dumps( {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999}, separators=(",", ":")).encode()) started = False bot.rail = None last_step = now continue dx = max(-1, min(1, tx - x)) dy = max(-1, min(1, ty - y)) if dx and dy and not (m.walkable(x + dx, y) or m.walkable(x, y + dy)): if m.walkable(x + dx, y): dy = 0 elif m.walkable(x, y + dy): dx = 0 d = next(i for i, (ox, oy) in enumerate(DIRS) if ox == dx and oy == dy) bot.step_dir(d, 1) last_step = now if now - rep >= 15: rep = now log(f"[STAT] pos=({bot.me['x']},{bot.me['y']}) rail={bot.rail} " f"发帧={bot.sent_log} 确认={confirmed} 拒绝={rejected}") if started: bot.send(5013, 2, 0, 0, 0, b"") log(f"[结束] pos=({bot.me['x']},{bot.me['y']}) 发帧={bot.sent_log} " f"确认={confirmed} 拒绝={rejected}") def main(): ap = argparse.ArgumentParser() ap.add_argument("-u", "--user", default="longwu988") ap.add_argument("-p", "--password", default="Longwu1988") ap.add_argument("--zone", type=int, default=14504562) ap.add_argument("--name", default="萌新8195") ap.add_argument("--port", type=int, default=0) ap.add_argument("--minutes", type=float, default=2) args = ap.parse_args() args.minutes = min(args.minutes, 2) m = map_load("0139") g, bot = enter(args) try: Q.P.enable_auto_heal(g, bot.sid, want_on=True) log("喝药保护: on") if bot.me.get("mapname") == Q.WAIT_MAP and bot.me["y"] <= 4: log("[1] 滞留带 → NPC传比奇") if not npc_talk_to_biqi(bot): log("[!] 未传比奇") return if bot.me.get("mapname") != Q.WAIT_MAP: log("[2] 比奇 → @renwu_mov 回等待图") if not renwu_back_to_wait(bot): log("[!] 未回等待图可走区") return log("[3] SC20102→CS5013→逐格走(完整链复刻)") rail_walk(bot, args.minutes * 60, m) finally: g.close() if __name__ == "__main__": main()