Explorar el Código

docs: design real cq2 game login

wuzilong hace 1 mes
padre
commit
6abc941c21

+ 285 - 0
docs/superpowers/plans/2026-09-18-real-game-login.md

@@ -0,0 +1,285 @@
+# Real CQ2 Game Login Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Implement and verify the real CQ2 TCP login through authoritative SC2 world initialization.
+
+**Architecture:** Correct the transport boundary first, then add a focused detailed-server-list decoder, then implement the login state machine over the tested transport, and finally wire it into `LauncherFacade` and `Cq2Client`. Network-facing behavior is dependency-injected or exercised with loopback servers so protocol tests remain deterministic.
+
+**Tech Stack:** Python 3.12, asyncio, Pydantic 2, httpx, pytest, pytest-asyncio
+
+**Spec:** `docs/superpowers/specs/2026-09-18-real-game-login-design.md`
+
+## Global Constraints
+
+- Header layout is exactly `param3,msg_id,recog,param1,param2,sdata,codec,body_len,sequence`.
+- Wire headers are 48-byte Encode6 values decoded to 36-byte `<9i` values; bodies remain plaintext for `codec=0`.
+- Production sequence starts at zero and resets on every reconnect.
+- Login succeeds only after SC658 gates CS1018 and SC2 is received.
+- Empty role lists require an explicitly supplied role name; never create a role implicitly during a smoke test.
+- Existing credentials, tokens, and capture logs must not be added to Git or emitted by tests.
+
+---
+
+### Task 1: Correct Header Semantics and Connection State
+
+**Files:**
+- Modify: `cq2_sdk/tests/unit/test_framing.py`
+- Modify: `cq2_sdk/tests/unit/test_tcp_client.py`
+- Modify: `cq2_sdk/tests/unit/test_message_registry.py`
+- Modify: `cq2_sdk/src/cq2_sdk/transport/framing.py`
+- Modify: `cq2_sdk/src/cq2_sdk/transport/tcp.py`
+
+**Interfaces:**
+- Produces: `FrameHeader.param3`, `FrameHeader.sdata`
+- Produces: `encode_frame(..., *, sdata=0, codec=0, sequence=0)`
+- Produces: `TcpClient.set_sdata(value: int) -> None`
+- Produces: `await TcpClient.reconnect(host: str, port: int) -> None`
+
+- [ ] **Step 1: Write failing framing tests**
+
+Assert that the literal captured heartbeat decodes to `param3=0`,
+`msg_id=60000`, `recog=1789709744`, `sdata=149`, and `sequence=112`. Add a
+round-trip test with distinct `param3=7` and `sdata=11` so swapping the words
+fails visibly.
+
+- [ ] **Step 2: Run the framing tests and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_framing.py -q`
+
+Expected: failures for the missing `sdata` field and wrong word-zero/word-five
+mapping.
+
+- [ ] **Step 3: Implement the corrected header mapping**
+
+Rename `pre` to `param3`, add `sdata`, pack business `param3` into word zero,
+and pack `sdata` into word five. Keep the positional send signature unchanged.
+
+- [ ] **Step 4: Run framing tests and verify GREEN**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_framing.py -q`
+
+- [ ] **Step 5: Write failing TCP state tests**
+
+Use loopback servers to assert that `set_sdata(149)` reaches word five, two
+sends use sequences 0 and 1, reconnect sends sequence 0 again, and heartbeat
+`recog` is an injected current Unix timestamp rather than zero.
+
+- [ ] **Step 6: Run TCP tests and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_tcp_client.py -q`
+
+- [ ] **Step 7: Implement connection state behavior**
+
+Add `sdata` ownership, reconnect cleanup/reset, and timestamped heartbeat while
+preserving receive-order error behavior.
+
+- [ ] **Step 8: Run transport and registry tests and verify GREEN**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_framing.py tests/unit/test_tcp_client.py tests/unit/test_message_registry.py tests/unit/test_recorder.py -q`
+
+- [ ] **Step 9: Commit Task 1**
+
+```bash
+git add cq2_sdk/src/cq2_sdk/transport cq2_sdk/tests/unit/test_framing.py cq2_sdk/tests/unit/test_tcp_client.py cq2_sdk/tests/unit/test_message_registry.py cq2_sdk/tests/unit/test_recorder.py
+git commit -m "fix: use verified cq2 header semantics"
+```
+
+### Task 2: Decode and Resolve the Detailed Server List
+
+**Files:**
+- Create: `cq2_sdk/src/cq2_sdk/login/server_list.py`
+- Create: `cq2_sdk/tests/unit/test_server_list.py`
+- Modify: `cq2_sdk/src/cq2_sdk/login/models.py`
+- Modify: `cq2_sdk/src/cq2_sdk/login/__init__.py`
+
+**Interfaces:**
+- Produces: `decode_detailed_server_list(payload: bytes) -> dict`
+- Produces: `resolve_server(data: dict, server_id: int, *, channel_id: str, game_id: int) -> ServerInfo`
+- Produces: `await fetch_server_info(server_id: int, *, channel_id: str, game_id: int, client: httpx.AsyncClient | None = None) -> ServerInfo`
+- Produces: expanded immutable `ServerInfo` fields required by login.
+
+- [ ] **Step 1: Write failing decoder tests**
+
+Generate one fixed encrypted fixture from a literal JSON document and assert
+decoded `srvRoot`, compact field expansion, explicit-IP precedence, main-server
+host construction, proxy port parsing, and typed failures for malformed input
+and unknown IDs.
+
+- [ ] **Step 2: Run decoder tests and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_server_list.py -q`
+
+- [ ] **Step 3: Implement the self-contained cocos XXTEA decoder and resolver**
+
+Decode Base64, validate `b"4meJnPyl"`, decrypt little-endian XXTEA with
+`b"34UFXMgtkz".ljust(16, b"\\0")`, validate the embedded plaintext length,
+parse JSON, and map compact fields `d,p,s,u,m,t,n,g,i,x,b`.
+
+- [ ] **Step 4: Run server-list tests and verify GREEN**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_server_list.py -q`
+
+- [ ] **Step 5: Commit Task 2**
+
+```bash
+git add cq2_sdk/src/cq2_sdk/login cq2_sdk/tests/unit/test_server_list.py
+git commit -m "feat: resolve cq2 tcp servers"
+```
+
+### Task 3: Implement the Login State Machine
+
+**Files:**
+- Replace: `cq2_sdk/tests/unit/test_game_login.py`
+- Modify: `cq2_sdk/src/cq2_sdk/login/game.py`
+- Modify: `cq2_sdk/src/cq2_sdk/messages/constants.py`
+- Modify: `cq2_sdk/src/cq2_sdk/messages/registry.py`
+
+**Interfaces:**
+- Produces: `GameLoginError(message_id: int, error_code: int, detail: str)`
+- Produces: `await GameLogin.login(session, server, *, char_name=None, job=0, sex=0, timeout=5.0) -> GameSession`
+- Produces: `GameSession(char_id: str, char_name: str, session_id: int, user_id: str, world_ready: bool)`
+
+- [ ] **Step 1: Write failing new-login/existing-role integration test**
+
+Run a real loopback `TcpClient` against a scripted server. Decode and record
+every outbound frame, send SC529/530/520/525/658/2 responses across the role
+and world connections, then assert exact literal bodies and header fields for
+CS2001, CS104, CS100, CS103, CS20, and CS1018.
+
+- [ ] **Step 2: Run the existing-role test and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_game_login.py::test_new_login_existing_role_enters_world -q`
+
+- [ ] **Step 3: Implement minimal existing-role state machine**
+
+Add Encode6 token/channel helpers, expected-message filtering with a shared
+deadline, new-login `sdata`, role selection, world reconnect, SC658 gating,
+CS1018 construction, and SC2 completion.
+
+- [ ] **Step 4: Run the existing-role test and verify GREEN**
+
+Run the command from Step 2.
+
+- [ ] **Step 5: Write failing create-role and old-login tests**
+
+Assert empty SC520 without `char_name` fails before CS101; with `char_name`,
+CS101 JSON is exact and SC521 feeds CS103. Separately assert `SC529.recog=0`
+uses the old CS104 body and SC530 body reconnects before CS100.
+
+- [ ] **Step 6: Run the new tests and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_game_login.py -q`
+
+- [ ] **Step 7: Implement role creation and old-login branches**
+
+Keep role ID as a string to avoid truncating the server's large identifiers.
+Prefer `last==1`, reject banned roles, and use compact UTF-8 JSON.
+
+- [ ] **Step 8: Write and verify failing error-path tests**
+
+Cover SC503, SC527, SC522, SC526, malformed slash bodies, malformed role JSON,
+and timeout while unrelated messages arrive.
+
+- [ ] **Step 9: Implement typed protocol errors and verify GREEN**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_game_login.py -q`
+
+- [ ] **Step 10: Commit Task 3**
+
+```bash
+git add cq2_sdk/src/cq2_sdk/login/game.py cq2_sdk/src/cq2_sdk/messages cq2_sdk/tests/unit/test_game_login.py
+git commit -m "feat: implement cq2 game login state machine"
+```
+
+### Task 4: Wire the High-Level Client
+
+**Files:**
+- Modify: `cq2_sdk/tests/unit/test_client.py`
+- Modify: `cq2_sdk/src/cq2_sdk/client.py`
+- Modify: `cq2_sdk/src/cq2_sdk/tasks/context.py`
+- Modify: `cq2_sdk/README.md`
+- Modify: `cq2_sdk/docs/runbook.md`
+- Modify: `cq2_sdk/CHANGELOG.md`
+
+**Interfaces:**
+- Produces: `LauncherFacade(box, session, owner=None)` with a working
+  `run_until_enter_game(..., char_name=None, job=0, sex=0) -> GameSession`.
+- `Cq2Client` owns the connected `TcpClient`, resulting `GameSession`, and
+  initialized `TaskRegistry`.
+
+- [ ] **Step 1: Write failing facade orchestration test**
+
+Use complete HTTP fixtures and injected server resolver/TCP/login factories.
+Assert today-server selection, `EnterGameClient`, detailed resolution, connect,
+login arguments, retained session, and heartbeat start.
+
+- [ ] **Step 2: Run client tests and verify RED**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_client.py -q`
+
+- [ ] **Step 3: Implement high-level ownership and orchestration**
+
+Remove obsolete imports and placeholder exceptions. Build `TaskContext` with
+the existing movement/combat/mission components after login without exposing
+credentials in logging.
+
+- [ ] **Step 4: Run client tests and verify GREEN**
+
+Run: `cd cq2_sdk && uv run pytest tests/unit/test_client.py -q`
+
+- [ ] **Step 5: Update user documentation**
+
+Document required `char_name` behavior, the SC2 success boundary, and that
+live smoke tests do not create roles unless explicitly requested.
+
+- [ ] **Step 6: Commit Task 4**
+
+```bash
+git add cq2_sdk/src/cq2_sdk/client.py cq2_sdk/src/cq2_sdk/tasks/context.py cq2_sdk/tests/unit/test_client.py cq2_sdk/README.md cq2_sdk/docs/runbook.md cq2_sdk/CHANGELOG.md
+git commit -m "feat: enable high-level cq2 game login"
+```
+
+### Task 5: Full Verification and Branch Review
+
+**Files:**
+- Modify only files required by failures or review findings.
+
+**Interfaces:**
+- Consumes all prior tasks.
+- Produces a verified, review-ready feature branch.
+
+- [ ] **Step 1: Run the full suite**
+
+Run: `cd cq2_sdk && uv run pytest -q`
+
+Expected: zero failures and zero errors.
+
+- [ ] **Step 2: Run compile and repository hygiene checks**
+
+```bash
+cd cq2_sdk && uv run python -m compileall -q src tests
+cd .. && git status --short && git diff --check
+```
+
+- [ ] **Step 3: Review the cumulative diff against the design**
+
+Check every header field, transition, timeout, failure response, ownership
+boundary, and sensitive-file exclusion named in the spec.
+
+- [ ] **Step 4: Request code review and resolve all Critical/Important findings**
+
+Use the requesting-code-review skill against the pre-plan base SHA and current
+HEAD. Re-run the focused tests for each correction.
+
+- [ ] **Step 5: Re-run complete verification after review changes**
+
+Run the commands from Steps 1 and 2 again and inspect their full output.
+
+- [ ] **Step 6: Commit remaining fixes and push the feature branch**
+
+```bash
+git add -A
+git commit -m "test: verify real cq2 game login"
+git push -u origin feat/real-tcp-wire-codec
+```

