# YouleNexus Platform Vertical Slice 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:** 建立可复用、可测试、与具体子游戏解耦的平台运行时,并跑通配置、登录、大厅、进房、准备和断线重连的第一条纵向链路。 **Architecture:** 平台核心通过单一 `PlatformRuntime` 装配配置、网络、协议、状态和场景端口;所有业务消息只经过一个 Router。子游戏通过版本化 `sdk/contracts` 接入,平台通过 `GameSessionHost` 调用游戏,游戏只能通过受限 `GameHost` 调用平台,不得导入任何平台内部实现。 **Tech Stack:** TypeScript 6、Node.js test runner、tsx、Cocos Creator 3.8+、原生 WebSocket、funplay-cocos MCP。 **Spec:** `docs/superpowers/specs/2026-09-04-platform-vertical-slice-design.md` ## Global Constraints - 服务器零改动:信封固定为 `{app:"youle",route,rpc,data}`,route/rpc/字段名/类型/时序与 `docs/protocol/` 一致。 - 配置服务和原生 App 零改动:`gameserver → data.urlserver`、`window.settings` 名称和数据格式逐字兼容。 - 配置、RPC route、玩家实体和房间座位各自只有一个权威来源;下游不补默认值。 - 平台不解析 `roomtype` 和 `deskinfo`;`deskinfo` 严格按原工程 `if (deskinfo)` 的真值语义触发 `GameModule.restore`,不进入 PlatformState。 - `sdk/contracts` 不得依赖 platform/net/protocol/ui/core 内部模块、Cocos `cc` 或具体子游戏。 - 具体子游戏不得导入 framework 的 platform/net/protocol/ui/core 内部模块。 - 所有 `.scene`、`.prefab`、`.anim`、`.meta` 修改必须通过 funplay-cocos MCP,禁止文本编辑。 - 每个任务遵循测试先行;提交时只暂存本任务文件,不夹带工作区已有 UI 迁移改动。 - 测试命令均从 `cocoscreator_projects/` 目录执行。 --- ## File Structure 本计划完成后的核心结构: ```text cocoscreator_projects/YouleNexus/assets/framework/ ├── config/ │ ├── runtime-config.ts │ ├── bootstrap.ts │ ├── remote-config.ts │ └── sources/native-settings.ts ├── net/ │ ├── net-client.ts │ └── ... ├── protocol/ │ ├── contracts/ │ │ ├── validation.ts │ │ ├── login-contract.ts │ │ ├── join-room-contract.ts │ │ └── room-event-contracts.ts │ ├── rpc-route.ts │ ├── platform-handlers.ts │ └── router.ts ├── platform/ │ ├── runtime.ts │ ├── ready-gate.ts │ ├── session.ts │ ├── commands.ts │ ├── game-host-adapter.ts │ ├── scene-port.ts │ └── stores/ │ ├── platform-store.ts │ ├── platform-types.ts │ ├── selectors.ts │ └── types.ts # legacy types, removed after migration ├── sdk/ │ ├── contracts/ │ │ ├── api-version.ts │ │ ├── game-descriptor.ts │ │ ├── game-host.ts │ │ ├── game-module.ts │ │ ├── game-seat-mapper.ts │ │ ├── platform-events.ts │ │ ├── snapshots.ts │ │ └── index.ts │ ├── runtime/ │ │ ├── game-registry.ts │ │ └── game-session-host.ts │ └── testing/game-contract-harness.ts └── ui/controllers/ ├── connection-overlay-controller.ts ├── join-room-controller.ts └── room-shell-controller.ts ``` 具体子游戏后续统一放在 `YouleNexus/assets/games//`,本计划只创建测试游戏,不创建真实玩法。 --- ### Task 1: 建立纯净、版本化的子游戏 SDK 契约 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/api-version.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-descriptor.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts` - Create: `cocoscreator_projects/framework-tests/sdk/contracts.test.ts` - Create: `cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs` **Interfaces:** - Produces: `PLATFORM_GAME_API_VERSION = 1`, `GameDescriptor`, `GameModule`, `GameHost`, `GameSeatMapper`, `PlatformToGameEvent`, `GameServerMessage`, `PlatformGameSnapshot`, `GameHostCommand`。 - Consumes: only TypeScript standard types. - [ ] **Step 1: Write the failing public-contract test** ```ts import { test } from 'node:test'; import assert from 'node:assert/strict'; import { PLATFORM_GAME_API_VERSION } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; test('platform game API version is exact', () => { assert.equal(PLATFORM_GAME_API_VERSION, 1); }); ``` - [ ] **Step 2: Write the failing import-boundary test** The test recursively reads `assets/framework/sdk/contracts/**/*.ts` and rejects imports containing `/platform/`, `/net/`, `/protocol/`, `/ui/`, `/core/`, `from 'cc'`, or `from "cc"`. ```js assert.equal( forbidden, undefined, `${file} imports forbidden dependency: ${forbidden}`, ); ``` - [ ] **Step 3: Run the tests and verify failure** Run: ```powershell node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs ``` Expected: FAIL because `sdk/contracts/index.ts` does not exist. - [ ] **Step 4: Add the exact contract types** ```ts export const PLATFORM_GAME_API_VERSION = 1 as const; export type PlatformGameApiVersion = typeof PLATFORM_GAME_API_VERSION; export interface GameDescriptor { readonly apiVersion: PlatformGameApiVersion; readonly gameId: string | number; readonly route: string; readonly key: string; resolveSeatCount(roomtype: readonly unknown[]): number; create(): GameModule; } export interface GameModule { attach(host: GameHost): void; handlePlatformEvent(event: PlatformToGameEvent): void; handleGameMessage(message: GameServerMessage): void; restore(deskinfo: unknown): void; dispose(): void; } ``` Define `GameHost` exactly as the approved spec: bound `seat: GameSeatMapper`, `getSnapshot`, immediate `subscribe`, route-bound `sendGameMessage`, and `execute` with only `room.prepare|room.exit`. `GameSeatMapper` exposes `toView(serverSeat)` and `toServer(viewSeat)`. Define snapshot DTOs locally in `sdk/contracts`; do not import Store types. - [ ] **Step 5: Add the new contracts without breaking legacy callers** ```ts export * from './contracts/index.ts'; ``` Add this export to the existing `sdk/index.ts` while temporarily retaining its legacy interfaces. Only `sdk/contracts` is the new public boundary; Task 7 migrates callers and removes the legacy definitions in the same green commit. - [ ] **Step 6: Run targeted tests and typecheck** ```powershell node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: SDK tests PASS and typecheck reports zero errors. - [ ] **Step 7: Commit** ```powershell git add -- YouleNexus/assets/framework/sdk framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs git commit -m "feat(sdk): define decoupled game contracts" ``` --- ### Task 2: 实现游戏注册表、会话生命周期与接入测试工具 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-registry.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts` - Create: `cocoscreator_projects/framework-tests/sdk/game-registry.test.ts` - Create: `cocoscreator_projects/framework-tests/sdk/game-session-host.test.ts` - Create: `cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts` **Interfaces:** - Consumes: Task 1 `GameDescriptor`, `GameModule`, `GameHost`, event/message contracts. - Produces: `GameRegistry.register/getByRoute/getByGameId`, `GameSessionHost.attach/enter/handleGameMessage/restore/dispose`, `assertGameContract`。 - [ ] **Step 1: Write registry failure tests** ```ts test('registry rejects duplicate route and API mismatch', () => { const registry = new GameRegistry(); registry.register(descriptor({ key: 'a', gameId: 1, route: 'g1' })); assert.throws(() => registry.register(descriptor({ key: 'b', gameId: 2, route: 'g1' })), /route.*g1/); assert.throws(() => registry.register({ ...descriptor({ key: 'c', gameId: 3, route: 'g3' }), apiVersion: 2 as never }), /apiVersion/); }); ``` Also reject a missing `resolveSeatCount` contract. The conformance harness calls it with fixture `roomtype` values and rejects zero, negative, fractional, or non-number results. - [ ] **Step 2: Write lifecycle and generation tests** Cover `detached → attached → entered → disposed`, illegal double attach, event before enter, dispose exactly once, and calls through an old Host after a new generation begins. ```ts session.attach(host); session.enter({ type: 'room-entered', roomtype: rawRoomtype }); session.dispose(); assert.throws(() => oldHost.sendGameMessage('play', {}), /disposed/); ``` - [ ] **Step 3: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts ``` Expected: FAIL because runtime classes do not exist. - [ ] **Step 4: Implement strict registry** Use three maps keyed by `key`, normalized `String(gameId)`, and `route`. Reject empty values, duplicates, API mismatch, and factories that return the same object twice during conformance testing. ```ts register(value: GameDescriptor): void { assertDescriptor(value); if (this.byRoute.has(value.route)) throw new Error(`duplicate game route: ${value.route}`); this.byRoute.set(value.route, value); this.byGameId.set(String(value.gameId), value); this.byKey.set(value.key, value); } ``` - [ ] **Step 5: Implement serialized session dispatch** `GameSessionHost` owns one module and one Host. Every public method checks state; `fail(error)` records the first failure, disposes once, and rejects later dispatch. Store commit ordering remains the platform adapter's responsibility. - [ ] **Step 6: Implement the reusable conformance harness** `assertGameContract(descriptor, makeHost)` runs descriptor validation, seat-count resolution, fresh factory, lifecycle, route-bound send, snapshot immutability, subscription cleanup, deskinfo identity, 2/4/10-seat mapper round trips, and two-session isolation. - [ ] **Step 7: Run tests and typecheck** ```powershell node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts framework-tests/sdk/game-contract-harness.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 8: Commit** ```powershell git add -- YouleNexus/assets/framework/sdk/runtime YouleNexus/assets/framework/sdk/testing framework-tests/sdk git commit -m "feat(sdk): add game registry and isolated sessions" ``` --- ### Task 3: 修正运行配置、原生身份与 urlserver 单一来源 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/sources/native-settings.ts` - Modify: `cocoscreator_projects/framework-tests/config/bootstrap.test.ts` - Modify: `cocoscreator_projects/framework-tests/config/remote-config.test.ts` - Modify: `cocoscreator_projects/framework-tests/config/profiles.test.ts` - Modify: `cocoscreator_projects/framework-tests/config/sources/native-settings.test.ts` - Create: `cocoscreator_projects/framework-tests/config/runtime-config.test.ts` **Interfaces:** - Produces: `RuntimeConfig`, `resolveRuntimeConfig(options)`, `decodeNativeGameConfig(value)`, `parseUrlServers(config)`. - Consumes: existing runtime mode and identity source abstractions. - [ ] **Step 1: Add failing urlserver tests** Before changing configuration or identity code, read `.agents/skills/native-bridge-contract/SKILL.md` completely and follow its referenced source map for `gameserver`, `gameconfig`, `urlserver`, and `window.settings`. Record the exact legacy source lines beside each fixture assertion; do not infer bridge names or transforms from current YouleNexus code. ```ts test('remote config uses data.urlserver as the only WS source', () => { assert.deepEqual(parseUrlServers({ data: { urlserver: ['10.0.0.1:3088', 'wss://b'] } }), [ 'ws://10.0.0.1:3088', 'wss://b', ]); assert.throws(() => parseUrlServers({ data: { player_server_tcp: 'wrong:1' } }), /data\.urlserver/); }); ``` - [ ] **Step 2: Add failing native branch tests** ```ts assert.equal(decodeNativeGameConfig('host-path#8080-config'), 'http://host/path:8080/config.txt'); assert.throws(() => nativeSettingsSource(nonUAgentHostWithoutSettings)(), /getothername/); assert.equal(nativeSettingsSource(uAgent3Host)().agentid, 'legacy-agent'); ``` The uAgent_3 case must be selected by an explicit environment discriminator, not by catch-all exception handling. - [ ] **Step 3: Run config tests and verify failure** ```powershell node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts ``` Expected: FAIL on `urlserver`, gameconfig decoding, and current fallback behavior. - [ ] **Step 4: Implement strict remote config parsing** ```ts export interface RemoteConfig { data: { urlserver: string | readonly string[]; [key: string]: unknown }; } export function parseUrlServers(input: unknown): string[] { const value = requireRecord(input, '$').data; const raw = requireRecord(value, '$.data').urlserver; const entries = typeof raw === 'string' ? [raw] : requireStringArray(raw, '$.data.urlserver'); if (entries.length === 0) throw new ConfigParseError('$.data.urlserver must not be empty'); return entries.map(toWebSocketUrl); } ``` Do not read `player_server_tcp`, `visitor_server_tcp`, or `game_server_tcp` for the first WebSocket path. - [ ] **Step 5: Correct profiles and bootstrap** Set the release default to the exact original source value from `Game_Surface_3/js/01_SubGame/00_SubGame_Config.js:11`. Keep explicit local/staging profiles, but remove `REPLACE_ME` data from production paths and prevent unknown profile names from silently selecting prod; unknown names throw in debug. - [ ] **Step 6: Build immutable RuntimeConfig** Validate channel identity and login identity before constructing NetClient. Return a frozen object and frozen server list. No controller may call `resolveBootstrap` directly after this task. - [ ] **Step 7: Run tests and typecheck** ```powershell node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 8: Commit** ```powershell git add -- YouleNexus/assets/framework/config framework-tests/config git commit -m "fix(config): align runtime configuration with legacy contract" ``` --- ### Task 4: 建立首批 RPC 的唯一 route 表、解析器与黄金样本 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/rpc-route.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/join-room-contract.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-event-contracts.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/routes.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts` - Replace: `cocoscreator_projects/YouleNexus/assets/framework/core/types/login.ts` - Create: `cocoscreator_projects/framework-tests/fixtures/protocol/player-login-success.json` - Create: `cocoscreator_projects/framework-tests/fixtures/protocol/player-login-room.json` - Create: `cocoscreator_projects/framework-tests/fixtures/protocol/self-join-room.json` - Create: `cocoscreator_projects/framework-tests/fixtures/protocol/room-events.json` - Create: `cocoscreator_projects/framework-tests/protocol/contracts.test.ts` - Modify: `cocoscreator_projects/framework-tests/protocol/routes.test.ts` - Modify: `cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts` - Modify: `cocoscreator_projects/framework-tests/protocol/login.test.ts` **Interfaces:** - Produces: exhaustive `RPC_ROUTE satisfies Record`, request/response parsers for login, join, prepare, join/exit/online/offline, switch-server, kick. - Consumes: `docs/protocol/01-05` and the contract inventory; no Store imports. - [ ] **Step 1: Add failing exhaustive route test** Before writing or changing a network contract, re-read `docs/protocol/01-网络通信与生命周期.md` through `05-游戏内协议与桥接.md` sections covering that RPC and verify every fixture field against the cited source. The contract inventory is an index, not a substitute for the protocol chapters. ```ts for (const rpc of Object.values(Rpc)) { assert.ok(RPC_ROUTE[rpc], `missing route for ${rpc}`); } assert.equal(RPC_ROUTE.self_join_room, Route.agent); assert.equal(RPC_ROUTE.player_prepare, Route.room); ``` - [ ] **Step 2: Add golden fixture tests** Copy the verified `player_login` response from `docs/protocol/04-数据结构.md`; give other fixtures only fields justified by protocol tables. Tests assert exact encoded envelope and parser output. Do not add values for undocumented fields. ```ts const deskinfo = roomFixture.data.deskinfo; const parsed = parseLoginResponse(roomFixture.data); assert.equal(parsed.reconnect.present, true); assert.equal(parsed.reconnect.value, deskinfo); ``` - [ ] **Step 3: Add failing negative tests** Cover numeric `version`, missing `playerid`, missing room `seat`, invalid players array, invalid `urlserver`, and the critical cases where `isbattle` is set but `deskinfo` is absent or falsey. ```ts const { deskinfo: _ignored, ...withoutDeskinfo } = room; assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1 }).reconnect.present, false); assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: null }).reconnect.present, false); assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: { round: 2 } }).reconnect.present, true); ``` Use `Boolean(data.deskinfo)` for this trigger because the original source is exactly `if (_msg.data.deskinfo)`. Keep the truthy value opaque and preserve its identity; do not validate or interpret the snapshot structure in platform code. - [ ] **Step 4: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/protocol/contracts.test.ts framework-tests/protocol/routes.test.ts framework-tests/protocol/net-typed-send.test.ts framework-tests/protocol/login.test.ts ``` Expected: FAIL because the production route table and strict parsers do not exist. - [ ] **Step 5: Implement reusable validation helpers** Provide `requireRecord`, `requireString`, `requireNumber`, `requireInteger`, `requireArray`, `optionalField`, and `hasOwn`. Every thrown message includes the JSON path. - [ ] **Step 6: Implement exact parsers and request builders** Request builders return fresh objects containing only protocol-defined fields. Response parsers preserve optional protocol values and keep `roomtype/deskinfo` opaque; the reconnect projection uses the original client's truthiness semantics. - [ ] **Step 7: Replace caller-supplied route maps** ```ts export function typedSend(rpc: RpcName, data: T): TypedEnvelope { return { route: RPC_ROUTE[rpc], rpc, data }; } ``` Delete the `map` parameter so controllers and games cannot provide a second route source. - [ ] **Step 8: Run protocol tests and typecheck** ```powershell node --import tsx --test framework-tests/protocol/*.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 9: Commit** ```powershell git add -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/core/types/login.ts framework-tests/protocol framework-tests/fixtures/protocol git commit -m "feat(protocol): add strict vertical-slice contracts" ``` --- ### Task 5: 用单一 PlatformStore 替换玩家与房间镜像状态 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts` - Create: `cocoscreator_projects/framework-tests/platform/platform-store.test.ts` - Create: `cocoscreator_projects/framework-tests/platform/selectors.test.ts` **Interfaces:** - Produces: `PlatformStore`, `PlatformState`, `PlayerDirectoryState`, `RoomState`, atomic actions and read-only selectors. - Consumes: Task 4 parsed DTOs; does not consume raw unknown server payloads. - [ ] **Step 1: Write failing atomic-state tests** ```ts const store = new PlatformStore(); let notifications = 0; store.state.subscribe(() => notifications++); store.applyLoginSuccess(parsedLogin); assert.equal(notifications, 1); assert.equal(store.state.value.players.selfPlayerId, parsedLogin.player.playerid); ``` Add a room fixture whose self player also appears in `players`; assert only one entity object exists and the room stores only player IDs. - [ ] **Step 2: Write failing push/update tests** Cover other join, exit, ready, offline, online, full reconnect replacement, and failure operations that leave the prior state reference unchanged. ```ts const before = store.state.value; assert.throws(() => store.playerReady({ seat: 99 }), /seat/); assert.equal(store.state.value, before); ``` - [ ] **Step 3: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts ``` Expected: FAIL because PlatformStore does not exist. - [ ] **Step 4: Define internal state without sentinel identities** ```ts export interface PlayerDirectoryState { readonly selfPlayerId: number | null; readonly entities: Readonly>; } export interface RoomState { readonly inRoom: boolean; readonly roomcode: string | null; readonly selfSeat: number | null; readonly seatPlayerIds: readonly (number | null)[]; readonly roomtype: readonly unknown[] | null; readonly stage: number; readonly needprepare: number; readonly infinite: number; } ``` There is no fake `playerid:0`, `seat:-1`, or empty server-derived player object. Keep the existing `stores/types.ts` unchanged until all legacy stores are removed in Task 12, so this task remains independently green. - [ ] **Step 5: Implement one-signal atomic actions** `PlatformStore` owns one `signal`. Every action validates preconditions, builds all changed subtrees, then assigns `this.s.value` exactly once. It never mutates `this.s.value` or nested arrays in place. On room entry/recovery, recursively freeze the received JSON `roomtype` tree without interpreting positions, then keep that one canonical reference; tests assert nested mutation throws and no normalized copy is introduced. - [ ] **Step 6: Implement read-only selectors** Provide `selectSelfPlayer`, `selectSeat`, `selectRoomPlayers`, and `selectGameSnapshot`. Snapshot creation returns a frozen DTO and maps protocol `onstate/isprepare` to public booleans without changing the stored protocol values. - [ ] **Step 7: Run tests and typecheck** ```powershell node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS while legacy stores remain temporarily available to old callers. - [ ] **Step 8: Commit** ```powershell git add -- YouleNexus/assets/framework/platform/stores framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts git commit -m "feat(platform): add atomic platform state store" ``` --- ### Task 6: 收紧 NetClient 前置条件和控制包行为 **Files:** - Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/net-client.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/transport.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/cocos-transport.ts` - Modify: `cocoscreator_projects/framework-tests/net/net-client.test.ts` - Modify: `cocoscreator_projects/framework-tests/helpers/fake-transport.ts` **Interfaces:** - Produces: fail-fast `NetClient.start/send/stop`, one message stream, exact login gate, switch/kick events. - Consumes: Task 3 `RuntimeConfig`, Task 4 request builders/envelope codec. - [ ] **Step 1: Add failing precondition tests** Before changing NetClient, re-read the lifecycle, control-packet, heartbeat, timeout, and reconnect sections in `docs/protocol/01-网络通信与生命周期.md` and `02-客户端发包.md`; record any discrepancy between current code and the documented original behavior in the test name. ```ts assert.throws(() => clientWithoutIdentity.start(), /login identity/); assert.throws(() => client.send('room', 'player_prepare', {}), /not connected/); ``` Also test transport `send` after close, duplicate `onerror/onclose`, and stop idempotency. - [ ] **Step 2: Add control packet negative tests** Missing `roomserver/agentserver` must emit a contract error or throw through the test boundary, not be ignored. `kick_server` must cancel login/reconnect/watchdog timers and close once. - [ ] **Step 3: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/net/net-client.test.ts framework-tests/net/transport.test.ts ``` Expected: FAIL on silent `sendLogin`, optional transport send, and malformed switch packets. - [ ] **Step 4: Implement explicit connection states** Use an internal union `idle|connecting|open|stopped`. `start()` requires identity and at least one validated server. `send()` requires `open`. `stop()` transitions once and clears all timers before closing. - [ ] **Step 5: Keep the verified legacy timing** Retain four-second login guard, ten-second reconnect delay, receive timeout, TCP generation filtering, login-period packet gate, and server rotation. Update tests rather than changing these values. - [ ] **Step 6: Run all net tests and typecheck** ```powershell node --import tsx --test framework-tests/net/*.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell git add -- YouleNexus/assets/framework/net framework-tests/net framework-tests/helpers/fake-transport.ts git commit -m "fix(net): fail fast on invalid client lifecycle" ``` --- ### Task 7: 统一平台 Handler、Router 与 Session **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/router.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/platform/session.ts` - Modify: `cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/room-rpc-bus.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/protocol/room-handlers.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts` - Modify: `cocoscreator_projects/framework-tests/protocol/router.test.ts` - Create: `cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts` - Replace: `cocoscreator_projects/framework-tests/platform/session.test.ts` - Delete: `cocoscreator_projects/framework-tests/platform/room-rpc-bus.test.ts` - Delete: `cocoscreator_projects/framework-tests/protocol/room-handlers.test.ts` - Delete: `cocoscreator_projects/framework-tests/protocol/active-game.test.ts` **Interfaces:** - Consumes: strict parsers, `PlatformStore`, `GameSessionHost`, and an injected `GameHostFactory` port. - Produces: one `Router.dispatch`, one `PlatformSession.handleLogin`, `GameHostFactory.create({ descriptor, selfSeat, seatCount })`, and the platform handler table for the first slice. - [ ] **Step 1: Rewrite router tests around one dispatch owner** ```ts router.dispatch({ route: 'room', rpc: 'player_prepare', data: { seat: 2 } }); assert.deepEqual(platformCalls, ['player_prepare']); router.dispatch({ route: activeDescriptor.route, rpc: 'deal', data: { card: 1 } }); assert.deepEqual(gameMessages, [{ rpc: 'deal', data: { card: 1 } }]); ``` Assert an unknown platform RPC throws `UnsupportedPlatformRpcError`; a route different from the active session descriptor throws and never reaches either side. - [ ] **Step 2: Write handler ordering tests** For each room push, subscribe to PlatformStore and log GameModule events. Assert store notification occurs before the event and that the snapshot read inside the event contains the committed state. - [ ] **Step 3: Write login/reconnect tests** ```ts session.handleLogin(roomLogin); assert.equal(store.state.value.room.inRoom, true); assert.equal(fakeGame.restoreCalls[0], roomLogin.deskinfo); assert.equal('deskinfo' in store.state.value.room, false); ``` Also assert `{ isbattle:1 }` with absent or `null` `deskinfo` does not call restore, while a truthy opaque object calls it exactly once. - [ ] **Step 4: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/protocol/router.test.ts framework-tests/protocol/platform-handlers.test.ts framework-tests/platform/session.test.ts ``` Expected: FAIL against the current parallel Router/RoomRPCBus implementation. - [ ] **Step 5: Implement PlatformHandlers** Handlers accept parsed data only. Implement `self_join_room`, `other_join_room`, `self_exit_room`, `other_exit_room`, `player_prepare`, `other_offline`, and `other_online`. Each calls one PlatformStore action, then one GameSession event where defined. - [ ] **Step 6: Replace ActiveGame with GameSessionHost in Router** ```ts if (isPlatformRoute(message.route)) { return this.platform.dispatch(message); } return this.gameSessions.dispatchGameMessage(message.route, { rpc: message.rpc, data: message.data, }); ``` No other module subscribes separately to `NetClient.message`. - [ ] **Step 7: Make PlatformSession a parsed orchestration boundary** `handleLogin(unknown)` parses once, rejects failure without partial Store writes, resolves the descriptor with `GameRegistry.getByGameId(RuntimeConfig.identity.gameid)`, obtains the seat count only through `descriptor.resolveSeatCount(roomtype)`, asks the injected `GameHostFactory` for a route/seat-bound Host, opens or replaces the correct game session, atomically replaces recovery state, then calls `restore` if and only if `Boolean(deskinfo)`. Task 7 tests use a strict fake factory; the production adapter arrives in Task 8, so there is no forward implementation dependency. - [ ] **Step 8: Remove obsolete buses/stores from this path** Delete the parallel bus/handler/active-game files and all imports. Replace `sdk/index.ts` with a pure re-export of `sdk/contracts/index.ts`. Do not leave forwarding shims that could become a second runtime path. - [ ] **Step 9: Run platform/protocol tests and typecheck** ```powershell node --import tsx --test framework-tests/protocol/*.test.ts framework-tests/platform/*.test.ts framework-tests/sdk/*.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 10: Commit** ```powershell git add -A -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/platform YouleNexus/assets/framework/sdk/index.ts framework-tests/protocol framework-tests/platform git commit -m "refactor(platform): unify message routing and session state" ``` --- ### Task 8: 实现受限 GameHost 适配器与平台命令 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts` - Create: `cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts` - Create: `cocoscreator_projects/framework-tests/platform/commands.test.ts` **Interfaces:** - Consumes: GameHost contract, Task 7 `GameHostFactory` port, descriptor route, PlatformStore selectors, typed send, NetClient. - Produces: the production `createGameHostAdapter(options)` implementation of `GameHostFactory` and `PlatformCommands.prepare/exitRoom`. - [ ] **Step 1: Write failing route-binding and command tests** ```ts host.sendGameMessage('play-card', { card: 7 }); assert.deepEqual(sent[0], { route: 'mock-game', rpc: 'play-card', data: { card: 7 } }); host.execute({ type: 'room.prepare' }); assert.equal(sent[1].rpc, 'player_prepare'); ``` Assert no Host method accepts a route argument. Assert `room.exit` rejects when `room.stage !== 0`. For seat mapping, cover 2-, 4-, and 10-seat sessions. Assert `host.seat.toView(selfSeat) === 0`, every server seat round-trips through `toView`/`toServer`, and an out-of-range or non-integer seat throws with the offending value. - [ ] **Step 2: Write subscription/disposal tests** The first callback is immediate. Reassigning internal state with no public snapshot change does not notify. Disposing removes all listeners and every subsequent Host method throws `GameSessionDisposedError`. - [ ] **Step 3: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts ``` Expected: FAIL because adapter and commands do not exist. - [ ] **Step 4: Implement commands through typedSend** ```ts prepare(): void { this.net.sendTyped(typedSend(Rpc.player_prepare, {})); } exitRoom(): void { if (this.store.state.value.room.stage !== 0) { throw new UnsupportedGameHostCommandError('room.exit requires stage=0'); } this.net.sendTyped(typedSend(Rpc.self_exit_room, {})); } ``` If NetClient does not expose `sendTyped`, add a narrow method accepting `TypedEnvelope`; do not expose raw transport. - [ ] **Step 5: Implement snapshot and seat-mapper isolation** Build DTOs with selectors, freeze every newly created public object/array, and expose the Store's already-recursively-frozen canonical `roomtype` reference without interpreting its contents. Construct `GameSeatMapper` once from the current session's validated `selfSeat` and descriptor-derived `seatCount`; the adapter must not import the legacy `core/seat` implementation or read changing Store values inside the formulas. Do not expose `PlatformStore.state`. - [ ] **Step 6: Run tests, conformance harness, and typecheck** ```powershell node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts framework-tests/sdk/game-contract-harness.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell git add -- YouleNexus/assets/framework/platform/game-host-adapter.ts YouleNexus/assets/framework/platform/commands.ts YouleNexus/assets/framework/net/net-client.ts framework-tests/platform framework-tests/sdk git commit -m "feat(platform): expose restricted game host adapter" ``` --- ### Task 9: 建立 PlatformRuntime 和启动四门闩 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/startup.ts` - Create: `cocoscreator_projects/framework-tests/platform/ready-gate.test.ts` - Create: `cocoscreator_projects/framework-tests/platform/runtime.test.ts` - Replace: `cocoscreator_projects/framework-tests/platform/startup.test.ts` **Interfaces:** - Consumes: RuntimeConfig resolver, NetClient factory, Router, PlatformSession, PlatformStore, GameRegistry, ScenePort. - Produces: single `PlatformRuntime.start/stop/joinRoom/prepare/exitRoom`, read-only state, four-gate readiness. - [ ] **Step 1: Write all 16 readiness permutation tests** For each subset of the four gates, assert ready is false unless all four are true. Assert each gate can be marked once and duplicate marks are harmless. ```ts gate.mark('resources'); gate.mark('config'); gate.mark('socket'); assert.equal(gate.ready, false); gate.mark('minimum-display'); assert.equal(gate.ready, true); ``` - [ ] **Step 2: Write runtime construction-order tests** Assert config resolves before NetClient creation, identity is set before `start`, the application composition root registers descriptors explicitly, `RuntimeConfig.identity.gameid` resolves exactly one descriptor before a room session can open, Router subscribes before the first post-login business message, and stop disposes game session before stores/network. - [ ] **Step 3: Write scene decision tests** No room login calls `scene.showLobby()`. Room login calls `scene.showRoom()` before game restore. Kick calls `scene.showKicked()`. Reconnecting/slow calls `scene.showReconnect()` without clearing room state. - [ ] **Step 4: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/platform/ready-gate.test.ts framework-tests/platform/runtime.test.ts ``` Expected: FAIL because runtime and gate do not exist. - [ ] **Step 5: Implement Cocos-free runtime** The runtime constructor accepts factories/ports; it never imports `cc`. `start()` launches resource/minimum-display promises plus config resolution, then creates and starts NetClient. All NetClient events are wired in one private method and unwired in `stop()`. - [ ] **Step 6: Define narrow ScenePort** ```ts export interface ScenePort { showLoading(): void; showLogin(): void; showLobby(): void; showRoom(): void; showReconnect(): void; showKicked(data: unknown): void; showFatal(error: Error): void; } ``` The port describes intent, not Cocos scene or node names. - [ ] **Step 7: Delete StartupOrchestrator and adapt tests** There must be only one startup owner. Remove all `RoomRPCBus` factory concepts and the old “login 后构造 NetClient” flow. - [ ] **Step 8: Run tests and typecheck** ```powershell node --import tsx --test framework-tests/platform/*.test.ts framework-tests/integration/login-flow.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS after the integration test is adapted to PlatformRuntime. - [ ] **Step 9: Commit** ```powershell git add -A -- YouleNexus/assets/framework/platform framework-tests/platform framework-tests/integration/login-flow.test.ts git commit -m "feat(platform): add single runtime and readiness gate" ``` --- ### Task 10: 编写不依赖 Cocos 的首批 Layer 控制器 **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/connection-overlay-controller.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/join-room-controller.ts` - Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/room-shell-controller.ts` - Create: `cocoscreator_projects/framework-tests/ui/connection-overlay-controller.test.ts` - Create: `cocoscreator_projects/framework-tests/ui/join-room-controller.test.ts` - Create: `cocoscreator_projects/framework-tests/ui/room-shell-controller.test.ts` **Interfaces:** - Consumes: narrow RuntimeCommands and read-only selector/subscription ports. - Produces: presentation models and user-intent methods; no Cocos imports. - [ ] **Step 1: Write JoinRoom input-state tests** ```ts const controller = new JoinRoomController(commands, view); for (const digit of [1, 2, 3, 4, 5, 6]) controller.inputDigit(digit); assert.equal(view.last.roomcode, '123456'); controller.confirm(); assert.deepEqual(commands.calls, [{ type: 'join-room', roomcode: '123456' }]); ``` Cover 0-9 only, six-digit maximum, delete, clear, confirm before six digits, double confirm while pending, success close, and failure retaining input. - [ ] **Step 2: Write connection overlay tests** Map `reconnecting/slow` to Layer 615, pending commands to Layer 614, kicked to Layer 616, and normal states to all hidden. Kicked has precedence over reconnect/loading. - [ ] **Step 3: Write room shell tests** Feed snapshots and assert roomcode, seats, online/ready state, prepare visibility, and exit command. The controller stores no PlayerState objects after render. - [ ] **Step 4: Run tests and verify failure** ```powershell node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts ``` Expected: FAIL because controllers do not exist. - [ ] **Step 5: Implement view-port-driven controllers** Each controller receives a view interface and commands. It owns only UI transient state such as roomcode digits and pending flag. All server state comes from snapshots/selectors. - [ ] **Step 6: Run UI tests and typecheck** ```powershell node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell git add -- YouleNexus/assets/framework/ui/controllers framework-tests/ui git commit -m "feat(ui): add vertical-slice presentation controllers" ``` --- ### Task 11: 添加 Cocos 适配器并通过 MCP 挂接首批 prefab **Files:** - Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/PlatformRootComponent.ts` - Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/CocosScenePort.ts` - Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/JoinRoomLayerComponent.ts` - Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/RoomShellComponent.ts` - Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/ConnectionOverlayComponent.ts` - Replace: `cocoscreator_projects/YouleNexus/assets/scripts/LaunchFlow.ts` - Replace: `cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts` - Modify via funplay-cocos MCP: `cocoscreator_projects/YouleNexus/assets/scenes/Login.scene` - Modify via funplay-cocos MCP: Layer 1、4、15、50、403、411、202、416、614、615、616 prefab resources - Create: `cocoscreator_projects/framework-tests/cocos-adapters/no-demo-fallback.test.mjs` **Interfaces:** - Consumes: Task 9 PlatformRuntime, Task 10 controllers. - Produces: thin Cocos Components and ScenePort adapter; no duplicated business logic. - [ ] **Step 1: Write the static failure test first** Scan production Cocos adapters for forbidden literals/patterns: ```js const forbidden = [ 'mock_openid', "search: '?profile=local'", 'fallback to ws://127.0.0.1:3088', 'setIdentity({ agentid:', ]; ``` Expected current `LoginFlow.ts` to fail. - [ ] **Step 2: Run the static test and verify failure** ```powershell node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs ``` Expected: FAIL on current demo code. - [ ] **Step 3: Implement thin Cocos adapters** `PlatformRootComponent` creates exactly one runtime and persists it across scene changes. `CocosScenePort` maps semantic scene intents to root Layer activation. Other components only translate Button/Label/Node events to/from the pure controllers. - [ ] **Step 4: Remove demo networking from LaunchFlow/LoginFlow** Delete local `WSTransport`, mock identity, bootstrap catch fallback, direct RoomRPCBus construction, and fixed one-second scene jump. Use `CocosTransport` and the root runtime. - [ ] **Step 5: Use the cocos-mcp skill before editor changes** Read `.agents/skills/cocos-mcp/SKILL.md`, verify the editor service is healthy, and use the injected `mcp__funplay_cocos__*` tools. If unavailable, stop this task and ask the user to start the editor service; never edit serialized files manually. Before the first editor mutation, record `git status --short` for every listed scene/prefab and its `.meta`. They must already be tracked or separately checkpointed as the completed UI-migration baseline. If any is still an untracked pre-existing UI artifact, stop Task 11 and ask the user to checkpoint that UI work first; an untracked serialized file cannot separate earlier UI content from this task's bindings. - [ ] **Step 6: Inspect exact prefab node paths via MCP** For each Layer, record the queried node/component path in the task log before binding. Do not infer paths from prefab text. Verify which existing Sprite nodes are intended as buttons and which Labels display room/player state. - [ ] **Step 7: Attach components and serialized references via MCP** Attach only the five adapter components, wire Buttons/Labels/Nodes through editor properties, and keep business state out of prefab components. Save each prefab/scene through the editor. - [ ] **Step 8: Preview the vertical UI states** Using injected fake runtime states, capture or visually inspect: loading, login, lobby, six-digit join input, room with two seats, ready, reconnect, and kicked. Verify inactive overlays do not block input. - [ ] **Step 9: Run static and framework checks** ```powershell node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit ``` Expected: PASS. Also run the Cocos editor's script compilation check through MCP and require zero component/UUID errors. - [ ] **Step 10: Commit** ```powershell git add -- YouleNexus/assets/scripts/platform YouleNexus/assets/scripts/LaunchFlow.ts YouleNexus/assets/scripts/LoginFlow.ts framework-tests/cocos-adapters git add -- YouleNexus/assets/scenes/Login.scene YouleNexus/assets/scenes/Login.scene.meta git add -- YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab.meta git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab.meta git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab.meta git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab.meta git commit -m "feat(cocos): connect platform runtime to migrated layers" ``` Before committing, inspect `git diff --cached --name-only` and unstage any prefab unrelated to the listed first-batch Layers. --- ### Task 12: 建立完整纵向回放测试并删除旧状态路径 **Files:** - Create: `cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts` - Replace: `cocoscreator_projects/framework-tests/integration/login-flow.test.ts` - Modify: `cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/app-store.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/player-store.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/room-store.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/types.ts` - Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/readonly.ts` - Modify: `cocoscreator_projects/scripts/run-framework-tests.mjs` - Delete or rewrite affected legacy tests under: `cocoscreator_projects/framework-tests/platform/` **Interfaces:** - Consumes: completed runtime and fake transport/clock/scene/game adapters. - Produces: one deterministic end-to-end regression test and final dependency guard. - [ ] **Step 1: Write the full replay test** Replay this exact sequence with fixtures: ```text runtime.start → config resolved → WS open / player_login sent → login success without room / lobby shown → joinRoom('123456') / self_join_room sent → self_join_room success / room shown / game session entered → other_join_room → player_prepare → socket close / reconnect overlay → reconnect open / player_login resent → login success with room + deskinfo / room state replaced / restore called once ``` At every arrow assert outbound envelope, PlatformState, ScenePort call, GameModule event, and notification count. - [ ] **Step 2: Add two-game isolation to the architecture guard** Register two descriptors, enter/leave both in sequence, and assert imports under `assets/games/` cannot reach framework internals. Use temporary in-memory source strings in the test; do not create fake production games. - [ ] **Step 3: Run the test and verify any remaining legacy path fails** ```powershell node --import tsx --test framework-tests/integration/platform-vertical-slice.test.ts framework-tests/architecture/import-boundaries.test.mjs ``` Expected before cleanup: FAIL if any runtime still constructs old AppStore/PlayerStore/RoomStore or RoomRPCBus. - [ ] **Step 4: Delete legacy Store facades and update remaining tests** All production state callers must use PlatformStore/selectors. Do not leave deprecated aliases, because they would recreate a second source of truth. Also add `--test-concurrency=1` to `run-framework-tests.mjs` so the complete suite is deterministic in the managed Windows environment: ```js const r = spawnSync( process.execPath, ['--import', 'tsx', '--test', '--test-concurrency=1', ...files], { stdio: 'inherit' }, ); ``` - [ ] **Step 5: Run the complete framework suite** ```powershell npm run typecheck:framework npm run test:framework ``` Expected: every framework test PASS with zero skipped tests and zero TypeScript errors. - [ ] **Step 6: Audit forbidden patterns** ```powershell rg -n "RoomRPCBus|new AppStore|new PlayerStore|new RoomStore|mock_openid|fallback to ws://|transport\?\.send|if \(!this\.identity\) return|hasBattle.*isbattle" YouleNexus/assets ``` Expected: no production matches. Mentions in migration documentation are allowed outside `YouleNexus/assets`. - [ ] **Step 7: Commit** ```powershell git add -A -- YouleNexus/assets/framework/platform/stores/app-store.ts YouleNexus/assets/framework/platform/stores/player-store.ts YouleNexus/assets/framework/platform/stores/room-store.ts YouleNexus/assets/framework/platform/stores/types.ts YouleNexus/assets/framework/platform/readonly.ts git add -- scripts/run-framework-tests.mjs framework-tests/integration/platform-vertical-slice.test.ts framework-tests/integration/login-flow.test.ts framework-tests/architecture/import-boundaries.test.mjs framework-tests/platform git commit -m "test(platform): cover complete vertical slice replay" ``` --- ### Task 13: 真服与编辑器验收 **Files:** - Modify only if evidence changes: `docs/superpowers/specs/2026-09-04-platform-vertical-slice-contract-inventory.md` - Create: `docs/superpowers/verification/2026-09-04-platform-vertical-slice.md` **Interfaces:** - Consumes: a user-authorized test account, the original configuration service, Cocos preview/device environment. - Produces: sanitized verification record with packet shapes, state transitions, screenshots/observations, and remaining out-of-scope items. - [ ] **Step 1: Run preflight verification** ```powershell npm run typecheck:framework npm run test:framework ``` Expected: PASS before opening a real connection. - [ ] **Step 2: Select an explicit debug profile** Put the authorized account only in the existing debug profile mechanism or injected runtime data. Do not commit openid, phone, token, machine ID, real IP, or location. - [ ] **Step 3: Run the original and Cocos clients against the same test service** Capture and sanitize these envelopes: first `player_login`, successful `self_join_room`, `player_prepare`, reconnect `player_login`, and one room push. Compare route/rpc, field presence, primitive types, and nested structure. - [ ] **Step 4: Verify UI/state behavior through Cocos MCP preview** Confirm lobby entry, join-room input, room player rendering, ready state, reconnect overlay, room recovery, and kicked overlay. Capture screenshots through MCP if the tool supports it. - [ ] **Step 5: Record evidence and discrepancies** The verification document must list each acceptance item as PASS or FAIL with the observed packet/state evidence. A discrepancy is a failed acceptance item; do not add a fallback to make the run continue. - [ ] **Step 6: Re-run complete automated verification after any correction** ```powershell npm run typecheck:framework npm run test:framework ``` Expected: PASS. - [ ] **Step 7: Commit the verification record** ```powershell git add -- docs/superpowers/verification/2026-09-04-platform-vertical-slice.md git commit -m "docs(verification): record platform vertical-slice results" ``` --- ## Final Acceptance Checklist - [ ] `sdk/contracts` has no framework implementation or Cocos dependencies. - [ ] Adding a test game changes only its own directory and application registration. - [ ] Game messages are route-bound and cannot impersonate platform RPCs. - [ ] Game sessions release subscriptions and reject stale calls after disposal. - [ ] Remote WebSocket addresses come only from `data.urlserver` or an explicit debug direct profile. - [ ] All first-batch server payloads pass strict boundary parsers. - [ ] There is one Router and one PlatformStore. - [ ] Login, room recovery, join, prepare, offline/online, reconnect and kick match source behavior. - [ ] `deskinfo` uses the original client's truthiness trigger, is never stored by the platform, and is passed unchanged to `restore`. - [ ] Descriptor-owned seat-count resolution and the Host's 2/4/10-seat round-trip mapping tests pass without importing framework internals into a game. - [ ] Layer adapters contain no server fallback, mock identity, network code, or writable business state. - [ ] All serialized Cocos changes were made and verified through funplay-cocos MCP. - [ ] `npm run typecheck:framework` passes. - [ ] `npm run test:framework` passes with zero skipped tests. - [ ] Real test-server packet comparison passes without server or native-side changes.