Files
AIC-Project/docs/superpowers/specs/2026-07-09-room-end-result-design.md
2026-07-10 10:24:29 +08:00

4.9 KiB

结算结果传递(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。)

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 endRoomAction<RoomEndResult>; EndRoom() → 回调(null);EndRoom(result) → 回调(result)。
  • Room:RequestEnd(RoomEndResult result) 存入 public RoomEndResult EndResult 属性(多次调用以最后一次为准);保留无参 RequestEnd() = 传 null。
  • RoomHost.TickAll 返回 IReadOnlyList<string>IReadOnlyList<EndedRoom>: 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):

var msg = new RoomEndMsg {
    WinnerPlayerId = result?.WinnerPlayerId ?? 0,
    ResultBlob     = result?.ResultBlob ?? Array.Empty<byte>()
};

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/<ver>/ (旧包兼容不崩,只是弹框无结算内容)。
  • 客户端:XWorld.LinkXWorld.Framework.Shared 热更 DLL 重出。

不做(YAGNI)

  • SetResult() 两步接口(无场景);
  • 结构化结算(每回合出招记录等,用户选了胜负+比分);
  • 宿主强制结束时的结果通道(结果恒 null → 弹框中性"游戏结束",可接受)。