|
|
@@ -4,45 +4,44 @@ Real protocol (2026-09-15 reverse-engineered):
|
|
|
1. Box996Login (api.jiyuhz.cn) — RSA loginInfo + HMAC X-BOX-AUTHORIZATION
|
|
|
2. get_today_open (pc-api.996hezi.com) — today's servers
|
|
|
3. enter_game_client — the 启动 button
|
|
|
- 4. TCP game login (gfgame:12000) — frame layout implemented; game login body/state remains unverified
|
|
|
+ 4. Detailed server-list resolution + TCP role/world login through SC2
|
|
|
|
|
|
-Usage (HTTP part works today):
|
|
|
+Usage:
|
|
|
async with Cq2Client(account="longwu988", password="...") as cli:
|
|
|
- servers = await cli.launcher.box.get_today_open() # 今天开了几个区
|
|
|
- await cli.launcher.enter_game(14414738) # 启动
|
|
|
+ await cli.launcher.run_until_enter_game(zone_index=0)
|
|
|
|
|
|
-The TCP part (run_until_enter_game) remains unavailable until a matching live
|
|
|
-session validates the CS2001 body, initial sequence, body codec, and state
|
|
|
-machine.
|
|
|
+When the selected zone has no role, pass char_name explicitly. The SDK never
|
|
|
+creates a role implicitly.
|
|
|
"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import time
|
|
|
from dataclasses import dataclass, field
|
|
|
from pathlib import Path
|
|
|
+from collections.abc import Awaitable, Callable
|
|
|
|
|
|
-from cq2_sdk.lua_bridge import get_runtime, LuaLoader, Cq2Lua
|
|
|
+from cq2_sdk.lua_bridge import get_runtime
|
|
|
from cq2_sdk.lua_bridge.runtime import DEFAULT_LUA_PATH
|
|
|
from cq2_sdk.transport import TcpClient
|
|
|
from cq2_sdk.login import (
|
|
|
Box996Login,
|
|
|
- Box996Error,
|
|
|
CaptchaRequiredError,
|
|
|
GeetestSolver,
|
|
|
ManualGeetestSolver,
|
|
|
TodayOpenServer,
|
|
|
+ GameLogin,
|
|
|
+ GameSession,
|
|
|
+ ServerInfo,
|
|
|
+ fetch_server_info,
|
|
|
)
|
|
|
from cq2_sdk.login.models import BoxSession
|
|
|
-from cq2_sdk.messages.registry import MessageRegistry
|
|
|
from cq2_sdk.messages import constants as C
|
|
|
from cq2_sdk.movement import Movement
|
|
|
from cq2_sdk.combat import Combat
|
|
|
from cq2_sdk.missions import Missions
|
|
|
from cq2_sdk.tasks.registry import TaskRegistry
|
|
|
from cq2_sdk.tasks.context import TaskContext
|
|
|
-from cq2_sdk.launcher import find_server, launch_game, wait_for_game_ready
|
|
|
-from cq2_sdk.launcher import ServerGroup, GameLaunchInfo
|
|
|
-from cq2_sdk.utils.snapshot import save as snapshot_save, load as snapshot_load
|
|
|
+from cq2_sdk.utils.snapshot import load as snapshot_load
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
@@ -55,13 +54,25 @@ class FlowResult:
|
|
|
class LauncherFacade:
|
|
|
"""High-level pre-game flow using the REAL 996box protocol.
|
|
|
|
|
|
- HTTP portion (login → today's servers → EnterGameClient) is complete.
|
|
|
- TCP wire framing is implemented, but its login payload and session state
|
|
|
- remain unverified.
|
|
|
+ Chains today's-server selection, EnterGameClient, detailed TCP endpoint
|
|
|
+ resolution, role login, and world initialization.
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, box: Box996Login) -> None:
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ box: Box996Login,
|
|
|
+ session: BoxSession | None = None,
|
|
|
+ *,
|
|
|
+ on_ready: Callable[
|
|
|
+ [TcpClient, ServerInfo, GameSession], Awaitable[None]
|
|
|
+ ] | None = None,
|
|
|
+ ) -> None:
|
|
|
self.box = box
|
|
|
+ self.session = session
|
|
|
+ self.on_ready = on_ready
|
|
|
+ self.tcp: TcpClient | None = None
|
|
|
+ self.server: ServerInfo | None = None
|
|
|
+ self.game_session: GameSession | None = None
|
|
|
|
|
|
async def get_today_servers(self, game_id: int = 206923) -> list[TodayOpenServer]:
|
|
|
"""今天开了几个区 — the real /box/v1/getTodayOpen."""
|
|
|
@@ -79,23 +90,55 @@ class LauncherFacade:
|
|
|
self,
|
|
|
server_name: str = "玖扬复古176",
|
|
|
zone_index: int = 0,
|
|
|
- ) -> GameLaunchInfo:
|
|
|
- """Full flow incl. TCP connect — blocked on unverified game login data.
|
|
|
-
|
|
|
- HTTP parts (find server, EnterGameClient) work; the TCP game login
|
|
|
- requires a validated CS2001 body, production initial sequence, body
|
|
|
- codec, and state machine.
|
|
|
- """
|
|
|
- servers = await self.box.get_today_open()
|
|
|
+ *,
|
|
|
+ game_id: int = 206923,
|
|
|
+ channel_id: str = "110585",
|
|
|
+ char_name: str | None = None,
|
|
|
+ job: int = 0,
|
|
|
+ sex: int = 0,
|
|
|
+ timeout: float = 10.0,
|
|
|
+ ) -> GameSession:
|
|
|
+ """Launch the selected zone and complete CQ2 TCP world login."""
|
|
|
+ del server_name # This facade currently targets one configured game.
|
|
|
+ if self.session is None:
|
|
|
+ raise RuntimeError("run_until_enter_game requires an authenticated BoxSession")
|
|
|
+
|
|
|
+ servers = await self.box.get_today_open(game_id)
|
|
|
if not servers:
|
|
|
raise RuntimeError("no servers open today")
|
|
|
+ if zone_index < 0:
|
|
|
+ raise ValueError("zone_index must be non-negative")
|
|
|
target = servers[min(zone_index, len(servers) - 1)]
|
|
|
- await self.box.enter_game_client(serverid=target.serverid)
|
|
|
- raise NotImplementedError(
|
|
|
- "TCP wire framing is implemented, but game login requires a validated "
|
|
|
- "CS2001 body, initial sequence, body codec, and state machine. "
|
|
|
- "HTTP parts completed: getTodayOpen + EnterGameClient."
|
|
|
+ await self.box.enter_game_client(game_id=game_id, serverid=target.serverid)
|
|
|
+ server = await fetch_server_info(
|
|
|
+ target.serverid,
|
|
|
+ channel_id=channel_id,
|
|
|
+ game_id=game_id,
|
|
|
)
|
|
|
+ tcp = TcpClient(server.host, server.proxy_port or server.port)
|
|
|
+ try:
|
|
|
+ await tcp.connect()
|
|
|
+ game_session = await GameLogin(tcp).login(
|
|
|
+ self.session,
|
|
|
+ server,
|
|
|
+ char_name=char_name,
|
|
|
+ job=job,
|
|
|
+ sex=sex,
|
|
|
+ timeout=timeout,
|
|
|
+ )
|
|
|
+ await tcp.start_heartbeat()
|
|
|
+ self.tcp = tcp
|
|
|
+ self.server = server
|
|
|
+ self.game_session = game_session
|
|
|
+ if self.on_ready is not None:
|
|
|
+ await self.on_ready(tcp, server, game_session)
|
|
|
+ except BaseException:
|
|
|
+ self.tcp = None
|
|
|
+ self.server = None
|
|
|
+ self.game_session = None
|
|
|
+ await tcp.close()
|
|
|
+ raise
|
|
|
+ return game_session
|
|
|
|
|
|
|
|
|
class Cq2Client:
|
|
|
@@ -124,6 +167,9 @@ class Cq2Client:
|
|
|
self.launcher: LauncherFacade | None = None
|
|
|
self.box_session: BoxSession | None = None
|
|
|
self.box: Box996Login | None = None
|
|
|
+ self.tcp: TcpClient | None = None
|
|
|
+ self.server_info: ServerInfo | None = None
|
|
|
+ self.game_session: GameSession | None = None
|
|
|
|
|
|
async def __aenter__(self) -> "Cq2Client":
|
|
|
if not self.password and not self.sms_code:
|
|
|
@@ -153,15 +199,41 @@ class Cq2Client:
|
|
|
rt._configure_path()
|
|
|
|
|
|
# 3. Launcher facade (HTTP parts fully functional)
|
|
|
- self.launcher = LauncherFacade(self.box)
|
|
|
+ self.launcher = LauncherFacade(
|
|
|
+ self.box,
|
|
|
+ self.box_session,
|
|
|
+ on_ready=self._bind_game_session,
|
|
|
+ )
|
|
|
return self
|
|
|
|
|
|
+ async def _bind_game_session(
|
|
|
+ self,
|
|
|
+ tcp: TcpClient,
|
|
|
+ server: ServerInfo,
|
|
|
+ game_session: GameSession,
|
|
|
+ ) -> None:
|
|
|
+ """Retain the live connection and expose gameplay task primitives."""
|
|
|
+ self.tcp = tcp
|
|
|
+ self.server_info = server
|
|
|
+ self.game_session = game_session
|
|
|
+ context = TaskContext(
|
|
|
+ tcp=tcp,
|
|
|
+ movement=Movement(tcp),
|
|
|
+ combat=Combat(tcp),
|
|
|
+ missions=Missions(tcp),
|
|
|
+ box_session=self.box_session,
|
|
|
+ server=server,
|
|
|
+ game_session=game_session,
|
|
|
+ snapshot_path=self.snapshot_path,
|
|
|
+ )
|
|
|
+ self.task = TaskRegistry(context)
|
|
|
+
|
|
|
async def __aexit__(self, *exc) -> None:
|
|
|
if self.box:
|
|
|
await self.box.close()
|
|
|
- if self.task and getattr(self.task.ctx, "tcp", None):
|
|
|
+ if self.tcp is not None:
|
|
|
try:
|
|
|
- await self.task.ctx.tcp.close()
|
|
|
+ await self.tcp.close()
|
|
|
except Exception:
|
|
|
pass
|
|
|
|