From 7d6191541fcd6b570cd7155481a755fee21f9754 Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 13:26:16 +0100 Subject: [PATCH 01/13] More detailed exception logging --- Bombd/Protocols/TCP/SslConnection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bombd/Protocols/TCP/SslConnection.cs b/Bombd/Protocols/TCP/SslConnection.cs index f5a6bf5..f34e1e3 100644 --- a/Bombd/Protocols/TCP/SslConnection.cs +++ b/Bombd/Protocols/TCP/SslConnection.cs @@ -207,9 +207,10 @@ private async void DoBlockAndReceive() } while (offset < payloadSize); } } - catch (Exception) + catch (Exception ex) { Logger.LogError("An error occurred while reading from socket. Closing connection."); + Logger.LogDebug(ex.ToString()); Disconnect(); return; } From 769a9a29e24f1180550be88f5b58499e497aaf07 Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 13:37:05 +0100 Subject: [PATCH 02/13] Add debugging profile required for development in WSL2, Add ability to adjust LogLevel via config --- Bombd/Core/BombdConfig.cs | 5 +++++ Bombd/Logging/Logger.cs | 9 +++------ Bombd/Program.cs | 2 ++ Bombd/Properties/launchSettings.json | 21 ++++++++++++++------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Bombd/Core/BombdConfig.cs b/Bombd/Core/BombdConfig.cs index c8b43a9..9ad24a8 100644 --- a/Bombd/Core/BombdConfig.cs +++ b/Bombd/Core/BombdConfig.cs @@ -66,6 +66,11 @@ static BombdConfig() /// How many times each service performs an update in a second. /// public int TickRate { get; set; } = 15; + + /// + /// The maximum log level to log messages from. + /// + public string MaxLogLevel { get; set; } = Enum.GetName(LogLevel.Info); /// /// Whether or not connections from LittleBigPlanet Karting are allowed. diff --git a/Bombd/Logging/Logger.cs b/Bombd/Logging/Logger.cs index 7a0785c..4a1cffc 100644 --- a/Bombd/Logging/Logger.cs +++ b/Bombd/Logging/Logger.cs @@ -6,13 +6,8 @@ namespace Bombd.Logging; public class Logger { -#if DEBUG - private const LogLevel MaxLevel = LogLevel.Trace; -#else - private const LogLevel MaxLevel = LogLevel.Info; -#endif - private static readonly ConcurrentQueue LogQueue = new(); + private static LogLevel MaxLevel = LogLevel.Info; static Logger() { @@ -48,6 +43,8 @@ private static ConsoleColor GetLogColor(LogLevel level) }; } + public static void SetLogMaxLevel(LogLevel level) => MaxLevel = level; + public static void LogError(string message) => Log(LogLevel.Error, message); public static void LogWarning(string message) => Log(LogLevel.Warning, message); public static void LogInfo(string message) => Log(LogLevel.Info, message); diff --git a/Bombd/Program.cs b/Bombd/Program.cs index b64baa0..4816c5d 100644 --- a/Bombd/Program.cs +++ b/Bombd/Program.cs @@ -3,6 +3,8 @@ using Bombd.Services; using Directory = Bombd.Services.Directory; +Logger.SetLogMaxLevel(Enum.Parse(BombdConfig.Instance.MaxLogLevel)); + string certificate = BombdConfig.Instance.PfxCertificate; if (string.IsNullOrEmpty(certificate)) { diff --git a/Bombd/Properties/launchSettings.json b/Bombd/Properties/launchSettings.json index 5f149f4..63be22c 100644 --- a/Bombd/Properties/launchSettings.json +++ b/Bombd/Properties/launchSettings.json @@ -1,10 +1,17 @@ { - "profiles": { - "Bombd": { - "commandName": "Project" - }, - "Docker": { - "commandName": "Docker" + "profiles": { + "Bombd": { + "commandName": "Project" + }, + "Docker": { + "commandName": "Docker" + }, + "WSL": { + "commandName": "WSL2", + "environmentVariables": { + "LD_LIBRARY_PATH": "/usr/local/lib64:/usr/local/lib:/usr/lib", + "OPENSSL_CONF": "/etc/ssl/openssl.cnf" + } + } } - } } \ No newline at end of file From 62aa9cbf577c7c7593195e94b80c42c291074dad Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 13:58:53 +0100 Subject: [PATCH 03/13] Make consistent with rest of codebase --- Bombd/Protocols/TCP/SslConnection.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Bombd/Protocols/TCP/SslConnection.cs b/Bombd/Protocols/TCP/SslConnection.cs index f34e1e3..a35b8f5 100644 --- a/Bombd/Protocols/TCP/SslConnection.cs +++ b/Bombd/Protocols/TCP/SslConnection.cs @@ -149,9 +149,10 @@ public override void Send(ArraySegment data, PacketType type) offset += size; } while (offset < messageSize); } - catch (Exception) + catch (Exception e) { Logger.LogError("An error occurred during send. Closing connection."); + Logger.LogDebug(e.ToString()); Disconnect(); } } @@ -207,10 +208,10 @@ private async void DoBlockAndReceive() } while (offset < payloadSize); } } - catch (Exception ex) + catch (Exception e) { Logger.LogError("An error occurred while reading from socket. Closing connection."); - Logger.LogDebug(ex.ToString()); + Logger.LogDebug(e.ToString()); Disconnect(); return; } From 01a4a292f157a16c2a8cd41366a7280299c90dc0 Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 14:05:47 +0100 Subject: [PATCH 04/13] Debug logging for loglevel --- Bombd/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Bombd/Program.cs b/Bombd/Program.cs index 4816c5d..871dacf 100644 --- a/Bombd/Program.cs +++ b/Bombd/Program.cs @@ -3,7 +3,9 @@ using Bombd.Services; using Directory = Bombd.Services.Directory; -Logger.SetLogMaxLevel(Enum.Parse(BombdConfig.Instance.MaxLogLevel)); +var logLevel = Enum.Parse(BombdConfig.Instance.MaxLogLevel); +Logger.SetLogMaxLevel(logLevel); +Logger.LogDebug($"Max log level is now {BombdConfig.Instance.MaxLogLevel} ({logLevel})"); string certificate = BombdConfig.Instance.PfxCertificate; if (string.IsNullOrEmpty(certificate)) From 33fa756deda9ee6a6367cbf73afd2380785222e7 Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 14:12:52 +0100 Subject: [PATCH 05/13] Update loglevel debug --- Bombd/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bombd/Program.cs b/Bombd/Program.cs index 871dacf..f49ee55 100644 --- a/Bombd/Program.cs +++ b/Bombd/Program.cs @@ -5,7 +5,7 @@ var logLevel = Enum.Parse(BombdConfig.Instance.MaxLogLevel); Logger.SetLogMaxLevel(logLevel); -Logger.LogDebug($"Max log level is now {BombdConfig.Instance.MaxLogLevel} ({logLevel})"); +Logger.LogInfo($"Max log level is now {BombdConfig.Instance.MaxLogLevel} ({logLevel})"); string certificate = BombdConfig.Instance.PfxCertificate; if (string.IsNullOrEmpty(certificate)) From c1c95c4aba9b27325c8dc9609d01df043d828c81 Mon Sep 17 00:00:00 2001 From: derole Date: Mon, 13 Oct 2025 14:19:19 +0100 Subject: [PATCH 06/13] Remove conditionals for logging --- Bombd/Logging/Logger.cs | 4 ---- Bombd/Program.cs | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Bombd/Logging/Logger.cs b/Bombd/Logging/Logger.cs index 4a1cffc..392d5c0 100644 --- a/Bombd/Logging/Logger.cs +++ b/Bombd/Logging/Logger.cs @@ -49,10 +49,8 @@ private static ConsoleColor GetLogColor(LogLevel level) public static void LogWarning(string message) => Log(LogLevel.Warning, message); public static void LogInfo(string message) => Log(LogLevel.Info, message); - [Conditional("DEBUG")] public static void LogDebug(string message) => Log(LogLevel.Debug, message); - [Conditional("DEBUG")] public static void LogTrace(string message) => Log(LogLevel.Trace, message); public static void Log(LogLevel level, string message) @@ -64,10 +62,8 @@ public static void Log(LogLevel level, string message) public static void LogWarning(Type type, string message) => Log(type, LogLevel.Warning, message); public static void LogInfo(Type type, string message) => Log(type, LogLevel.Info, message); - [Conditional("DEBUG")] public static void LogDebug(Type type, string message) => Log(type, LogLevel.Debug, message); - [Conditional("DEBUG")] public static void LogTrace(Type type, string message) => Log(type, LogLevel.Trace, message); public static void Log(Type type, LogLevel level, string message) diff --git a/Bombd/Program.cs b/Bombd/Program.cs index f49ee55..4816c5d 100644 --- a/Bombd/Program.cs +++ b/Bombd/Program.cs @@ -3,9 +3,7 @@ using Bombd.Services; using Directory = Bombd.Services.Directory; -var logLevel = Enum.Parse(BombdConfig.Instance.MaxLogLevel); -Logger.SetLogMaxLevel(logLevel); -Logger.LogInfo($"Max log level is now {BombdConfig.Instance.MaxLogLevel} ({logLevel})"); +Logger.SetLogMaxLevel(Enum.Parse(BombdConfig.Instance.MaxLogLevel)); string certificate = BombdConfig.Instance.PfxCertificate; if (string.IsNullOrEmpty(certificate)) From 367a3c85dd8bbe4ff9cb0a9e40d0f828d67d6d4e Mon Sep 17 00:00:00 2001 From: derole Date: Tue, 14 Oct 2025 01:07:43 +0100 Subject: [PATCH 07/13] Temp for debugging --- Bombd/Core/BombdService.cs | 1 + Bombd/Protocols/ConnectionBase.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Bombd/Core/BombdService.cs b/Bombd/Core/BombdService.cs index 80b6ad6..2d7eb87 100644 --- a/Bombd/Core/BombdService.cs +++ b/Bombd/Core/BombdService.cs @@ -167,6 +167,7 @@ public bool Login(ConnectionBase connection, NetcodeTransaction request, Netcode int userId = CryptoHelper.StringHash32Upper(ticket.Username + (isRPCN ? "RPCN" : "PSN")); if (UserInfo.ContainsKey(userId)) { + Logger.LogError(_type, $"User already has a session with state {UserInfo[userId].State}"); response.Error = "alreadyLoggedIn"; return false; } diff --git a/Bombd/Protocols/ConnectionBase.cs b/Bombd/Protocols/ConnectionBase.cs index b93d847..16e2ab9 100644 --- a/Bombd/Protocols/ConnectionBase.cs +++ b/Bombd/Protocols/ConnectionBase.cs @@ -11,7 +11,7 @@ public abstract class ConnectionBase public readonly IServer Server; public readonly BombdService Service; - protected ConnectionState State = ConnectionState.Disconnected; + public ConnectionState State = ConnectionState.Disconnected; protected ConnectionBase(BombdService service, IServer server) { From 4268f2205abd59526e11899e0e02461757920744 Mon Sep 17 00:00:00 2001 From: derole Date: Wed, 15 Oct 2025 17:20:03 +0100 Subject: [PATCH 08/13] Replace SemaphoreSlim with a traditional lock to fix double-lock that occurs that can cause a deadlock on the GameServer tick thread in some rare cases, remove logging for alreadyLoggedIn state --- Bombd/Core/BombdService.cs | 1 - Bombd/Services/GameServer.cs | 510 +++++++++++++++++------------------ 2 files changed, 243 insertions(+), 268 deletions(-) diff --git a/Bombd/Core/BombdService.cs b/Bombd/Core/BombdService.cs index 2d7eb87..80b6ad6 100644 --- a/Bombd/Core/BombdService.cs +++ b/Bombd/Core/BombdService.cs @@ -167,7 +167,6 @@ public bool Login(ConnectionBase connection, NetcodeTransaction request, Netcode int userId = CryptoHelper.StringHash32Upper(ticket.Username + (isRPCN ? "RPCN" : "PSN")); if (UserInfo.ContainsKey(userId)) { - Logger.LogError(_type, $"User already has a session with state {UserInfo[userId].State}"); response.Error = "alreadyLoggedIn"; return false; } diff --git a/Bombd/Services/GameServer.cs b/Bombd/Services/GameServer.cs index 6416417..c5de833 100644 --- a/Bombd/Services/GameServer.cs +++ b/Bombd/Services/GameServer.cs @@ -26,7 +26,7 @@ public class GameServer : BombdService private readonly Dictionary _reservationGroups = new(); private readonly List _playerJoinQueue = []; private readonly List _playerLeaveQueue = []; - private readonly SemaphoreSlim _playerLock = new(1, 1); + private readonly object _playerLock = new(); private readonly List _playerMigrationGroups = []; public event EventHandler? OnPlayerJoined; public event EventHandler? OnPlayerLeft; @@ -41,11 +41,10 @@ public void NotifyHotSeatReset() public void UpdateGuestStatuses(GamePlayer player, GuestStatusBlock block) { - _playerLock.Wait(); - try + lock (_playerLock) { player.Room.UpdateGuestStatuses(player, block); - + // Tell everybody else in the gameroom about any guests // that were either attached or detached var gamemanager = Bombd.GetService(); @@ -53,14 +52,14 @@ public void UpdateGuestStatuses(GamePlayer player, GuestStatusBlock block) { bool wasAttached = guestStatus.Status == GuestStatusCode.AttachSuccess; bool wasDetached = guestStatus.Status == GuestStatusCode.Detached; - + if (!wasAttached && !wasDetached) continue; - + var transaction = NetcodeTransaction.MakeRequest("gamemanager", wasAttached ? "guestJoined" : "guestLeft"); transaction["gamename"] = player.Room.Game.GameName; transaction["playername"] = player.Username; transaction["guestname"] = guestStatus.Username; - + foreach (var peer in player.Room.Game.Players) { if (peer == player) continue; @@ -68,22 +67,17 @@ public void UpdateGuestStatuses(GamePlayer player, GuestStatusBlock block) } } } - finally - { - _playerLock.Release(); - } } public bool ReserveSlotsInGame(string gameName, int numSlots, [MaybeNullWhen(false)] out string reservationKey) { reservationKey = null; - _playerLock.Wait(); - try + lock (_playerLock) { GameRoom? room = Bombd.RoomManager.GetRoomByName(gameName); if (room == null) return false; if (room.NumFreeSlots < numSlots) return false; - + var slots = new Queue(); for (int i = 0; i < numSlots; ++i) { @@ -101,25 +95,20 @@ public bool ReserveSlotsInGame(string gameName, int numSlots, [MaybeNullWhen(fal return true; } - finally - { - _playerLock.Release(); - } } public void AddMigrationGroup(GameMigrationRequest request) { - _playerLock.Wait(); - try + lock (_playerLock) { // This shouldn't be normally possible, so we don't need to send back errors or anything, just return. GameRoom? currentRoom = Bombd.RoomManager.GetRoomByUser(request.HostUserId); if (currentRoom == null) return; - + var gamemanager = Bombd.GetService(); bool isCreatingGame = string.IsNullOrEmpty(request.GameName); bool isJoiningGame = !isCreatingGame; - + GameRoom migratedRoom; if (isCreatingGame) { @@ -128,7 +117,7 @@ public void AddMigrationGroup(GameMigrationRequest request) Attributes = request.Attributes!, OwnerUserId = request.HostUserId, Platform = request.Platform - }); + }); } else { @@ -142,13 +131,13 @@ public void AddMigrationGroup(GameMigrationRequest request) } } - + // Fairly sure with migration requests, guests are only ever attached when // starting a game from the co-op menu, so it should only ever be the "host's" single guest. string? guest = request.Guest; int numSlotsRequired = request.PlayerIdList.Count; if (guest != null) numSlotsRequired++; - + // If we're creating the game, we'll always have enough slots // So we have to make sure there are when joining if (isJoiningGame) @@ -159,9 +148,9 @@ public void AddMigrationGroup(GameMigrationRequest request) transaction.Error = JoinFailReason.NotEnoughSlots; gamemanager.SendTransaction(request.HostUserId, transaction); return; - } + } } - + var group = new MigrationGroup { OldRoom = currentRoom, @@ -171,22 +160,22 @@ public void AddMigrationGroup(GameMigrationRequest request) OwnerGuest = request.Guest, Players = [] }; - + // Make sure we get a slot for the guest if (guest != null) { migratedRoom.RequestSlot(out int guestId); group.OwnerGuestId = guestId; } - + foreach (GenericInt32 playerId in request.PlayerIdList) { // If the player isn't in the game, just ignore them if (!currentRoom.IsPlayerInGame(playerId)) continue; - + GamePlayer player = currentRoom.GetPlayer(playerId); ConnectionBase connection = UserInfo[player.UserId]; - + migratedRoom.RequestSlot(out int slot); group.Players.Add(new MigratingPlayer { @@ -206,16 +195,11 @@ public void AddMigrationGroup(GameMigrationRequest request) _playerMigrationGroups.Add(group); } - finally - { - _playerLock.Release(); - } } public void AddPlayerToLeaveQueue(int userId, string username, string reason) { - _playerLock.Wait(); - try + lock (_playerLock) { _playerLeaveQueue.Add(new PlayerLeaveRequest { @@ -224,43 +208,34 @@ public void AddPlayerToLeaveQueue(int userId, string username, string reason) Reason = reason }); } - finally - { - _playerLock.Release(); - } } public void AddPlayerToJoinQueue(PlayerJoinRequest request) { - _playerLock.Wait(); - try + lock (_playerLock) { _playerJoinQueue.Add(request); } - finally - { - _playerLock.Release(); - } } private void HandlePlayerLeaveRequests() { - _playerLock.Wait(); - - foreach (PlayerLeaveRequest request in _playerLeaveQueue) + lock (_playerLock) { - GamePlayer? player = Bombd.RoomManager.GetPlayerInRoom(request.UserId); - if (player == null) + foreach (PlayerLeaveRequest request in _playerLeaveQueue) { - Logger.LogWarning($"{request.Username} tried to leave a game, but they aren't in one!"); - continue; + GamePlayer? player = Bombd.RoomManager.GetPlayerInRoom(request.UserId); + if (player == null) + { + Logger.LogWarning($"{request.Username} tried to leave a game, but they aren't in one!"); + continue; + } + + LeaveGameInternal(player, request.Reason); } - - LeaveGameInternal(player, request.Reason); - } - _playerLeaveQueue.Clear(); - _playerLock.Release(); + _playerLeaveQueue.Clear(); + } } private void LeaveGameInternal(GamePlayer player, string reason) @@ -295,259 +270,260 @@ private void LeaveGameInternal(GamePlayer player, string reason) private void HandlePlayerJoinRequests() { - _playerLock.Wait(); - for (int i = 0; i < _playerJoinQueue.Count; ++i) + lock (_playerLock) { - PlayerJoinRequest request = _playerJoinQueue[i]; - if (TimeHelper.LocalTime > request.Timestamp + JoinTimeout) + for (int i = 0; i < _playerJoinQueue.Count; ++i) { - Logger.LogWarning( - "A player took too long to join and was disconnected!"); - _playerJoinQueue.RemoveAt(i--); - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) - pendingConnection.Disconnect(); - continue; - } - - // This generally shouldn't happen if someone isn't manually making requests, - // but still make sure to handle these cases. - GameRoom? gameRoom = Bombd.RoomManager.GetRoomByName(request.GameName); - if (gameRoom == null) - { - Logger.LogWarning( - "A player tried to join a game room, but the room doesn't exist."); - _playerJoinQueue.RemoveAt(i--); - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) - pendingConnection.Disconnect(); - continue; - } + PlayerJoinRequest request = _playerJoinQueue[i]; + if (TimeHelper.LocalTime > request.Timestamp + JoinTimeout) + { + Logger.LogWarning( + "A player took too long to join and was disconnected!"); + _playerJoinQueue.RemoveAt(i--); + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) + pendingConnection.Disconnect(); + continue; + } - // Wait until the game room is ready to join before letting the player in - if (!gameRoom.IsReadyToJoin(request.UserId)) continue; - - // Karting doesn't use migrations and just switches games, so make sure we leave the old room. - // TEMP: Wait until player has left? - GamePlayer? existingPlayer = Bombd.RoomManager.GetPlayerInRoom(request.UserId); - if (existingPlayer != null) LeaveGameInternal(existingPlayer, "gameMigration"); + // This generally shouldn't happen if someone isn't manually making requests, + // but still make sure to handle these cases. + GameRoom? gameRoom = Bombd.RoomManager.GetRoomByName(request.GameName); + if (gameRoom == null) + { + Logger.LogWarning( + "A player tried to join a game room, but the room doesn't exist."); + _playerJoinQueue.RemoveAt(i--); + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) + pendingConnection.Disconnect(); + continue; + } - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? connection)) - { - if (!connection.IsAuthenticated) continue; + // Wait until the game room is ready to join before letting the player in + if (!gameRoom.IsReadyToJoin(request.UserId)) continue; + + // Karting doesn't use migrations and just switches games, so make sure we leave the old room. + // TEMP: Wait until player has left? + GamePlayer? existingPlayer = Bombd.RoomManager.GetPlayerInRoom(request.UserId); + if (existingPlayer != null) LeaveGameInternal(existingPlayer, "gameMigration"); - GamePlayer? player; - if (request.ReservationKey != null) + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? connection)) { - // Make sure the reservation key actually exists - if (!_reservationGroups.TryGetValue(request.ReservationKey, out ReservationGroup group)) - { - Logger.LogWarning( - "A player tried to join a game room with an invalid reservation key!"); - _playerJoinQueue.RemoveAt(i--); - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) - pendingConnection.Disconnect(); - continue; - } - - // Shouldn't happen, but if it does, make sure we don't mismatch reservations - if (group.Room != gameRoom) + if (!connection.IsAuthenticated) continue; + + GamePlayer? player; + if (request.ReservationKey != null) { - Logger.LogWarning( - "A player tried to join a game room with a mismatched reservation key!"); - _playerJoinQueue.RemoveAt(i--); - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) - pendingConnection.Disconnect(); - continue; + // Make sure the reservation key actually exists + if (!_reservationGroups.TryGetValue(request.ReservationKey, out ReservationGroup group)) + { + Logger.LogWarning( + "A player tried to join a game room with an invalid reservation key!"); + _playerJoinQueue.RemoveAt(i--); + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) + pendingConnection.Disconnect(); + continue; + } + + // Shouldn't happen, but if it does, make sure we don't mismatch reservations + if (group.Room != gameRoom) + { + Logger.LogWarning( + "A player tried to join a game room with a mismatched reservation key!"); + _playerJoinQueue.RemoveAt(i--); + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) + pendingConnection.Disconnect(); + continue; + } + + Logger.LogDebug($"{request.Username} is joining room using a reservation (guest={request.Guest})"); + + // Reservations in Karting are a bit wonky right now, and I can't really test it, + // so we'll need this check to prevent any exceptions + string? guest = request.Guest; + int numSlotsRequired = 1; + if (guest != null) numSlotsRequired++; + if (group.Slots.Count < numSlotsRequired) + { + Logger.LogWarning("A player tried to join a game room with a reservation that doesn't have enough slots!"); + continue; + } + ; + + int playerId = group.Slots.Dequeue(); + if (guest == null) + { + player = Bombd.RoomManager.JoinRoom(connection.Username, connection.UserId, playerId, + group.Room); + } + else + { + int guestId = group.Slots.Dequeue(); + player = Bombd.RoomManager.JoinRoomWithGuest(connection.Username, guest, connection.UserId, playerId, + guestId, group.Room); + } + + if (group.Slots.Count == 0) + { + Logger.LogDebug($"Destroying reservation {request.ReservationKey} since all slots have been used!"); + _reservationGroups.Remove(request.ReservationKey); + } } - - Logger.LogDebug($"{request.Username} is joining room using a reservation (guest={request.Guest})"); - - // Reservations in Karting are a bit wonky right now, and I can't really test it, - // so we'll need this check to prevent any exceptions - string? guest = request.Guest; - int numSlotsRequired = 1; - if (guest != null) numSlotsRequired++; - if (group.Slots.Count < numSlotsRequired) - { - Logger.LogWarning("A player tried to join a game room with a reservation that doesn't have enough slots!"); - continue; - }; + else player = Bombd.RoomManager.RequestJoinRoom(connection.Username, connection.UserId, gameRoom, request.Guest); - int playerId = group.Slots.Dequeue(); - if (guest == null) + if (player != null) { - player = Bombd.RoomManager.JoinRoom(connection.Username, connection.UserId, playerId, - group.Room); + // For convenience, we attach the send method directly to the game player object, + // so that no lookups have to be performed within the simulation server environment. + player.Send = (bytes, type) => SendMessage(player.UserId, bytes, type); + player.Disconnect = () => Disconnect(player.UserId); + + Logger.LogInfo($"{player.Username} joined {gameRoom.Game.GameName}."); + + // Make sure to tell both the simulation instance and anything subscribed to game events + // about the new player that joined. + gameRoom.Simulation.OnPlayerJoin(player); + OnPlayerJoined?.Invoke(this, new PlayerJoinEventArgs + { + Room = gameRoom, + Player = player, + WasMigration = false + }); } else { - int guestId = group.Slots.Dequeue(); - player = Bombd.RoomManager.JoinRoomWithGuest(connection.Username, guest, connection.UserId, playerId, - guestId, group.Room); + Logger.LogWarning( + $"{connection.Username} tried to join {gameRoom.Game.GameName}, but operation failed."); + if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) + pendingConnection.Disconnect(); } - if (group.Slots.Count == 0) - { - Logger.LogDebug($"Destroying reservation {request.ReservationKey} since all slots have been used!"); - _reservationGroups.Remove(request.ReservationKey); - } + _playerJoinQueue.RemoveAt(i--); } - else player = Bombd.RoomManager.RequestJoinRoom(connection.Username, connection.UserId, gameRoom, request.Guest); - - if (player != null) - { - // For convenience, we attach the send method directly to the game player object, - // so that no lookups have to be performed within the simulation server environment. - player.Send = (bytes, type) => SendMessage(player.UserId, bytes, type); - player.Disconnect = () => Disconnect(player.UserId); - - Logger.LogInfo($"{player.Username} joined {gameRoom.Game.GameName}."); - - // Make sure to tell both the simulation instance and anything subscribed to game events - // about the new player that joined. - gameRoom.Simulation.OnPlayerJoin(player); - OnPlayerJoined?.Invoke(this, new PlayerJoinEventArgs - { - Room = gameRoom, - Player = player, - WasMigration = false - }); - } - else - { - Logger.LogWarning( - $"{connection.Username} tried to join {gameRoom.Game.GameName}, but operation failed."); - if (UserInfo.TryGetValue(request.UserId, out ConnectionBase? pendingConnection)) - pendingConnection.Disconnect(); - } - - _playerJoinQueue.RemoveAt(i--); } - } - _playerLock.Release(); + } } private void HandlePlayerMigrationRequests() { - _playerLock.Wait(); - int time = TimeHelper.LocalTime; - for (int i = 0; i < _playerMigrationGroups.Count; ++i) + lock (_playerLock) { - MigrationGroup group = _playerMigrationGroups[i]; - foreach (MigratingPlayer player in group.Players) + int time = TimeHelper.LocalTime; + for (int i = 0; i < _playerMigrationGroups.Count; ++i) { - if (player.Status >= MigrationStatus.Migrated) continue; - if (time > group.Timestamp + MigrationTimeout) + MigrationGroup group = _playerMigrationGroups[i]; + foreach (MigratingPlayer player in group.Players) { - player.Status = MigrationStatus.MigrationFailed; - continue; - } + if (player.Status >= MigrationStatus.Migrated) continue; + if (time > group.Timestamp + MigrationTimeout) + { + player.Status = MigrationStatus.MigrationFailed; + continue; + } - if (!UserInfo.TryGetValue(player.UserId, out ConnectionBase? connection)) - { - if (player.Status == MigrationStatus.WaitingForDisconnect) - player.Status = MigrationStatus.WaitingForConnect; + if (!UserInfo.TryGetValue(player.UserId, out ConnectionBase? connection)) + { + if (player.Status == MigrationStatus.WaitingForDisconnect) + player.Status = MigrationStatus.WaitingForConnect; - continue; - } + continue; + } - if (player.Status == MigrationStatus.WaitingForConnect && connection.IsAuthenticated) - player.Status = MigrationStatus.Migrated; - } + if (player.Status == MigrationStatus.WaitingForConnect && connection.IsAuthenticated) + player.Status = MigrationStatus.Migrated; + } - bool isMigrationComplete = group.Players.All(player => player.Status >= MigrationStatus.Migrated); - if (!isMigrationComplete) continue; + bool isMigrationComplete = group.Players.All(player => player.Status >= MigrationStatus.Migrated); + if (!isMigrationComplete) continue; - // Now that all users are connected to the gameserver, let's add them to the game room - GameRoom room = group.NewRoom; - foreach (MigratingPlayer player in group.Players) - { - bool isOwner = player.UserId == group.Owner; - string? guest = isOwner ? group.OwnerGuest : null; - - // In case the player closed their game during migration or if something else caused a disconnection. - if (!UserInfo.TryGetValue(player.UserId, out ConnectionBase? connection) || !connection.IsAuthenticated) + // Now that all users are connected to the gameserver, let's add them to the game room + GameRoom room = group.NewRoom; + foreach (MigratingPlayer player in group.Players) { - player.Status = MigrationStatus.MigrationFailed; - group.NewRoom.FreeSlot(player.NewPlayerId); + bool isOwner = player.UserId == group.Owner; + string? guest = isOwner ? group.OwnerGuest : null; + + // In case the player closed their game during migration or if something else caused a disconnection. + if (!UserInfo.TryGetValue(player.UserId, out ConnectionBase? connection) || !connection.IsAuthenticated) + { + player.Status = MigrationStatus.MigrationFailed; + group.NewRoom.FreeSlot(player.NewPlayerId); + if (isOwner && guest != null) + group.NewRoom.FreeSlot(group.OwnerGuestId); + continue; + } + + GamePlayer gamePlayer; if (isOwner && guest != null) - group.NewRoom.FreeSlot(group.OwnerGuestId); - continue; - } + { + gamePlayer = Bombd.RoomManager.JoinRoomWithGuest(connection.Username, guest, connection.UserId, + player.NewPlayerId, group.OwnerGuestId, group.NewRoom); + } + else + { + gamePlayer = Bombd.RoomManager.JoinRoom(connection.Username, connection.UserId, + player.NewPlayerId, group.NewRoom); + } - GamePlayer gamePlayer; - if (isOwner && guest != null) - { - gamePlayer = Bombd.RoomManager.JoinRoomWithGuest(connection.Username, guest, connection.UserId, - player.NewPlayerId, group.OwnerGuestId, group.NewRoom); + gamePlayer.Send = (bytes, type) => SendMessage(gamePlayer.UserId, bytes, type); + gamePlayer.Disconnect = () => Disconnect(player.UserId); + Logger.LogInfo($"{gamePlayer.Username} migrated to {room.Game.GameName}."); + room.Simulation.OnPlayerJoin(gamePlayer); + OnPlayerJoined?.Invoke(this, new PlayerJoinEventArgs + { + Room = room, + Player = gamePlayer, + WasMigration = true + }); } - else + + List migratedPlayers = group.Players + .Where(player => player.Status == MigrationStatus.Migrated) + .Select(player => new GenericInt32(player.OldPlayerId)).ToList(); + + List unmigratedPlayers = group.Players + .Where(player => player.Status == MigrationStatus.MigrationFailed) + .Select(player => new GenericInt32(player.OldPlayerId)).ToList(); + + // Tell the old room about the migration that just occurred + var gamemanager = Bombd.GetService(); + var transaction = NetcodeTransaction.MakeRequest("gamemanager", "gameMigrationOccured"); + transaction["numPlayersMigrated"] = migratedPlayers.Count.ToString(); + transaction["numPlayersNotMigrated"] = unmigratedPlayers.Count.ToString(); + transaction["playersNotMigrated"] = Convert.ToBase64String(NetworkWriter.Serialize(unmigratedPlayers)); + transaction["playersMigrated"] = Convert.ToBase64String(NetworkWriter.Serialize(migratedPlayers)); + foreach (GamePlayer player in group.OldRoom.Game.Players) + gamemanager.SendTransaction(player.UserId, transaction); + + // If any of the old players are still connected to the gamemanager + // tell them that they failed to migrate + foreach (MigratingPlayer player in group.Players) { - gamePlayer = Bombd.RoomManager.JoinRoom(connection.Username, connection.UserId, - player.NewPlayerId, group.NewRoom); + if (player.Status != MigrationStatus.MigrationFailed) continue; + transaction = NetcodeTransaction.MakeRequest("gamemanager", "gameMigrationFailure"); + transaction.Error = "timeout"; + SendTransaction(player.UserId, transaction); } - - gamePlayer.Send = (bytes, type) => SendMessage(gamePlayer.UserId, bytes, type); - gamePlayer.Disconnect = () => Disconnect(player.UserId); - Logger.LogInfo($"{gamePlayer.Username} migrated to {room.Game.GameName}."); - room.Simulation.OnPlayerJoin(gamePlayer); - OnPlayerJoined?.Invoke(this, new PlayerJoinEventArgs - { - Room = room, - Player = gamePlayer, - WasMigration = true - }); - } - List migratedPlayers = group.Players - .Where(player => player.Status == MigrationStatus.Migrated) - .Select(player => new GenericInt32(player.OldPlayerId)).ToList(); - - List unmigratedPlayers = group.Players - .Where(player => player.Status == MigrationStatus.MigrationFailed) - .Select(player => new GenericInt32(player.OldPlayerId)).ToList(); - - // Tell the old room about the migration that just occurred - var gamemanager = Bombd.GetService(); - var transaction = NetcodeTransaction.MakeRequest("gamemanager", "gameMigrationOccured"); - transaction["numPlayersMigrated"] = migratedPlayers.Count.ToString(); - transaction["numPlayersNotMigrated"] = unmigratedPlayers.Count.ToString(); - transaction["playersNotMigrated"] = Convert.ToBase64String(NetworkWriter.Serialize(unmigratedPlayers)); - transaction["playersMigrated"] = Convert.ToBase64String(NetworkWriter.Serialize(migratedPlayers)); - foreach (GamePlayer player in group.OldRoom.Game.Players) - gamemanager.SendTransaction(player.UserId, transaction); - - // If any of the old players are still connected to the gamemanager - // tell them that they failed to migrate - foreach (MigratingPlayer player in group.Players) - { - if (player.Status != MigrationStatus.MigrationFailed) continue; - transaction = NetcodeTransaction.MakeRequest("gamemanager", "gameMigrationFailure"); - transaction.Error = "timeout"; - SendTransaction(player.UserId, transaction); + _playerMigrationGroups.RemoveAt(i--); } - - _playerMigrationGroups.RemoveAt(i--); } - - _playerLock.Release(); } private void ClearExpiredReservations() { - _playerLock.Wait(); - - int time = TimeHelper.LocalTime; - foreach ((string? key, ReservationGroup group) in _reservationGroups.ToList()) + lock (_playerLock) { - if (time <= group.Timestamp + ReservationTimeout) continue; - - while (group.Slots.TryDequeue(out int slot)) - group.Room.FreeSlot(slot); - _reservationGroups.Remove(key); + int time = TimeHelper.LocalTime; + foreach ((string? key, ReservationGroup group) in _reservationGroups.ToList()) + { + if (time <= group.Timestamp + ReservationTimeout) continue; + + while (group.Slots.TryDequeue(out int slot)) + group.Room.FreeSlot(slot); + _reservationGroups.Remove(key); + } } - - _playerLock.Release(); } protected override void OnGamedata(ConnectionBase connection, ArraySegment data) From 96bc50f907d42901e5ad8b1e69fda314e0a4bb2b Mon Sep 17 00:00:00 2001 From: derole <38840902+derole1@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:01:42 +0100 Subject: [PATCH 09/13] Fix stale sockets when a client uncleanly disconnects from an SSL service --- Bombd/Protocols/TCP/SslConnection.cs | 68 +++++++++++++++++----------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/Bombd/Protocols/TCP/SslConnection.cs b/Bombd/Protocols/TCP/SslConnection.cs index f5a6bf5..9f59ebc 100644 --- a/Bombd/Protocols/TCP/SslConnection.cs +++ b/Bombd/Protocols/TCP/SslConnection.cs @@ -13,7 +13,9 @@ public class SslConnection : ConnectionBase { private const int MaxMessageSize = 8192; private const int KeepAliveFrequency = 3000; - + private const int ReadTimeout = 10_000; + private const int WriteTimeout = 10_000; + // u32 MsgLength // char Md5Digest[16] // char Protocol @@ -47,12 +49,12 @@ public async void Connect(Socket socket) try { _sslStream = new SslStream(new NetworkStream(_socket, false), false); - + // Prevent lingering connections, the game should periodically send keep alive packets // in response to our own. - _sslStream.ReadTimeout = 10_000; - _sslStream.WriteTimeout = 10_000; - + _sslStream.ReadTimeout = ReadTimeout; + _sslStream.WriteTimeout = WriteTimeout; + await _sslStream.AuthenticateAsServerAsync(_server.Certificate, false, SslProtocols.Ssl3, false); _keepAliveTimer = new Timer(KeepAliveFrequency); @@ -66,7 +68,7 @@ public async void Connect(Socket socket) return; } - DoBlockAndReceive(); + DoBlockAndReceive(ReadTimeout); } public override void Disconnect() @@ -157,7 +159,7 @@ public override void Send(ArraySegment data, PacketType type) } } - private async void DoBlockAndReceive() + private async void DoBlockAndReceive(int readTimeout) { while (State != ConnectionState.Disconnected) { @@ -165,20 +167,23 @@ private async void DoBlockAndReceive() PacketType type; try { - int len = await _sslStream.ReadAsync(_recv.AsMemory(0, MessageHeaderSize)); - - // Socket has been shutdown on the other side - if (len == 0) + using (var cTokenSrc = CreateCancellationTokenTimeout(readTimeout)) { - Disconnect(); - return; - } - - if (len != MessageHeaderSize) - { - Logger.LogError("Received message with invalid header. Closing connection."); - Disconnect(); - return; + int len = await _sslStream.ReadAsync(_recv.AsMemory(0, MessageHeaderSize), cTokenSrc.Token); + + // Socket has been shutdown on the other side + if (len == 0) + { + Disconnect(); + return; + } + + if (len != MessageHeaderSize) + { + Logger.LogError("Received message with invalid header. Closing connection."); + Disconnect(); + return; + } } payloadSize = ((_recv[0] << 24) | (_recv[1] << 16) | (_recv[2] << 8) | _recv[3]) - @@ -196,14 +201,18 @@ private async void DoBlockAndReceive() int offset = 0; do { - len = await _sslStream.ReadAsync(_recv.AsMemory(offset, payloadSize - offset)); - if (len == 0) + using (var cTokenSrc = CreateCancellationTokenTimeout(readTimeout)) { - Disconnect(); - return; - } + int len = await _sslStream.ReadAsync(_recv.AsMemory(offset, payloadSize - offset), cTokenSrc.Token); - offset += len; + if (len == 0) + { + Disconnect(); + return; + } + + offset += len; + } } while (offset < payloadSize); } } @@ -224,4 +233,11 @@ private async void DoBlockAndReceive() } } } + + private CancellationTokenSource CreateCancellationTokenTimeout(int timeout) + { + var cTokenSrc = new CancellationTokenSource(); + cTokenSrc.CancelAfter(timeout); + return cTokenSrc; + } } \ No newline at end of file From f8bc0877b6a37b2942641ff5cb2a71f9a84919c0 Mon Sep 17 00:00:00 2001 From: derole <38840902+derole1@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:29:14 +0100 Subject: [PATCH 10/13] Add ability to join mid-race --- Bombd/Core/RoomManager.cs | 10 ++++------ Bombd/Core/SimServer.cs | 4 ++++ Bombd/Data/Matchmaking/ModNation.xml | 4 +++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Bombd/Core/RoomManager.cs b/Bombd/Core/RoomManager.cs index 1bd9038..60a6cc3 100644 --- a/Bombd/Core/RoomManager.cs +++ b/Bombd/Core/RoomManager.cs @@ -110,7 +110,8 @@ public GameRoom CreateRoom(CreateGameRequest request) request.Attributes["__JOIN_MODE"] = "OPEN"; request.Attributes["__MM_MODE_G"] = "OPEN"; request.Attributes["__MM_MODE_P"] = "OPEN"; - + request.Attributes["IS_LOCKED"] = "0"; + // Set default server type if none was provided, although this // generally shouldn't happen request.Attributes.TryAdd("SERVER_TYPE", "kartPark"); @@ -253,10 +254,6 @@ public List SearchRooms(GameAttributes attributes, Platform id, // before advertising the session. if (!room.Simulation.HasRaceSettings) return false; - - // If the race is already in progress, don't advertise the session - if (room.Simulation.RaceState >= RaceState.LoadingIntoRace || !room.Simulation.CanJoinAsRacer()) - return false; } foreach (KeyValuePair attribute in attributes) @@ -285,7 +282,8 @@ public void UpdateRoom(GameRoom room, EventSettings settings) attr["__MM_MODE_G"] = visibility; attr["__MM_MODE_P"] = visibility; attr["__JOIN_MODE"] = visibility; - + attr["IS_LOCKED"] = (room.Simulation.RaceState >= RaceState.LoadingIntoRace || !room.Simulation.CanJoinAsRacer()) ? "1" : "0"; + attr["__MAX_PLAYERS"] = settings.MaxHumans.ToString(); // TODO: Adjust player counts on Karting? diff --git a/Bombd/Core/SimServer.cs b/Bombd/Core/SimServer.cs index 58c2aad..507d402 100644 --- a/Bombd/Core/SimServer.cs +++ b/Bombd/Core/SimServer.cs @@ -420,6 +420,8 @@ private void SetCurrentGameroomState(RoomState state) { BroadcastPlayerState(); SwitchAllToRacers(); + if (_raceSettings != null) + Room.UpdateAttributes(_raceSettings.Value); break; } case RoomState.RaceInProgress: @@ -427,6 +429,8 @@ private void SetCurrentGameroomState(RoomState state) StartEvent(); BroadcastSessionInfo(); BroadcastPlayerState(); + if (_raceSettings != null) + Room.UpdateAttributes(_raceSettings.Value); break; } case RoomState.Ready: diff --git a/Bombd/Data/Matchmaking/ModNation.xml b/Bombd/Data/Matchmaking/ModNation.xml index f692ebc..96f72a3 100644 --- a/Bombd/Data/Matchmaking/ModNation.xml +++ b/Bombd/Data/Matchmaking/ModNation.xml @@ -36,4 +36,6 @@ kartPark competitive - \ No newline at end of file + + + \ No newline at end of file From 7d4d3d285bdd05e18a7e818827f47e4227513889 Mon Sep 17 00:00:00 2001 From: derole <38840902+derole1@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:08:20 +0100 Subject: [PATCH 11/13] Dont advertise sessions where players are still loading into race as it currently causes instability --- Bombd/Core/RoomManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bombd/Core/RoomManager.cs b/Bombd/Core/RoomManager.cs index 60a6cc3..9e6564e 100644 --- a/Bombd/Core/RoomManager.cs +++ b/Bombd/Core/RoomManager.cs @@ -254,6 +254,10 @@ public List SearchRooms(GameAttributes attributes, Platform id, // before advertising the session. if (!room.Simulation.HasRaceSettings) return false; + + // Joining in this state causes issues, to find out why + if (room.Simulation.RaceState == RaceState.LoadingIntoRace) + return false; } foreach (KeyValuePair attribute in attributes) From d8bf1e0e019e46baf0c9cdfde6e297ae8b2cca33 Mon Sep 17 00:00:00 2001 From: derole <38840902+derole1@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:32:14 +0100 Subject: [PATCH 12/13] Prevent in progress lobby joins --- Bombd/Core/RoomManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bombd/Core/RoomManager.cs b/Bombd/Core/RoomManager.cs index 9e6564e..d79be4d 100644 --- a/Bombd/Core/RoomManager.cs +++ b/Bombd/Core/RoomManager.cs @@ -255,8 +255,8 @@ public List SearchRooms(GameAttributes attributes, Platform id, if (!room.Simulation.HasRaceSettings) return false; - // Joining in this state causes issues, to find out why - if (room.Simulation.RaceState == RaceState.LoadingIntoRace) + // For now just prevent joins into in-progress lobbies entirely, causes instability + if (room.Simulation.RaceState >= RaceState.LoadingIntoRace || !room.Simulation.CanJoinAsRacer()) return false; } From b4b8280d5245ec39838fbb301af0db7971479878 Mon Sep 17 00:00:00 2001 From: derole <38840902+derole1@users.noreply.github.com> Date: Mon, 20 Oct 2025 01:04:57 +0100 Subject: [PATCH 13/13] Fix issue with XP races --- Bombd/Core/RoomManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bombd/Core/RoomManager.cs b/Bombd/Core/RoomManager.cs index d79be4d..27001fe 100644 --- a/Bombd/Core/RoomManager.cs +++ b/Bombd/Core/RoomManager.cs @@ -286,7 +286,7 @@ public void UpdateRoom(GameRoom room, EventSettings settings) attr["__MM_MODE_G"] = visibility; attr["__MM_MODE_P"] = visibility; attr["__JOIN_MODE"] = visibility; - attr["IS_LOCKED"] = (room.Simulation.RaceState >= RaceState.LoadingIntoRace || !room.Simulation.CanJoinAsRacer()) ? "1" : "0"; + attr["IS_LOCKED"] = (room.Simulation != null && (room.Simulation.RaceState >= RaceState.LoadingIntoRace || !room.Simulation.CanJoinAsRacer())) ? "1" : "0"; attr["__MAX_PLAYERS"] = settings.MaxHumans.ToString();