|
|
@@ -0,0 +1,152 @@
|
|
|
+# 真实游戏登录实现说明
|
|
|
+
|
|
|
+## 当前状态
|
|
|
+
|
|
|
+高层游戏登录已经实现,不再是占位接口。
|
|
|
+
|
|
|
+公开入口 `LauncherFacade.run_until_enter_game()` 会依次完成:
|
|
|
+
|
|
|
+1. 获取当天开放的区服;
|
|
|
+2. 调用 996 盒子的 `EnterGameClient`;
|
|
|
+3. 下载并解密详细服务器列表;
|
|
|
+4. 解析 TCP 登录地址;
|
|
|
+5. 建立角色服务器连接;
|
|
|
+6. 完成账号认证和角色选择,必要时按调用方明确提供的角色名创建角色;
|
|
|
+7. 重连世界服务器;
|
|
|
+8. 等待服务器返回 `SC2`,确认已经进入游戏世界;
|
|
|
+9. 启动心跳并创建后续游戏任务所需的上下文。
|
|
|
+
|
|
|
+登录成功条件是:服务器先返回 `SC658`,客户端发送 `CS1018`,随后服务器返回 `SC2`。仅建立 TCP 连接或获取角色列表不代表登录成功。
|
|
|
+
|
|
|
+## Python 实现位置
|
|
|
+
|
|
|
+### 高层调用入口
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/client.py`](../src/cq2_sdk/client.py)
|
|
|
+
|
|
|
+- `LauncherFacade`:组织区服选择、启动、TCP 连接和登录流程。
|
|
|
+- `LauncherFacade.run_until_enter_game()`:一键执行到进入游戏世界。
|
|
|
+- `Cq2Client`:管理盒子登录会话、TCP 连接、游戏会话和任务上下文。
|
|
|
+- `Cq2Client._bind_game_session()`:登录成功后创建移动、战斗、任务组件。
|
|
|
+
|
|
|
+### 游戏登录状态机
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/login/game.py`](../src/cq2_sdk/login/game.py)
|
|
|
+
|
|
|
+- `GameLogin.login()`:执行完整角色服和世界服登录协议。
|
|
|
+- `GameSession`:保存角色 ID、角色名、会话 ID、用户 ID 和世界就绪状态。
|
|
|
+- `GameLoginError`:保存服务器消息号、错误码和错误说明。
|
|
|
+- 所有发送、接收和重连操作共用一个登录截止时间,避免网络操作无限等待。
|
|
|
+- 每次全新认证前都会把 TCP `sdata` 重置为零;角色服和世界服之间重连时保留服务器下发的新会话值。
|
|
|
+
|
|
|
+### 区服列表解密和地址解析
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/login/server_list.py`](../src/cq2_sdk/login/server_list.py)
|
|
|
+
|
|
|
+- `decode_detailed_server_list()`:完成 Base64、标记校验、XXTEA 解密和 JSON 解析。
|
|
|
+- `resolve_server()`:展开紧凑区服字段并生成不可变的 `ServerInfo`。
|
|
|
+- `fetch_server_info()`:通过启用 TLS 证书校验的 HTTPS 请求获取详细区服列表。
|
|
|
+- 当区服没有明确 IP 时,按照 `s{main_server_id}-{srvRoot}` 生成服务器地址。
|
|
|
+
|
|
|
+### 登录数据模型
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/login/models.py`](../src/cq2_sdk/login/models.py)
|
|
|
+
|
|
|
+- `BoxSession`:保存盒子登录阶段返回的会话数据。
|
|
|
+- `ServerInfo`:保存选中区服、主服、地址、端口、代理端口及客户端元数据。
|
|
|
+- `ServerInfo` 为不可变数据类,配置版本映射也不能被外部修改。
|
|
|
+
|
|
|
+### TCP 连接和心跳
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/transport/tcp.py`](../src/cq2_sdk/transport/tcp.py)
|
|
|
+
|
|
|
+- `TcpClient.connect()`:建立异步 TCP 连接。
|
|
|
+- `TcpClient.send()` 和 `TcpClient.recv()`:发送和接收完整协议帧。
|
|
|
+- `TcpClient.reconnect()`:切换角色服或世界服,并重置消息序列号。
|
|
|
+- `TcpClient.start_heartbeat()`:登录成功后发送 `CS60000` 心跳。
|
|
|
+
|
|
|
+### TCP 帧编解码
|
|
|
+
|
|
|
+文件:[`src/cq2_sdk/transport/framing.py`](../src/cq2_sdk/transport/framing.py)
|
|
|
+
|
|
|
+协议头解码后是 9 个小端 32 位整数:
|
|
|
+
|
|
|
+```text
|
|
|
+param3, msg_id, recog, param1, param2, sdata, codec, body_len, sequence
|
|
|
+```
|
|
|
+
|
|
|
+36 字节明文头经过 Encode6 后形成 48 字节网络头;当前登录流程支持 `codec=0` 的明文消息体。
|
|
|
+
|
|
|
+## 登录协议流程
|
|
|
+
|
|
|
+```text
|
|
|
+CS2001 -> SC529 -> CS104 -> SC530 -> CS100 -> SC520
|
|
|
+ |
|
|
|
+ +-- 已有角色 -> CS103
|
|
|
+ |
|
|
|
+ +-- 没有角色且提供 char_name
|
|
|
+ -> CS101 -> SC521 -> CS103
|
|
|
+
|
|
|
+CS103 -> SC525 -> 重连世界服 -> CS20 -> SC658 -> CS1018 -> SC2
|
|
|
+```
|
|
|
+
|
|
|
+主要约束:
|
|
|
+
|
|
|
+- `SC529.recog == 1` 使用新登录分支,否则使用旧登录分支。
|
|
|
+- 角色 ID 始终按字符串保存,避免大整数被截断。
|
|
|
+- 已有角色优先选择 `last == 1` 且未被封禁的角色。
|
|
|
+- 角色列表为空时必须显式传入 `char_name`,SDK 不会自行生成角色名。
|
|
|
+- `SC503`、`SC522`、`SC526` 的错误码来自 `recog`;`SC527` 的错误码来自 `param1`。
|
|
|
+- `SC658` 是发送 `CS1018` 的前置门槛,`SC2` 是进入世界成功的最终依据。
|
|
|
+
|
|
|
+## 调用示例
|
|
|
+
|
|
|
+```python
|
|
|
+import asyncio
|
|
|
+
|
|
|
+from cq2_sdk import Cq2Client
|
|
|
+
|
|
|
+
|
|
|
+async def main() -> None:
|
|
|
+ async with Cq2Client(
|
|
|
+ account="your-account",
|
|
|
+ password="your-password",
|
|
|
+ ) as client:
|
|
|
+ assert client.launcher is not None
|
|
|
+ session = await client.launcher.run_until_enter_game(
|
|
|
+ server_name="玖扬复古176",
|
|
|
+ zone_index=0,
|
|
|
+ # 仅在新区没有现有角色时提供:
|
|
|
+ # char_name="角色名",
|
|
|
+ # job=0,
|
|
|
+ # sex=0,
|
|
|
+ timeout=10.0,
|
|
|
+ )
|
|
|
+ print(session.char_name, session.world_ready)
|
|
|
+
|
|
|
+
|
|
|
+asyncio.run(main())
|
|
|
+```
|
|
|
+
|
|
|
+不要把真实账号、密码或 Token 写入源代码。建议通过进程环境或其他本地安全配置传入,并确保相关文件被 Git 忽略。
|
|
|
+
|
|
|
+## 测试位置
|
|
|
+
|
|
|
+- 完整登录状态机和世界入口门槛:[`tests/unit/test_game_login.py`](../tests/unit/test_game_login.py)
|
|
|
+- 高层客户端编排和资源所有权:[`tests/unit/test_client.py`](../tests/unit/test_client.py)
|
|
|
+- 服务器列表解密与解析:[`tests/unit/test_server_list.py`](../tests/unit/test_server_list.py)
|
|
|
+- TCP 连接、重连和心跳:[`tests/unit/test_tcp_client.py`](../tests/unit/test_tcp_client.py)
|
|
|
+- 帧格式和真实心跳头:[`tests/unit/test_framing.py`](../tests/unit/test_framing.py)
|
|
|
+
|
|
|
+当前完整测试套件为 `288 passed`。此外,公开服务器列表已经在启用 TLS 证书校验的情况下完成在线解析烟测。
|
|
|
+
|
|
|
+## 已知边界
|
|
|
+
|
|
|
+- 真实账号端到端登录可能触发验证码或产生游戏侧会话,因此默认自动测试不执行该步骤。
|
|
|
+- 在线烟测不会隐式创建角色;只有调用者明确提供 `char_name` 时才允许走创建角色分支。
|
|
|
+- 登录后的 13 个新手任务仍属于后续协议开发范围,本文件只说明进入游戏世界之前的登录链路。
|
|
|
+
|
|
|
+## 设计资料
|
|
|
+
|
|
|
+- [真实游戏登录设计](../../docs/superpowers/specs/2026-09-18-real-game-login-design.md)
|
|
|
+- [真实游戏登录实施计划](../../docs/superpowers/plans/2026-09-18-real-game-login.md)
|