| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- # -*- coding: utf-8 -*-
- """
- quest_bot.py —— 事件驱动任务机器人(常驻连接, 服务器推送驱动状态机)。
- 架构(对照真实客户端 C++ recv_loop + Lua RegisterMsgHandler):
- 常驻 socket + 心跳线程 + 主循环持续泵收
- 状态: ENTER_WORLD → GOTO_TASK(服务器驱动寻路, 跟踪SC11/13/634/801)
- → FIGHT(@startguaji 挂机, SC32 计击杀) → STOP(@stopguaji) → DONE
- 关键协议(2026-09-22 真实客户端抓包实证):
- CS1011 {"Act":"@renwu_mov,1","UserID":"999999999","index":999999999} 任务前往
- CS1010 {"UserID":"10032","index":34} 点NPC(新手接待)
- CS3199 {"act":"@startguaji","index":104,"subid":2} 挂机开
- CS3199 {"act":"@stopguaji","index":104,"subid":2} 挂机停
- CS5286/CS5283 挂机开/停伴生帧
- SC11/13 走/跑广播: body=JSON{UserID}, p1=X p2=Y, p3=dir|type<<16
- 用法:
- .venv/bin/python quest_bot.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 # noqa: E402
- def log(m):
- print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)
- class QuestBot(CombatBot):
- """事件驱动扩展: 自己的移动跟踪 + 任务状态机。"""
- def __init__(self, sock, sid, name):
- super().__init__(sock, sid, name)
- self.state = "ENTER_WORLD"
- self.last_event_t = time.time()
- self.last_move_t = time.time()
- self.mon_kills = 0 # 视野内怪物死亡(SC32, 非自己击杀也算)
- self.attack_seen = 0 # SC14 攻击动作广播
- self.task_pos = None # 任务点坐标(到达后记录)
- def _dispatch(self, mid, recog, p1, p2, p3, body):
- ev = []
- if mid in (11, 13): # 走/跑广播: body={UserID}, p1=X p2=Y
- j = self._jbody(body) or {}
- uid = str(j.get("UserID", ""))
- myuid = self.me.get("uid")
- if uid and (uid == myuid or (myuid and uid in myuid) or (myuid and myuid in uid)):
- self.me["x"], self.me["y"] = p1, p2
- self.last_move_t = time.time()
- ev.append(("self_move", p1, p2))
- else:
- self.last_event_t = time.time()
- elif mid == 1: # 行为确认(自己也走这里)
- self.last_event_t = time.time()
- elif mid == 32:
- try:
- aid = int(body.decode("utf-8", "replace").strip())
- except ValueError:
- aid = 0
- if aid in self.monsters:
- self.mon_kills += 1
- ev.append(("mon_die", aid))
- self.last_event_t = time.time()
- elif mid in (634, 801): # 换图/传送特效
- self.last_move_t = time.time() # 传送也算移动中
- ev.append(("teleport", mid))
- else:
- ev += super()._dispatch(mid, recog, p1, p2, p3, body)
- if mid in (51, 50): # 地图/出生
- self.last_move_t = time.time()
- if ev:
- self.last_event_t = time.time()
- return ev
- def enter_world(s, jwt, uid, zone, name, port):
- P.enter_game(s, jwt, zone)
- 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 SystemExit as e:
- log(f" 端口{pt}: {str(e)[:50]}")
- 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 = QuestBot(g, sid, rname)
- bot.me["uid"] = None # SC50 后填
- 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
- log(f"SC658 {'✓' if 658 in bot.frame_log else '✗'}")
- send_init_frames(bot, uid)
- for _ in range(24):
- bot.pump(0.5)
- if bot.me["id"] and bot.me.get("mapname"):
- break
- # 主玩家完整 UserID(SC42/50 body info[0]) 供 SC11/13 识别自己
- log(f"进世界: {bot.me.get('mapname')} @{bot.me['x']},{bot.me['y']} id={bot.me['id']}")
- 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)
- ap.add_argument("--minutes", type=float, default=12)
- 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, index, subid):
- send(3199, 0, 0, 0, bot.sid, json.dumps(
- {"act": act, "index": index, "subid": subid}, separators=(",", ":")).encode())
- # ---- 喝药保护 ----
- state = P.enable_auto_heal(g, bot.sid, want_on=True)
- log(f"喝药保护: {state}")
- # ---- 状态机主循环(常驻, 事件驱动) ----
- deadline = time.time() + args.minutes * 60
- rep = 0.0
- started_guaji = False
- task_sent = False
- while time.time() < deadline and bot.alive:
- evs = bot.pump(0.4) # 持续泵, 永不长时间阻塞
- for e in evs:
- if e[0] in ("self_move", "teleport", "mon_die", "map", "me_born"):
- log(f"EV {e}")
- # 通知态: 打印进度
- if time.time() - rep > 10:
- rep = time.time()
- log(f"[{bot.state}] pos=({bot.me['x']},{bot.me['y']}) map={bot.me.get('mapname')} "
- f"kills={bot.mon_kills} SC13={bot.frame_log.get(13,0)} SC32={bot.frame_log.get(32,0)}")
- if bot.state == "GOTO_TASK":
- still = time.time() - bot.last_move_t > 10
- if still and not task_sent:
- pass
- if not task_sent:
- send(1011, 0, 0, 0, bot.sid, json.dumps(
- {"Act": "@renwu_mov,1", "UserID": "999999999", "index": 999999999},
- separators=(",", ":")).encode())
- task_sent = True
- bot.last_move_t = time.time()
- log("已发 @renwu_mov,1 (服务器驱动寻路)")
- elif time.time() - bot.last_move_t > 12 and time.time() - bot.last_event_t > 5:
- bot.state = "FIGHT"
- bot.task_pos = (bot.me["x"], bot.me["y"])
- log(f"寻路静止, 任务点认定 @({bot.me['x']},{bot.me['y']})")
- elif bot.state == "FIGHT" and not started_guaji:
- act_3199("@startguaji", 104, 2)
- send(5286, 0, 0, 0, bot.sid)
- started_guaji = True
- bot.last_event_t = time.time()
- log("已发 @startguaji, 等待击杀(SC32)...")
- elif bot.state == "FIGHT" and started_guaji:
- if bot.mon_kills >= args.kill:
- act_3199("@stopguaji", 104, 2)
- send(5283, 0, 0, 0, bot.sid)
- log(f"★ 目标达成: {bot.mon_kills} 杀, 已停挂机")
- bot.state = "DONE"
- break
- # 长时间无事件 → 重发一次挂机(保持活性)
- if time.time() - bot.last_event_t > 60:
- act_3199("@startguaji", 104, 2)
- send(5286, 0, 0, 0, bot.sid)
- bot.last_event_t = time.time()
- log("(活性) 重发 @startguaji")
- if bot.state == "ENTER_WORLD":
- bot.state = "GOTO_TASK"
- log(f"结束: state={bot.state} kills={bot.mon_kills} pos=({bot.me['x']},{bot.me['y']}) map={bot.me.get('mapname')}")
- log(f"收帧: {sorted(bot.frame_log.items(), key=lambda kv: -kv[1])[:14]}")
- g.close()
- if __name__ == "__main__":
- main()
|