diff --git a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs index 3ee54af303..4acbdf6dcd 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs @@ -234,8 +234,6 @@ public sealed class AddTests : ContentIntegrationTest await server.WaitIdleAsync(); var sDatabase = server.ResolveDependency(); - var sEntities = server.ResolveDependency(); - var sMaps = server.ResolveDependency(); var sSystems = server.ResolveDependency(); var sAdminLogSystem = sSystems.GetEntitySystem(); @@ -245,10 +243,7 @@ public sealed class AddTests : ContentIntegrationTest await server.WaitPost(() => { - var coordinates = GetMainEntityCoordinates(sMaps); - var entity = sEntities.SpawnEntity(null, coordinates); - - sAdminLogSystem.Add(LogType.Unknown, $"{entity} test log: {guid}"); + sAdminLogSystem.Add(LogType.Unknown, $"test log: {guid}"); }); await server.WaitPost(() => @@ -284,8 +279,7 @@ public sealed class AddTests : ContentIntegrationTest await foreach (var json in sDatabase.GetAdminLogsJson(filter)) { var root = json.RootElement; - - Assert.That(root.TryGetProperty("entity", out _), Is.True); + Assert.That(root.TryGetProperty("guid", out _), Is.True); json.Dispose(); diff --git a/Content.Server/GameTicking/GameTicker.CVars.cs b/Content.Server/GameTicking/GameTicker.CVars.cs index 5c86dc53bd..ba7aa70468 100644 --- a/Content.Server/GameTicking/GameTicker.CVars.cs +++ b/Content.Server/GameTicking/GameTicker.CVars.cs @@ -27,6 +27,11 @@ namespace Content.Server.GameTicking [ViewVariables] public float MaxStationOffset { get; private set; } = 0f; +#if EXCEPTION_TOLERANCE + [ViewVariables] + public int RoundStartFailShutdownCount { get; private set; } = 0; +#endif + private void InitializeCVars() { _configurationManager.OnValueChanged(CCVars.GameLobbyEnabled, value => LobbyEnabled = value, true); @@ -37,6 +42,9 @@ namespace Content.Server.GameTicking _configurationManager.OnValueChanged(CCVars.StationOffset, value => StationOffset = value, true); _configurationManager.OnValueChanged(CCVars.StationRotation, value => StationRotation = value, true); _configurationManager.OnValueChanged(CCVars.MaxStationOffset, value => MaxStationOffset = value, true); +#if EXCEPTION_TOLERANCE + _configurationManager.OnValueChanged(CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true); +#endif } } } diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs index e35548a86d..7252a13fa6 100644 --- a/Content.Server/GameTicking/GameTicker.JobController.cs +++ b/Content.Server/GameTicking/GameTicker.JobController.cs @@ -108,6 +108,9 @@ namespace Content.Server.GameTicking private string? PickBestAvailableJob(IPlayerSession playerSession, HumanoidCharacterProfile profile, StationId station) { + if (station == StationId.Invalid) + return null; + var available = _stationSystem.StationInfo[station].JobList; bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId) diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 3a782e5994..dda8f0b2a1 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -1,31 +1,21 @@ -using System; -using System.Collections.Generic; using System.Linq; -using Content.Server.Database; using Content.Server.GameTicking.Events; using Content.Server.Ghost; using Content.Server.Maps; using Content.Server.Mind; using Content.Server.Players; using Content.Server.Station; -using Content.Shared.CCVar; using Content.Shared.Coordinates; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Station; using Prometheus; using Robust.Server.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; -using Robust.Shared.ViewVariables; namespace Content.Server.GameTicking { @@ -39,7 +29,10 @@ namespace Content.Server.GameTicking "ss14_round_length", "Round length in seconds."); - [Dependency] private readonly IServerDbManager _db = default!; +#if EXCEPTION_TOLERANCE + [ViewVariables] + private int _roundStartFailCount = 0; +#endif [ViewVariables] private TimeSpan _roundStartTimeSpan; @@ -75,7 +68,6 @@ namespace Content.Server.GameTicking DefaultMap = _mapManager.CreateMap(); _mapManager.AddUninitializedMap(DefaultMap); - _startingRound = false; var startTime = _gameTiming.RealTime; var maps = new List() { _gameMapManager.GetSelectedMapChecked(true) }; @@ -173,97 +165,113 @@ namespace Content.Server.GameTicking } } - public async void StartRound(bool force = false) + public void StartRound(bool force = false) { #if EXCEPTION_TOLERANCE try { #endif - // If this game ticker is a dummy or the round is already being started, do nothing! - if (DummyTicker || _startingRound) - return; + // If this game ticker is a dummy or the round is already being started, do nothing! + if (DummyTicker || _startingRound) + return; - _startingRound = true; + _startingRound = true; - DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); - Logger.InfoS("ticker", "Starting round!"); + DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); + Logger.InfoS("ticker", "Starting round!"); - SendServerMessage(Loc.GetString("game-ticker-start-round")); + SendServerMessage(Loc.GetString("game-ticker-start-round")); - StartGamePresetRules(); + LoadMaps(); - RoundLengthMetric.Set(0); + StartGamePresetRules(); - var playerIds = _playersInLobby.Keys.Select(player => player.UserId.UserId).ToArray(); - RoundId = await _db.AddNewRound(playerIds); + RoundLengthMetric.Set(0); - var startingEvent = new RoundStartingEvent(); - RaiseLocalEvent(startingEvent); + var playerIds = _playersInLobby.Keys.Select(player => player.UserId.UserId).ToArray(); + RoundId = _db.AddNewRound(playerIds).Result; - List readyPlayers; - if (LobbyEnabled) + var startingEvent = new RoundStartingEvent(); + RaiseLocalEvent(startingEvent); + + List readyPlayers; + if (LobbyEnabled) + { + readyPlayers = _playersInLobby.Where(p => p.Value == LobbyPlayerStatus.Ready).Select(p => p.Key) + .ToList(); + } + else + { + readyPlayers = _playersInLobby.Keys.ToList(); + } + + readyPlayers.RemoveAll(p => + { + if (_roleBanManager.GetRoleBans(p.UserId) != null) + return false; + Logger.ErrorS("RoleBans", $"Role bans for player {p} {p.UserId} have not been loaded yet."); + return true; + }); + + // Get the profiles for each player for easier lookup. + var profiles = _prefsManager.GetSelectedProfilesForPlayers( + readyPlayers + .Select(p => p.UserId).ToList()) + .ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value); + + foreach (var readyPlayer in readyPlayers) + { + if (!profiles.ContainsKey(readyPlayer.UserId)) { - readyPlayers = _playersInLobby.Where(p => p.Value == LobbyPlayerStatus.Ready).Select(p => p.Key) - .ToList(); - } - else - { - readyPlayers = _playersInLobby.Keys.ToList(); + profiles.Add(readyPlayer.UserId, HumanoidCharacterProfile.Random()); } + } - readyPlayers.RemoveAll(p => - { - if (_roleBanManager.GetRoleBans(p.UserId) != null) - return false; - Logger.ErrorS("RoleBans", $"Role bans for player {p} {p.UserId} have not been loaded yet."); - return true; - }); + var origReadyPlayers = readyPlayers.ToArray(); - // Get the profiles for each player for easier lookup. - var profiles = _prefsManager.GetSelectedProfilesForPlayers( - readyPlayers - .Select(p => p.UserId).ToList()) - .ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value); + if (!StartPreset(origReadyPlayers, force)) + return; - foreach (var readyPlayer in readyPlayers) - { - if (!profiles.ContainsKey(readyPlayer.UserId)) - { - profiles.Add(readyPlayer.UserId, HumanoidCharacterProfile.Random()); - } - } + // MapInitialize *before* spawning players, our codebase is too shit to do it afterwards... + _mapManager.DoMapInitialize(DefaultMap); - var origReadyPlayers = readyPlayers.ToArray(); + SpawnPlayers(readyPlayers, origReadyPlayers, profiles, force); - if (!StartPreset(origReadyPlayers, force)) - return; + _roundStartDateTime = DateTime.UtcNow; + RunLevel = GameRunLevel.InRound; - // MapInitialize *before* spawning players, our codebase is too shit to do it afterwards... - _mapManager.DoMapInitialize(DefaultMap); - - SpawnPlayers(readyPlayers, origReadyPlayers, profiles, force); - - _roundStartDateTime = DateTime.UtcNow; - RunLevel = GameRunLevel.InRound; - - _startingRound = false; - - _roundStartTimeSpan = _gameTiming.RealTime; - SendStatusToAll(); - ReqWindowAttentionAll(); - UpdateLateJoinStatus(); - UpdateJobsAvailable(); + _roundStartTimeSpan = _gameTiming.RealTime; + SendStatusToAll(); + ReqWindowAttentionAll(); + UpdateLateJoinStatus(); + UpdateJobsAvailable(); #if EXCEPTION_TOLERANCE } - catch(Exception e) + catch (Exception e) { + _roundStartFailCount++; - Logger.WarningS("ticker", $"Exception caught while trying to start the round! Restarting..."); + if (RoundStartFailShutdownCount > 0 && _roundStartFailCount >= RoundStartFailShutdownCount) + { + Logger.FatalS("ticker", + $"Failed to start a round {_roundStartFailCount} time(s) in a row... Shutting down!"); + _runtimeLog.LogException(e, nameof(GameTicker)); + _baseServer.Shutdown("Restarting server"); + return; + } + + Logger.WarningS("ticker", $"Exception caught while trying to start the round! Restarting round..."); _runtimeLog.LogException(e, nameof(GameTicker)); + _startingRound = false; RestartRound(); + return; } + + // Round started successfully! Reset counter... + _roundStartFailCount = 0; #endif + _startingRound = false; } private void RefreshLateJoinAllowed() @@ -373,7 +381,6 @@ namespace Content.Server.GameTicking RunLevel = GameRunLevel.PreRoundLobby; LobbySong = _robustRandom.Pick(_lobbyMusicCollection.PickFiles).ToString(); ResettingCleanup(); - LoadMaps(); if (!LobbyEnabled) { @@ -411,18 +418,16 @@ namespace Content.Server.GameTicking unCastData.ContentData()?.WipeMind(); } - // Delete all entities. - foreach (var entity in EntityManager.GetEntities().ToList()) + _mapManager.Restart(); + + // Delete all remaining entities. + foreach (var entity in EntityManager.GetEntities().ToArray()) { // TODO: Maybe something less naive here? // FIXME: Actually, definitely. EntityManager.DeleteEntity(entity); } - _startingRound = false; - - _mapManager.Restart(); - _roleBanManager.Restart(); // Clear up any game rules. @@ -465,8 +470,7 @@ namespace Content.Server.GameTicking RoundLengthMetric.Inc(frameTime); } - if (RunLevel != GameRunLevel.PreRoundLobby || - Paused || + if (RunLevel != GameRunLevel.PreRoundLobby || Paused || _roundStartTime > _gameTiming.CurTime || _roundStartCountdownHasNotStartedYetDueToNoPlayers) { diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index e3b1ef5518..a74684a47a 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.CharacterAppearance.Systems; using Content.Server.Chat.Managers; +using Content.Server.Database; using Content.Server.Ghost; using Content.Server.Maps; using Content.Server.PDA; @@ -89,6 +90,7 @@ namespace Content.Server.GameTicking [Dependency] private readonly IBaseServer _baseServer = default!; [Dependency] private readonly IWatchdogApi _watchdogApi = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; + [Dependency] private readonly IServerDbManager _db = default!; #if EXCEPTION_TOLERANCE [Dependency] private readonly IRuntimeLog _runtimeLog = default!; #endif diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 5e0e20d96a..e8ca923a7e 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -60,6 +60,9 @@ namespace Content.Shared.CCVar public static readonly CVarDef EventsEnabled = CVarDef.Create("events.enabled", true, CVar.ARCHIVE | CVar.SERVERONLY); + /// + /// Disables most functionality in the GameTicker. + /// public static readonly CVarDef GameDummyTicker = CVarDef.Create("game.dummyticker", false, CVar.ARCHIVE | CVar.SERVERONLY); @@ -154,6 +157,15 @@ namespace Content.Shared.CCVar public static readonly CVarDef SoftMaxPlayers = CVarDef.Create("game.soft_max_players", 30, CVar.SERVERONLY | CVar.ARCHIVE); +#if EXCEPTION_TOLERANCE + /// + /// Amount of times round start must fail before the server is shut down. + /// Set to 0 or a negative number to disable. + /// + public static readonly CVarDef RoundStartFailShutdownCount = + CVarDef.Create("game.round_start_fail_shutdown_count", 5, CVar.SERVERONLY | CVar.SERVER); +#endif + /* * Discord */