| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- # -*- 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()
|