host.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """宿主: HTTP 登录 + 网关连接 + 创角驱动。复用 protocol_bot 已验证资产。"""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import random
  6. import sys
  7. import time
  8. from typing import Any
  9. ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  10. sys.path.insert(0, os.path.join(ROOT, "protocol_bot"))
  11. import requests as rq # noqa: E402
  12. import login_996pc as L # type: ignore # noqa: E402
  13. import cq2_pure_enter as P # type: ignore # noqa: E402
  14. from encode6bitbuf_final import encode6, decode6 # type: ignore # noqa: E402
  15. from apilot.bridge import bootstrap_runtime # noqa: E402
  16. from apilot.stubs.env import EnvData # noqa: E402
  17. from apilot.stubs.netclient import NetClientStub # noqa: E402
  18. from apilot.stubs.networkctl import NetworkCtlStub, dispatch_frame # noqa: E402
  19. from apilot.stubs.scheduler import Scheduler # noqa: E402
  20. from apilot.wire import Frame # noqa: E402
  21. GW_HOST = "s{zone}-jycq-206923.dhsf.xqhuyu.com"
  22. CHANNEL = "110585"
  23. GAME_ID = 206923
  24. class Host:
  25. def __init__(self, zone_id: int, user: str, password: str,
  26. state_dir: str = ".apilot_state") -> None:
  27. self.zone = zone_id
  28. self.user, self.password = user, password
  29. self.state_dir = state_dir
  30. self.nc = NetClientStub()
  31. self.ctl = NetworkCtlStub(self.nc)
  32. self.sched = Scheduler()
  33. self.env = EnvData({})
  34. self.lua: Any = None
  35. self.jwt = ""
  36. self.uid = ""
  37. self._login_port = 0
  38. def build_2001_body(self, uid: str, jwt: str) -> bytes:
  39. return (f"{uid}/".encode() + encode6(jwt.encode()) + b"/" +
  40. encode6(CHANNEL.encode()) + b"/1/0")
  41. def build_101_body(self, name: str, uid: str, job: int = 0, sex: int = 0) -> bytes:
  42. return json.dumps({"server_name": self.zone, "chr": name, "secsdk": "0",
  43. "job": job, "sex": sex, "account": uid,
  44. "sdkid": CHANNEL}, ensure_ascii=False).encode()
  45. def http_login(self) -> dict:
  46. s = rq.Session()
  47. j, _ = L.do_login(s, self.user, self.password)
  48. if j.get("code") != 200:
  49. raise SystemExit(f"[!] 登录失败: {j.get('msg')}")
  50. self.jwt = j["data"]["token"]
  51. self.uid = str((j["data"].get("userInfo") or {}).get("id") or "")
  52. self.env.data.update({"uid": self.uid, "token": self.jwt,
  53. "new_boxtoken": self.jwt, "isBoxLogin": 1})
  54. return j
  55. def boot_lua(self) -> None:
  56. self.lua = bootstrap_runtime(self.nc, self.ctl, self.sched, self.env, self.state_dir)
  57. def connect_login_gate(self, port: int = 8041) -> None:
  58. self._login_port = port
  59. self.nc.connect(GW_HOST.format(zone=self.zone), port)
  60. def pump(self, seconds: float, on_frame=None) -> list:
  61. """收帧→分发→调度, 驱动 seconds 墙钟。"""
  62. end = time.time() + seconds
  63. got: list = []
  64. while time.time() < end:
  65. for f in self.nc.tick(0.2):
  66. if self.lua is not None:
  67. dispatch_frame(self.lua, self.ctl, f)
  68. got.append(f)
  69. if on_frame:
  70. on_frame(f)
  71. self.sched.tick(0.05)
  72. return got
  73. def tick(self, dt: float = 0.05) -> None:
  74. for f in self.nc.tick(dt):
  75. if self.lua is not None:
  76. dispatch_frame(self.lua, self.ctl, f)
  77. self.sched.tick(dt)
  78. def _send(self, mid: int, recog: int, p1: int, p2: int, p3: int,
  79. tail: int, body: bytes) -> None:
  80. """登录链统一发包: body 字节由 host 构造,帧头走 pack_frame(wire.py)唯一真源。"""
  81. self.nc.send_frame(Frame(recog=recog, msg_id=mid, p1=p1, p2=p2,
  82. p3=p3, tail=tail, body=body))
  83. def _wait_for(self, mid: int, timeout: float):
  84. """收帧直到命中 mid 或超时。返回 (帧, 已收列表)。"""
  85. deadline = time.time() + timeout
  86. got: list = []
  87. while time.time() < deadline:
  88. remain = max(0.05, deadline - time.time())
  89. for f in self.pump(min(remain, 0.2)):
  90. got.append(f)
  91. if f.msg_id == mid:
  92. return f, got
  93. return None, got
  94. def _recv_with_pre(self, target_mid: int, timeout: float):
  95. """等帧时同时拿回 m[0]("pre" 字段,apilot Frame 抽象丢弃)。
  96. 仅 SC530 用 — cq2_pure_enter.login_gate:138 sid=m[0]。"""
  97. import struct
  98. wire = self.nc.wire
  99. if wire is None:
  100. return None, None
  101. deadline = time.time() + timeout
  102. while time.time() < deadline:
  103. remain = max(0.05, deadline - time.time())
  104. wire.sock.settimeout(min(0.2, remain))
  105. try:
  106. d = wire.sock.recv(65536)
  107. if not d:
  108. break
  109. except (socket.timeout, ConnectionResetError, OSError):
  110. d = b""
  111. consumed = 0
  112. found_frame: Frame | None = None
  113. found_pre: int | None = None
  114. while consumed + 48 <= len(d):
  115. try:
  116. m = struct.unpack("<9i", decode6(d[consumed:consumed + 48]))
  117. except Exception:
  118. break
  119. blen = m[7]
  120. if m[1] == target_mid:
  121. found_frame = Frame(recog=m[2], msg_id=m[1], p1=m[3], p2=m[4],
  122. p3=m[5], tail=m[8],
  123. body=d[consumed + 48:consumed + 48 + blen])
  124. found_pre = m[0]
  125. after = d[consumed + 48 + blen:]
  126. wire._buf = wire._buf + d[:consumed] + after
  127. return found_frame, found_pre
  128. consumed += 48 + blen
  129. wire._buf = wire._buf + d
  130. return None, None
  131. def run_create_role(self, name: str | None = None, timeout: float = 30) -> dict:
  132. """登录链逐帧驱动(对照 cq2_pure_enter.login_gate 唯一真源,逐帧注释)。
  133. 2001(tail=0,recog=random) → 等 529 取 key32(decode6 split /) →
  134. 104(body=encode6(key32),tail=1) → 等 530 取 sid(recog 字段,cq2_pure_enter:138) →
  135. nc.set_sdata(sid) → 100(uid/sid/zone/channel/gameid/key32,tail=2) →
  136. 等 520 角色 JSON(decode6 后 json.loads) →
  137. 角色列表含 name→复用;否则 101(tail=3) → 等 521(decode6 后 split /)
  138. /522(换名重试 ≤5 次)。
  139. """
  140. port = self._login_port
  141. # CS2001 ─ 登录服首帧,带 uid/encode6(jwt)/encode6(channel)/1/0
  142. self._send(2001, random.randint(1, 2**30), self.zone, port, 0, 0,
  143. self.build_2001_body(self.uid, self.jwt))
  144. # 等 SC529 → 会话密钥 key32(body 形如 "key32/...")
  145. f529, _ = self._wait_for(529, timeout)
  146. if f529 is None:
  147. raise RuntimeError("[!] 登录服未下发 529 key")
  148. key32 = decode6(f529.body).split(b"/")[0]
  149. # CS104 ─ 回传 key32 协商
  150. self._send(104, 0, 0, 0, 0, 1, encode6(key32))
  151. # 等 SC530 → sid 真实位置 m[0](首 int,"pre" 字段),不是 Frame.recog(m[2])。
  152. # cq2_pure_enter.login_gate:138 sid=m[0] 唯一真源;apilot Frame 抽象丢弃 m[0],
  153. # 这里 _recv_with_pre 直读 raw 字节补回。未命中超时即报错。
  154. _, sid = self._recv_with_pre(530, timeout)
  155. if sid is None:
  156. raise RuntimeError("[!] 登录服未下发 530 sid")
  157. self.nc.set_sdata(sid)
  158. # CS100 ─ 拉角色列表(body 形如 "uid/sid/zone/channel/gameid/"+key32)
  159. body100 = (f"{self.uid}/{sid}/{self.zone}/{CHANNEL}/{GAME_ID}/"
  160. .encode() + key32)
  161. self._send(100, random.randint(1, 2**30), self.zone, port, sid, 2, body100)
  162. # 等 SC520 → 角色列表 JSON(cq2_pure_enter 兼容: encode6 包裹时先 decode6)
  163. roles: dict | None = None
  164. deadline = time.time() + timeout
  165. while time.time() < deadline:
  166. remain = max(0.05, deadline - time.time())
  167. for f in self.pump(min(remain, 0.2)):
  168. if f.msg_id == 520 and f.body:
  169. try:
  170. b = f.body
  171. if all(0x3C <= c <= 0x7B for c in b[:200]):
  172. b = decode6(b)
  173. roles = json.loads(b)
  174. except Exception:
  175. roles = None
  176. if roles is not None:
  177. break
  178. if roles is not None:
  179. break
  180. # 选/创角
  181. if name is None:
  182. name = "萌新" + str(random.randint(1000, 9999))
  183. chr_info = next((c for c in (roles or {}).get("list", [])
  184. if c.get("chr") == name), None)
  185. retries = 0
  186. while chr_info is None and retries <= 5:
  187. self._send(101, 0, 0, 0, sid, 3,
  188. self.build_101_body(name, self.uid))
  189. ticket: str | None = None
  190. got_522 = False
  191. sub_deadline = time.time() + timeout
  192. while time.time() < sub_deadline:
  193. remain = max(0.05, sub_deadline - time.time())
  194. for f in self.pump(min(remain, 0.2)):
  195. if f.msg_id == 521 and f.body:
  196. try:
  197. ticket = decode6(f.body).decode("utf-8", "replace")
  198. except Exception:
  199. ticket = f.body.decode("utf-8", "replace")
  200. if f.msg_id == 522:
  201. got_522 = True
  202. if ticket or got_522:
  203. break
  204. if got_522:
  205. retries += 1
  206. name = "萌新" + str(random.randint(1000, 9999))
  207. continue
  208. if ticket:
  209. chr_info = {"chr": name, "UserID": ticket.split("/", 1)[1]}
  210. break
  211. break
  212. if chr_info is None:
  213. lst = (roles or {}).get("list") or []
  214. if lst:
  215. chr_info = lst[-1]
  216. if chr_info is None:
  217. raise RuntimeError("[!] 无角色且创角失败")
  218. return {"name": chr_info["chr"], "sid": sid,
  219. "key32": key32, "user_id": chr_info["UserID"]}
  220. def run_zone(zone_id: int, user: str, password: str, name: str | None = None,
  221. port: int = 8041) -> dict:
  222. """一站式: HTTP 登录 → 网关连接 → 创角 → 返回票据。"""
  223. h = Host(zone_id, user, password)
  224. h.http_login()
  225. h.connect_login_gate(port=port)
  226. info = h.run_create_role(name)
  227. return {"uid": h.uid, "jwt": h.jwt, "zone": h.zone, **info}