Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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<PlayerInfo> 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<PlayerInfo> 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<T>(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<PlayerInfo> 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<PlayerInfo> 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<PlayerInfo> 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<PlayerInfo> 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<float> _probe;
|
||||
private IRoomCtx _ctx;
|
||||
public ClockProbeRoom(Action<float> probe) { _probe = probe; }
|
||||
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) => _ctx = ctx;
|
||||
public void OnMessage(int playerId, NetMessage message) { }
|
||||
public void OnTick(float dt) => _probe(_ctx.Timer.Now);
|
||||
public void OnRoomEnd() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user