fix: improve minigame reconnect flow
This commit is contained in:
+165
-23
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user