1211 lines
56 KiB
Markdown
1211 lines
56 KiB
Markdown
# 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/<game-key>/`,本计划只创建测试游戏,不创建真实玩法。
|
||
|
||
---
|
||
|
||
### 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<RpcName, RouteName>`, 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<T>(rpc: RpcName, data: T): TypedEnvelope<T> {
|
||
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<Record<number, PlayerState>>;
|
||
}
|
||
|
||
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<PlatformState>`. 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/<key>` 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.
|