Files
youle_cocos/docs/superpowers/specs/2026-09-04-platform-vertical-slice-design.md
T

346 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# YouleNexus 平台纵向链路逻辑迁移设计
> 状态:已确认设计方向,待用户审阅本文。
>
> 适用工程:`cocoscreator_projects/YouleNexus`。
>
> 契约清单:`docs/superpowers/specs/2026-09-04-platform-vertical-slice-contract-inventory.md`。
## 1. 决策
第一批逻辑迁移采用“平台纵向链路优先”,只交付一个能独立验证的闭环:
```text
远程配置 / 原生身份
→ WebSocket 连接
→ player_login
→ 大厅
→ self_join_room
→ 房间玩家与准备状态
→ 断线、重登、deskinfo 透传重连
```
本批不按 Layer 编号逐个补事件,也不一次性迁移全部平台功能。这样可以先固定协议边界、状态归属和运行时编排,后续 Layer 控制器只消费稳定接口。
## 2. 不可妥协的约束
1. **服务器零改动**:信封、route、rpc、字段名、字段类型、时序与旧客户端一致。
2. **唯一数据源**:配置、协议映射、玩家实体和房间座位各有唯一权威来源。
3. **下游不兜底**:边界数据缺失或非法时显式报错;Store、控制器和视图不得猜默认值。
4. **roomtype 不解析**:平台只保存和透传具体子游戏的嵌套数组。
5. **deskinfo 不解析**:平台只按“响应中存在 deskinfo”判断并原样传给 `IGameModule.onReconnect(deskinfo)`;不得改用 `isbattle` 作为触发条件。
6. **Cocos 资源只经编辑器 MCP 修改**:本设计涉及 prefab 挂载或场景操作时,执行阶段必须使用 funplay-cocos MCP。
7. **旧引擎机制不复刻**:渲染循环、命中检测、对象表和逐精灵定时器交给 Cocos;只迁移业务状态机和协议行为。
权威来源按以下顺序裁决冲突:
1. `docs/protocol/` 与其引用的原工程源码行;
2. `projects/Game_Surface_3/js/00_Surface/` 实际行为;
3. 本设计与契约清单;
4. 现有 YouleNexus 实现和测试。
现有测试若与前三级冲突,修改测试,不保留错误兼容。
## 3. 范围
### 3.1 本批包含
- release/debug 运行模式判定;
- H5 查询参数与原生 `window.settings` 身份读取;
- `gameconfig` 覆盖、`gameserver` 远程配置抓取与 `data.urlserver` 解析;
- 启动四门闩:资源完成、配置完成、WebSocket open、最短展示时间到达;
- WebSocket 信封、握手、心跳、收包超时、登录守护和服务器切换;
- `player_login` 请求、响应和登录期间收包门控;
- 登录后进入大厅或恢复房间;
- `self_join_room`、`other_join_room`、`self_exit_room`、`other_exit_room`;
- `player_prepare`、`other_offline`、`other_online`;
- `deskwar` 触发开战钩子,`deskinfo` 触发恢复钩子;
- Loading、Login、MainMenu、JoinRoom、MainScene、准备、玩家座位、重连和踢下线界面的最小控制器接线;
- 单元测试、协议黄金测试和纵向集成测试。
### 3.2 本批不包含
- 创建房间选项与具体 `roomtype` 生成;
- 解散投票、换桌、战绩、任务、仓库、排行、支付;
- 分享、语音、电话、通讯录、电量、网络、定位、摇一摇等完整 WVJB 功能;
- 任何具体子游戏的出牌、结算、`roomtype` 位含义和 `deskinfo` 内部结构;
- 为了迁移而改变服务器或原生 App。
这些能力后续按独立子项目设计和实施,不能扩入本批。
## 4. 当前基线与必须纠正的偏差
现有框架不是推倒重写对象。`NetClient`、信封编解码、心跳、重连策略、事件总线和响应式原语可以保留,但以下偏差必须在本批纠正:
- `LaunchFlow.ts` 用一秒定时器模拟加载,未实现四门闩。
- `LoginFlow.ts` 固定 debug/local、mock 身份,并在 bootstrap 失败时回退本地地址。
- `StartupOrchestrator` 在 login 之后才构造网络相关对象,生命周期倒置。
- `remote-config.ts` 从 `*_server_tcp` 推导连接地址,未复刻原工程 `ServerUrl_Succ` 直接读取 `data.urlserver` 的契约。
- `profiles.ts` 的默认 gameserver 与 `Game_Surface_3/js/01_SubGame/00_SubGame_Config.js:11` 不一致。
- `NetClient.sendFrame` 在 transport 缺失时静默不发送,`sendLogin` 在 identity 缺失时静默返回。
- `Router` 与 `RoomRPCBus` 并行承担消息分发,所有权不唯一。
- `roomHandlers` 直接修改 `signal.value` 内部对象,不能保证订阅者收到更新;部分 handler 是空实现。
- `PlayerStore` 和 `RoomStore.players` 同时保存自己的玩家信息,存在状态镜像。
- `PlayerStore`、`RoomStore` 和多个 handler 用 `??` 构造服务器未提供的数据。
- `parseLoginResponse.hasBattle` 同时判断 `isbattle` 和 `deskinfo`,与源码“deskinfo 存在才调用 Reconnect”不一致。
- `native-bridge.ts` 把入站注册名和出站调用名合并成同一份 14 项白名单;原工程两者并不相同。
## 5. 目标结构
### 5.1 唯一运行时入口
新增一个平台运行时组合根,负责按固定顺序创建并持有:
```text
PlatformRuntime
├── RuntimeConfigResolver
├── NetClient
├── Router
├── PlatformHandlers
├── PlatformState
├── ActiveGame
└── ScenePort
```
`LaunchFlow` 和 `LoginFlow` 不再各自构造网络、身份和 Store。Cocos 组件只调用组合根的命令并订阅只读状态。
组合根的职责仅是装配和生命周期管理,不包含具体 RPC 业务分支。
### 5.2 配置边界
`RuntimeConfigResolver` 在网络连接前一次性产出不可变结果:
```ts
interface RuntimeConfig {
mode: 'debug' | 'release';
identity: ChannelIdentity;
loginIdentity: LoginIdentity;
servers: readonly string[];
debugLogging: boolean;
}
```
- release 的默认 `gameserver` 必须来自唯一 profile,并与原工程目标版本一致;
- 原生环境的 `gameconfig` 可按原算法把 `-` 还原为 `/`、`#` 还原为 `:`,再构造 `http://<value>.txt`;
- 远程配置成功后,WebSocket 地址只取 `data.urlserver`;值可为单个地址或候选数组;
- debug 直连必须由显式 profile 定义,不允许捕获错误后临时改用 localhost;
- 缺少必要身份、登录身份、gameserver 或 urlserver 时,解析器抛出带字段路径的错误。
### 5.3 唯一协议注册表
生产代码提供一份完整的 `RPC_ROUTE` 常量,覆盖 `routes.ts` 中的所有 `RpcName`。第一批只实现本设计范围内的请求/响应类型和 handler,但其它已知平台 RPC 收到时必须明确报告“未实现”,不能被子游戏接走。
每个已实现 RPC 由一个契约对象定义:
```ts
interface RpcContract<Request, Response> {
readonly route: RouteName;
readonly rpc: RpcName;
parseRequest(input: unknown): Request;
parseResponse(input: unknown): Response;
}
```
发包和收包都经过同一契约。控制器不得直接写 route/rpc 字符串,也不得绕过解析器调用 `NetClient.send`。
### 5.4 单一路由
`NetClient` 只负责传输与连接状态,业务包全部交给一个 `Router`:
```text
NetClient.message
→ Router
├── platform / agent / room → PlatformHandlers
└── activeGame.route → IGameModule.onReceive
```
`RoomRPCBus` 的独立监听职责被移除。不得出现第二个消费者再次筛选 `route === 'room'`。
`player_login`、`kick_server` 和切服指令仍可在 NetClient 的连接状态机中作为控制包识别,但成功解析后的业务数据必须进入统一会话入口,不能由不同模块重复落 Store。
### 5.5 状态模型
第一批采用三个逻辑域,但玩家实体只保存一份:
```ts
interface PlatformState {
app: AppState;
players: PlayerDirectoryState;
room: RoomState;
}
interface PlayerDirectoryState {
selfPlayerId: number | null;
entities: Readonly<Record<number, PlayerState>>;
}
interface RoomState {
inRoom: boolean;
roomcode: string | null;
seatPlayerIds: readonly (number | null)[];
selfSeat: number | null;
roomtype: unknown[] | null;
// 其余字段按 protocol/04 的 B 组显式定义
}
```
- 登录 A 组写入 `players.entities[playerid]`,同时设置 `selfPlayerId`;
- 登录 B 组和进房响应先写玩家实体,再写座位到玩家 ID 的映射;
- 房间 UI 通过 selector 组合座位与玩家实体,不保存玩家副本;
- `deskinfo` 不进入 PlatformState;解析成功后由会话入口一次性原样派发给激活子游戏;
- Store 只提供原子 action,每次 action 替换新的 state 值并通知订阅者;
- 初始空状态可以有明确语义值,但服务器响应缺失字段不能用初始值补齐。
### 5.6 UI 边界
第一批只建立下列控制器:
- 启动/加载控制器:Layer 1、Layer 614、Layer 615、Layer 616;
- 登录控制器:`Login_Layer.prefab`;
- 大厅控制器:Layer 4;
- 加入房间控制器:Layer 15;
- 房间壳控制器:Layer 50、Layer 403、Layer 411、Layer 202、Layer 416。
控制器遵循同一规则:
```text
用户事件 → Runtime command → RPC contract → NetClient
Store action → selector → 控制器 render → Cocos 节点
```
视图不能直接访问 WebSocket、Router 或可写 Store。prefab 节点引用与 Button 事件必须在执行阶段通过 Cocos MCP 添加。
### 5.7 子游戏边界
第一批只要求一个测试替身实现以下接口:
```ts
interface IGameModule {
readonly route: string;
onReceive(rpc: string, data: unknown): void;
onEnterRoom(roomtype: unknown[]): void;
onStartWar(data: unknown): void;
onReconnect(deskinfo: unknown): void;
onPlayerReady(seat: number): void;
onPlayerOffline(seat: number): void;
onPlayerOnline(seat: number, ip: string): void;
}
```
平台不得查看 `deskinfo` 内部字段。未指定真实子游戏前,只验证调用时机、参数引用和调用次数,不声称玩法重连完成。
## 6. 核心时序
### 6.1 首次启动
1. 组合根同时启动资源加载、最短展示计时和配置解析。
2. 配置解析成功后构造 NetClient,设置完整登录身份并连接 `urlserver`。
3. WebSocket open 只表示连接门闩完成;是否立即发 login 由登录身份是否已具备决定。
4. 资源、配置、open、计时四门闩全部完成后,显示登录/授权入口。
5. 任一必需门闩失败,进入明确错误状态;不得切到 Login 后再使用 mock 数据继续。
### 6.2 登录
1. `player_login` 请求由契约构造器注入所有条件字段。
2. 等待响应期间只接受 `player_login` 和 `kick_server`;其它业务包按旧协议丢弃。
3. `state !== 0` 进入登录失败状态,不写玩家或房间数据。
4. `state === 0` 时一次 action 提交 A 组状态。
5. 无 `roomcode`:清理房间域并显示大厅。
6. 有 `roomcode`:一次 action 提交 B 组状态,进入房间壳。
7. 响应存在 `deskinfo`:进入房间后调用一次 `onReconnect(deskinfo)`;没有则不调用。
### 6.3 主动加入房间
1. JoinRoom 控制器维护最多六位的本地输入状态。
2. 输入完成并确认后构造 `self_join_room` 请求,字段严格来自登录态和输入值。
3. 失败响应只更新命令结果和提示,不污染 RoomState。
4. 成功响应原子提交玩家目录和房间座位状态。
5. `deskwar` 为真时调用 `onStartWar`;否则若存在 `deskinfo`,调用 `DeskInfo` 等价入口;普通进房不调用对局恢复。
### 6.4 准备和房内推送
1. 点击准备发送无业务字段的 `player_prepare` data,通用信封字段由协议层补齐。
2. 收到 `player_prepare` 后更新对应玩家的准备状态并通知 UI/子游戏。
3. `deskwar` 为真时只触发一次开战入口。
4. 玩家加入、退出、离线、上线都通过原子 action 更新目录或座位状态。
### 6.5 断线重连
1. 连接关闭或收包超时后进入 reconnecting,显示 Layer 615。
2. 按旧策略延时并轮换候选服务器。
3. open 后用同一份登录身份重发 `player_login`。
4. 登录成功后用服务器完整快照覆盖房间态,不在旧 RoomState 上打补丁。
5. 响应存在 `deskinfo` 时原样调用 `onReconnect`。
6. `kick_server` 停止重连并显示 Layer 616。
## 7. 错误处理
错误分为三类:
- **契约错误**:字段缺失、类型错误、未知 route/rpc。抛出包含 route、rpc 和字段路径的错误,测试必须失败。
- **可恢复运行错误**:连接关闭、超时、服务器切换。进入明确 phase,并由 NetClient 状态机处理。
- **业务失败**:RPC `state !== 0`。保留原服务端错误语义,转换为命令结果供 UI 展示,不修改成功态 Store。
禁止:空 catch、`as any` 穿透边界、静默 return、localhost 回退、从旧 Store 猜缺失字段。
## 8. 测试设计
### 8.1 契约黄金测试
为每个第一批 RPC 保存最小成功、失败和可选字段样本。断言:
- 编码后的 JSON 信封字段和值完全一致;
- 解码后字段类型不改变;
- 缺失必需字段时解析失败;
- `roomtype` 和 `deskinfo` 保持引用内容不变。
### 8.2 状态迁移测试
每个 handler 同时断言最终 state 和订阅通知次数,防止再次出现原地修改不通知 UI。
### 8.3 时序测试
使用 fake clock 和 fake transport 覆盖:四门闩排列组合、四秒登录守护、十秒重连、候选服务器轮换、踢下线停止重连、重登覆盖房间快照。
### 8.4 纵向集成测试
用固定消息序列验证:
```text
bootstrap → open → login(no room) → join → other join
→ prepare → close → reconnect → login(with room + deskinfo)
```
断言每一步的出站包、phase、Store、场景命令和 `IGameModule` 调用。
### 8.5 真服验证
单元与集成测试通过后,使用真实测试账号和原服务器完成一次抓包验证。测试账号和服务器地址只写入调试 profile,不写入控制器或测试快照。
## 9. 交付与验收
本批完成必须同时满足:
1. 不修改服务器和原生 App;
2. release 启动不存在 mock、localhost 或捕获错误后的回退;
3. 所有业务消息只经过一个 Router;
4. 玩家实体只有一个权威存储;
5. 第一批 RPC 均有请求/响应解析器和黄金样本;
6. 四门闩、登录、大厅、进房、准备和断线重连集成测试通过;
7. `deskinfo` 仅以“字段存在”为触发条件并原样传给游戏模块;
8. Layer 控制器只依赖 command 与只读 selector;
9. TypeScript 类型检查与全部 framework tests 通过;
10. 真实服务器抓包与原工程信封、字段和关键时序一致。
## 10. 后续子项目边界
本批验收后按以下顺序单独设计:
1. 创建房间与目标子游戏 `roomtype`;
2. 房间解散、换桌及完整房间壳;
3. 大厅资产、战绩、任务、仓库和排行;
4. 分享、语音、电话、定位、支付等原生能力;
5. 指定子游戏的对局协议和 `deskinfo` 恢复。
每个子项目继续使用同一套黄金报文与新旧行为差分验收方式。