docs(plan): add platform vertical-slice implementation plan

This commit is contained in:
2026-09-04 22:34:03 +08:00
parent c9f6df6e10
commit 621eb8722e
3 changed files with 1232 additions and 10 deletions
File diff suppressed because it is too large Load Diff
@@ -129,7 +129,7 @@ videoConfig, shortcode, match, matchid, agreefree, isbet, deskinfo
- `roomcode` 存在决定是否恢复房间;
- `isbattle` 只写房间 stage;
- `deskinfo` 字段存在决定是否调用 `onReconnect(deskinfo)`;
- `deskinfo` 值为真决定是否调用 `onReconnect(deskinfo)`,严格复刻原工程 `if (deskinfo)`;
- `deskinfo` 值不被平台复制、补字段或解析;
- `roomtype` 保留完整嵌套数组结构。
@@ -171,6 +171,8 @@ videoConfig, shortcode, match, matchid, agreefree, isbet, deskinfo
| GAME-13 | snapshot 是冻结 DTO,不暴露内部 Store 类型 | mutation failure/type test |
| GAME-14 | dispose 后 Host 失效并释放全部订阅 | lifecycle/leak test |
| GAME-15 | 不同游戏、不同房间会话之间无状态泄漏 | two-game isolation test |
| GAME-16 | `resolveSeatCount(roomtype)` 是座位总数的唯一游戏侧解释入口,且必须返回正整数 | descriptor validation test |
| GAME-17 | Host 座位映射绑定当前 self seat/seat count,子游戏不导入内部 seat 实现 | 2/4/10-seat round-trip test |
第一版 `GameHostCommand` 只包含:
@@ -1,6 +1,6 @@
# YouleNexus 平台纵向链路逻辑迁移设计
> 状态:已确认平台纵向链路方向;已纳入子游戏解耦架构,待用户复审。
> 状态:已确认平台纵向链路与子游戏解耦架构。
>
> 适用工程:`cocoscreator_projects/YouleNexus`。
>
@@ -30,7 +30,7 @@
2. **唯一数据源**:配置、协议映射、玩家实体和房间座位各有唯一权威来源。
3. **下游不兜底**:边界数据缺失或非法时显式报错;Store、控制器和视图不得猜默认值。
4. **roomtype 不解析**:平台只保存和透传具体子游戏的嵌套数组。
5. **deskinfo 不解析**:平台只按“响应中存在 deskinfo”判断并原样传给 `GameModule.restore(deskinfo)`;不得改用 `isbattle` 作为触发条件。
5. **deskinfo 不解析**:平台严格复刻原工程 `if (deskinfo)`,仅当响应中的 `deskinfo` 值为真时原样传给 `GameModule.restore(deskinfo)`;不得改用 `isbattle` 或单纯字段存在作为触发条件。
6. **Cocos 资源只经编辑器 MCP 修改**:本设计涉及 prefab 挂载或场景操作时,执行阶段必须使用 funplay-cocos MCP。
7. **旧引擎机制不复刻**:渲染循环、命中检测、对象表和逐精灵定时器交给 Cocos;只迁移业务状态机和协议行为。
8. **依赖方向单向**:`game implementation → sdk contracts ← platform adapters`;SDK contracts 不反向依赖 platform/net/ui 实现。
@@ -87,7 +87,7 @@
- `roomHandlers` 直接修改 `signal.value` 内部对象,不能保证订阅者收到更新;部分 handler 是空实现。
- `PlayerStore` 和 `RoomStore.players` 同时保存自己的玩家信息,存在状态镜像。
- `PlayerStore`、`RoomStore` 和多个 handler 用 `??` 构造服务器未提供的数据。
- `parseLoginResponse.hasBattle` 同时判断 `isbattle` 和 `deskinfo`,与源码“deskinfo 存在才调用 Reconnect”不一致。
- `parseLoginResponse.hasBattle` 同时判断 `isbattle` 和 `deskinfo`,与源码“`if (deskinfo)` 为真才调用 Reconnect”不一致。
- `native-bridge.ts` 把入站注册名和出站调用名合并成同一份 14 项白名单;原工程两者并不相同。
- 现有 `sdk/index.ts` 直接导入 platform Store 类型和内部 EventBus,使 SDK 与平台实现耦合。
- 现有 `GameNet.send(route,rpc,data)` 允许子游戏任意指定 route,能够绕过平台协议边界。
@@ -267,6 +267,7 @@ interface GameDescriptor {
readonly gameId: string | number;
readonly route: string;
readonly key: string;
resolveSeatCount(roomtype: readonly unknown[]): number;
create(): GameModule;
}
```
@@ -276,6 +277,7 @@ interface GameDescriptor {
- `key`、`gameId`、`route` 均非空且分别唯一;
- `apiVersion` 与平台支持版本完全相等;
- `create()` 每次返回新的 GameModule 实例;
- `resolveSeatCount(roomtype)` 是平台获得该游戏座位总数的唯一入口,结果必须是正整数;具体 `roomtype` 索引含义只存在于游戏实现;
- 重复注册或版本不匹配显式抛错,不选择“最接近版本”。
进入房间时依据权威 gameId/route 激活 descriptor。`GameSessionHost` 使用 `detached → attached → entered → disposed|failed` 状态机;非法次序显式抛错。离房或切换游戏时先 dispose 当前会话,下一次进房创建新实例,禁止把上一桌状态带入下一桌。每个 Host 带内部 generation,旧会话的异步回调在新会话建立后继续发包时必须失败。
@@ -320,7 +322,7 @@ interface GameModule {
- `attach`、每个房间进入事件、`restore` 和 `dispose` 的调用次数有状态机校验;
- 游戏 route 的服务端消息只形成 `GameServerMessage`,平台不解析 data;
- `restore` 的唯一触发条件是响应对象拥有 `deskinfo` 字段;
- `restore` 的唯一触发条件是响应中的 `deskinfo` 值为真,与原工程 `if (deskinfo)` 完全一致;缺失、`null`、`false`、`0` 或空字符串均不触发;
- 对同一会话的状态提交、PlatformToGameEvent 和游戏消息严格串行派发;平台先提交 Store,再发对应事件,游戏读取 snapshot 时一定看到事件后的权威状态;
- snapshot 表达“现在是什么”,event 表达“刚发生什么”,不得用 event 重建平台状态,也不得用 snapshot 猜测一次性动画事件;
- 平台不得吞掉 GameModule 抛出的契约错误;运行时转入明确 failed 状态并停止本局继续派发。
@@ -332,6 +334,7 @@ interface GameModule {
```ts
interface GameHost {
readonly apiVersion: PlatformGameApiVersion;
readonly seat: GameSeatMapper;
getSnapshot(): PlatformGameSnapshot;
subscribe(listener: (snapshot: PlatformGameSnapshot) => void): Unsubscribe;
sendGameMessage(rpc: string, data: unknown): void;
@@ -340,6 +343,11 @@ interface GameHost {
type Unsubscribe = () => void;
interface GameSeatMapper {
toView(serverSeat: number): number;
toServer(viewSeat: number): number;
}
type PublicConnectionPhase =
| 'connected'
| 'logged-in'
@@ -377,7 +385,8 @@ type GameHostCommand =
| { readonly type: 'room.exit' };
```
- `PlatformGameSnapshot` 是冻结的只读 DTO,只包含公开身份、房间、座位和连接状态,不暴露 Store 类型或 reactive 实现。
- `PlatformGameSnapshot` 是冻结的只读 DTO,只包含公开身份、房间、座位和连接状态,不暴露 Store 类型或 reactive 实现;`roomtype` 在入 Store 边界仅做通用递归冻结、不解释任何位置,并以同一权威引用透传到 snapshot。
- `GameSeatMapper` 在会话建立时绑定当前 `self.seat` 和 descriptor 解析出的 `seatCount`;公式分别为 `(serverSeat-selfSeat+seatCount)%seatCount` 与 `(viewSeat+selfSeat)%seatCount`,子游戏不导入平台 `core/seat`,也不重复保存自己的换算参数。
- `sendGameMessage` 自动绑定 descriptor 的 `route` 与固定 app,子游戏不能指定 platform/agent/room route。
- `GameHostCommand` 是受控判别联合。第一批只开放 `room.prepare` 和 `room.exit`;命令只负责发起协议动作,成功状态由服务器响应/推送驱动。增加命令必须同步契约、适配器和一致性测试。
- `room.exit` 在第一批仅允许未开局房间;已开局时显式抛出 unsupported command,不能误发 `self_exit_room` 代替尚未迁移的开局退出流程。
@@ -427,7 +436,7 @@ one game → another game
4. `state === 0` 时一次 action 提交 A 组状态。
5. 无 `roomcode`:清理房间域并显示大厅。
6. 有 `roomcode`:一次 action 提交 B 组状态,进入房间壳。
7. 响应存在 `deskinfo`:进入房间后调用一次 `onReconnect(deskinfo)`;没有则不调用。
7. 响应 `deskinfo` 值为真:进入房间后调用一次 `onReconnect(deskinfo)`;缺失或假值不调用。
### 6.3 主动加入房间
@@ -435,7 +444,7 @@ one game → another game
2. 输入完成并确认后构造 `self_join_room` 请求,字段严格来自登录态和输入值。
3. 失败响应只更新命令结果和提示,不污染 RoomState。
4. 成功响应原子提交玩家目录和房间座位状态。
5. `deskwar` 为真时调用 `onStartWar`;否则若存在 `deskinfo`,调用 `DeskInfo` 等价入口;普通进房不调用对局恢复。
5. `deskwar` 为真时调用 `onStartWar`;否则若 `deskinfo` 值为真,调用 `DeskInfo` 等价入口;普通进房不调用对局恢复。
### 6.4 准备和房内推送
@@ -450,7 +459,7 @@ one game → another game
2. 按旧策略延时并轮换候选服务器。
3. open 后用同一份登录身份重发 `player_login`。
4. 登录成功后用服务器完整快照覆盖房间态,不在旧 RoomState 上打补丁。
5. 响应存在 `deskinfo` 时原样调用 `onReconnect`。
5. 响应 `deskinfo` 值为真时原样调用 `onReconnect`。
6. `kick_server` 停止重连并显示 Layer 616。
## 7. 错误处理
@@ -508,6 +517,7 @@ bootstrap → open → login(no room) → join → other join
- snapshot 只读且不泄露 Store/Reactive/EventBus;
- dispose 自动释放订阅并使旧 Host 失效;
- 两个测试游戏顺序进入时,状态、订阅和消息不串局;
- 2、4、10 人座位的双向换算可逆,且服务端座位到自己的视图位置恒为 0;
- `deskinfo` 以同一引用和值交给 `restore`,平台不解析。
## 9. 交付与验收
@@ -520,7 +530,7 @@ bootstrap → open → login(no room) → join → other join
4. 玩家实体只有一个权威存储;
5. 第一批 RPC 均有请求/响应解析器和黄金样本;
6. 四门闩、登录、大厅、进房、准备和断线重连集成测试通过;
7. `deskinfo` 仅以“字段存在”为触发条件并原样传给游戏模块;
7. `deskinfo` 仅以原工程 `if (deskinfo)` 的真值语义触发,并原样传给游戏模块;
8. Layer 控制器只依赖 command 与只读 selector;
9. TypeScript 类型检查与全部 framework tests 通过;
10. 新子游戏只需提供 descriptor/module 并在应用组合根注册,不修改 framework 核心;