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.
This commit is contained in:
2026-09-09 03:13:18 +08:00
parent a7ec8aa6a7
commit 35216f75e7
1028 changed files with 835556 additions and 21415 deletions
@@ -0,0 +1,103 @@
# 房间平台重构实施记录
日期:2026-09-08。工作区:主 checkout;分支 `codex/web-native-settings`。本次仅修改前端,没有修改服务器、原工程或协议字段。
## 已落地的结构
### 游戏装配
唯一注册入口是 `assets/app/composition/game-definitions.ts`。游戏 key、route、工厂、聊天配置、创建页/房间组件名称及资源路径在这里关联。工厂接收装配身份,不再各自重复定义 key/route。
公共 `PlatformStartup` 依赖 `GameComposition`,不导入具体游戏或应用注册表。原 `SubgameAssets` 已通过 Cocos asset-db 移到组合根,UUID 保持 `ec19e470-cf36-40e8-b420-e4314e5fbf07`。模板与二七王房间视图共用 `CommonRoomViewBase`,二七王不再继承另一个游戏。
聊天内容与敏感词迁到对应游戏目录;SDK 只持有聊天配置契约。边界检查扫描 framework、实际 assets/games 和公共 scripts,禁止公共层依赖游戏/组合根、禁止游戏相互依赖。旧版 LoginFlow/RoomEventProbe/RoomSceneStart 明确隔离,现代代码不能导入这些原型入口。
资源检查直接读取同一注册表,再解析 TypeScript 继承关系及 meta UUID,验证组件唯一、根节点挂载、脚本可解析、资源类型和所属游戏一致。检查不会自动删除资源组件。装配校验不预执行游戏工厂,真实 open 时才创建并校验模块;100 次 Runtime 进退恰好创建、挂载、释放各 100 次。
### 协议与输入
`Roomtype = JsonValue`。平台验证可传输的有限、无循环 JSON,保留值与类型;不读取配置位、长度或推断游戏规则。非法 undefined、稀疏数组、非有限数字、访问器和非 JSON 对象在边界拒绝。保存配置读取和 VIP 入口也不再限制为 string/array。
`room.entered.entry` 保留 created、login-waiting、login-restored、joined-waiting、joined-snapshot、joined-started 来源。原始载荷和快照不因内部事件转换而丢失。
`room.started` 覆盖 self_makewar、other_makewar,以及 self_join_room / other_join_room / player_prepare 携带 deskwar 的路径。原工程 `makewar` 是处理函数名,线上 RPC 是 `other_makewar`。准备状态提交先于开战通知;deskwar 优先于 deskinfo,避免同一加入包同时执行 StartWar 和恢复。
兼容期保留现有 `restore(deskinfo)`。当前游戏只记录恢复快照,不宣称已有二七王牌桌恢复显示。
### 房间生命周期
`RoomCoordinator`、`RoomScope`、`RoomMailbox` 管理异步挂载、generation、排队和清理。实际 Runtime 已接入,不是独立未使用的工具类。
房间业务包在挂载期间排队;就绪后按整包执行 Router、Store 提交及游戏回调。同一个包的游戏回调在该事务内执行,因此第一条 player_prepare 的处理器不会读到第二条包提交后的状态。游戏路由采用相同顺序。登录/入房替换、踢出和换服等控制消息按原协议屏障处理,不被慢场景阻塞。
挂载、UI 绑定完成后才投递进入/恢复事件。退出或进入失败先撤销现有 Host 租约,再释放所属 UI、队列、订阅和计时器。旧 Promise 晚到不能绑定新房间;多项清理继续执行,保留原始错误。同步异常仍由调用链报告,异步失败走统一 fatal 诊断。
普通换服与解散换服分开:后者仍保留服务端结算所需的房间上下文。没有实现的游戏结算继续显式报错,不用空处理器冒充接入。
队列预算单独声明于 `framework/config/room-lifecycle.ts`:256 项、等待 30000 ms。这是可调整的客户端运行预算,不是测得的设备性能阈值。
### SDK 能力与事实投影
生产 Host 提供 `room / messages / ui / scope` 分组能力。准备与退出复用平台身份填充和命令策略;两个游戏工厂已迁移到这些能力。旧 Host 方法仍作为兼容表面存在,`requireRoomCapabilities` 在缺少新端口时显式报错,不提供伪默认实现。
数字、数字文本、倍率请求返回 confirmed/cancelled。取消原因是 user、scope-ended、replaced。同类重叠请求拒绝;离房、替换和用户关闭都有确定结果;迟到确认不能继续回调。数字工具的等待绑定机制受同一租约清理。
loading 与业务 tips 已接入真实 FeedbackPanels 的所有者句柄。一个来源结束不会关闭其他 loading,也不会关闭更新的业务提示。`Layer612_Tips` 仍只显示面向玩家的业务内容,程序错误走诊断。
游戏投影由游戏模型拥有,消费者只读;完整版本含 revision、roundPhase、selfParticipation、scoresBySeat、restrictions。投影不修改平台玩家、准备状态或资产。命令发送及公共按钮都消费 deny-only 限制,游戏不能放开平台已经禁止的操作。当前两款游戏没有已迁移的局内模型,明确返回 null;不从平台 stage 猜出局内事实。
**音频所有者端口未发布**:现有 RoomVoiceAdapter 的原生契约没有播放停止接口,不能承诺“释放句柄即可停止原生音频”。原有语音适配保持工作,未修改原生接口,也未构造空 audio 能力。
### 展示与资源
Store 为聊天消息分配客户端本地稳定 ID,不改变收发包。RoomChatPanel 按 ID 增量维护历史行;公告也保留已有节点。同帧状态变化合并为一次历史刷新。用户上翻历史时保留滚动位置,位于底部时继续跟随新消息。
RoomUi 通过共享头像租约缓存去重请求和 SpriteFrame/Texture2D。URL 替换和节点释放撤销旧所有者,迟到加载不能覆盖新头像。空闲缓存上限 32 项;活动引用不因缓存淘汰被销毁。资源关闭逐项执行,异常不会阻止其余清理。
投票倒计时仍依据 deadline 与 Date.now 计算,后台恢复后重新计算剩余时间;计时器不决定服务端结算。当前数据规模未提供可变行高虚拟列表的必要证据,因此没有引入虚拟化。
## 验证与性能证据
- 实施前相关基线:137 项通过。已有未提交修改 131 项、序列化资产哈希 47 项已记录于 `out/room-platform-refactor/`。
- 最终完整 Node 测试 **950/950 通过**;framework 与 Cocos 两套 TypeScript 检查通过;导入边界和两游戏资源装配检查通过。
- 通过 Cocos builder 生成独立 `build/web-mobile-room-refactor`,任务 `1788869153827` 成功(19 s)。产物启动场景为 PlatformStartup,主脚本包含新的 GameComposition、房间队列和生命周期代码;工程保持 `script.loose=false`。构建成功不代表已经完成真实账号整局联调。
- 真实 Runtime 测试覆盖创建/加入/登录恢复、开战、投票/结算边界、退出、慢挂载、踢出、断线、换服、队列超限、清理失败、整包状态顺序和 100 次进退。
- Cocos Creator 3.8.8 / Windows / Electron 31.3.1 的 Game View:二七王实际提交按钮的 32 种组合均输出 11 位字符串;模板提交自己的数组。
- 实际聊天预览:50 行追加到 51 行,原 50 个 Node 均保留;5 次同帧更新产生 1 次历史刷新;上翻位置保持。Cocos 有 3 次 Layout 检查、2 次实际布局,不能把“1 次历史刷新”写成“引擎只布局 1 次”。测量包含人工等待帧的 82 ms,不作为渲染耗时指标。
- 真实头像测试:相同 URL 的两个节点共用同一个 SpriteFrame;100 次 Cocos RoomScope + 房间聊天 UI 挂载/释放后,Canvas 节点数 8 → 8,活动头像租约为 0,保留 1 个有界空闲缓存资源。
- 预览复现脚本:`scripts/verify-room-refactor-preview.mjs`、`scripts/verify-room-resource-cycles.mjs`;原始结果:`out/room-platform-refactor/preview-result.json`、`resource-cycles.json`。测试停止真实 socket,使用本地夹具,没有向真实账号创建房间或扣卡。
这里只报告当前 Windows/Cocos 测量。未据此承诺移动设备帧率、内存上限或线上吞吐量。
## 尚未覆盖的业务与验收
- 二七王完整出牌、牌桌 deskinfo 恢复渲染、结算界面、回放、未支持的 VIP/百人场不属于本次平台核心接入成果。
- 没有进行真实服务器账号的创建/开战/重连整局联调,也没有执行 Android/iOS 原生设备验收。
- `room.entered.entry` 与 Host 旧方法保留兼容期。新接入使用来源事件和分组能力;后续迁移全部旧调用方后再移除兼容表面。
- 游戏投影为 null 的当前游戏不能展示未实现的局内数据;接入模型时由该游戏提供源,公共命令和视图已经消费该源。
## 工作区保护
没有提交、合并、stash 或服务器改动。没有手改 prefab、scene、anim、meta。原装配脚本通过编辑器移动并保留 UUID;旧公共敏感词模块通过编辑器删除,由对应游戏文件接管。
资产基线对比中,已有场景/prefab/动画等哈希保持;两处原路径不存在:迁出的公共敏感词模块 meta,以及编辑器 refresh 清理的 `assets/.meta` 孤立文件。详细差异保留在 `baseline-asset-differences.json`,不以批量 restore 覆盖工作区。
## 2026-09-08 补充:子游戏身份与导出选择边界
接入、切换子游戏不得修改 framework 代码或资源。框架只消费应用注入的完整身份和 GameDefinition,不内置具体 gameid。
- 各子游戏的 gameid 唯一来源为 `assets/games/<game>/game-config.ts`。
- 应用注册表 `assets/app/composition/game-definitions.ts` 引用对应配置。
- 当前游戏唯一选择项是启动场景应用组件 `SubgameAssets.gameKey`;当前为 `erqiwang`。在 Cocos 编辑器配置该组件并绑定对应 createRoom、room、roomScene 资源。禁止手改场景或 meta。
- 应用层 `assets/app/composition/build-identity.ts` 保存渠道默认值,组合所选游戏身份;`PlatformStartup` 经 `GameComposition` 契约注入 `resolveRuntimeConfig`。
- H5 保持 agentid/channelid/marketid/version 的 URL 覆盖。gameid 不接受 URL 覆盖。原生仍从原接口读取渠道,仅 gameid/version 来自注入配置;没有原生渠道时显式报错,不回退到 H5 渠道默认值。
- 缺少身份或游戏配置直接报错;不猜测默认游戏。旧 bootstrap 入口也必须显式注入身份。
- 二七王保留当前联调注册 ID。template ID 来自原工程 Game_Surface_3/version.js,其在当前开发服务器的注册状态未验证。
本次落实选择与身份注入,未新增按游戏裁剪构建产物的工具。当前注册表仍引用两个游戏;实际导出包是否排除未选游戏代码和资源,需要在后续构建隔离流程中单独落实与检查,不能以运行时只选一个游戏作为裁剪证明。
验证:953/953 自动测试通过;框架与 Cocos 严格类型检查通过;import boundaries、两套房间资源检查和 diff whitespace 检查通过。Cocos MCP 已导入三个新增脚本并生成 meta;重启 Game View 后,PlatformLogin 中 SubgameAssets.gameKey=erqiwang,注入身份与 PlatformStartup 实际解析身份的 gameid 一致,均为当前二七王注册值。未发送创建房间请求,未验证 template 当前服务器注册。
## 后续修订:version.xml 单一来源
以上“game-config.ts 保存 gameid、build-identity.ts 保存渠道和版本”的阶段方案已被 `2026-09-08-version-xml.md` 替代。当前身份与版本唯一来源是各游戏 version.xml;game-config.ts 只定位资源,build-identity.ts 仅保留 XML 没有的网页 marketid。XML value 映射 versionCode(登录协议数值 version),XML name 映射展示 version。此前将原生更新版本与协议版本分别配置的建议不适用,以用户最终确认的映射为准。
@@ -0,0 +1,47 @@
# 子游戏 version.xml:网页预览与原生发布共用
## 日常使用
1. 在 Cocos 启动场景的应用组件 `SubgameAssets` 中选择 `gameKey`,并绑定对应游戏页面和房间资源。当前为 `erqiwang`。
2. 编辑该游戏唯一的 `assets/games/<目录>/resources/<gameKey>/version.xml`。
3. 网页预览直接读取该文件。Web Desktop / Web Mobile 正常构建后,扩展自动将原文件放在输出根目录,与 `index.html` 同级,不需要手动复制。
当前文件:
- 二七王:`assets/games/erqiwang/resources/erqiwang/version.xml`
- 模板游戏:`assets/games/template/resources/Game_Surface_3/version.xml`
## XML 契约
保留原生既有 XML 声明、game 根节点及 agent/game/channel/version 节点,不新增 market 字段。
- `game/agent@id`、`game/channel@id`:网页身份。
- `game/game@id`:所选游戏 gameid。
- `game/version@value`:非负安全整数 versionCode,转换为数值后发送至 `player_login.data.version`。
- `game/version@name`:字符串展示版本 version,例如 `1.8`;登录页显示 `v1.8`,不由 versionCode 拼造。
- 各 name 属性保留供原生读取。
网页 marketid 唯一配置位于应用层 `app/composition/build-identity.ts` 的 `WEB_MARKET_ID`,当前为 4。XML 不包含 marketid,因此不重复维护该字段。现有显式 URL 渠道/版本调试覆盖仍遵循通用启动解析器规则;日常预览无需 URL 身份参数,直接使用 XML。
原生环境的 agentid、channelid、marketid 仍通过既有 settings/uAgent_3 接口读取,缺失时报错,不回退到网页身份。游戏与版本信息来自 XML。未更改服务器、原生接口名或登录协议。
当前 XML 保留迁移前的联调身份与数值版本 1;展示版本采用原游戏 XML 的名称(二七王 1.8、模板 1.1)。旧 XML 的 versionCode 不覆盖当前联调版本;正式发布需在这一份 XML 中配置部署要求的实际版本和身份。
## 接入新游戏
`game-config.ts` 仅指定 `versionResource: '<gameKey>/version'`,不再保存身份和版本。应用注册表引用该配置。
在编辑器中把游戏的 resources 文件夹配置为 Asset Bundle:名称必须是 `version-<gameKey>`。运行时只加载所选版本包,读取 `<gameKey>/version` TextAsset,解析后释放该文本资源。使用编辑器操作资源及 meta,禁止手改序列化文件。
构建扩展 `extensions/youle-version-manifest` 在当前编辑器已启用;新环境需要在项目扩展管理器中启用。它从构建任务真正的启动场景读取游戏选择,检查唯一 XML、合法版本、对应 bundle 元数据和构建结果的 bundle 列表。构建中修改 XML、启动场景或相关元数据会使构建失败,避免生成身份不一致的包。
框架只消费注入身份,不包含具体游戏配置。新增、选择子游戏不需要修改框架代码或资源。本次未实现排除所有未选游戏代码和资源的完整构建裁剪。
## 验证记录
- 956 项框架测试、22 项构建扩展测试通过;框架/Cocos 严格类型检查、import boundaries、两款游戏房间资源检查通过。
- 实际 Cocos Game View:二七王解析 gameid 为当前联调值、versionCode=1、version=1.8;登录页显示 v1.8。创建独立测试组件选择模板后读取到模板 gameid、versionCode=1、version=1.1,未发送业务请求。
- 同时修复现有场景切换清理问题:隐藏已销毁的加载节点时不再访问其子节点;实际预览确认销毁后 dispose 成功,有效但缺失必要组件的节点仍明确报错。
- 最终真实 Web Mobile 构建任务 1788872259776 成功,用时 28 秒,输出目录 `build/web-mobile-version-xml`。构建日志确认扩展在 onAfterBuild 输出根目录 version.xml。
- 源 XML 与导出根目录 XML 的 SHA-256 均为 `6871c9a71f7363261c85de52bd72d1d9fc3cc7dfbdf0c1ef949b11367cb657f7`。
- Web Desktop 已注册同一 hook 并通过注册测试,尚未另跑真实构建;真实原生壳读取未执行。
@@ -0,0 +1,202 @@
# 房间平台重构实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 本计划按依赖顺序执行,不隐含并行代理授权。
**Goal:** 在服务器与协议不变的前提下,实现平台无具体游戏依赖、明确房间生命周期和易用的子游戏接入边界。
**Architecture:** 模块化单体;保留现有网络、状态缓存、会话租约,逐步引入唯一游戏装配、RoomCoordinator/Scope、带来源的输入和公共能力端口。
**Tech Stack:** TypeScript、Cocos Creator 3.8.8、现有 Node 测试、Funplay Cocos MCP。
**Status:** 平台核心重构已实施,执行证据见 [实施记录](../implementations/2026-09-08-room-platform-refactor.md)。未支持的音频所有权与真实账号/原生设备验收单独标记,不作为已通过项。
设计依据:[架构设计](../specs/2026-09-08-room-platform-architecture.md)、[接口迁移](../specs/2026-09-08-room-platform-api-migration.md)。
## 0. 执行约束
- 使用主工作区 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus`,按用户约定在主 checkout 分支工作,不自动 stash 或创建 worktree。
- 每阶段开始记录已有修改;序列化资产必须经 Cocos MCP。不得为精简 diff 手改 prefab/scene/meta。
- 不修改 server;只读核对协议来源。不修改 `projects/Game_Surface_3` 的旧实现来迎合新测试。
- 新旧处理器不同时消费同一业务消息。每阶段都保持一个权威状态来源。
- 测试失败先确认原因,不能通过降低断言、吞异常、补默认值实现“通过”。
- 以下建议新文件名是实施目标;若现有模块已承担相同职责,优先提取/复用而非再建一份。
- 完成各任务后保留可独立审查的 diff;提交/合并按用户另行授权,不自动提交当前混合修改。
## 1. 任务依赖
```text
P0 基线与契约夹具
→ P1 唯一游戏装配
→ P2 不透明载荷与游戏输入
→ P3 房间生命周期和顺序
→ P4 公共能力与游戏投影
→ P5 增量展示与资源
→ P6 双游戏接入和发布边界验证
```
P5 不与 P3/P4 同时改同一 UI 生命周期。完整玩法、回放和非当前支持的 VIP/百人场另行拆任务,不扩大本计划范围。
## 2. P0:冻结兼容基线与资源检查
涉及文件:
- `framework-tests/platform/runtime.test.ts`、`runtime-session.test.ts`。
- `framework-tests/protocol/`,新增房间入口/开战矩阵测试。
- `framework-tests/fixtures/contracts/`,增加经脱敏且标明来源的夹具。
- `scripts/check-import-boundaries.mjs` 及现有 architecture 测试。
- 新增资源装配检查脚本/测试,路径以现有 architecture 测试组织为准。
步骤:
- [x] 记录 git 状态和任务可能涉及的 scene/prefab/meta 哈希,不覆盖未提交工作。
- [x] 对照原 Desk/Net 为创建、登录、加入、开战、解散建立事件顺序断言。
- [x] 用含两个 CreateRoomPage 的夹具验证资源检查会失败,避免重复本次模板脚本残留问题。
- [x] 给现有资产执行只读装配检查,记录发现而非批量自动删除组件。
- [x] 运行当前相关测试,区分已有失败与此次新增断言揭示的缺口。
- [x] 记录目标设备、场景加载、消息密度与聊天长度的测量方法;绝对阈值未确认时不得填假数字。
完成条件:有清晰的已支持范围、真实业务路径基线和可复现的装配错误检查。尚未接入的开战路径标明缺口,不用空钩子变绿。
## 3. P1:唯一游戏装配
涉及文件:
- `assets/scripts/platform-login/PlatformStartup.ts`、`SubgameAssets.ts`、`lobby-panel.ts`。
- `assets/games/erqiwang/`、`assets/games/template/` 的工厂和页面。
- 新增 `assets/app/composition/` 和共享装配契约。
- `framework/presentation/room-chat-config.ts` 的具体游戏内容迁移到游戏定义。
步骤:
- [x] 先写注册/装配测试:两款游戏分别得到自己的创建页、房间 View、规则和 route。
- [x] 建立一个权威游戏定义;配置身份按引用注入,不复制渠道常量。
- [x] 将 PlatformStartup 中具体游戏工厂/View 分支移入组合根。
- [x] 将游戏聊天内容与声明的策略移出公共 framework,公共解析器接收注入内容。
- [x] 更新边界检查,公共 scripts/ui 与 framework 一并检查,仅允许明确 app 根依赖两侧。
- [x] 通过 MCP 更新需要变化的资源引用,保留资源 UUID,验证页面唯一性。
- [x] 在编辑器从真实创建按钮验证:二七王提交 11 位字符串、模板仍提交自己的数组。
完成条件:增加游戏只改该游戏目录与组合注册,公共启动代码不含 gameKey 条件分支;保存后的资源重新加载仍正确。
## 4. P2:不透明载荷与完整游戏输入
涉及文件:
- `framework/sdk/contracts/roomtype.ts`、`game-module.ts`、`platform-events.ts`。
- `framework/protocol/contracts/room-contracts.ts` 与 validation。
- `framework/protocol/platform-handlers.ts`、`first-slice-routes.ts`。
- `framework/platform/runtime-session.ts`、`platform/stores/`。
- 游戏协议适配与相应 runtime/protocol 测试。
步骤:
- [x] 编写传输保持测试:字符串、数组、对象、有限标量在允许的字段位置保持值和类型;undefined/循环/非有限数字拒绝。
- [x] 搜索全部 roomtype 消费方,消除平台的 length/index/格式判断;游戏校验保持在游戏入口。
- [x] 定义带来源的 create/login/join/started/settlement 输入;不删除未知载荷字段。
- [x] 对照原协议补齐 self_makewar/makewar 和附带 deskwar 的 player_prepare/other_join_room。
- [x] 验证准备提交在 started 之前,StartWar 等效入口拿到整包,Reconnect/DeskInfo 拿到各自快照。
- [x] 逐个切换游戏适配器,不让旧 restore 与新输入重复执行。
- [x] 测试非法游戏配置仍由游戏拒绝,而不是平台补值。
完成条件:入口语义无合并丢失,公共协议与玩法协议边界清晰;实际发包内容不因内部类型升级而变化。
## 5. P3:房间协调器与异步生命周期
涉及文件:
- 提取 `framework/platform/room/{room-coordinator,room-scope,room-mailbox,room-entry}.ts`。
- `runtime.ts` 的 SceneOrderedGameSessionHost、`runtime-session.ts`。
- `PlatformStartup.ts` 的 showRoom/房间释放部分和场景适配。
- `game-host-adapter.ts` 的租约和数字工具延期逻辑。
步骤:
- [x] 用可控 Promise 写失败测试:mount 未完成时不得调用依赖就绪的进入逻辑。
- [x] 增加 generation 与 scope,复用现有租约失效机制,避免两套互相独立的有效性判断。
- [x] 实现预检、进入、就绪、失败清理;失败后不得暴露 active 半状态。
- [x] 为加载期间消息建立有界 FIFO;配置中显式提供容量/超时。
- [x] 验证踢出/断线/取消能越过慢加载终止 scope,旧回调晚到只清理自身资源。
- [x] 覆盖普通换服与解散换服,不误销毁需要继续结算的上下文。
- [x] 将重复页面延迟逻辑收敛到统一就绪/取消契约,保留必要的能力冲突规则。
- [x] 验证清理抛错时其他清理仍执行,正常离开和进入失败使用同一 scope 释放路径。
完成条件:创建/加入/恢复在慢加载、连续切房和控制消息插入情况下顺序可预测;业务消息不等待动画,陈旧回调不能发包/写状态。
## 6. P4:能力端口与游戏事实投影
涉及文件:
- `sdk/contracts/game-host.ts`,提取能力契约文件。
- `platform/game-host-adapter.ts`、房间公共命令与策略。
- `presentation/deferred-numeric-tools.ts`、数字输入/倍率模型。
- `RoomPlatformPanels.ts`、`RoomHud.ts`、`NumericToolsPanel.ts`。
- 游戏模型的只读 GameRoomProjection 与展示选择器。
步骤:
- [x] 写接口边界测试:游戏不能直接修改公共玩家、资产、准备状态。
- [x] 区分平台 room stage、游戏 round phase、自身参与状态和局内分数。
- [x] 实现只读投影订阅与组合选择器,测试同一消息只产生完整的展示版本。
- [x] 定义 room/messages/ui/audio/scope 分组能力,移除公共类型对具体 adapter 的反向引用。
- [x] 数字/倍率请求返回 confirmed/cancelled,离房和替换有确定结果;重叠策略测试先行。
- [ ] loading/tips 已接入所有者句柄并通过冲突清理测试;audio 因原生没有停止契约,未发布所有者端口。
- [x] 平台 start/prepare/exit 等命令统一填身份,按钮和摇一摇复用命令策略。
- [x] 保留浏览器适配器来源定义的默认行为,不在 UI 再造 fallback。
完成条件:无任意 patchRoom;游戏可以表达玩法公共展示需要的事实;输入与能力资源不会跨房间泄漏。
## 7. P5:聊天与资源增量优化
涉及文件:
- `RoomChatPanel.ts`、`RoomUi.ts`、聊天状态模型。
- 新增头像资源租约适配与必要的缓存策略。
- `ApplyVotePanel.ts`、`RoomNoticePanel.ts`、HUD 倒计时相关代码仅在明确优化点修改。
步骤:
- [x] 建立追加聊天的基线:新消息前后节点身份、纹理创建数、布局次数和滚动位置。
- [x] 为消息增加本地稳定 ID,按 ID 追加/更新/移除,网络包不增加字段。
- [x] 头像请求去重与租约释放;验证 URL 变化时旧加载不覆盖新行。
- [x] 增加“用户已上翻历史则保持位置”的交互测试。
- [x] 合并同帧绘制并跳过无关选择器更新,业务事件顺序保持不变。
- [x] 后台恢复时倒计时重新计算,定时器不作为结算判断依据。
- [x] 根据基线决定是否需要可变行高虚拟列表;数据规模不足时不引入。
完成条件:追加一条聊天不销毁已有历史行;同 URL 不重复建纹理;真实 Cocos 交互通过;性能报告列出设备与测试场景,不只报告 Node 测试耗时。
## 8. P6:接入套件与整体验收
步骤:
- [x] 模板与二七王使用相同公共 SDK,验证不同 roomtype 类型端到端保持。
- [x] 运行创建/加入/登录恢复/开战/投票/结算/退出矩阵,记录未实现玩法。
- [x] 加载中退出、踢出、换服、重复回包、队列超限、清理失败均有负例。
- [x] 100 次房间进入/退出后检查节点、订阅、计时器和资源租约回到基线。
- [x] 检查编译产物中的资源与调用路径,避免源码正确而预览仍使用旧资产。
- [x] 对每个迁移过的原接口更新本迁移清单的落地位置和状态。
- [ ] Cocos/Windows 本地预览和协议夹具验证已执行;真实服务器账号整局联调、Android/iOS 原生设备验收未执行。Web 构建证据见实施记录。
完成条件:新增第二款游戏无需改平台核心;源码与资源边界均通过;明确列出游戏玩法和模式尚未支持项。
## 9. 验证命令
以下从 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects` 执行。具体测试集随任务扩大,未改变的测试不反复运行;先跑相关集,再在集成阶段跑完整要求。
```powershell
node --experimental-transform-types --test framework-tests/platform/runtime.test.ts framework-tests/platform/runtime-session.test.ts
node --experimental-transform-types --test framework-tests/net/wire-client.test.ts framework-tests/presentation/erqiwang-room-entry.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
node node_modules/typescript/bin/tsc -p YouleNexus/tsconfig.json --noEmit --ignoreDeprecations 6.0 --skipLibCheck --lib ES2020,DOM --types node --strict
node scripts/check-import-boundaries.mjs
```
新增测试路径在创建后加入命令;不把未创建的测试列为已经执行。Cocos prefab/scene 校验与真实交互走 MCP,结构引用通过不等于视觉/交互通过。
## 10. 回退与交付
每个阶段限定文件和资产清单,留存阶段前后验证结果。回退以该阶段精确 diff 为单位,不批量 restore 用户工作区,也不删除整个 library 缓存。
若生命周期改动暴露业务缺口,停止扩大范围,补齐该路径契约和测试;不能恢复为宽松默认值。阶段交付报告写清:改变了什么、兼容路径、测试证据、未实现能力、下一阶段依赖。
本计划不附带部署、提交或合并动作;这些与完成本地阶段实现分别管理。
@@ -0,0 +1,240 @@
# 原接口迁移清单与子游戏接入契约
版本:1.1 · 2026-09-08 · 状态:已落地 API 与兼容边界见第 7 节及[实施记录](../implementations/2026-09-08-room-platform-refactor.md)。前文保留原接口迁移目标。
主文档:[房间平台架构设计](2026-09-08-room-platform-architecture.md)。本清单基于原工程 Input/Output 文件及其调用点,区分“保留业务能力”与“保留旧调用方式”。默认不保留全局 Game_Modify、Utl、Desk、C_Player 接口。
## 1. 迁移规则
- 保留:协议语义、原始载荷、来源定义的顺序、玩家可观察的正确结果。
- 改造:直接读写全局状态、精灵编号、跨层调用、手工拼公共身份、全局输入回调。
- 分离:在线与回放、局内分数与账户资产、请求与确认、状态重绘与一次性业务副作用。
- 不照搬:示例默认值、空钩子掩盖未接入、吞异常、以显示状态判断业务阶段。
- 必需项缺失在装配时失败;可选项必须声明“未提供”的行为,不能通过任意 fallback 冒充支持。
## 2. Input:平台通知游戏
源码:[02_SubGame_Input.js](../../../projects/Game_Surface_3/js/01_SubGame/02_SubGame_Input.js)。
### 2.1 房间进入与游戏协议
`StartWar(_msg)` → started 输入。保留触发 sourceRpc 和原整包;包括 self_makewar、makewar、加入/准备附带 deskwar,不仅监听单个 rpc。
`_ReceiveData(_msg)` → game-message 输入。游戏自己的协议适配器校验 rpc/payload,平台只校验路由归属与传输外层。
`createRoom(roomtype,infinite)`、`onCreateRoom(data)` → created 成功输入的游戏适配。旧包装层可能在失败后仍调用钩子,必须记录并改为明确的结果/成功边界,禁止把失败视为建房成功。
`myJoinRoom(_msg)` → joined 输入及对应 waiting/snapshot/started 分支。不能因为有快照而省略主动加入语义。
`Reconnect(deskinfo)` → login-restored 输入;`ReconnectNoMakewar` 虽未在此文件声明,但在 Desk.login 调用,应纳入 login-waiting 契约。
`DeskInfo(deskinfo)` → joined-snapshot。与登录重连区分,游戏可以映射到共享内部还原函数,但平台不得强迫两者等同。
`onCreateDesk(roomtype)` → 游戏初始化/规则预检。座位规则可以先运行,任何需要 View 的工作移到挂载就绪后。
### 2.2 玩家与公共状态
`playerJoinRoom`、`playerLeaveRoom`、`playerOffline`、`playerOnline`、`onReady`、`changeSeat` → 类型明确的公共成员输入,始终使用服务器座位。
`myExitRoom`、`breakRoom` → 保留服务端确认的自身离开原因,在旧上下文仍可读取的收尾阶段通知,然后统一释放。游戏不能在通知中再次发送同一退出命令。
`playerphonestate` → 设备/玩家通话状态能力输入。旧参数 0/1 的实际语义以 Desk.call_phone/hangup_phone 为准,不照抄注释中的歧义文本。
### 2.3 视图生命周期
`onEnterMainScene` → View 挂载就绪后的明确通知;不通过读取精灵可见性判断。
`onExitMainScene`、`closeGameScene`、`stopAllSounds` → 由 scope 组织的停止/卸载/音频释放。音频使用房间或游戏专属 owner,不误停应用其他声音。
`updateScene` → 从现有游戏状态重建展示。不会重新解释创建响应、再次发命令或再次产生分享/活动结果。
`onMainMenuScene` → 应用/大厅导航生命周期,非每房间 GameModel 必需能力。
### 2.4 结算与活动
`Free` → dissolution-settlement,区分普通投票确认与 freeNow。deskfree 内容由游戏解析。
`onSurrender` → 对应来源的业务结果输入,游戏解释;公共投降入口只负责协议封装和展示流程。
`onEnterVideo` → 可选回放能力,绑定独立 ReplayContext。
`onCloseVip`、`getShareRoom` → 大厅/房间列表功能的可选扩展,不强迫纯房间游戏实现。
### 2.5 同步规则与描述
`getRoomInfo`、`getFullRoomInfo`、`getRoomTopDescAry` → GameRoomDescription。返回标题、说明项等结构化内容,UI 决定换行和字体,不让游戏按“18/26 个字符”进行硬布局。
`getRoomMode`、`getStarLimit`、`getLeaveLimit`、`getMult`、`getVideoByRoomType` → 游戏规则/策略查询;不修改 roomtype、不发包、不写 UI。仅保留项目实际支持的功能,未提供时有明确的能力边界。
创建前的规则预览与入房后的权威公共回包不能混用:例如 roommode 已由服务端明确给出时,不用本地规则覆盖。
`onGameConfig` → 游戏配置生命周期。配置来源唯一,更新时有版本/替换语义,游戏不得自行从另一个 URL 再读一套。
### 2.6 用户输入与设备
`calResult`、`onCheckInput` → 对应具体输入请求的结果,不再使用全局回调入口。
`shakeEvent` → 平台设备意图适配。平台提供的“摇动开始”与开始按钮调用同一个 start 命令;游戏自定义摇动行为可以声明独立能力,不能复制公共身份拼包代码。
`onLocationInfo` → Location 能力的结果/订阅。浏览器替身数据的缺省由浏览器适配器明确定义,游戏不猜地址或坐标。
`onOpenHelp(spid)` → 帮助内容/页面能力,不暴露原精灵编号。
### 2.7 gameHallImport
appStart、jumpMenuScene、gameStart、setGameList、clearGameinfo、getWebdata、isInstalled、up_imgurl、getphoto 属于宿主/大厅适配。应与房间游戏 SDK 分开,不要求每个子游戏定义一套全局对象。
isInstalled 等示例固定返回值不能成为新实现的真实能力证明。
## 3. Output:游戏使用平台能力
源码:[08_Utl_Output.js](../../../projects/Game_Surface_3/js/00_Surface/08_Utl_Output.js)。按职责归属迁移,不按原函数顺序重新堆成大工具类。
### 3.1 身份、玩家和房间查询
涉及:getMyInfo、getGameID、getAgentID、getPlayeridBySeat、getNicknameBySeat、getSexBySeat、getMyPlayerid、getMySex、getRoomcode、getMySeat、getMyOpenid、getPlayerInfoBySeat、getPlayerList、getPlayerCnt、getPlayerReadyState、getBeanBySeat、getShortCode、getIsInfinite、getInfMode。
归属:RoomReadPort 的只读快照、必要身份投影和具名选择器。occupiedCount 与 seatCount 分开,空位有明确表示;缺失玩家不能返回 -1 后让 UI 猜测。
getMyOpenid 不应作为普通玩法的默认必需字段;确有原业务用途时,通过最小身份能力提供,不把完整登录响应暴露给所有游戏。
changeToStatus → 统一 seat mapper。服务端座位和显示座位可使用不同类型别名,减少混用,转换只在边界进行。
getOnState/isMainScene → 分别读取状态投影和生命周期,不能读取 UI 数组或 Node.active 来决定业务。
### 3.2 网络与房间命令
sendData → 当前游戏 route 绑定的 GameMessagePort。app/route 不由子游戏每次传入,rpc/data 仍由游戏定义。
sendExitRoom/sendChangeRoom/sendText/enterShareRoom/getShareRoom → 公共具名命令。平台负责身份和房间参数,调用方只提供真正变化的业务参数。
Exit → 不暴露为可任意清空平台状态的方法。区分请求退出、已确认的本地会话结束、结算返回大厅,由协调器内部执行。
openMatchUrl、getMathInfo/getIsMathInfo、getAdvanced/getPlayerAdvanced、openSnrOption、getRebateRange → 对应比赛、VIP/房间策略能力。不要混入通用游戏传输端口;不支持的模式明确标记。
### 3.3 状态修改与展示
setGrade → 游戏状态中的局内分数,经 GameRoomProjection/HUD 展示模型更新,布局由 View 负责。
changeBean → 先追踪调用源。若是玩法结算的相对得分,应写游戏投影;若是权威资产更新,走对应协议来源。禁止 initialBean + delta 直接覆盖平台账户余额。
setPlayerPrepare/setDeskStage/changePlayerState → 拆分公共协议事实、游戏阶段/参与状态、公共动作限制。必要映射显式声明,禁止通用 setter 任意修改平台状态。
closeMainSceneButton/closeInvitation/closeCommunication/updatePlayerInfoUI → 公共展示模型的状态或具名 UI 操作。业务长期显隐通过模型表达,短暂关闭模态框通过所有者句柄执行,不直接查找平台节点。
getExitVisible/getChangeVisible → RoomPolicy 派生值。命令执行时仍重新验证当前 scope 与策略,不能只靠按钮隐藏防止错误操作。
### 3.4 UI、资源和声音
openTips/openTips2/closeTips → 有所有者的提示能力,返回可关闭句柄;一个游戏不能关闭其他来源的提示。
Layer612_Tips 的内容仅面向玩家业务。reportError/catch/Error.message 不得直接转接提示端口;配置、协议、资源及代码异常写诊断日志,业务提示通过单独的 notify/showBusinessTip 入口。不能把技术异常转移到 Kick 等其他玩家面板。
startLoad/endLoad → 任务绑定的 loading 句柄或计数租约;并发两个任务时,一个结束不能关闭另一个的加载提示。
openInputPanel/closeInputPanel/openTextInput → 有请求 ID(仅本地)、明确结果和取消语义的输入能力。
getMultipleResult → 纯计算与倍率偏好读取分开;展示倍率选择交给 UI 端口,禁止计算函数暗改 GameData.OrgArr 或存储。
playMusic/playSound/stopMusic/stopSound → AudioPort。声音标识来自游戏资源定义,播放策略遵循平台设置,句柄受 scope 管理。
getHeadimgSrc/openInfo/openInfo1 → 头像资源租约和玩家资料能力;不返回 116+seat 这类精灵 ID。
setFontColor/convertNumberToImg/getNameImgFrame_1/getNameImgFrame_2 → 展示适配层。Cocos 文本使用 Label/BitmapFont,具体业务不依赖字符替换 b/c/d 或图集帧号。
getRoomCardName/getstarName → 平台展示配置投影,保留单一来源,不在游戏里重复定义。
### 3.5 持久化与工具
SetStorage、Config.pre_* → 存储命名空间与键定义集中管理;账户/游戏作用域明确,不能以当前全局身份拼出不确定的键。
SaveData/ReadData/checkKey/RemoveItemByKey/ClearStorage、setCookie/getCookie/delCookie → 存储适配器。游戏只获得自身命名空间,不能清空应用全部登录/平台数据。
saveRoomtype/getRoomtype/delRoomtype → 游戏配置历史能力;保存与读取不转换 roomtype,版本迁移由游戏负责。
saveGradeInfo/readGradeInfo → 战绩/回放数据仓储,不与房间临时模型混放;来源、保留上限与账号隔离必须明确。
clone/removeItemFromArray → 普通内部工具或删除;不作为平台业务 SDK。新架构使用明确的数据所有权与不可变更新,避免原 clone 破坏数组类型。
### 3.6 结算、分享和回放
onGameFinished → 拆为已解析的结算事实、游戏结果展示和显式分享请求;渲染函数重复调用不会重复分享。
typeForActivity → 可选活动能力的一次性业务通知,绑定结算来源;不能在重绘中调用。
gameOver/mainScene → 由明确结束原因、结算状态与导航结果驱动。结束一局不等于结束房间。
openVideo/closeVideo → ReplaySession,独立状态、时钟、玩家视角,不修改在线房间数据。
### 3.7 设备与宿主环境
getLocation/getPhoneInfo/gameCopytext/getAppService/closeWindow → 设备/宿主能力;浏览器和原生各自实现相同契约。
getGameConfig/getVersionState/getH5Version/getIsDebugger/getShowShare → 配置与能力投影。新游戏优先判断声明的能力,而非散布平台版本号分支;调试配置不进入玩法规则。
getPayCodeBySeat、支付/VIP/分享等与房间非核心功能相连的读取 → 在迁移对应模式时核对实际字段来源,不能为满足接口数量提前返回假值。
## 4. 新接口的使用约定
### 4.1 查询
一次业务处理读取同一 revision 的快照。对象只读,任何修改都不通过 getter 返回对象完成。需要最新状态时再次读取;异步回调读取前确认 scope 仍有效。
### 4.2 命令
具名命令检查:scope 有效、登录有效、房间身份匹配、当前策略允许、是否已有同类 pending。发送成功不代表业务确认。
服务器没有关联 ID 时不得虚构关联。对于同房间互斥操作采用本地串行 pending,收到对应合法回复或超时后解除。重连恢复以服务器状态为准,不自动重发非幂等命令。
### 4.3 UI 请求
```ts
type InputResult<T> =
| { kind: 'confirmed'; value: T }
| { kind: 'cancelled'; reason: 'user' | 'scope-ended' | 'replaced' };
```
输入请求在离房时必须结束。数值模式返回 number,允许前导零的文本模式返回 string,不能在平台随意转换。类型签名可按不同请求模式精确约束。
### 4.4 投影
游戏模型是事实来源,投影只负责提取公共 HUD 所需字段。投影更新不能反向产生同一输入事件,避免状态→事件→状态循环。
平台准备/资产信息与投影若冲突,优先按各字段定义的唯一来源处理,并报告不一致,不采用“最后写入者胜出”。
### 4.5 一次性事件
结算活动、音效、分享等与状态渲染分开。重连直接重建状态,不默认重放历史音效、公告或分享。哪些效果需要重放由具体玩法契约明确规定。
## 5. 接入交付清单
- 一个权威 GameDefinition,明确必需和可选能力。
- roomtype 编码/解析与规则测试,不要求平台理解格式。
- 一个游戏模型以及进入来源/游戏 rpc 到模型的映射。
- 创建页、房间 View 和资源定义;恰好一个页面契约实现。
- SDK 回归夹具:创建、加入、重连有/无快照、开战、结算、取消、退出。
- Cocos 实际创建按钮提交与 WebSocket 发包验证。
- 已实现/未实现能力清单,不以空处理器通过检查。
只增加游戏目录和应用注册项即可接入。若仍必须修改平台菜单脚本、公共协议路由或提供很多固定返回值,说明边界设计尚未达标。
## 6. 过渡期
旧接口兼容层仅用于逐条迁移现有游戏,不作为新 SDK 的正式表面。每项兼容入口必须有目标能力、测试和移除条件。
不要求一次迁移所有历史宿主/VIP/回放功能,但必须明确未支持范围。任何尚未找到调用源或协议依据的接口不得凭名称实现。
## 7. 本轮实际落地位置
- StartWar:`PlatformToGameEvent.room.started`,保留完整 message/sourceRpc;线上 other_makewar 对应原 makewar 函数。原始游戏包由 `GameModule.handleGameMessage` 接收。
- createRoom/onCreateRoom、myJoinRoom、Reconnect/DeskInfo:`room.entered.entry` 的来源分支与兼容 `restore`。开战优先于加入快照恢复。游戏自身牌桌显示仍未迁移。
- 房间查询、座位映射:`requireRoomCapabilities(host).room.getSnapshot / subscribe / seat`。只读数据来自平台 Store。
- 准备、退出、申请解散:同一 room 端口的 `prepare / requestExit / applyDissolution`,公共身份与投影权限由 PlatformCommands 处理。自定义游戏包使用 `messages.send`。
- 数字、数字文本、倍率:`ui.requestNumber / requestDigitText / requestMultiplier`,返回 InputResult;本地取消、替换和 scope 结束均有结果。原回调式 Host 命令暂留兼容。
- loading、玩家业务提示:`ui.beginLoading / showBusinessTip` 返回 `{close()}`,通过真实 FeedbackPanels 仲裁所有者。技术错误不得传入这些接口。
- 资源生命周期:`scope.active / defer`;底层 RoomScope 复用既有 Host 租约失效机制。取消先撤销能力,再逐项释放资源。
- 局内公共事实:`createGameRoomProjection` 的 owner 提交完整版本,GameModule 暴露 source;公共 render 第三个参数和命令权限读取同一来源。现有模板/二七王返回 null,未生成伪 phase 或局内分数。
- 头像、聊天:RoomUi 使用共享租约;RoomChatPanel 使用本地 ID 增量行和同帧合并。协议不增加消息 ID。
- 音频所有者接口:未提供,原生契约没有播放停止能力。原有 RoomVoiceAdapter 的语音功能保留;支付、分享、回放和其他未迁移 Output 接口继续按其原边界单独接入。
新游戏只需要自己的代码/资源、组合根注册,以及编辑器中对应的 SubgameAssets 资源配置。运行期不会预执行工厂来推断模块或资源;真实 open 时只创建一个模块并校验。
@@ -0,0 +1,391 @@
# 房间平台与子游戏接入架构设计
版本: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 基线确定并写入单一配置来源。第一阶段按平台核心与契约优先安排;二七王完整玩法和回放另立范围,不隐含在此次重构中。