|
@@ -27,6 +27,11 @@ from apilot.wire import Frame # noqa: E402
|
|
|
GW_HOST = "s{zone}-jycq-206923.dhsf.xqhuyu.com"
|
|
GW_HOST = "s{zone}-jycq-206923.dhsf.xqhuyu.com"
|
|
|
CHANNEL = "110585"
|
|
CHANNEL = "110585"
|
|
|
GAME_ID = 206923
|
|
GAME_ID = 206923
|
|
|
|
|
+LOGIN_PORTS = (8041, 8021, 8061, 8001) # 端口轮询顺序(README: 8041 优先)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class _GateError(RuntimeError):
|
|
|
|
|
+ """单端口登录链失败(超时/断连)→ 换下一端口重试。"""
|
|
|
|
|
|
|
|
|
|
|
|
|
class Host:
|
|
class Host:
|
|
@@ -148,9 +153,13 @@ class Host:
|
|
|
wire._buf = wire._buf + data
|
|
wire._buf = wire._buf + data
|
|
|
return None, None
|
|
return None, None
|
|
|
|
|
|
|
|
- def run_create_role(self, name: str | None = None, timeout: float = 30) -> dict:
|
|
|
|
|
|
|
+ def run_create_role(self, name: str | None = None, timeout: float = 30,
|
|
|
|
|
+ ports: tuple[int, ...] = LOGIN_PORTS) -> dict:
|
|
|
"""登录链逐帧驱动(对照 cq2_pure_enter.login_gate 唯一真源,逐帧注释)。
|
|
"""登录链逐帧驱动(对照 cq2_pure_enter.login_gate 唯一真源,逐帧注释)。
|
|
|
|
|
|
|
|
|
|
+ 内部: 逐端口 connect(8041/8021/8061/8001 轮询)→ 2001 链 → 101 → 521。
|
|
|
|
|
+ 返回 {"role_name", "ticket", "sid", "key32", "port"}。
|
|
|
|
|
+
|
|
|
2001(tail=0,recog=random) → 等 529 取 key32(decode6 split /) →
|
|
2001(tail=0,recog=random) → 等 529 取 key32(decode6 split /) →
|
|
|
104(body=encode6(key32),tail=1) → 等 530 取 sid(m[0]/pre 字段,
|
|
104(body=encode6(key32),tail=1) → 等 530 取 sid(m[0]/pre 字段,
|
|
|
cq2_pure_enter:138) →
|
|
cq2_pure_enter:138) →
|
|
@@ -159,14 +168,31 @@ class Host:
|
|
|
角色列表含 name→复用;否则 101(tail=3) → 等 521(decode6 后 split /)
|
|
角色列表含 name→复用;否则 101(tail=3) → 等 521(decode6 后 split /)
|
|
|
/522(换名重试 ≤5 次)。
|
|
/522(换名重试 ≤5 次)。
|
|
|
"""
|
|
"""
|
|
|
- port = self._login_port
|
|
|
|
|
|
|
+ last_err: Exception | None = None
|
|
|
|
|
+ for port in ports:
|
|
|
|
|
+ if not self.nc.is_connected():
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.connect_login_gate(port)
|
|
|
|
|
+ except OSError as e:
|
|
|
|
|
+ last_err = e
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ return self._create_role_once(name, timeout, port)
|
|
|
|
|
+ except _GateError as e:
|
|
|
|
|
+ last_err = e
|
|
|
|
|
+ self.nc.disconnect()
|
|
|
|
|
+ continue
|
|
|
|
|
+ raise RuntimeError(f"[!] 所有候选端口失败: {last_err}")
|
|
|
|
|
+
|
|
|
|
|
+ def _create_role_once(self, name: str | None, timeout: float,
|
|
|
|
|
+ port: int) -> dict:
|
|
|
# CS2001 ─ 登录服首帧,带 uid/encode6(jwt)/encode6(channel)/1/0
|
|
# CS2001 ─ 登录服首帧,带 uid/encode6(jwt)/encode6(channel)/1/0
|
|
|
self._send(2001, random.randint(1, 2**30), self.zone, port, 0, 0,
|
|
self._send(2001, random.randint(1, 2**30), self.zone, port, 0, 0,
|
|
|
self.build_2001_body(self.uid, self.jwt))
|
|
self.build_2001_body(self.uid, self.jwt))
|
|
|
# 等 SC529 → 会话密钥 key32(body 形如 "key32/...")
|
|
# 等 SC529 → 会话密钥 key32(body 形如 "key32/...")
|
|
|
f529, _ = self._wait_for(529, timeout)
|
|
f529, _ = self._wait_for(529, timeout)
|
|
|
if f529 is None:
|
|
if f529 is None:
|
|
|
- raise RuntimeError("[!] 登录服未下发 529 key")
|
|
|
|
|
|
|
+ raise _GateError("[!] 登录服未下发 529 key")
|
|
|
key32 = decode6(f529.body).split(b"/")[0]
|
|
key32 = decode6(f529.body).split(b"/")[0]
|
|
|
# CS104 ─ 回传 key32 协商
|
|
# CS104 ─ 回传 key32 协商
|
|
|
self._send(104, 0, 0, 0, 0, 1, encode6(key32))
|
|
self._send(104, 0, 0, 0, 0, 1, encode6(key32))
|
|
@@ -175,7 +201,7 @@ class Host:
|
|
|
# 这里 _recv_with_pre 直读 raw 字节补回。未命中超时即报错。
|
|
# 这里 _recv_with_pre 直读 raw 字节补回。未命中超时即报错。
|
|
|
_, sid = self._recv_with_pre(530, timeout)
|
|
_, sid = self._recv_with_pre(530, timeout)
|
|
|
if sid is None:
|
|
if sid is None:
|
|
|
- raise RuntimeError("[!] 登录服未下发 530 sid")
|
|
|
|
|
|
|
+ raise _GateError("[!] 登录服未下发 530 sid")
|
|
|
self.nc.set_sdata(sid)
|
|
self.nc.set_sdata(sid)
|
|
|
# CS100 ─ 拉角色列表(body 形如 "uid/sid/zone/channel/gameid/"+key32)
|
|
# CS100 ─ 拉角色列表(body 形如 "uid/sid/zone/channel/gameid/"+key32)
|
|
|
body100 = (f"{self.uid}/{sid}/{self.zone}/{CHANNEL}/{GAME_ID}/"
|
|
body100 = (f"{self.uid}/{sid}/{self.zone}/{CHANNEL}/{GAME_ID}/"
|
|
@@ -238,19 +264,20 @@ class Host:
|
|
|
if lst:
|
|
if lst:
|
|
|
chr_info = lst[-1]
|
|
chr_info = lst[-1]
|
|
|
if chr_info is None:
|
|
if chr_info is None:
|
|
|
- raise RuntimeError("[!] 无角色且创角失败")
|
|
|
|
|
- return {"name": chr_info["chr"], "sid": sid,
|
|
|
|
|
- "key32": key32, "user_id": chr_info["UserID"]}
|
|
|
|
|
|
|
+ raise _GateError("[!] 无角色且创角失败")
|
|
|
|
|
+ ticket = chr_info["UserID"]
|
|
|
|
|
+ return {"role_name": chr_info["chr"], "ticket": ticket,
|
|
|
|
|
+ "sid": sid, "key32": key32, "port": port}
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_zone(zone_id: int, user: str, password: str, name: str | None = None,
|
|
def run_zone(zone_id: int, user: str, password: str, name: str | None = None,
|
|
|
- port: int = 8041) -> dict:
|
|
|
|
|
- """一站式: HTTP 登录 → enter_game → 网关连接 → 创角 → 返回票据。"""
|
|
|
|
|
|
|
+ port: int | None = None) -> dict:
|
|
|
|
|
+ """一站式: HTTP 登录 → enter_game → 网关连接(端口轮询) → 创角 → 返回票据。"""
|
|
|
h = Host(zone_id, user, password)
|
|
h = Host(zone_id, user, password)
|
|
|
session = h.http_login()
|
|
session = h.http_login()
|
|
|
j2 = P.enter_game(session, h.jwt, zone_id)
|
|
j2 = P.enter_game(session, h.jwt, zone_id)
|
|
|
if j2.get("code") != 200:
|
|
if j2.get("code") != 200:
|
|
|
raise SystemExit(f"[!] enter_game 失败: {j2.get('msg') or j2}")
|
|
raise SystemExit(f"[!] enter_game 失败: {j2.get('msg') or j2}")
|
|
|
- h.connect_login_gate(port=port)
|
|
|
|
|
- info = h.run_create_role(name)
|
|
|
|
|
|
|
+ ports = (port,) if port else LOGIN_PORTS
|
|
|
|
|
+ info = h.run_create_role(name, ports=ports)
|
|
|
return {"uid": h.uid, "jwt": h.jwt, "zone": h.zone, **info}
|
|
return {"uid": h.uid, "jwt": h.jwt, "zone": h.zone, **info}
|