# 结算结果传递(RoomEndResult)设计 日期:2026-07-09 状态:已与用户确认(方案 A:`IRoomCtx.EndRoom` 加带结果重载) ## 背景 / 问题 对局结束后框架结算弹框只能显示"游戏结束 + 房间号":网关 `ServerLoop.OnRoomEnded` 硬编码 `RoomEndMsg { WinnerPlayerId = 0, ResultBlob = empty }`(ServerLoop.cs:412),而游戏逻辑 (`RpsServerRoom`)的胜负与比分只写了日志,框架 `IRoomCtx.EndRoom()` 没有传结果的通道。 这是既有占位欠账(见 minigame 进度备忘"`RoomEndMsg.WinnerPlayerId=0` 是占位")。 协议层 `RoomEndMsg`(FrameworkMessages.cs:219)早已有 `WinnerPlayerId` + `ResultBlob` 两字段,客户端 `MiniGameHost.ShowResultDialog` 也已按这两字段渲染——缺的只是服务端从 游戏逻辑到网关的管道。**零协议改动。** 用户确认的结算内容:**胜负 + 比分**(标题胜利/失败/平局,正文最终比分文本)。 ## 设计 ### 1. 共享接口(`Client/Assets/Framework/Shared/`) (实施修订:`RoomEndResult` 按仓库 DTO 惯例落在 `GameTypes.cs`,接口重载在 `IGameServerRoom.cs`。) ```csharp public sealed class RoomEndResult { public int WinnerPlayerId; // 0 = 平局/无胜者(与 RoomEndMsg 语义一致) public byte[] ResultBlob; // 小游戏自定义结算数据;约定:放 UTF-8 文本时框架弹框直接显示 } public interface IRoomCtx { // ...现有成员不变... void EndRoom(); // 保留:等价 EndRoom(null) void EndRoom(RoomEndResult result); // 新增 } ``` 兼容性:游戏 DLL 只**消费**不**实现** `IRoomCtx`,给接口加成员不破坏已发布旧模块 (旧模块调无参版 → 结果为 null → 弹框行为同现状)。实现方仅框架内 `RoomCtx` 与测试 fake,一起改。不使用默认接口方法(避开 HybridCLR/mono DIM 运行时风险)。 ### 2. 服务端宿主(`Server/Server.Host/`) - `RoomCtx`:构造参数 `Action endRoom` → `Action`; `EndRoom()` → 回调(null);`EndRoom(result)` → 回调(result)。 - `Room`:`RequestEnd(RoomEndResult result)` 存入 `public RoomEndResult EndResult` 属性(多次调用以最后一次为准);保留无参 `RequestEnd()` = 传 null。 - `RoomHost.TickAll` 返回 `IReadOnlyList` → `IReadOnlyList`: `public readonly struct EndedRoom { public string RoomId; public RoomEndResult Result; }`, 在 `ReleaseRoom` 前捕获 `room.EndResult`。现有使用 TickAll 的调用点与测试同步更新。 - "Start 即结束"路径:`ServerLoop.CreateRoomForSeats` 持有 `Room` 引用,直接读 `room.EndResult`。 ### 3. 网关(`Server/Gateway/ServerLoop.cs`) `OnRoomEnded(string roomId, RoomEndResult result)`: ```csharp var msg = new RoomEndMsg { WinnerPlayerId = result?.WinnerPlayerId ?? 0, ResultBlob = result?.ResultBlob ?? Array.Empty() }; ``` ### 4. RPS 服务端(`Server/games-src/RPS/RPS.Server/RpsServerRoom.cs`) - `OnRoomStart` 记录 `_pidOfSeat[2]`(座位→pid;AI 玩家也有 pid)。 - 结束时(`Phase == Finished`): - `Winner == 2`(平局)→ `WinnerPlayerId = 0`,文本 `"平局 {s0} : {s1}"`; - 否则 → `WinnerPlayerId = _pidOfSeat[winner]`,文本 `"最终比分 {s0} : {s1}"`。 - 文本按座位序,双端同文本(广播);UTF-8 编码进 `ResultBlob`,调 `EndRoom(result)`。 ### 5. 客户端弹框(`Client/Assets/Script/xmain/MiniGame/MiniGameHost.cs`,框架层) RPS 客户端零改动。 - `ResultTitle`: - `winner == _selfPlayerId`(非 0)→ "胜利"; - `winner != 0` 且非自己 → "失败"; - `winner == 0`:blob 有文本 → "平局";blob 空 → "游戏结束" (旧模块/异常结束无结果时保持中性标题,不误报平局)。 - `ResultMessage`:blob 解出非空文本 → 只显示 blob 文本(去掉裸 pid 行); 空 → 维持现状回退(房间号)。 ## 测试(TDD) - **Server.Host.Tests**:①`RoomCtx.EndRoom(result)` → `Room.EndResult` 捕获; ②`TickAll` 返回的 `EndedRoom` 携带结果;③未设结果时 `Result == null`; ④`RpsServerRoomTests`:对局打完,以正确胜者 pid 与比分文本调了 `EndRoom` (含平局→pid 0 分支)。 - **Gateway.Tests**:RPS 端到端断言收到的 RoomEnd 帧 `WinnerPlayerId`/`ResultBlob` 非占位;`build-test-games.sh` 重建 rps 测试夹具(RPS.Server 源码变更)。 - **客户端**(本环境无 Unity):IDE 诊断/csc 0 错;用户 Unity Play 一局人工核对弹框 (胜利/失败/平局标题 + 比分文本,点确定正常退出)。 ## 部署影响 - 服务端:网关 + Server.Host 一体重编部署即生效。 - RPS 服务器模块:须经发布流水线重发 `deploy/minigame/rps//` (旧包兼容不崩,只是弹框无结算内容)。 - 客户端:`XWorld.Link` 与 `XWorld.Framework.Shared` 热更 DLL 重出。 ## 不做(YAGNI) - `SetResult()` 两步接口(无场景); - 结构化结算(每回合出招记录等,用户选了胜负+比分); - 宿主强制结束时的结果通道(结果恒 null → 弹框中性"游戏结束",可接受)。