+ 120 - 0
docs/superpowers/specs/2026-09-18-real-game-login-design.md

@@ -0,0 +1,120 @@
+# CQ2 Real Game Login Design
+
+## Goal
+
+Replace the deliberately unimplemented high-level game login with the
+protocol used by the captured 996 client, while keeping every wire-level
+assumption covered by deterministic tests.
+
+## Evidence
+
+The design is based on three matching sources:
+
+- the decrypted `LoginProxy.lua` login flow;
+- ARM64 disassembly of the macOS game client network message setters and
+  send/receive functions;
+- captured heartbeat headers and a successful 2026-09-18 login log.
+
+The decoded 36-byte header contains nine signed little-endian words in this
+order:
+
+```text
+param3, msg_id, recog, param1, param2, sdata, codec, body_len, sequence
+```
+
+The header is encoded into 48 printable bytes with the existing 6-bit codec.
+The body follows without that header encoding. `codec=0` is the supported
+login-path body representation.
+
+## Transport behavior
+
+`encode_frame(msg_id, recog, param1, param2, param3, body, *, sdata=0,
+codec=0, sequence=0)` keeps the public business-parameter order while placing
+`param3` in decoded word zero and `sdata` in decoded word five.
+
+`TcpClient` owns the connection's current `sdata` value and sequence counter.
+The first transmitted sequence is zero in production. Disconnect/reconnect
+resets the sequence to zero, clears receive state, and retains `sdata` because
+the original client retains it while moving from the role server to the world
+server. Heartbeats put the current Unix timestamp in `recog`.
+
+## Server resolution
+
+The public box list identifies the selected server, but the TCP host and login
+port come from the channel-specific detailed server list. For channel 110585,
+the detailed response is Base64 text containing the `4meJnPyl` marker followed
+by cocos XXTEA data encrypted with the padded `34UFXMgtkz` key.
+
+The resolver decodes this document, expands compact fields, and constructs the
+host as `s{main_server_id}-{srvRoot}` unless an explicit IP is present. It
+returns a `ServerInfo` carrying the selected ID, main ID, host, login port,
+optional proxy port, channel ID, game ID, and client metadata needed later.
+Malformed Base64, a wrong marker, invalid XXTEA length, missing server data,
+and unknown server IDs are explicit errors.
+
+## Login state machine
+
+The state machine follows the original client:
+
+```text
+CS2001 -> SC529 -> CS104 -> SC530 -> CS100 -> SC520
+                                     |             |
+                                     |             +-- roles exist -> CS103
+                                     |             +-- no roles -> CS101 -> SC521 -> CS103
+                                     |
+                                     +-- set sdata
+
+CS103 -> SC525 -> reconnect world server -> CS20 -> SC658 -> CS1018 -> SC2
+```
+
+Payloads and headers:
+
+- CS2001: `uid/Encode6(token)/Encode6(channel)/1/0`; `recog=900000`, random
+  `param1`, main server ID in `param2`, login port in `param3`, `sdata=0`.
+- SC529: body `loginKey/userID`; `recog==1` is the new-login route.
+- CS104: encoded login key for new login, otherwise the main/selected ID.
+- SC530: new login takes `sdata` from `recog`; old login parses
+  `host/port/sdata` and reconnects before continuing.
+- CS100: `uid/sdata/server/channel/game[/loginKey]`, with the same watermark
+  header shape as CS2001.
+- SC520: JSON role list. Existing roles prefer `last==1`, otherwise the first
+  non-banned role.
+- CS101: compact UTF-8 JSON with account, requested name, job, sex, server,
+  channel, and `secsdk="0"`; SC521 returns `name/id`.
+- CS103: `uid/roleName`.
+- SC525: `host/port`; reconnect using the selected host and returned port
+  unless a proxy port is configured.
+- CS20: `*#uid/name/id/sdata/platform/0/channel/whitelist/cert/adult/box/batch`.
+- SC658 is the protocol-ready gate for a headless client.
+- CS1018: compact JSON containing cached config versions plus game ID, channel
+  ID, package name, and `tripartite`; its header carries platform, reconnect /
+  emulator flags, merged width/height, and device code.
+- SC2 is the authoritative enter-world success response. SC50 may arrive later
+  and is not required to return the login result.
+
+Every receive waits through unrelated messages until the expected response or
+a known login error arrives. Each step has a bounded timeout. SC503, SC527,
+SC522, and SC526 become typed `GameLoginError` failures preserving the message
+ID and header error code.
+
+## Public API and ownership
+
+`GameLogin.login()` accepts optional role creation fields and returns
+`GameSession(char_id, char_name, session_id, user_id, world_ready=True)`.
+It does not silently invent a role name: when the role list is empty,
+`char_name` is required.
+
+`LauncherFacade.run_until_enter_game()` selects the requested entry from
+today's servers, calls `EnterGameClient`, resolves the detailed TCP endpoint,
+connects, and invokes `GameLogin`. `Cq2Client` supplies its authenticated
+`BoxSession`, retains the resulting TCP/game session, starts heartbeats only
+after SC2, and builds the task context used by later gameplay work.
+
+## Verification
+
+Unit tests use real frame encoding and loopback scripted servers. They assert
+literal captured header bytes, sequence and `sdata` behavior, detailed-list
+decoding, every outbound login payload, both role branches, reconnection, the
+SC658/CS1018/SC2 gate, and error responses. The complete test suite must pass
+before commit. A live smoke login is optional evidence and must never create a
+role unless a role name was explicitly supplied.