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,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 基线确定并写入单一配置来源。第一阶段按平台核心与契约优先安排;二七王完整玩法和回放另立范围,不隐含在此次重构中。