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

554 lines
24 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 平台纵向链路逻辑迁移设计
> 状态:已废止。本文包含运行时游戏注册及工程布局的旧假设,不得继续作为实施依据。
>
> 替代规范:`docs/superpowers/specs/2026-09-04-framework-subgame-zero-coupling-migration-design.md`。
>
> 适用工程:`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 控制器只消费稳定接口。
平台与子游戏采用端口/适配器结构:平台核心只认识稳定 SDK 契约,不导入任何具体子游戏;子游戏只依赖 SDK 契约,不导入平台 Store、Router、NetClient 或平台 UI。具体子游戏由应用组合根注册,接入新游戏不修改平台核心。
## 2. 不可妥协的约束
1. **服务器零改动**:信封、route、rpc、字段名、字段类型、时序与旧客户端一致。
2. **唯一数据源**:配置、协议映射、玩家实体和房间座位各有唯一权威来源。
3. **下游不兜底**:边界数据缺失或非法时显式报错;Store、控制器和视图不得猜默认值。
4. **roomtype 不解析**:平台只保存和透传具体子游戏的嵌套数组。
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 实现。
9. **能力最小化**:子游戏只能调用显式开放的 Host API;不得获得原始 EventBus、可写 Store、WebSocket 或任意 platform RPC 发送权。
权威来源按以下顺序裁决冲突:
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` 触发恢复钩子;
- 稳定的子游戏 SDK 契约、游戏注册表、会话隔离器与接入一致性测试工具;
- 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`,与源码“`if (deskinfo)` 为真才调用 Reconnect”不一致。
- `native-bridge.ts` 把入站注册名和出站调用名合并成同一份 14 项白名单;原工程两者并不相同。
- 现有 `sdk/index.ts` 直接导入 platform Store 类型和内部 EventBus,使 SDK 与平台实现耦合。
- 现有 `GameNet.send(route,rpc,data)` 允许子游戏任意指定 route,能够绕过平台协议边界。
- 现有 `IGameModule.serialize()` 没有对应的客户端上送协议证据;服务器重连快照来自登录/进房响应,不能保留这个推测性 API。
- 现有 `ActiveGame.set()` 只是可变单槽,没有描述符校验、注册机制、会话释放和 API 版本检查。
## 5. 目标结构
### 5.1 唯一运行时入口
新增一个平台运行时组合根,负责按固定顺序创建并持有:
```text
PlatformRuntime
├── RuntimeConfigResolver
├── NetClient
├── Router
├── PlatformHandlers
├── PlatformState
├── GameRegistry
├── GameSessionHost
└── 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 → GameModule.handleGameMessage
```
`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 子游戏 SDK 的物理边界
SDK 分为纯契约、平台适配器和测试工具,目录职责固定:
```text
assets/framework/sdk/
├── contracts/
│ ├── game-descriptor.ts # 游戏身份、route、API 版本、factory
│ ├── game-module.ts # 子游戏生命周期入口
│ ├── game-host.ts # 子游戏可调用的平台能力
│ ├── platform-events.ts # 平台→子游戏的判别联合事件
│ └── snapshots.ts # 只读平台快照 DTO
├── runtime/
│ ├── game-registry.ts # 注册、唯一性与版本校验
│ └── game-session-host.ts # 单局 attach/enter/leave/dispose
└── testing/
└── game-contract-harness.ts # 子游戏接入一致性测试
```
`sdk/contracts` 只能依赖同目录的类型和 TypeScript 标准类型。它不能导入 `platform/`、`net/`、`protocol/`、`ui/`、Cocos `cc` 或具体游戏。
具体游戏统一放在:
```text
assets/games/<game-key>/
├── game-descriptor.ts
├── game-module.ts
├── protocol/
├── state/
├── components/
└── prefabs/
```
平台核心不扫描目录、不动态猜模块;应用组合根显式导入 descriptor 并注册,保证构建期可追踪。
### 5.8 注册契约
每个子游戏只向应用层暴露一个描述符:
```ts
type PlatformGameApiVersion = 1;
interface GameDescriptor {
readonly apiVersion: PlatformGameApiVersion;
readonly gameId: string | number;
readonly route: string;
readonly key: string;
resolveSeatCount(roomtype: readonly unknown[]): number;
create(): GameModule;
}
```
`GameRegistry.register(descriptor)` 必须检查:
- `key`、`gameId`、`route` 均非空且分别唯一;
- `apiVersion` 与平台支持版本完全相等;
- `create()` 每次返回新的 GameModule 实例;
- `resolveSeatCount(roomtype)` 是平台获得该游戏座位总数的唯一入口,结果必须是正整数;具体 `roomtype` 索引含义只存在于游戏实现;
- 重复注册或版本不匹配显式抛错,不选择“最接近版本”。
进入房间时依据权威 gameId/route 激活 descriptor。`GameSessionHost` 使用 `detached → attached → entered → disposed|failed` 状态机;非法次序显式抛错。离房或切换游戏时先 dispose 当前会话,下一次进房创建新实例,禁止把上一桌状态带入下一桌。每个 Host 带内部 generation,旧会话的异步回调在新会话建立后继续发包时必须失败。
### 5.9 平台 → 子游戏
平台不直接调用不断增长的可选方法集合,而是通过稳定生命周期和判别联合事件:
```ts
type PlatformToGameEvent =
| { type: 'room-entered'; roomtype: readonly unknown[] }
| { type: 'self-joined'; seat: number }
| { type: 'player-joined'; seat: number }
| { type: 'player-left'; seat: number }
| { type: 'player-ready'; seat: number }
| { type: 'player-offline'; seat: number }
| { type: 'player-online'; seat: number; ip: string }
| { type: 'war-started'; payload: unknown }
| { type: 'room-left'; reason: RoomLeaveReason };
type RoomLeaveReason =
| 'self-exit'
| 'room-broken'
| 'kicked'
| 'session-replaced';
interface GameServerMessage {
readonly rpc: string;
readonly data: unknown;
}
interface GameModule {
attach(host: GameHost): void;
handlePlatformEvent(event: PlatformToGameEvent): void;
handleGameMessage(message: GameServerMessage): void;
restore(deskinfo: unknown): void;
dispose(): void;
}
```
约束:
- `attach`、每个房间进入事件、`restore` 和 `dispose` 的调用次数有状态机校验;
- 游戏 route 的服务端消息只形成 `GameServerMessage`,平台不解析 data;
- `restore` 的唯一触发条件是响应中的 `deskinfo` 值为真,与原工程 `if (deskinfo)` 完全一致;缺失、`null`、`false`、`0` 或空字符串均不触发;
- 对同一会话的状态提交、PlatformToGameEvent 和游戏消息严格串行派发;平台先提交 Store,再发对应事件,游戏读取 snapshot 时一定看到事件后的权威状态;
- snapshot 表达“现在是什么”,event 表达“刚发生什么”,不得用 event 重建平台状态,也不得用 snapshot 猜测一次性动画事件;
- 平台不得吞掉 GameModule 抛出的契约错误;运行时转入明确 failed 状态并停止本局继续派发。
### 5.10 子游戏 → 平台
`GameHost` 是子游戏获得平台能力的唯一入口:
```ts
interface GameHost {
readonly apiVersion: PlatformGameApiVersion;
readonly seat: GameSeatMapper;
getSnapshot(): PlatformGameSnapshot;
subscribe(listener: (snapshot: PlatformGameSnapshot) => void): Unsubscribe;
sendGameMessage(rpc: string, data: unknown): void;
execute(command: GameHostCommand): void;
}
type Unsubscribe = () => void;
interface GameSeatMapper {
toView(serverSeat: number): number;
toServer(viewSeat: number): number;
}
type PublicConnectionPhase =
| 'connected'
| 'logged-in'
| 'reconnecting'
| 'slow'
| 'kicked';
interface PlatformGameSnapshot {
readonly connection: { readonly phase: PublicConnectionPhase };
readonly self: {
readonly playerId: number;
readonly seat: number;
};
readonly room: {
readonly roomcode: string;
readonly roomtype: readonly unknown[];
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
};
readonly seats: readonly GameSeatSnapshot[];
}
interface GameSeatSnapshot {
readonly seat: number;
readonly playerId: number;
readonly nickname: string;
readonly avatar: string;
readonly online: boolean;
readonly ready: boolean;
}
type GameHostCommand =
| { readonly type: 'room.prepare' }
| { readonly type: 'room.exit' };
```
- `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` 代替尚未迁移的开局退出流程。
- `subscribe` 返回明确的取消函数;会话 dispose 时 Host 自动取消全部订阅。
- `subscribe` 注册成功后立即收到一次当前 snapshot,后续只在公开 DTO 发生变化时通知。
- dispose 后继续调用 Host 必须抛出 `GameSessionDisposedError`。
- 子游戏内部事件、状态和 Cocos 组件由子游戏自行管理,不借用平台 EventBus。
第一批用测试 GameModule 验证完整契约。未指定真实子游戏前,只验证调用时机、隔离性、参数引用和生命周期,不声称玩法重连完成。
### 5.11 依赖规则
允许的依赖方向:
```text
app composition → platform runtime → sdk contracts
app composition → concrete game → sdk contracts
platform adapters → sdk contracts
```
禁止:
```text
platform runtime → concrete game
concrete game → platform/net/protocol/ui/core internals
sdk contracts → platform/net/protocol/ui/cc
one game → another game
```
通过自动化 import-boundary 测试扫描源码导入路径。违反依赖方向时测试失败,而不是依赖评审人员记忆。
## 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、场景命令和 `GameModule` 事件记录。
### 8.5 真服验证
单元与集成测试通过后,使用真实测试账号和原服务器完成一次抓包验证。测试账号和服务器地址只写入调试 profile,不写入控制器或测试快照。
### 8.6 子游戏接入一致性测试
`game-contract-harness` 对任意 descriptor 运行同一组测试:
- descriptor 字段、route 与 API 版本有效;
- 两次 `create()` 返回不同实例;
- 生命周期顺序非法时显式失败;
- 游戏消息只能发往自身 route;
- snapshot 只读且不泄露 Store/Reactive/EventBus;
- dispose 自动释放订阅并使旧 Host 失效;
- 两个测试游戏顺序进入时,状态、订阅和消息不串局;
- 2、4、10 人座位的双向换算可逆,且服务端座位到自己的视图位置恒为 0;
- `deskinfo` 以同一引用和值交给 `restore`,平台不解析。
## 9. 交付与验收
本批完成必须同时满足:
1. 不修改服务器和原生 App;
2. release 启动不存在 mock、localhost 或捕获错误后的回退;
3. 所有业务消息只经过一个 Router;
4. 玩家实体只有一个权威存储;
5. 第一批 RPC 均有请求/响应解析器和黄金样本;
6. 四门闩、登录、大厅、进房、准备和断线重连集成测试通过;
7. `deskinfo` 仅以原工程 `if (deskinfo)` 的真值语义触发,并原样传给游戏模块;
8. Layer 控制器只依赖 command 与只读 selector;
9. TypeScript 类型检查与全部 framework tests 通过;
10. 新子游戏只需提供 descriptor/module 并在应用组合根注册,不修改 framework 核心;
11. 子游戏源码没有导入 platform/net/protocol/ui 内部模块;
12. game-contract-harness 与 import-boundary 测试通过;
13. 真实服务器抓包与原工程信封、字段和关键时序一致。
## 10. 后续子项目边界
本批验收后按以下顺序单独设计:
1. 创建房间与目标子游戏 `roomtype`;
2. 房间解散、换桌及完整房间壳;
3. 大厅资产、战绩、任务、仓库和排行;
4. 分享、语音、电话、定位、支付等原生能力;
5. 指定子游戏的对局协议和 `deskinfo` 恢复。
每个子项目继续使用同一套黄金报文与新旧行为差分验收方式。