using System; using System.Collections.Generic; using Xunit; using XWorld.Framework; using XWorld.Server.Host; namespace XWorld.Server.Host.Tests { public class RoomTests { private sealed class FakeOutput : IRoomOutput { public int Broadcasts; public void Broadcast(NetMessage m) => Broadcasts++; public void SendTo(int playerId, NetMessage m) { } } // 正常房间:第 N 次 tick 时通过 ctx.EndRoom 请求结束 private sealed class CountingRoom : IGameServerRoom { private IRoomCtx _ctx; private int _ticks; public int EndCalls; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) => _ctx = ctx; public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) { _ticks++; _ctx.Broadcast(new NetMessage(1, new byte[0])); if (_ticks >= 2) _ctx.EndRoom(); } public void OnRoomEnd() => EndCalls++; } // 故障房间:OnTick 抛异常 private sealed class ThrowingRoom : IGameServerRoom { public int EndCalls; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) { } public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) => throw new InvalidOperationException("boom"); public void OnRoomEnd() => EndCalls++; } private static (Room room, RoomCtx ctx, T logic) Build(string id, T logic) where T : IGameServerRoom { var clock = new ManualClock(); Room room = null; var ctx = new RoomCtx(new DeterministicRandom(1), clock, new ConsoleLogger(), new InMemoryStorage(), new FakeOutput(), r => room.RequestEnd(r)); room = new Room(id, logic, clock, ctx, new ConsoleLogger()); return (room, ctx, logic); } private static readonly IReadOnlyList Players = new[] { new PlayerInfo { PlayerId = 1, Name = "P", IsAI = false } }; [Fact] public void NormalRoom_EndsAfterRequest_AndCallsOnRoomEndOnce() { var (room, _, logic) = Build("r1", new CountingRoom()); room.Start(Players); Assert.True(room.IsActive); room.Tick(1f); // _ticks=1 Assert.True(room.IsActive); room.Tick(1f); // _ticks=2 → EndRoom 请求 → 本 tick 后结束 Assert.False(room.IsActive); Assert.Equal(1, logic.EndCalls); room.Tick(1f); // 已结束,再 tick 无效 Assert.Equal(1, logic.EndCalls); } [Fact] public void ThrowingRoom_FaultsOut_Safely_WithoutPropagating() { var (room, _, logic) = Build("r2", new ThrowingRoom()); room.Start(Players); var ex = Record.Exception(() => room.Tick(1f)); // 不应向外抛 Assert.Null(ex); Assert.False(room.IsActive); Assert.True(room.FaultedOut); Assert.Equal(1, logic.EndCalls); // 故障也走一次安全结束 } [Fact] public void ClockAdvances_BeforeOnTick() { float seen = -1f; var capture = new ClockProbeRoom(now => seen = now); var (room, _, _) = Build("r3", capture); room.Start(Players); room.Tick(2f); Assert.Equal(2f, seen); // OnTick 内可见已推进的时钟 } // 在 OnRoomStart / OnMessage 抛异常的故障房间 private sealed class ThrowOnStartRoom : IGameServerRoom { public int EndCalls; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) => throw new InvalidOperationException("start boom"); public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) { } public void OnRoomEnd() => EndCalls++; } private sealed class ThrowOnMessageRoom : IGameServerRoom { public int EndCalls; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) { } public void OnMessage(int playerId, NetMessage message) => throw new InvalidOperationException("msg boom"); public void OnTick(float dt) { } public void OnRoomEnd() => EndCalls++; } [Fact] public void ThrowOnStart_FaultsOut_Safely() { var (room, _, logic) = Build("rs", new ThrowOnStartRoom()); var ex = Record.Exception(() => room.Start(Players)); Assert.Null(ex); // 不向宿主外抛 Assert.False(room.IsActive); Assert.True(room.FaultedOut); Assert.Equal(1, logic.EndCalls); // 安全结束一次 } [Fact] public void ThrowOnMessage_FaultsOut_Safely() { var (room, _, logic) = Build("rm", new ThrowOnMessageRoom()); room.Start(Players); Assert.True(room.IsActive); var ex = Record.Exception(() => room.Deliver(1, new NetMessage(0, new byte[0]))); Assert.Null(ex); Assert.False(room.IsActive); Assert.True(room.FaultedOut); Assert.Equal(1, logic.EndCalls); } // 结束时带结算结果的房间 private sealed class ResultRoom : IGameServerRoom { public RoomEndResult Result; private IRoomCtx _ctx; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) => _ctx = ctx; public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) => _ctx.EndRoom(Result); public void OnRoomEnd() { } } [Fact] public void EndRoomWithResult_IsCapturedOnRoom() { var result = new RoomEndResult { WinnerPlayerId = 7, ResultBlob = new byte[] { 9 } }; var (room, _, _) = Build("rr", new ResultRoom { Result = result }); room.Start(Players); room.Tick(1f); Assert.False(room.IsActive); Assert.Same(result, room.EndResult); } [Fact] public void EndRoomWithoutResult_EndResultIsNull() { var (room, _, _) = Build("rn", new CountingRoom()); // CountingRoom 调无参 EndRoom room.Start(Players); room.Tick(1f); room.Tick(1f); // 第 2 tick 触发 EndRoom() Assert.False(room.IsActive); Assert.Null(room.EndResult); } private sealed class ClockProbeRoom : IGameServerRoom { private readonly Action _probe; private IRoomCtx _ctx; public ClockProbeRoom(Action probe) { _probe = probe; } public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) => _ctx = ctx; public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) => _probe(_ctx.Timer.Now); public void OnRoomEnd() { } } } }