fix: improve minigame reconnect flow

This commit is contained in:
ud18010
2026-07-29 19:30:50 +08:00
parent 6a5e534d00
commit 42c3874d82
21 changed files with 979 additions and 52 deletions
@@ -18,10 +18,11 @@ namespace XWorld.Framework.Tests
[Fact]
public void MatchRequest_RoundTrips()
{
var m = new MatchRequestMsg { GameId = "rock_paper_scissors", Version = 3 };
var m = new MatchRequestMsg { GameId = "rock_paper_scissors", Version = 3, RequestId = 42 };
var d = MatchRequestMsg.Decode(m.Encode());
Assert.Equal(m.GameId, d.GameId);
Assert.Equal(m.Version, d.Version);
Assert.Equal(m.RequestId, d.RequestId);
}
[Fact]
@@ -29,9 +30,11 @@ namespace XWorld.Framework.Tests
{
var m = new MatchFoundMsg
{
GameId = "rps",
RoomId = "room-42",
Version = 3,
SelfPlayerId = 1001,
RequestId = 99,
Players =
{
new PlayerInfo
@@ -55,9 +58,11 @@ namespace XWorld.Framework.Tests
},
};
var d = MatchFoundMsg.Decode(m.Encode());
Assert.Equal("rps", d.GameId);
Assert.Equal("room-42", d.RoomId);
Assert.Equal(3, d.Version);
Assert.Equal(1001, d.SelfPlayerId);
Assert.Equal(99, d.RequestId);
Assert.Equal(2, d.Players.Count);
Assert.Equal(1001, d.Players[0].PlayerId);
Assert.Equal("Alice", d.Players[0].Name);
@@ -101,10 +106,30 @@ namespace XWorld.Framework.Tests
[Fact]
public void Error_RoundTrips()
{
var m = new ErrorMsg { Code = 404, Message = "version mismatch" };
var m = new ErrorMsg { Code = 404, Message = "version mismatch", RequestId = 7 };
var d = ErrorMsg.Decode(m.Encode());
Assert.Equal(404, d.Code);
Assert.Equal("version mismatch", d.Message);
Assert.Equal(7, d.RequestId);
}
[Fact]
public void MatchControl_RoundTrips()
{
var m = new MatchControlMsg { RequestId = 123 };
var d = MatchControlMsg.Decode(m.Encode());
Assert.Equal(123, d.RequestId);
Assert.Equal(0, MatchControlMsg.Decode(System.Array.Empty<byte>()).RequestId);
}
[Fact]
public void MatchAssigned_RoundTrips()
{
var m = new MatchAssignedMsg { GameId = "rps", Version = 2, RequestId = 88 };
var d = MatchAssignedMsg.Decode(m.Encode());
Assert.Equal("rps", d.GameId);
Assert.Equal(2, d.Version);
Assert.Equal(88, d.RequestId);
}
[Fact]
@@ -141,6 +166,7 @@ namespace XWorld.Framework.Tests
Assert.Equal((ushort)7, (ushort)FrameworkOpcode.Error);
Assert.Equal((ushort)16, (ushort)FrameworkOpcode.WorldChatSend);
Assert.Equal((ushort)17, (ushort)FrameworkOpcode.WorldChatMessage);
Assert.Equal((ushort)18, (ushort)FrameworkOpcode.MatchCancel);
}
[Fact]
@@ -209,6 +235,7 @@ namespace XWorld.Framework.Tests
MatchFoundMsg d = MatchFoundMsg.Decode(w.ToArray());
Assert.Equal("legacy-room", d.RoomId);
Assert.Equal(0, d.RequestId);
Assert.Equal(2, d.Players.Count);
Assert.Equal(1001, d.Players[0].PlayerId);
Assert.Equal("LegacyAlice", d.Players[0].Name);
+7 -2
View File
@@ -14,6 +14,9 @@ namespace XWorld.Server.Gateway.Runner
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Server.Host.Tests", "TestGames"));
int port = int.TryParse(GetArg(args, "--port"), out int p) ? p : 5005;
int tickMs = int.TryParse(GetArg(args, "--tickMs"), out int t) ? t : 100;
long reconnectWindowTicks = long.TryParse(GetArg(args, "--reconnectWindowTicks"), out long rw)
? rw
: ServerLoop.DefaultReconnectWindowTicks;
long matchTimeoutTicks = long.TryParse(GetArg(args, "--matchTimeoutTicks"), out long mt) ? mt : 150;
string devToken = GetArg(args, "--devToken");
string advertiseWs = GetArg(args, "--advertiseWs");
@@ -49,13 +52,15 @@ namespace XWorld.Server.Gateway.Runner
}
var host = new GameServerHost();
await host.StartAsync(gamesRoot, tickMs, matchTimeoutTicks: matchTimeoutTicks, listenPort: port,
await host.StartAsync(gamesRoot, tickMs, reconnectWindowTicks: reconnectWindowTicks,
matchTimeoutTicks: matchTimeoutTicks, listenPort: port,
bindAllInterfaces: lan,
devDiscoveryToken: devToken, advertiseGatewayUrl: advertiseWs, advertiseResourceBaseUrl: advertiseCdn,
authSecret: authSecret, authDbPath: authDbPath);
Console.WriteLine("XWorld Gateway smoke server started.");
Console.WriteLine(" gamesRoot: " + gamesRoot);
Console.WriteLine(" tickMs: " + tickMs + ", matchTimeoutTicks: " + matchTimeoutTicks);
Console.WriteLine(" tickMs: " + tickMs + ", reconnectWindowTicks: " + reconnectWindowTicks +
", matchTimeoutTicks: " + matchTimeoutTicks);
Console.WriteLine(" ws: " + host.WsBaseUrl + "/ws?pid=1");
if (!string.IsNullOrEmpty(devToken))
Console.WriteLine(" dev-discovery: ON (UDP " + host.DevDiscoveryPort + "), advertise ws=" +
@@ -192,6 +192,67 @@ namespace XWorld.Server.Gateway.Tests
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
}
[Fact]
public void MatchCancel_AfterRoomCreated_ClearsRoomAndAllowsPlayerToMatchAgain()
{
var loop = NewLoop(matchTimeoutTicks: 0);
var c = new FakeConnection(5);
loop.Submit(new ConnectCommand { PlayerId = 5, Connection = c });
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.RandomMatchRequest) });
loop.DrainAndTick(1f);
Assert.True(HasMatchFound(c), "precondition: player should be in a room before cancel");
c.Sent.Clear();
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.MatchCancel) });
loop.DrainAndTick(1f);
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.RandomMatchRequest) });
loop.DrainAndTick(1f);
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchAssigned);
Assert.DoesNotContain(Frames(c), f =>
f.Channel == Channel.Framework &&
f.Opcode == (ushort)FrameworkOpcode.Error &&
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
}
[Fact]
public void MatchCancel_InTwoPlayerRoom_ClearsRoomForAllPlayers()
{
var loop = NewLoop(matchTimeoutTicks: 100);
var c1 = new FakeConnection(1);
var c2 = new FakeConnection(2);
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1 });
loop.Submit(new ConnectCommand { PlayerId = 2, Connection = c2 });
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(RpsJoinFrame()) });
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(RpsJoinFrame()) });
loop.DrainAndTick(1f);
Assert.True(HasMatchFound(c1), "precondition: player 1 should be in a room before cancel");
Assert.True(HasMatchFound(c2), "precondition: player 2 should be in a room before cancel");
c1.Sent.Clear();
c2.Sent.Clear();
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameworkFrame(FrameworkOpcode.MatchCancel) });
loop.DrainAndTick(1f);
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(RpsJoinFrame()) });
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(RpsJoinFrame()) });
loop.DrainAndTick(1f);
Assert.True(HasMatchFound(c1), "player 1 should be able to match again after cancel");
Assert.True(HasMatchFound(c2), "player 2 should be able to match again after the room was cancelled");
Assert.DoesNotContain(Frames(c1), f =>
f.Channel == Channel.Framework &&
f.Opcode == (ushort)FrameworkOpcode.Error &&
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
Assert.DoesNotContain(Frames(c2), f =>
f.Channel == Channel.Framework &&
f.Opcode == (ushort)FrameworkOpcode.Error &&
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
}
/// <summary>
/// Player 1 disconnects while still queued (before match forms).
/// DrainAndTick processes the DisconnectCommand (which dequeues pid=1) BEFORE Poll,
+57
View File
@@ -140,6 +140,42 @@ namespace XWorld.Server.Gateway.Tests
Assert.Contains(Frames(c1b), f => f.Channel == Channel.Game);
}
[Fact]
public void Reconnect_ReplaysMatchFoundAndLatestGameSnapshot()
{
var loop = NewLoop(reconnectWindow: 100);
var c1 = new FakeConnection(1);
var c2 = new FakeConnection(2);
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1 });
loop.Submit(new ConnectCommand { PlayerId = 2, Connection = c2 });
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
loop.DrainAndTick(1f);
loop.Submit(new DisconnectCommand { PlayerId = 1, Connection = c1 });
loop.DrainAndTick(1f);
var c1b = new FakeConnection(1);
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1b });
loop.DrainAndTick(0f);
Frame resumeFrame = Frames(c1b).Find(f =>
f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchFound);
Assert.NotEqual(default, resumeFrame);
MatchFoundMsg resume = MatchFoundMsg.Decode(resumeFrame.Payload);
Assert.Equal("rps", resume.GameId);
Assert.Equal(1, resume.Version);
Assert.Equal(1, resume.SelfPlayerId);
Assert.Equal(2, resume.Players.Count);
Assert.Contains(Frames(c1b), f => f.Channel == Channel.Game);
}
[Fact]
public void DefaultReconnectWindow_IsThreeMinutesAtDefaultTickRate()
{
Assert.Equal(1800, ServerLoop.DefaultReconnectWindowTicks);
}
[Fact]
public void ExpiredSession_IsSwept()
{
@@ -173,6 +209,27 @@ namespace XWorld.Server.Gateway.Tests
Assert.Contains(Frames(c1), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
}
[Fact]
public void MatchCancel_WhileQueued_CancelsQueuedMatchAndAllowsPlayerToMatchAgain()
{
var loop = NewLoop();
var c = new FakeConnection(1);
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c });
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
loop.DrainAndTick(1f);
c.Sent.Clear();
loop.Submit(new FrameCommand { PlayerId = 1, Frame = new Frame(Channel.Framework, (ushort)FrameworkOpcode.MatchCancel, System.Array.Empty<byte>()) });
loop.Submit(new FrameCommand { PlayerId = 1, Frame = new Frame(Channel.Framework, (ushort)FrameworkOpcode.RandomMatchRequest, System.Array.Empty<byte>()) });
loop.DrainAndTick(1f);
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchAssigned);
Assert.DoesNotContain(Frames(c), f =>
f.Channel == Channel.Framework &&
f.Opcode == (ushort)FrameworkOpcode.Error &&
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
}
[Fact]
public void GameFrame_WithNoRoom_IsIgnoredSafely()
{
+1 -1
View File
@@ -31,7 +31,7 @@ namespace XWorld.Server.Gateway
public string WsBaseUrl { get; private set; } // 例如 ws://127.0.0.1:54321
public int DevDiscoveryPort => _discovery?.Port ?? 0;
public async Task StartAsync(string gamesRoot, int tickIntervalMs, long reconnectWindowTicks = 100,
public async Task StartAsync(string gamesRoot, int tickIntervalMs, long reconnectWindowTicks = ServerLoop.DefaultReconnectWindowTicks,
long matchTimeoutTicks = 150, float? logicalDt = null, string publicKeyPem = null, int listenPort = 0,
bool bindAllInterfaces = false,
string devDiscoveryToken = null, string advertiseGatewayUrl = null, string advertiseResourceBaseUrl = null,
+5 -5
View File
@@ -4,10 +4,10 @@ namespace XWorld.Server.Gateway
{
public sealed class Matchmaker
{
public sealed class Seat { public int PlayerId; public bool IsAi; }
public sealed class Seat { public int PlayerId; public bool IsAi; public int RequestId; }
public sealed class Match { public string GameId; public int Version; public List<Seat> Seats = new List<Seat>(); }
private sealed class Waiter { public int Pid; public long EnqueuedTick; }
private sealed class Waiter { public int Pid; public int RequestId; public long EnqueuedTick; }
private sealed class Bucket { public int PlayerCount; public List<Waiter> Waiters = new List<Waiter>(); }
private readonly long _timeoutTicks;
@@ -17,11 +17,11 @@ namespace XWorld.Server.Gateway
public Matchmaker(long timeoutTicks) { _timeoutTicks = timeoutTicks; }
private static string Key(string g, int v) => $"{g}@{v}";
public void Enqueue(int pid, string gameId, int version, int playerCount, long tick)
public void Enqueue(int pid, string gameId, int version, int playerCount, long tick, int requestId = 0)
{
string k = Key(gameId, version);
if (!_buckets.TryGetValue(k, out var b)) { b = new Bucket { PlayerCount = playerCount }; _buckets[k] = b; }
b.Waiters.Add(new Waiter { Pid = pid, EnqueuedTick = tick });
b.Waiters.Add(new Waiter { Pid = pid, RequestId = requestId, EnqueuedTick = tick });
}
// 从等待队列移除某玩家(断线时调用);返回是否移除
@@ -51,7 +51,7 @@ namespace XWorld.Server.Gateway
var m = new Match { GameId = gv[0], Version = int.Parse(gv[1]) };
int take = b.Waiters.Count >= b.PlayerCount ? b.PlayerCount : b.Waiters.Count;
for (int i = 0; i < take; i++)
m.Seats.Add(new Seat { PlayerId = b.Waiters[i].Pid, IsAi = false });
m.Seats.Add(new Seat { PlayerId = b.Waiters[i].Pid, RequestId = b.Waiters[i].RequestId, IsAi = false });
b.Waiters.RemoveRange(0, take);
while (m.Seats.Count < b.PlayerCount)
m.Seats.Add(new Seat { PlayerId = -(++_aiSeq), IsAi = true }); // AI 用负 pid
+5 -2
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using XWorld.Framework;
using XWorld.Framework.Protocol;
@@ -11,14 +12,16 @@ namespace XWorld.Server.Gateway
private readonly string _roomId; // 保留作诊断/日志标识
private readonly IReadOnlyList<int> _players;
private readonly SessionManager _sessions;
private readonly Action<NetMessage> _onBroadcast;
public RoomOutputSink(string roomId, IReadOnlyList<int> players, SessionManager sessions)
public RoomOutputSink(string roomId, IReadOnlyList<int> players, SessionManager sessions, Action<NetMessage> onBroadcast = null)
{
_roomId = roomId; _players = players; _sessions = sessions;
_roomId = roomId; _players = players; _sessions = sessions; _onBroadcast = onBroadcast;
}
public void Broadcast(NetMessage message)
{
_onBroadcast?.Invoke(message);
byte[] frame = FrameCodec.Encode(new Frame(Channel.Game, message.Opcode, message.Payload));
foreach (var pid in _players)
_sessions.GetConnection(pid)?.Send(frame);
+165 -23
View File
@@ -21,16 +21,31 @@ namespace XWorld.Server.Gateway
private readonly Matchmaker _matchmaker;
private readonly ChannelsNS.Channel<ServerCommand> _cmds =
ChannelsNS.Channel.CreateUnbounded<ServerCommand>();
public const long DefaultReconnectWindowTicks = 1800;
private const int MaxWorldChatTextLength = 200;
private readonly Dictionary<string, List<int>> _roomPlayers = new Dictionary<string, List<int>>();
private readonly Dictionary<string, ActiveRoomInfo> _activeRooms = new Dictionary<string, ActiveRoomInfo>();
private readonly Dictionary<int, LobbyPlayerMsg> _lobbyPlayers = new Dictionary<int, LobbyPlayerMsg>();
private readonly Dictionary<string, int> _playerCountCache = new Dictionary<string, int>(); // "gameId@version" -> playerCount
private readonly HashSet<int> _queuedPids = new HashSet<int>(); // pids currently in matchmaker queue
private readonly Dictionary<int, int> _queuedRequestIds = new Dictionary<int, int>();
private readonly Dictionary<int, int> _activeRoomRequestIds = new Dictionary<int, int>();
private readonly Random _random = new Random(1);
private long _tick;
private int _roomSeq;
public ServerLoop(string gamesRoot, ILogger logger, long reconnectWindowTicks = 100,
private sealed class ActiveRoomInfo
{
public string RoomId;
public string GameId;
public int Version;
public List<PlayerInfo> Players;
public Dictionary<int, int> RequestIds = new Dictionary<int, int>();
public bool HasLastGameMessage;
public NetMessage LastGameMessage;
}
public ServerLoop(string gamesRoot, ILogger logger, long reconnectWindowTicks = DefaultReconnectWindowTicks,
long matchTimeoutTicks = 150, string publicKeyPem = null)
{
_logger = logger;
@@ -104,11 +119,12 @@ namespace XWorld.Server.Gateway
case ConnectCommand cc:
_sessions.OnConnect(cc.PlayerId, cc.Connection);
_logger.Info($"session connect pid={cc.PlayerId}");
SendRoomResumeIfNeeded(cc.PlayerId);
break;
case DisconnectCommand dc:
_sessions.OnDisconnect(dc.PlayerId, dc.Connection, _tick);
_logger.Info($"session disconnect pid={dc.PlayerId}");
if (_queuedPids.Remove(dc.PlayerId)) _matchmaker.Remove(dc.PlayerId);
CancelQueuedMatch(dc.PlayerId);
if (_lobbyPlayers.Remove(dc.PlayerId)) BroadcastLobbyState();
break;
case FrameCommand fc: Route(fc.PlayerId, fc.Frame); break;
@@ -140,13 +156,16 @@ namespace XWorld.Server.Gateway
break;
case FrameworkOpcode.MatchRequest:
var req = MatchRequestMsg.Decode(frame.Payload);
EnqueueMatch(pid, req.GameId, req.Version);
EnqueueMatch(pid, req.GameId, req.Version, req.RequestId);
break;
case FrameworkOpcode.GameListRequest:
SendFramework(pid, FrameworkOpcode.GameListResponse, BuildGameList().Encode());
break;
case FrameworkOpcode.RandomMatchRequest:
EnqueueRandomMatch(pid);
EnqueueRandomMatch(pid, MatchControlMsg.Decode(frame.Payload).RequestId);
break;
case FrameworkOpcode.MatchCancel:
CancelMatchState(pid, MatchControlMsg.Decode(frame.Payload).RequestId);
break;
case FrameworkOpcode.LobbyJoin:
HandleLobbyJoin(pid, frame.Payload);
@@ -155,6 +174,7 @@ namespace XWorld.Server.Gateway
HandleLobbyMove(pid, frame.Payload);
break;
case FrameworkOpcode.LobbyLeave:
CancelQueuedMatch(pid);
if (_lobbyPlayers.Remove(pid)) BroadcastLobbyState();
break;
case FrameworkOpcode.WorldChatSend:
@@ -272,21 +292,23 @@ namespace XWorld.Server.Gateway
}
}
private void EnqueueRandomMatch(int pid)
private void EnqueueRandomMatch(int pid, int requestId)
{
var games = BuildGameList().Games;
if (games.Count == 0)
{
SendFramework(pid, FrameworkOpcode.Error,
new ErrorMsg { Code = 1, Message = "no minigame available" }.Encode());
new ErrorMsg { Code = 1, Message = "no minigame available", RequestId = requestId }.Encode());
return;
}
var selected = games[_random.Next(games.Count)];
_logger.Info($"random match pid={pid} assigned {selected.GameId}@{selected.Version}");
SendFramework(pid, FrameworkOpcode.MatchAssigned,
new MatchAssignedMsg { GameId = selected.GameId, Version = selected.Version }.Encode());
EnqueueMatch(pid, selected.GameId, selected.Version);
if (EnqueueMatch(pid, selected.GameId, selected.Version, requestId))
{
_logger.Info($"random match pid={pid} assigned {selected.GameId}@{selected.Version}");
SendFramework(pid, FrameworkOpcode.MatchAssigned,
new MatchAssignedMsg { GameId = selected.GameId, Version = selected.Version, RequestId = requestId }.Encode());
}
}
private GameListResponseMsg BuildGameList()
@@ -332,17 +354,17 @@ namespace XWorld.Server.Gateway
return msg;
}
private void EnqueueMatch(int pid, string gameId, int version)
private bool EnqueueMatch(int pid, string gameId, int version, int requestId = 0)
{
var s = _sessions.Get(pid);
if (s == null) return; // 未建立会话,忽略
if (s == null) return false; // 未建立会话,忽略
if (s.RoomId != null || _queuedPids.Contains(pid))
{
// 已在房间内或已在队列中:拒绝
SendFramework(pid, FrameworkOpcode.Error,
new ErrorMsg { Code = 2, Message = "already in room or queue" }.Encode());
return;
new ErrorMsg { Code = 2, Message = "already in room or queue", RequestId = requestId }.Encode());
return false;
}
// 读取游戏的 playerCount(缓存)
@@ -360,14 +382,76 @@ namespace XWorld.Server.Gateway
{
_logger.Error($"读取 game.json 失败 {gameId}@{version}: {ex.Message}");
SendFramework(pid, FrameworkOpcode.Error,
new ErrorMsg { Code = 1, Message = "game not found" }.Encode());
return;
new ErrorMsg { Code = 1, Message = "game not found", RequestId = requestId }.Encode());
return false;
}
}
_queuedPids.Add(pid);
_matchmaker.Enqueue(pid, gameId, version, playerCount, _tick);
_logger.Info($"enqueue match pid={pid} game={gameId}@{version} playerCount={playerCount} tick={_tick}");
_queuedRequestIds[pid] = requestId;
_matchmaker.Enqueue(pid, gameId, version, playerCount, _tick, requestId);
_logger.Info($"enqueue match pid={pid} req={requestId} game={gameId}@{version} playerCount={playerCount} tick={_tick}");
return true;
}
private void CancelQueuedMatch(int pid, int requestId = 0)
{
if (requestId != 0 &&
_queuedRequestIds.TryGetValue(pid, out int queuedRequestId) &&
queuedRequestId != requestId)
{
return;
}
if (_queuedPids.Remove(pid))
{
_queuedRequestIds.Remove(pid);
_matchmaker.Remove(pid);
_logger.Info($"cancel queued match pid={pid} req={requestId}");
}
}
private void CancelMatchState(int pid, int requestId)
{
CancelQueuedMatch(pid, requestId);
CancelActiveRoom(pid, requestId);
}
private void CancelActiveRoom(int pid, int requestId)
{
var s = _sessions.Get(pid);
if (s?.RoomId == null) return;
if (requestId != 0 &&
_activeRoomRequestIds.TryGetValue(pid, out int activeRequestId) &&
activeRequestId != requestId)
{
return;
}
string roomId = s.RoomId;
if (_roomPlayers.TryGetValue(roomId, out var players))
{
var roomPids = new List<int>(players);
_roomPlayers.Remove(roomId);
_activeRooms.Remove(roomId);
for (int i = 0; i < roomPids.Count; i++)
{
int roomPid = roomPids[i];
var roomSession = _sessions.Get(roomPid);
if (roomSession?.RoomId == roomId) roomSession.RoomId = null;
_activeRoomRequestIds.Remove(roomPid);
}
_host.EndRoom(roomId);
}
else
{
_activeRooms.Remove(roomId);
s.RoomId = null;
_activeRoomRequestIds.Remove(pid);
}
_logger.Info($"cancel active match pid={pid} req={requestId} room={roomId}");
}
private void CreateRoomForSeats(Matchmaker.Match match)
@@ -397,10 +481,23 @@ namespace XWorld.Server.Gateway
{
realPids.Add(seat.PlayerId);
_queuedPids.Remove(seat.PlayerId);
_queuedRequestIds.Remove(seat.PlayerId);
}
}
var sink = new RoomOutputSink(roomId, realPids, _sessions);
var allPlayers = new List<PlayerInfo>(playerInfos);
var roomInfo = new ActiveRoomInfo
{
RoomId = roomId,
GameId = match.GameId,
Version = match.Version,
Players = allPlayers
};
var sink = new RoomOutputSink(roomId, realPids, _sessions, message =>
{
roomInfo.HasLastGameMessage = true;
roomInfo.LastGameMessage = new NetMessage(message.Opcode, message.Payload ?? Array.Empty<byte>());
});
var cfg = new RoomConfig
{
GameId = match.GameId,
@@ -409,6 +506,7 @@ namespace XWorld.Server.Gateway
PlayerCount = seats.Count
};
_activeRooms[roomId] = roomInfo;
Room room;
try
{
@@ -417,6 +515,7 @@ namespace XWorld.Server.Gateway
catch (Exception ex)
{
_logger.Error($"建房失败 {match.GameId}@{match.Version}: {ex.Message}");
_activeRooms.Remove(roomId);
// Notify real players of error
var errPayload = new ErrorMsg { Code = 1, Message = "create room failed" }.Encode();
foreach (int rpid in realPids)
@@ -424,20 +523,30 @@ namespace XWorld.Server.Gateway
return;
}
// Build MatchFound message with all players info
var allPlayers = new List<PlayerInfo>(playerInfos);
// For each real player: set session.RoomId + send MatchFound
foreach (int rpid in realPids)
{
var s = _sessions.Get(rpid);
if (s != null) s.RoomId = roomId;
int requestId = 0;
for (int i = 0; i < seats.Count; i++)
{
if (!seats[i].IsAi && seats[i].PlayerId == rpid)
{
requestId = seats[i].RequestId;
break;
}
}
_activeRoomRequestIds[rpid] = requestId;
roomInfo.RequestIds[rpid] = requestId;
var found = new MatchFoundMsg
{
GameId = match.GameId,
RoomId = roomId,
Version = match.Version,
SelfPlayerId = rpid
SelfPlayerId = rpid,
RequestId = requestId
};
foreach (var pi in allPlayers) found.Players.Add(pi);
SendFramework(rpid, FrameworkOpcode.MatchFound, found.Encode());
@@ -452,6 +561,7 @@ namespace XWorld.Server.Gateway
{
if (!_roomPlayers.TryGetValue(roomId, out var players)) return;
_roomPlayers.Remove(roomId);
_activeRooms.Remove(roomId);
var msg = new RoomEndMsg
{
WinnerPlayerId = result?.WinnerPlayerId ?? 0,
@@ -464,6 +574,7 @@ namespace XWorld.Server.Gateway
SendFramework(pid, FrameworkOpcode.RoomEnd, payload);
var s = _sessions.Get(pid);
if (s != null) s.RoomId = null;
_activeRoomRequestIds.Remove(pid);
}
}
@@ -472,5 +583,36 @@ namespace XWorld.Server.Gateway
_sessions.GetConnection(pid)?.Send(
FrameCodec.Encode(new Frame(Channel.Framework, (ushort)op, payload)));
}
private void SendGame(int pid, NetMessage message)
{
_sessions.GetConnection(pid)?.Send(
FrameCodec.Encode(new Frame(Channel.Game, message.Opcode, message.Payload)));
}
private void SendRoomResumeIfNeeded(int pid)
{
var session = _sessions.Get(pid);
if (session?.RoomId == null) return;
if (!_activeRooms.TryGetValue(session.RoomId, out var room))
{
session.RoomId = null;
_activeRoomRequestIds.Remove(pid);
return;
}
var found = new MatchFoundMsg
{
GameId = room.GameId,
RoomId = room.RoomId,
Version = room.Version,
SelfPlayerId = pid,
RequestId = room.RequestIds.TryGetValue(pid, out int requestId) ? requestId : 0
};
for (int i = 0; i < room.Players.Count; i++) found.Players.Add(room.Players[i]);
SendFramework(pid, FrameworkOpcode.MatchFound, found.Encode());
if (room.HasLastGameMessage) SendGame(pid, room.LastGameMessage);
_logger.Info($"resume room pid={pid} room={room.RoomId} game={room.GameId}@{room.Version}");
}
}
}
+17
View File
@@ -74,6 +74,23 @@ namespace XWorld.Server.Host.Tests
() => host.CreateRoom("dup", "rps", 1, Cfg(), Players, new Capture()));
}
[Fact]
public void EndRoom_ReleasesRoomImmediately()
{
var host = NewHost();
var cap = new Capture();
host.CreateRoom("manual", "rps", 1, Cfg(), Players, cap);
Assert.True(host.EndRoom("manual"));
Assert.Equal(0, host.ActiveRoomCount);
int broadcasts = cap.Broadcasts.Count;
host.DeliverTo("manual", 1, new NetMessage(1, new byte[] { (byte)Choice.Rock }));
host.TickAll(0.1f);
Assert.Equal(broadcasts, cap.Broadcasts.Count);
}
[Fact]
public void TickAll_ReturnsEndedRoomIds()
{
+8
View File
@@ -84,6 +84,14 @@ namespace XWorld.Server.Host
entry.Room.Deliver(playerId, message);
}
public bool EndRoom(string roomId)
{
if (!_rooms.TryGetValue(roomId, out var entry)) return false;
entry.Room.End();
ReleaseRoom(roomId);
return true;
}
private void ReleaseRoom(string roomId)
{
if (!_rooms.TryGetValue(roomId, out var entry)) return;