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
+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}");
}
}
}