quest_bot2.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. # -*- coding: utf-8 -*-
  2. """
  3. quest_bot2.py —— 客户端驱动任务机器人(双向通信完整版)。
  4. 2026-09-22 真实客户端抓包破译的走路闭环:
  5. CS3013(跑2格)/CS3011(走1格): p1=dir(0-7), p2=起点point(Y<<16|X), body=地图名
  6. CS1(步末确认): recog=新X, p1=新Y, p2=dir, body="+GOOD/<服务器运行毫秒>", 步长≈547ms
  7. SC11/13 广播: body=encode6(JSON{UserID}), p1=X p2=Y → 自识别更新坐标
  8. SC28 拉回: p1=X p2=Y p3=dir(强制校正)
  9. 流程: @renwu_mov,1 → 走到NPC → CS1010(10032,34) → 传送比奇 → 走到任务点
  10. → @startguaji 挂机 → SC32×N → @stopguaji
  11. 用法:
  12. .venv/bin/python quest_bot2.py --zone 14492165 --name 矿工6543 [--kill 2]
  13. """
  14. import argparse
  15. import json
  16. import random
  17. import socket
  18. import struct
  19. import sys
  20. import threading
  21. import time
  22. import requests as rq
  23. import urllib3
  24. urllib3.disable_warnings()
  25. HERE = __file__.rsplit("/", 1)[0]
  26. sys.path.insert(0, HERE)
  27. from encode6bitbuf_final import encode6, decode6 # noqa: E402
  28. import login_996pc as L # noqa: E402
  29. import cq2_pure_enter as P # noqa: E402
  30. from combat_bot import CombatBot, send_init_frames, DIRS, log # noqa: E402
  31. from mir_map import load as map_load # noqa: E402
  32. STEP_MS = 547 # 实测步长(CS1 body 毫秒差)
  33. MAP_FILES = { # 地图名 → .map 文件主名(实测对照)
  34. "等待地图": "0139",
  35. "比奇大城": "3",
  36. "盟重土城": "0",
  37. }
  38. class WalkBot(CombatBot):
  39. """双向通信引擎: 服务器推送(SC11/13/28)实时校正 + 客户端驱动逐格走。"""
  40. def __init__(self, sock, sid, name):
  41. super().__init__(sock, sid, name)
  42. self.pos_fixed = False # 已知真实坐标
  43. self.last_walk_ok = True
  44. def _dispatch(self, mid, recog, p1, p2, p3, body):
  45. ev = []
  46. myuid = self.me.get("uid") or ""
  47. if mid in (11, 13): # 走/跑广播: p1=X p2=Y
  48. j = self._jbody(body) or {}
  49. uid = str(j.get("UserID", ""))
  50. if uid and myuid and (uid == myuid or uid in myuid or myuid in uid):
  51. self.me["x"], self.me["y"] = p1, p2
  52. self.me["dir"] = p3 & 0xFF
  53. self.pos_fixed = True
  54. ev.append(("self_move", p1, p2))
  55. elif mid == 32:
  56. try:
  57. aid = int(body.decode("utf-8", "replace").strip())
  58. except ValueError:
  59. aid = 0
  60. if aid in self.monsters:
  61. self.mon_kills = getattr(self, "mon_kills", 0) + 1
  62. ev.append(("mon_die", aid))
  63. else:
  64. ev += super()._dispatch(mid, recog, p1, p2, p3, body)
  65. if mid == 50 and self.me.get("uid"):
  66. self.pos_fixed = True
  67. return ev
  68. # ---- 客户端驱动走路(抓包同款) ----
  69. def step(self, dx, dy, run=True):
  70. """向 (dx,dy) 迈一步: CS3013/3011 + 停STEP_MS + CS1确认。"""
  71. d = None
  72. for i, (ox, oy) in enumerate(DIRS):
  73. if ox == dx and oy == dy:
  74. d = i
  75. break
  76. if d is None:
  77. return False
  78. x, y = self.me["x"], self.me["y"]
  79. nx, ny = x + dx * (2 if run else 1), y + dy * (2 if run else 1)
  80. if not (0 <= nx <= 65535 and 0 <= ny <= 65535):
  81. return False
  82. mid = 3013 if run else 3011
  83. self.send(mid, 0, d, (y & 0xFFFF) * 65536 + (x & 0xFFFF), self.sid,
  84. str(self.me["map"]).encode())
  85. self._step_t0 = getattr(self, "_step_t0", 0) or self._server_ms() - STEP_MS
  86. return True
  87. def confirm_step(self, dx, dy):
  88. """步末 CS1: recog=新X p1=新Y p2=dir body=+GOOD/毫秒。"""
  89. d = next((i for i, (ox, oy) in enumerate(DIRS) if ox == dx and oy == dy), 2)
  90. x, y = self.me["x"], self.me["y"]
  91. self._step_ms = getattr(self, "_step_ms", 52550543671 // 1000 // 1000) or 52550543671
  92. # 毫秒计数器: 自维护递增(初值取实测区间, 每次+STEP_MS)
  93. self._good_ms = getattr(self, "_good_ms", 52550543671)
  94. self._good_ms += STEP_MS
  95. self.send(1, x & 0xFFFF, y & 0xFFFF, d, 32171624,
  96. f"+GOOD/{self._good_ms}".encode(), tail=943)
  97. self.last_walk_t = time.time()
  98. def _server_ms(self):
  99. return int(time.time() * 1000) % (2**32)
  100. def _fix_spawn_y(self, m):
  101. """出生帧 Y 带编码位(2048=bit11): 剥掉; 不可走则落到最近可走格。"""
  102. if not m.in_bounds(self.me["x"], self.me["y"]):
  103. self.me["y"] &= 0x7FF
  104. if not m.walkable(self.me["x"], self.me["y"]):
  105. fixed = None
  106. for r in range(1, 40):
  107. for dx in range(-r, r + 1):
  108. for dy in range(-r, r + 1):
  109. nx, ny = self.me["x"] + dx, self.me["y"] + dy
  110. 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"])):
  111. fixed = (nx, ny)
  112. if fixed:
  113. break
  114. if fixed:
  115. log(f"出生点校正: ({self.me['x']},{self.me['y']}) → {fixed}")
  116. self.me["x"], self.me["y"] = fixed
  117. def walk_loop(self, tx, ty, timeout=120, run=True, on_frame=None):
  118. """A* 寻路 + 逐格走(547ms/步) + SC11/13/28 实时校正 + 撞墙重算。"""
  119. mfile = MAP_FILES.get(self.me.get("mapname") or "")
  120. if not mfile:
  121. log(f"[!] 未知地图 {self.me.get('mapname')}, 退化为直线走")
  122. return self._walk_straight(tx, ty, timeout, run)
  123. m = map_load(mfile)
  124. self._fix_spawn_y(m)
  125. t0 = time.time()
  126. last_send = 0.0
  127. path = None
  128. recompute_t = 0.0
  129. while time.time() - t0 < timeout and self.alive:
  130. self.pump(0.1)
  131. x, y = self.me["x"], self.me["y"]
  132. if abs(tx - x) <= 1 and abs(ty - y) <= 1:
  133. return True
  134. if time.time() - recompute_t > 2 or path is None:
  135. path = m.find_path(x, y, tx, ty)
  136. recompute_t = time.time()
  137. if not path or len(path) < 2:
  138. log(f"[!] A* 无路 ({x},{y})→({tx},{ty})")
  139. return False
  140. nxt = path[1] if len(path) > 1 else (tx, ty)
  141. # 服务器校正后路径偏了 → 重算
  142. if abs(nxt[0] - x) > 3 or abs(nxt[1] - y) > 3:
  143. path = None
  144. continue
  145. if time.time() - last_send >= STEP_MS / 1000.0:
  146. dx = max(-1, min(1, nxt[0] - x))
  147. dy = max(-1, min(1, nxt[1] - y))
  148. # 斜走需两个直邻格均可走(Mir2 斜穿限制)
  149. if dx and dy:
  150. if not (m.walkable(x + dx, y) and m.walkable(x, y + dy)):
  151. if m.walkable(x + dx, y):
  152. dy = 0
  153. elif m.walkable(x, y + dy):
  154. dx = 0
  155. else:
  156. path = None # 两边都堵, 重算
  157. continue
  158. # 连续被拉回(原地) → 换方向试探
  159. if getattr(self, "_last_pos", None) == (x, y):
  160. self._stuck = getattr(self, "_stuck", 0) + 1
  161. if self._stuck > 3:
  162. alt = [(ax, ay) for ax, ay in [(1,0),(-1,0),(0,1),(0,-1)]
  163. if m.walkable(x + ax, y + ay)]
  164. if alt:
  165. dx, dy = alt[self._stuck % len(alt)]
  166. self._stuck = 0
  167. path = None
  168. else:
  169. self._stuck = 0
  170. self._last_pos = (x, y)
  171. if self.step(dx, dy, run=run):
  172. n = 2 if run else 1
  173. self.me["x"] += dx * n
  174. self.me["y"] += dy * n
  175. last_send = time.time()
  176. self.confirm_at = (dx, dy, time.time() + STEP_MS / 1000.0)
  177. if path and len(path) > 1:
  178. path = path[1:]
  179. ca = getattr(self, "confirm_at", None)
  180. if ca and time.time() >= ca[2]:
  181. self.confirm_step(ca[0], ca[1])
  182. self.confirm_at = None
  183. return False
  184. def _walk_straight(self, tx, ty, timeout, run):
  185. t0 = time.time()
  186. last_send = 0.0
  187. while time.time() - t0 < timeout and self.alive:
  188. self.pump(0.1)
  189. x, y = self.me["x"], self.me["y"]
  190. if abs(tx - x) <= 1 and abs(ty - y) <= 1:
  191. return True
  192. if time.time() - last_send >= STEP_MS / 1000.0:
  193. dx = max(-1, min(1, tx - x))
  194. dy = max(-1, min(1, ty - y))
  195. if self.step(dx, dy, run=run):
  196. n = 2 if run else 1
  197. self.me["x"] += dx * n
  198. self.me["y"] += dy * n
  199. last_send = time.time()
  200. self.confirm_at = (dx, dy, time.time() + STEP_MS / 1000.0)
  201. ca = getattr(self, "confirm_at", None)
  202. if ca and time.time() >= ca[2]:
  203. self.confirm_step(ca[0], ca[1])
  204. self.confirm_at = None
  205. return False
  206. def enter_world(s, jwt, uid, zone, name, port):
  207. for a in range(4):
  208. try:
  209. P.enter_game(s, jwt, zone)
  210. break
  211. except Exception:
  212. time.sleep(12)
  213. got = None
  214. for pt in ((port,) if port else (8021, 8061, 8001, 8041)):
  215. try:
  216. r = P.login_gate(zone, jwt, name, pt, uid)
  217. if r[0]:
  218. got = r
  219. break
  220. except Exception as e:
  221. log(f" 端口{pt}: {str(e)[:50]}")
  222. if not got:
  223. raise SystemExit("[!] 登录服失败")
  224. key32, sid, rname, tkey, gp = got
  225. g = P.enter_game_gate(zone, rname, tkey, sid, gp, uid)
  226. bot = WalkBot(g, sid, rname)
  227. P.heartbeat_loop(g, sid, bot.lock)
  228. t0 = time.time()
  229. while time.time() - t0 < 10:
  230. bot.pump(0.3)
  231. if 658 in bot.frame_log:
  232. break
  233. send_init_frames(bot, uid)
  234. for _ in range(24):
  235. bot.pump(0.5)
  236. if bot.me["id"] and bot.me.get("mapname"):
  237. break
  238. log(f"进世界: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']}")
  239. return g, bot
  240. def main():
  241. ap = argparse.ArgumentParser()
  242. ap.add_argument("-u", "--user", default="longwu988")
  243. ap.add_argument("-p", "--password", default="Longwu1988")
  244. ap.add_argument("--zone", type=int, default=14492165)
  245. ap.add_argument("--name", default="矿工6543")
  246. ap.add_argument("--port", type=int, default=0)
  247. ap.add_argument("--kill", type=int, default=2)
  248. args = ap.parse_args()
  249. for a in range(5):
  250. try:
  251. s = rq.Session()
  252. j, _ = L.do_login(s, args.user, args.password)
  253. if j.get("code") == 200:
  254. break
  255. except Exception as e:
  256. log(f"登录重试{a+1}: {str(e)[:60]}")
  257. time.sleep(12)
  258. jwt = j["data"]["token"]
  259. uid = str(j["data"]["userInfo"]["id"])
  260. g, bot = enter_world(s, jwt, uid, args.zone, args.name, args.port)
  261. tail = 100
  262. def send(mid, recog, p1, p2, p3, body=b""):
  263. nonlocal tail
  264. tail += 1
  265. g.sendall(encode6(struct.pack("<9i", 0, mid, recog, p1, p2, p3, 0, len(body), tail)) + body)
  266. def act_3199(act):
  267. send(3199, 0, 0, 0, bot.sid, json.dumps(
  268. {"act": act, "index": 104, "subid": 2}, separators=(",", ":")).encode())
  269. P.enable_auto_heal(g, bot.sid, want_on=True)
  270. log("喝药: on")
  271. mname = bot.me.get("mapname") or ""
  272. # ---- 阶段1: 若在等待图 → 走到新手接待NPC → 点击传送 ----
  273. if mname == "等待地图":
  274. log("等待图: 寻找新手接待NPC坐标...")
  275. # 收集NPC视野
  276. deadline = time.time() + 6
  277. while time.time() < deadline and not bot.npcs:
  278. bot.pump(0.5)
  279. npc = None
  280. for aid, n in bot.npcs.items():
  281. if "接待" in n["name"] or "新手" in n["name"]:
  282. npc = n
  283. break
  284. if not npc and bot.npcs:
  285. npc = list(bot.npcs.values())[0]
  286. if npc:
  287. log(f"NPC {npc['name']} @({npc['x']},{npc['y']}) — 走过去")
  288. ok = bot.walk_loop(npc["x"], npc["y"], timeout=90, run=False)
  289. log(f"walk result={ok} pos=({bot.me['x']},{bot.me['y']})")
  290. send(1010, 0, 0, 0, bot.sid, json.dumps(
  291. {"UserID": npc["aid"], "index": 34}, separators=(",", ":")).encode())
  292. log("已点NPC, 等待传送...")
  293. deadline = time.time() + 20
  294. while time.time() < deadline and bot.alive:
  295. bot.pump(0.4)
  296. if bot.me.get("mapname") and bot.me["mapname"] != "等待地图":
  297. break
  298. log(f"当前: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']}")
  299. # ---- 阶段2: 任务寻路(领目标→自走到任务点) ----
  300. send(1011, 0, 0, 0, bot.sid, json.dumps(
  301. {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999},
  302. separators=(",", ":")).encode())
  303. log("@renwu_mov,1 已发")
  304. # 目标: 比奇任务点(293,619)(抓包实测); 若已在附近则跳过
  305. tx, ty = 293, 619
  306. x, y = bot.me["x"], bot.me["y"]
  307. if abs(tx - x) > 8 or abs(ty - y) > 8:
  308. log(f"走向任务点 ({tx},{ty}) ...")
  309. ok = bot.walk_loop(tx, ty, timeout=600, run=True)
  310. log(f"walk result={ok} pos=({bot.me['x']},{bot.me['y']})")
  311. # ---- 阶段3: 挂机杀怪 ----
  312. act_3199("@startguaji")
  313. send(5286, 0, 0, 0, bot.sid)
  314. log("@startguaji — 等待击杀(SC32)...")
  315. deadline = time.time() + 300
  316. rep = 0.0
  317. while time.time() < deadline and bot.alive:
  318. bot.pump(0.4)
  319. k = getattr(bot, "mon_kills", 0)
  320. if time.time() - rep > 10:
  321. rep = time.time()
  322. log(f"[FIGHT] pos=({bot.me['x']},{bot.me['y']}) kills={k} "
  323. f"SC32={bot.frame_log.get(32,0)} SC14={bot.frame_log.get(14,0)}")
  324. if k >= args.kill:
  325. act_3199("@stopguaji")
  326. send(5283, 0, 0, 0, bot.sid)
  327. log(f"★ 目标达成: {k} 杀")
  328. break
  329. log(f"结束: kills={getattr(bot,'mon_kills',0)} map={bot.me.get('mapname')} "
  330. f"@({bot.me['x']},{bot.me['y']})")
  331. g.close()
  332. if __name__ == "__main__":
  333. main()