Files
youle_cocos/docs/superpowers/specs/2026-09-08-room-platform-architecture.md
T
joywayer 35216f75e7 feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
2026-09-09 03:13:18 +08:00

392 lines
23 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.
# 房间平台与子游戏接入架构设计
版本:1.1 · 日期:2026-09-08 · 状态:平台核心已实施;验收与未支持边界见[实施记录](../implementations/2026-09-08-room-platform-refactor.md)。
本文保留设计契约与目标示例;具体 API 名称、落地位置及兼容期以实施记录和代码为准。音频所有权、完整子游戏玩法与原生设备验收不因平台核心落地而自动视为完成。网络协议、原生接口与序列化资源约束继续以仓库规定及对应权威文档为准。
## 文档导航
- [源码审查、方案优缺点与原流程依据](../../房间流程审查与架构重构建议.md)
- [旧接口迁移与新接入契约](2026-09-08-room-platform-api-migration.md)
- [分阶段实施计划与验收](../plans/2026-09-08-room-platform-refactor.md)
- [平台协议目录](../../protocol/README.md)
- [二七王当前玩法协议](../../../server/games/erqiwang/docs/protocol/packet_protocol.md)
## 1. 目标与非目标
### 1.1 目标
1. 原服务器零修改即可兼容,公开协议字段、route/rpc、数据类型与重连快照保持不变。
2. 平台不依赖具体子游戏;子游戏不访问平台内部 Store、场景和网络实现。
3. 纯逻辑无需启动 Cocos 即可测试;资源装配与真实交互必须另行在编辑器/构建中验证。
4. 子游戏只开发规则、协议映射、游戏模型和展示,不重复实现大厅、聊天、投票、设置、原生桥。
5. 房间生命周期、消息顺序、资源所有权、失败处理都具有明确契约。
6. 对已证实的全量重建、重复头像资源等问题做增量优化,性能结论以基线测量为准。
### 1.2 非目标
- 不重新实现服务器,不添加前端方便但服务器不存在的字段或 requestId。
- 不将原 Game_Modify/Utl 全量搬成同名 TypeScript 类。
- 不在本阶段引入 ECS、Worker、全局响应流框架、反射容器或远程热插件系统。
- 不以此文档承诺完成二七王叫分、出牌、计分、回放或结算画面。
- 不自动修复已经用错误 roomtype 创建的服务器房间,不静默改写用户缓存。
### 1.3 “零耦合”的可验证定义
零具体游戏依赖:除应用组合根外,公共代码不得 import games/*。
零内部访问:游戏只能引用 sdk/contracts 和自身模块,不引用平台 Store、Router、PlatformStartup。
零引擎依赖的逻辑:房间应用层、状态与游戏规则/模型不 import cc,不读取 window、localStorage 或原生全局对象。
允许必要契约依赖:游戏和平台共同依赖小而稳定的 SDK。完全没有任何依赖既不可实现,也不是本方案目标。
## 2. 权威数据与兼容边界
### 2.1 平台数据
身份、连接/认证状态、房间公共元数据、成员、座位、在线/准备、公告、聊天、投票结果,以各自协议来源为准。平台负责公共协议字段的校验、归一化和状态提交。
归一化必须有实际协议依据、在入口单点执行。例如此前数字房号的兼容不能推广成“所有字段都 String/Number 一遍”。出站类型仍按对应 rpc 契约生成。
### 2.2 子游戏载荷
roomtype、deskinfo、deskwar、deskfree 及游戏 route 的数据,由游戏解释。平台只保存、传递、按协议判断载荷是否存在,不读取内部选项、手牌或分数。
拟议的通用可传输类型:
```ts
type JsonValue = null | boolean | number | string
| readonly JsonValue[]
| { readonly [key: string]: JsonValue };
type Roomtype = JsonValue;
```
运行时仅允许有限数字、无循环的 JSON 值,不允许函数、BigInt、引擎对象和 undefined。这是传输限制,不是玩法格式限制。每个外层协议若规定字段必需,则必须存在;不能把缺失字段改成 null。
“原样”指保持 JSON 数据结构、值、类型、数组顺序和未知字段;不要求保留 JSON 文本空格或对象键的序列化顺序。若协议某个字段本身是字符串编码,则字符串内容必须完整保留。
二七王创建编码仍由其编码器生成 11 位字符串;模板可以发送数组。平台不得为了接入方便转换任何一种格式。
### 2.3 游戏事实与公共展示
玩法包有时才包含下一局、参与状态和局内分数。因此平台不能要求所有公共展示变化只能来自平台 route,也不能允许游戏任意改 Store。
采用单向的 GameRoomProjection:游戏模型解析自己的协议后发布只读投影;公共展示选择器组合平台快照与游戏投影。
必须区分:
- 服务端公共 room stage 与游戏自己的 round phase,不用同一个字段存两者。
- 账户余额与局内分数,不用 changeBean 写同一份值。
- 公共准备状态与玩法参与/等待下一局状态,不把派生按钮显隐反写准备值。
- 平台规则已禁止的动作与游戏附加限制:游戏可以收紧权限,不可扩大平台明确禁止的权限。
确有玩法包需要同步公共协议状态时,建立具名、带来源的映射,并在对应游戏适配器与协调器测试;禁止 patchRoom/setPlayerState 等任意写入接口。
## 3. 模块结构与依赖
以下为目标职责分布,不要求第一批任务一次搬完目录:
```text
assets/
app/ 唯一知道具体游戏的组合入口
bootstrap/
composition/
framework/
sdk/contracts/ 公共纯类型与稳定端口
net/ 保留 WireClient、心跳、连接代际
protocol/ 公共信封与协议适配
platform/room/
room-coordinator.ts 流程与事务编排
room-scope.ts 会话作用域和清理
room-mailbox.ts 有界业务消息等待队列
room-entry.ts 不同进入来源的描述
room-policy.ts 平台公共动作策略
platform/stores/ 保留并渐进拆分公共状态
presentation/ 无 Cocos 的展示模型与选择器
adapters/ 网络、原生、存储等端口实现
ui/room/ 公共 Cocos 面板与场景适配
games/<game>/
definition.ts 唯一游戏定义
protocol/ 玩法解析、rpc 类型和入口映射
rules/ roomtype、座位、描述等纯规则
model/ 游戏状态和同步输入处理
presentation/ 游戏展示模型
cocos/ 创建页、房间 View、资源
```
依赖关系:
```mermaid
flowchart TD
App[应用组合入口] --> Platform[平台房间应用层]
App --> Game[具体游戏模块]
App --> Cocos[Cocos 与设备适配]
Platform --> SDK[共享契约]
Game --> SDK
Cocos --> SDK
Platform --> Protocol[公共协议与状态]
Game --> Rules[游戏规则与游戏模型]
```
SDK 不能反向 import 平台实现。当前部分能力类型声明在 game-host-adapter.ts,迁移时应提取到契约目录,不能让 presentation 因一个类型依赖具体适配器。
## 4. 游戏定义与资源装配
### 4.1 唯一游戏定义
一个权威定义提供:本地 key、游戏协议 route、身份配置关联、规则、模块工厂、创建页/房间展示引用、游戏内容和可选能力声明。
身份中的 agentid/channelid/gameid 继续由配置来源提供。游戏定义引用该配置关联,不在多个文件复制实际渠道身份。
Cocos 组件可以引用生成的资源定义或唯一的装配资源,但不得与 TypeScript 定义分别维护两份 gameKey、route、prefab。实施时优先选择构建阶段验证明确静态引用的方式;不要运行时按名称扫描并猜测页面类型。
### 4.2 装配验证
- 每个创建 prefab 恰好一个创建页契约实现。
- 每个房间 prefab 恰好一个房间展示契约实现。
- 所有必需资源可解析,引用目标与声明的游戏一致。
- 声明的协议 route 不与平台保留 route 冲突。
- 不通过“随便创建再销毁一个游戏实例”验证声明;在真实创建时验证实例契约,避免工厂预检产生业务副作用。
- 未使用的可选能力无需假实现;声明了必需能力却未绑定时装配失败。
最近二七王 prefab 中模板脚本残留的问题,必须由上述检查自动发现,而不是等创建 RPC 发出去才发现。
## 5. SDK 开发者接口
### 5.1 原则
SDK 面向开发者只暴露少量分组能力:room、messages、ui、audio、scope。内部实现可按职责拆分,不把每个旧函数变成独立服务。
禁止 getService(name)、任意 Store 写入、任意 app/route 发包、直接传平台节点。消息传输使用绑定当前游戏 route 的端口;通用房间命令由平台构建身份和房号。
### 5.2 核心形状
以下用于说明边界;完整字段按实施阶段测试补全,不是可直接粘贴的 SDK 实现:
```ts
interface GameModel {
// context 已绑定一个房间;不得缓存到跨房间全局。
initialize(context: RoomContext): void;
// 同步处理;不可返回等待动画结束的 Promise。
handle(input: GameInput): void;
dispose(): void;
}
interface RoomContext {
readonly room: RoomReadPort;
readonly commands: RoomCommandPort;
readonly messages: GameMessagePort;
readonly ui: GameUiPort;
readonly audio: AudioPort;
readonly scope: ScopePort;
}
interface GameMessagePort {
send(rpc: string, data: JsonValue): void;
}
```
GameMessagePort 的 void 表示传输操作本身,不表示服务器业务成功。实际回复仍按协议作为输入处理。游戏内部可以将 rpc 与 payload 建成强类型映射,公共平台不需要知道映射内容。
RoomReadPort 提供只读快照、按选择器订阅和座位映射。快照变更具有本地 revision;本地 revision 不加入网络包、不冒充服务器序列号。
### 5.3 输入事件保留来源
必须区分:
- created:成功创建结果,原 roomtype 和 infinite 等数据。
- login-restored:登录恢复,携带原 deskinfo。
- login-waiting:登录已有房间但无恢复快照。
- joined-waiting:普通主动加入。
- joined-snapshot:主动加入已有 deskinfo。
- started:加入、准备、成员变化、self_makewar/makewar 引起的开战,携带 sourceRpc 和原整包。
- player-ready/joined/left/online/offline/seat-changed:公共成员事件。
- game-message:当前游戏 route 的原消息。
- dissolution-settlement:服务端解散结果与原 deskfree,保留 freeNow/确认时机。
内部可用联合类型表示,不扩展服务器包。类型名称可以调整,但这些语义不能被一个 restore(unknown) 覆盖。
### 5.4 只读投影
游戏模型拥有 GameRoomProjection 并允许订阅;它只包含公共 HUD 真正需要的事实,不泄露完整手牌与内部模型。
第一阶段字段以模板和二七王实际需求为准:玩法阶段、自己的参与状态、按服务器座位的玩法分数、公共操作限制。投影是游戏状态的派生结果,不另设可任意写入的第二份状态。
RoomSnapshot revision 与 GameProjection revision 分别可追踪;在一次协议处理事务完成后向公共 UI 发布组合快照,避免 UI 看到同一消息的一半更新。
### 5.5 页面接口
创建页接收上次配置和 submit/cancel。编码由游戏完成,平台收到 roomtype 只封装与发送;未知或错误旧配置由游戏迁移策略处理,没有迁移策略就明确提示并允许用户显式重置。
房间 View 的职责只有挂载、根据模型绘制、将用户操作交给控制器、取消动画与卸载。它不读取 WebSocket、不直接恢复协议快照、不以节点可见性决定业务状态。
## 6. 生命周期与消息时序
### 6.1 分开建模
- Connection:连接中、已连接、重连中、停止。
- Authentication:未登录、等待回复、已登录、拒绝/被踢。
- RoomLifecycle:outside、entering、active、recovering、leaving、failed。
- GamePhase:子游戏自己的轮次/行动阶段。
- Dissolution/Settlement:投票和结果展示独立于 GamePhase。
这些维度存在明确约束,但不要混成一个巨大的状态枚举。
### 6.2 进入事务
1. 验证公共外层协议,保留原始载荷。
2. 在游戏规则入口验证其拥有的配置并解析需要的座位约束;不在 UI 层补缺失数据。
3. 完成公共状态构建的预检。已接受的新服务端房间不能在失败时偷偷恢复成“旧房间仍有效”。
4. 使旧 scope 失效并取消旧任务,创建新的 generation,提交进入状态。
5. 创建游戏模型、绑定能力,启动异步资源与界面挂载。
6. 界面挂载就绪后,以正确来源发送进入输入;恢复使用原快照,不自行填快照字段。
7. 标记可交互,按顺序处理进入后等待的业务消息。
失败策略:步骤 1–3 失败不创建半初始化 scope;步骤 4 后失败将新会话标为 failed 并完整释放,不伪装 active。是否重新登录恢复,由现有连接/恢复契约决定,不能对格式错误无休止自动重试。
### 6.3 异步就绪
```mermaid
sequenceDiagram
participant Net as 协议入口
participant Room as 房间协调器
participant View as Cocos 场景适配
participant Game as 游戏模型
Net->>Room: 接受创建/加入/登录结果
Room->>Room: 预检并建立新 scope
Room->>Game: initialize(context)
Room->>View: mount(scope)
Net->>Room: 后续房间业务消息
Room->>Room: 暂存有界 FIFO
View-->>Room: ready
Room->>Game: 带来源的进入输入
Room->>Game: 按序处理暂存消息
```
model.initialize 只绑定上下文与初始模型,不要求 View 已可用。需要页面的操作通过能力端口等待就绪,且离房时可取消。禁止每种能力各自无界积压请求来弥补没有统一就绪契约。
旧 mainSceneLoaded 的通知时点与“可以访问节点”的内部就绪点分开。兼容映射保留原 create/login/join 的调用语义,不用新名字掩盖顺序变化。
### 6.4 队列规则
队列只保存当前已认证、当前 generation 的房间业务消息,FIFO;不改变原有登录屏障规则。换服尚未建立目标会话的消息按连接意图处理。
容量与最长等待时间由平台配置来源显式给出,先通过基线和压力测试确定;消费方不使用随意 fallback。溢出或超时导致明确诊断和恢复,不丢最旧的游戏包继续假装成功。
踢出、断线、用户取消等控制信号可立即使 scope 失效,不等待场景/动画。没有服务器序列号时,不按 rpc 名或 payload 相等对玩法事件去重。
### 6.5 准备和开战
准备请求发出只更新本地 pending,不提前更改权威 ready。回复提交 ready 后产生成员事件;如包中携带 deskwar,再提交相应阶段并触发 started,原整包保留。
同步处理状态不等待动画。动画队列属于 View,断线/恢复可以取消旧动画并直接渲染当前状态。开始按钮的条件来自统一策略,不由各按钮、摇一摇与子游戏分别实现。
### 6.6 解散与结算
投票倒计时不宣布结果。服务端拒绝结束本轮投票;普通通过等待结果确认后传递结算;freeNow 按协议立即传递。没有 deskfree 时按已核实的退出路径结束房间。
有 deskfree 时保留游戏与必要玩家上下文直到结算结束。明确区分 round-finished、room-finished 和 result-view-closed;不允许一个模糊 gameOver 同时决定分享、清空状态和跳场景。
### 6.7 退出与换服
requestExit 是请求,不是本地 leave。平台按当前模式和阶段决定退出/申请解散等合法动作,等待对应响应。
换服保留既定 connection intent 和重发信封;解散换服可能仍需保留结算 scope。这里不能用统一“换服就销毁游戏”替代原协议路径。
scope 失效是立即、幂等的;任何晚到回调只能释放它自己的资源,不能发包、提交状态或挂载节点。
## 7. 能力、错误与资源
### 7.1 输入与模态框
数字/文本/倍率输入返回明确的 confirmed/cancelled 结果。取消原因区分用户取消、房间结束、请求被替代;真实失败则拒绝并提供原因。
每种单例输入面板明确声明冲突策略。默认设计为:同一 scope 的重叠请求拒绝,调用方显式结束前一个后再开新请求;若现有业务必须替换,则旧请求必须得到 cancelled,不能丢失回调。
不会因为 Promise 方便就把没有业务回复的发包动作包装成“等待成功”。
### 7.2 Scope
拥有订阅、定时器、原生回调、资源租约、界面实例和未完成输入。释放顺序:停止接收/提交业务 → 取消异步输入与动画 → 释放游戏模型和 View → 释放能力资源与订阅。使用逆序登记的清理栈,异常逐项收集,不因一个 disposer 抛错而跳过其余清理。
共享头像/音频缓存不属于单个房间;房间只持有可释放租约。缓存有容量限制和明确逐出策略,不能以“缓存”名义永久持有所有资源。
### 7.3 错误分类
**Layer612_Tips 专用于玩家业务提示。** 游戏和平台的业务拒绝、输入校验、操作结果及可操作的超时提示可以使用;代码异常、堆栈、协议/配置校验失败、资源导入/加载错误、模块未接入与调试信息只能进入诊断日志,不得直接显示 error.message。禁止为了显示技术错误而改用踢出面板;踢出面板仅处理真实业务踢出。业务提示与诊断端口必须分开命名、分别注入。
- 协议/状态不一致:阻止当前事务,显式失败,记录来源和字段。
- 业务拒绝:展示服务端约定信息、解除 pending,不终止正常连接。
- 用户取消:正常结果,不上报 fatal。
- 展示资源失败:局部能力按明确定义处理;没有 fallback 定义时报告资源错误,不能影响无关网络逻辑。
- 清理失败:继续清理其余项,保留原始异常链。
诊断包含 direction、route/rpc、连接代际、房间 generation、生命周期阶段和原始 cause。完整包日志由 debugIO 控制;对外提交的错误报告与生产日志需要独立的敏感字段处理,不复用完整调试包。
## 8. 性能策略
优先处理已证实的结构性成本:
1. 聊天使用本地稳定消息 ID 维护行,追加不重建旧行。ID 不发送到服务器。
2. 头像按 URL 去重加载、共享纹理租约;行复用时验证当前 URL/generation,避免旧图片写到新玩家。
3. 选择器按最小相关状态通知;同帧合并的是绘制,不是业务消息。
4. 投票保留按座位复用;小座位列表不引入不必要的虚拟化。
5. 长聊天历史在测量后决定可变行高虚拟列表;上翻历史不被新消息强制滚底。
6. 倒计时按来源建立截止时间并按差值绘制;后台恢复后重算,不累计定时器减法误差。
7. 不在每个 UI 消费点递归 clone;入口隔离后结构共享。大型 deskinfo 的所有权策略应单独测量验证。
基线指标:进入/恢复至可交互耗时、主线程 p95、帧时间、长任务、节点数、纹理数、聊天追加开销、重复进退后的活跃订阅/计时器。
绝对耗时目标由目标设备测量确定。本版本不编造性能数字;结构性目标可以立即验收:一条消息不重建已有行,重复头像不重复创建纹理,100 次进入/退出后作用域资源数量回到基线。
## 9. 回放隔离
回放使用独立 ReplayContext、玩家列表、视角、时钟和消息源。可以复用游戏模型/展示,但不能共享在线 RoomStore、认证身份或真实网络发送能力。
未声明回放能力的游戏无需实现空回调。回放是后续独立阶段,本阶段只保证 SDK 不以全局状态阻止未来隔离。
## 10. 子游戏接入流程
1. 创建游戏定义并关联唯一配置身份与资源。
2. 实现 roomtype 编码、读取、座位规则和纯描述。
3. 将不同进入来源和游戏协议映射到自身模型输入。
4. 创建页提交自己的配置;房间 View 订阅模型,操作走绑定能力。
5. 声明需要的可选能力,不填写无意义的示例返回值。
6. 运行接入测试和 prefab 唯一性检查,再在 Cocos 中验证真实按钮→实际包。
接入验收的核心是第二款游戏加入时不修改平台核心、公共 UI 或公共协议分发器。开发者不需要了解 PlatformStartup 内部字段,也不需要手动复制 agentid/playerid/roomcode。
## 11. 兼容与迁移策略
保留现有网络、状态缓存和会话租约,逐条迁移。每条消息在同一时刻只由一套业务处理器负责;严禁双跑旧新处理器发送重复命令或产生两份状态。
旧接口只在过渡适配层存在,并记录移除条件。新游戏不能新增 Utl 依赖。资源操作通过 Cocos MCP,主工作区已有修改不自动 stash、不覆盖、不批量恢复。
迁移检查从现有 framework/games 扩展到公共 scripts/ui,唯一豁免为明确的 app 组合根。CI 同时检查静态 import、契约测试和资源装配。
## 12. 风险与设计取舍
接口分层增加初期代码量,但减少后续游戏接入修改面;通过少量能力分组控制抽象数量。
原流程存在不合理顺序和混合状态,不能逐行复制。保留协议与业务效果,对改变的生命周期时点明确记录兼容映射及测试。
扩大 Roomtype 的平台承载类型需要检查全部消费方,防止 UI 或存储再次假设 length/index。未知格式不得在平台描述或转为字符串显示为玩法规则。
等待队列只解决界面加载窗口,不是事件日志系统。缺少服务器序列号时不承诺精确一次业务投递;重连依赖服务器权威快照及原协议屏障。
## 13. 完成标准
- 两种不同配置格式的游戏通过接入套件;新游戏无需修改平台核心。
- 创建、加入、登录恢复、各开战入口的来源与载荷不丢失。
- 加载中取消/踢出/换服、旧回调晚到、结算后释放均有测试。
- 公共核心无 cc/window,游戏无平台内部依赖;装配检查发现重复页面。
- 房间命令不由游戏手工拼公共身份;roomtype 不被平台解释或转换。
- 聊天与头像增量策略通过资源计数/真实交互验证。
- 文档明确未实现玩法,不以空函数或单元测试替代真实重连显示。
## 14. 待实施阶段收敛的参数
队列容量/等待时间、缓存容量、性能目标设备和绝对耗时阈值,由 P0/P3 基线确定并写入单一配置来源。第一阶段按平台核心与契约优先安排;二七王完整玩法和回放另立范围,不隐含在此次重构中。