quest_bot.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # -*- coding: utf-8 -*-
  2. """
  3. quest_bot.py —— 事件驱动任务机器人(常驻连接, 服务器推送驱动状态机)。
  4. 架构(对照真实客户端 C++ recv_loop + Lua RegisterMsgHandler):
  5. 常驻 socket + 心跳线程 + 主循环持续泵收
  6. 状态: ENTER_WORLD → GOTO_TASK(服务器驱动寻路, 跟踪SC11/13/634/801)
  7. → FIGHT(@startguaji 挂机, SC32 计击杀) → STOP(@stopguaji) → DONE
  8. 关键协议(2026-09-22 真实客户端抓包实证):
  9. CS1011 {"Act":"@renwu_mov,1","UserID":"999999999","index":999999999} 任务前往
  10. CS1010 {"UserID":"10032","index":34} 点NPC(新手接待)
  11. CS3199 {"act":"@startguaji","index":104,"subid":2} 挂机开
  12. CS3199 {"act":"@stopguaji","index":104,"subid":2} 挂机停
  13. CS5286/CS5283 挂机开/停伴生帧
  14. SC11/13 走/跑广播: body=JSON{UserID}, p1=X p2=Y, p3=dir|type<<16
  15. 用法:
  16. .venv/bin/python quest_bot.py --zone 14492165 --name 矿工6543 [--kill 2]
  17. """
  18. import argparse
  19. import json
  20. import random
  21. import socket
  22. import struct
  23. import sys
  24. import threading
  25. import time
  26. import requests as rq
  27. import urllib3
  28. urllib3.disable_warnings()
  29. HERE = __file__.rsplit("/", 1)[0]
  30. sys.path.insert(0, HERE)
  31. from encode6bitbuf_final import encode6, decode6 # noqa: E402
  32. import login_996pc as L # noqa: E402
  33. import cq2_pure_enter as P # noqa: E402
  34. from combat_bot import CombatBot, send_init_frames # noqa: E402
  35. def log(m):
  36. print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)
  37. class QuestBot(CombatBot):
  38. """事件驱动扩展: 自己的移动跟踪 + 任务状态机。"""
  39. def __init__(self, sock, sid, name):
  40. super().__init__(sock, sid, name)
  41. self.state = "ENTER_WORLD"
  42. self.last_event_t = time.time()
  43. self.last_move_t = time.time()
  44. self.mon_kills = 0 # 视野内怪物死亡(SC32, 非自己击杀也算)
  45. self.attack_seen = 0 # SC14 攻击动作广播
  46. self.task_pos = None # 任务点坐标(到达后记录)
  47. def _dispatch(self, mid, recog, p1, p2, p3, body):
  48. ev = []
  49. if mid in (11, 13): # 走/跑广播: body={UserID}, p1=X p2=Y
  50. j = self._jbody(body) or {}
  51. uid = str(j.get("UserID", ""))
  52. myuid = self.me.get("uid")
  53. if uid and (uid == myuid or (myuid and uid in myuid) or (myuid and myuid in uid)):
  54. self.me["x"], self.me["y"] = p1, p2
  55. self.last_move_t = time.time()
  56. ev.append(("self_move", p1, p2))
  57. else:
  58. self.last_event_t = time.time()
  59. elif mid == 1: # 行为确认(自己也走这里)
  60. self.last_event_t = time.time()
  61. elif mid == 32:
  62. try:
  63. aid = int(body.decode("utf-8", "replace").strip())
  64. except ValueError:
  65. aid = 0
  66. if aid in self.monsters:
  67. self.mon_kills += 1
  68. ev.append(("mon_die", aid))
  69. self.last_event_t = time.time()
  70. elif mid in (634, 801): # 换图/传送特效
  71. self.last_move_t = time.time() # 传送也算移动中
  72. ev.append(("teleport", mid))
  73. else:
  74. ev += super()._dispatch(mid, recog, p1, p2, p3, body)
  75. if mid in (51, 50): # 地图/出生
  76. self.last_move_t = time.time()
  77. if ev:
  78. self.last_event_t = time.time()
  79. return ev
  80. def enter_world(s, jwt, uid, zone, name, port):
  81. P.enter_game(s, jwt, zone)
  82. got = None
  83. for pt in ((port,) if port else (8021, 8061, 8001, 8041)):
  84. try:
  85. r = P.login_gate(zone, jwt, name, pt, uid)
  86. if r[0]:
  87. got = r
  88. break
  89. except SystemExit as e:
  90. log(f" 端口{pt}: {str(e)[:50]}")
  91. except Exception as e:
  92. log(f" 端口{pt}: {str(e)[:50]}")
  93. if not got:
  94. raise SystemExit("[!] 登录服全端口失败")
  95. key32, sid, rname, tkey, gp = got
  96. g = P.enter_game_gate(zone, rname, tkey, sid, gp, uid)
  97. bot = QuestBot(g, sid, rname)
  98. bot.me["uid"] = None # SC50 后填
  99. P.heartbeat_loop(g, sid, bot.lock)
  100. t0 = time.time()
  101. while time.time() - t0 < 10:
  102. bot.pump(0.3)
  103. if 658 in bot.frame_log:
  104. break
  105. log(f"SC658 {'✓' if 658 in bot.frame_log else '✗'}")
  106. send_init_frames(bot, uid)
  107. for _ in range(24):
  108. bot.pump(0.5)
  109. if bot.me["id"] and bot.me.get("mapname"):
  110. break
  111. # 主玩家完整 UserID(SC42/50 body info[0]) 供 SC11/13 识别自己
  112. log(f"进世界: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']} id={bot.me['id']}")
  113. return g, bot
  114. def main():
  115. ap = argparse.ArgumentParser()
  116. ap.add_argument("-u", "--user", default="longwu988")
  117. ap.add_argument("-p", "--password", default="Longwu1988")
  118. ap.add_argument("--zone", type=int, default=14492165)
  119. ap.add_argument("--name", default="矿工6543")
  120. ap.add_argument("--port", type=int, default=0)
  121. ap.add_argument("--kill", type=int, default=2)
  122. ap.add_argument("--minutes", type=float, default=12)
  123. args = ap.parse_args()
  124. for a in range(5):
  125. try:
  126. s = rq.Session()
  127. j, _ = L.do_login(s, args.user, args.password)
  128. if j.get("code") == 200:
  129. break
  130. except Exception as e:
  131. log(f"登录重试{a+1}: {str(e)[:60]}")
  132. time.sleep(12)
  133. jwt = j["data"]["token"]
  134. uid = str(j["data"]["userInfo"]["id"])
  135. g, bot = enter_world(s, jwt, uid, args.zone, args.name, args.port)
  136. tail = 100
  137. def send(mid, recog, p1, p2, p3, body=b""):
  138. nonlocal tail
  139. tail += 1
  140. g.sendall(encode6(struct.pack("<9i", 0, mid, recog, p1, p2, p3, 0, len(body), tail)) + body)
  141. def act_3199(act, index, subid):
  142. send(3199, 0, 0, 0, bot.sid, json.dumps(
  143. {"act": act, "index": index, "subid": subid}, separators=(",", ":")).encode())
  144. # ---- 喝药保护 ----
  145. state = P.enable_auto_heal(g, bot.sid, want_on=True)
  146. log(f"喝药保护: {state}")
  147. # ---- 状态机主循环(常驻, 事件驱动) ----
  148. deadline = time.time() + args.minutes * 60
  149. rep = 0.0
  150. started_guaji = False
  151. task_sent = False
  152. while time.time() < deadline and bot.alive:
  153. evs = bot.pump(0.4) # 持续泵, 永不长时间阻塞
  154. for e in evs:
  155. if e[0] in ("self_move", "teleport", "mon_die", "map", "me_born"):
  156. log(f"EV {e}")
  157. # 通知态: 打印进度
  158. if time.time() - rep > 10:
  159. rep = time.time()
  160. log(f"[{bot.state}] pos=({bot.me['x']},{bot.me['y']}) map={bot.me.get('mapname')} "
  161. f"kills={bot.mon_kills} SC13={bot.frame_log.get(13,0)} SC32={bot.frame_log.get(32,0)}")
  162. if bot.state == "GOTO_TASK":
  163. still = time.time() - bot.last_move_t > 10
  164. if still and not task_sent:
  165. pass
  166. if not task_sent:
  167. send(1011, 0, 0, 0, bot.sid, json.dumps(
  168. {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999},
  169. separators=(",", ":")).encode())
  170. task_sent = True
  171. bot.last_move_t = time.time()
  172. log("已发 @renwu_mov,1 (服务器驱动寻路)")
  173. elif time.time() - bot.last_move_t > 12 and time.time() - bot.last_event_t > 5:
  174. bot.state = "FIGHT"
  175. bot.task_pos = (bot.me["x"], bot.me["y"])
  176. log(f"寻路静止, 任务点认定 @({bot.me['x']},{bot.me['y']})")
  177. elif bot.state == "FIGHT" and not started_guaji:
  178. act_3199("@startguaji", 104, 2)
  179. send(5286, 0, 0, 0, bot.sid)
  180. started_guaji = True
  181. bot.last_event_t = time.time()
  182. log("已发 @startguaji, 等待击杀(SC32)...")
  183. elif bot.state == "FIGHT" and started_guaji:
  184. if bot.mon_kills >= args.kill:
  185. act_3199("@stopguaji", 104, 2)
  186. send(5283, 0, 0, 0, bot.sid)
  187. log(f"★ 目标达成: {bot.mon_kills} 杀, 已停挂机")
  188. bot.state = "DONE"
  189. break
  190. # 长时间无事件 → 重发一次挂机(保持活性)
  191. if time.time() - bot.last_event_t > 60:
  192. act_3199("@startguaji", 104, 2)
  193. send(5286, 0, 0, 0, bot.sid)
  194. bot.last_event_t = time.time()
  195. log("(活性) 重发 @startguaji")
  196. if bot.state == "ENTER_WORLD":
  197. bot.state = "GOTO_TASK"
  198. log(f"结束: state={bot.state} kills={bot.mon_kills} pos=({bot.me['x']},{bot.me['y']}) map={bot.me.get('mapname')}")
  199. log(f"收帧: {sorted(bot.frame_log.items(), key=lambda kv: -kv[1])[:14]}")
  200. g.close()
  201. if __name__ == "__main__":
  202. main()