# -*- coding: utf-8 -*- """ quest_bot2.py —— 客户端驱动任务机器人(双向通信完整版)。 2026-09-22 真实客户端抓包破译的走路闭环: CS3013(跑2格)/CS3011(走1格): p1=dir(0-7), p2=起点point(Y<<16|X), body=地图名 CS1(步末确认): recog=新X, p1=新Y, p2=dir, body="+GOOD/<服务器运行毫秒>", 步长≈547ms SC11/13 广播: body=encode6(JSON{UserID}), p1=X p2=Y → 自识别更新坐标 SC28 拉回: p1=X p2=Y p3=dir(强制校正) 流程: @renwu_mov,1 → 走到NPC → CS1010(10032,34) → 传送比奇 → 走到任务点 → @startguaji 挂机 → SC32×N → @stopguaji 用法: .venv/bin/python quest_bot2.py --zone 14492165 --name 矿工6543 [--kill 2] """ import argparse import json import random import socket import struct import sys import threading import time import requests as rq import urllib3 urllib3.disable_warnings() HERE = __file__.rsplit("/", 1)[0] sys.path.insert(0, HERE) from encode6bitbuf_final import encode6, decode6 # noqa: E402 import login_996pc as L # noqa: E402 import cq2_pure_enter as P # noqa: E402 from combat_bot import CombatBot, send_init_frames, DIRS, log # noqa: E402 from mir_map import load as map_load # noqa: E402 STEP_MS = 547 # 实测步长(CS1 body 毫秒差) MAP_FILES = { # 地图名 → .map 文件主名(实测对照) "等待地图": "0139", "比奇大城": "3", "盟重土城": "0", } class WalkBot(CombatBot): """双向通信引擎: 服务器推送(SC11/13/28)实时校正 + 客户端驱动逐格走。""" def __init__(self, sock, sid, name): super().__init__(sock, sid, name) self.pos_fixed = False # 已知真实坐标 self.last_walk_ok = True def _dispatch(self, mid, recog, p1, p2, p3, body): ev = [] myuid = self.me.get("uid") or "" if mid in (11, 13): # 走/跑广播: p1=X p2=Y j = self._jbody(body) or {} uid = str(j.get("UserID", "")) if uid and myuid and (uid == myuid or uid in myuid or myuid in uid): self.me["x"], self.me["y"] = p1, p2 self.me["dir"] = p3 & 0xFF self.pos_fixed = True ev.append(("self_move", p1, p2)) elif mid == 32: try: aid = int(body.decode("utf-8", "replace").strip()) except ValueError: aid = 0 if aid in self.monsters: self.mon_kills = getattr(self, "mon_kills", 0) + 1 ev.append(("mon_die", aid)) else: ev += super()._dispatch(mid, recog, p1, p2, p3, body) if mid == 50 and self.me.get("uid"): self.pos_fixed = True return ev # ---- 客户端驱动走路(抓包同款) ---- def step(self, dx, dy, run=True): """向 (dx,dy) 迈一步: CS3013/3011 + 停STEP_MS + CS1确认。""" d = None for i, (ox, oy) in enumerate(DIRS): if ox == dx and oy == dy: d = i break if d is None: return False x, y = self.me["x"], self.me["y"] nx, ny = x + dx * (2 if run else 1), y + dy * (2 if run else 1) if not (0 <= nx <= 65535 and 0 <= ny <= 65535): return False mid = 3013 if run else 3011 self.send(mid, 0, d, (y & 0xFFFF) * 65536 + (x & 0xFFFF), self.sid, str(self.me["map"]).encode()) self._step_t0 = getattr(self, "_step_t0", 0) or self._server_ms() - STEP_MS return True def confirm_step(self, dx, dy): """步末 CS1: recog=新X p1=新Y p2=dir body=+GOOD/毫秒。""" d = next((i for i, (ox, oy) in enumerate(DIRS) if ox == dx and oy == dy), 2) x, y = self.me["x"], self.me["y"] self._step_ms = getattr(self, "_step_ms", 52550543671 // 1000 // 1000) or 52550543671 # 毫秒计数器: 自维护递增(初值取实测区间, 每次+STEP_MS) self._good_ms = getattr(self, "_good_ms", 52550543671) self._good_ms += STEP_MS self.send(1, x & 0xFFFF, y & 0xFFFF, d, 32171624, f"+GOOD/{self._good_ms}".encode(), tail=943) self.last_walk_t = time.time() def _server_ms(self): return int(time.time() * 1000) % (2**32) def _fix_spawn_y(self, m): """出生帧 Y 带编码位(2048=bit11): 剥掉; 不可走则落到最近可走格。""" if not m.in_bounds(self.me["x"], self.me["y"]): self.me["y"] &= 0x7FF if not m.walkable(self.me["x"], self.me["y"]): fixed = None for r in range(1, 40): for dx in range(-r, r + 1): for dy in range(-r, r + 1): nx, ny = self.me["x"] + dx, self.me["y"] + dy if m.walkable(nx, ny) and (fixed is None or abs(dx) + abs(dy) < abs(fixed[0] - self.me["x"]) + abs(fixed[1] - self.me["y"])): fixed = (nx, ny) if fixed: break if fixed: log(f"出生点校正: ({self.me['x']},{self.me['y']}) → {fixed}") self.me["x"], self.me["y"] = fixed def walk_loop(self, tx, ty, timeout=120, run=True, on_frame=None): """A* 寻路 + 逐格走(547ms/步) + SC11/13/28 实时校正 + 撞墙重算。""" mfile = MAP_FILES.get(self.me.get("mapname") or "") if not mfile: log(f"[!] 未知地图 {self.me.get('mapname')}, 退化为直线走") return self._walk_straight(tx, ty, timeout, run) m = map_load(mfile) self._fix_spawn_y(m) t0 = time.time() last_send = 0.0 path = None recompute_t = 0.0 while time.time() - t0 < timeout and self.alive: self.pump(0.1) x, y = self.me["x"], self.me["y"] if abs(tx - x) <= 1 and abs(ty - y) <= 1: return True if time.time() - recompute_t > 2 or path is None: path = m.find_path(x, y, tx, ty) recompute_t = time.time() if not path or len(path) < 2: log(f"[!] A* 无路 ({x},{y})→({tx},{ty})") return False nxt = path[1] if len(path) > 1 else (tx, ty) # 服务器校正后路径偏了 → 重算 if abs(nxt[0] - x) > 3 or abs(nxt[1] - y) > 3: path = None continue if time.time() - last_send >= STEP_MS / 1000.0: dx = max(-1, min(1, nxt[0] - x)) dy = max(-1, min(1, nxt[1] - y)) # 斜走需两个直邻格均可走(Mir2 斜穿限制) if dx and dy: if not (m.walkable(x + dx, y) and m.walkable(x, y + dy)): if m.walkable(x + dx, y): dy = 0 elif m.walkable(x, y + dy): dx = 0 else: path = None # 两边都堵, 重算 continue # 连续被拉回(原地) → 换方向试探 if getattr(self, "_last_pos", None) == (x, y): self._stuck = getattr(self, "_stuck", 0) + 1 if self._stuck > 3: alt = [(ax, ay) for ax, ay in [(1,0),(-1,0),(0,1),(0,-1)] if m.walkable(x + ax, y + ay)] if alt: dx, dy = alt[self._stuck % len(alt)] self._stuck = 0 path = None else: self._stuck = 0 self._last_pos = (x, y) if self.step(dx, dy, run=run): n = 2 if run else 1 self.me["x"] += dx * n self.me["y"] += dy * n last_send = time.time() self.confirm_at = (dx, dy, time.time() + STEP_MS / 1000.0) if path and len(path) > 1: path = path[1:] ca = getattr(self, "confirm_at", None) if ca and time.time() >= ca[2]: self.confirm_step(ca[0], ca[1]) self.confirm_at = None return False def _walk_straight(self, tx, ty, timeout, run): t0 = time.time() last_send = 0.0 while time.time() - t0 < timeout and self.alive: self.pump(0.1) x, y = self.me["x"], self.me["y"] if abs(tx - x) <= 1 and abs(ty - y) <= 1: return True if time.time() - last_send >= STEP_MS / 1000.0: dx = max(-1, min(1, tx - x)) dy = max(-1, min(1, ty - y)) if self.step(dx, dy, run=run): n = 2 if run else 1 self.me["x"] += dx * n self.me["y"] += dy * n last_send = time.time() self.confirm_at = (dx, dy, time.time() + STEP_MS / 1000.0) ca = getattr(self, "confirm_at", None) if ca and time.time() >= ca[2]: self.confirm_step(ca[0], ca[1]) self.confirm_at = None return False def enter_world(s, jwt, uid, zone, name, port): for a in range(4): try: P.enter_game(s, jwt, zone) break except Exception: time.sleep(12) got = None for pt in ((port,) if port else (8021, 8061, 8001, 8041)): try: r = P.login_gate(zone, jwt, name, pt, uid) if r[0]: got = r break except Exception as e: log(f" 端口{pt}: {str(e)[:50]}") if not got: raise SystemExit("[!] 登录服失败") key32, sid, rname, tkey, gp = got g = P.enter_game_gate(zone, rname, tkey, sid, gp, uid) bot = WalkBot(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 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"进世界: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']}") return g, bot 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=14492165) ap.add_argument("--name", default="矿工6543") ap.add_argument("--port", type=int, default=0) ap.add_argument("--kill", type=int, default=2) args = ap.parse_args() 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) jwt = j["data"]["token"] uid = str(j["data"]["userInfo"]["id"]) g, bot = enter_world(s, jwt, uid, args.zone, args.name, args.port) tail = 100 def send(mid, recog, p1, p2, p3, body=b""): nonlocal tail tail += 1 g.sendall(encode6(struct.pack("<9i", 0, mid, recog, p1, p2, p3, 0, len(body), tail)) + body) def act_3199(act): send(3199, 0, 0, 0, bot.sid, json.dumps( {"act": act, "index": 104, "subid": 2}, separators=(",", ":")).encode()) P.enable_auto_heal(g, bot.sid, want_on=True) log("喝药: on") mname = bot.me.get("mapname") or "" # ---- 阶段1: 若在等待图 → 走到新手接待NPC → 点击传送 ---- if mname == "等待地图": log("等待图: 寻找新手接待NPC坐标...") # 收集NPC视野 deadline = time.time() + 6 while time.time() < deadline and not bot.npcs: bot.pump(0.5) npc = None for aid, n in bot.npcs.items(): if "接待" in n["name"] or "新手" in n["name"]: npc = n break if not npc and bot.npcs: npc = list(bot.npcs.values())[0] if npc: log(f"NPC {npc['name']} @({npc['x']},{npc['y']}) — 走过去") ok = bot.walk_loop(npc["x"], npc["y"], timeout=90, run=False) log(f"walk result={ok} pos=({bot.me['x']},{bot.me['y']})") send(1010, 0, 0, 0, bot.sid, json.dumps( {"UserID": npc["aid"], "index": 34}, separators=(",", ":")).encode()) log("已点NPC, 等待传送...") deadline = time.time() + 20 while time.time() < deadline and bot.alive: bot.pump(0.4) if bot.me.get("mapname") and bot.me["mapname"] != "等待地图": break log(f"当前: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']}") # ---- 阶段2: 任务寻路(领目标→自走到任务点) ---- send(1011, 0, 0, 0, bot.sid, json.dumps( {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999}, separators=(",", ":")).encode()) log("@renwu_mov,1 已发") # 目标: 比奇任务点(293,619)(抓包实测); 若已在附近则跳过 tx, ty = 293, 619 x, y = bot.me["x"], bot.me["y"] if abs(tx - x) > 8 or abs(ty - y) > 8: log(f"走向任务点 ({tx},{ty}) ...") ok = bot.walk_loop(tx, ty, timeout=600, run=True) log(f"walk result={ok} pos=({bot.me['x']},{bot.me['y']})") # ---- 阶段3: 挂机杀怪 ---- act_3199("@startguaji") send(5286, 0, 0, 0, bot.sid) log("@startguaji — 等待击杀(SC32)...") deadline = time.time() + 300 rep = 0.0 while time.time() < deadline and bot.alive: bot.pump(0.4) k = getattr(bot, "mon_kills", 0) if time.time() - rep > 10: rep = time.time() log(f"[FIGHT] pos=({bot.me['x']},{bot.me['y']}) kills={k} " f"SC32={bot.frame_log.get(32,0)} SC14={bot.frame_log.get(14,0)}") if k >= args.kill: act_3199("@stopguaji") send(5283, 0, 0, 0, bot.sid) log(f"★ 目标达成: {k} 杀") break log(f"结束: kills={getattr(bot,'mon_kills',0)} map={bot.me.get('mapname')} " f"@({bot.me['x']},{bot.me['y']})") g.close() if __name__ == "__main__": main()