diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 60c4aaff3f..77dd716eaa 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -18,6 +18,7 @@ using Content.Server.LandMines;
using Content.Server.Maps;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Preferences.Managers;
+using Content.Server.ServerUpdates;
using Content.Server.Voting.Managers;
using Content.Shared.Administration;
using Content.Shared.CCVar;
@@ -38,6 +39,7 @@ namespace Content.Server.Entry
{
private EuiManager _euiManager = default!;
private IVoteManager _voteManager = default!;
+ private ServerUpdateManager _updateManager = default!;
///
public override void Init()
@@ -75,6 +77,7 @@ namespace Content.Server.Entry
{
_euiManager = IoCManager.Resolve();
_voteManager = IoCManager.Resolve();
+ _updateManager = IoCManager.Resolve();
var playerManager = IoCManager.Resolve();
@@ -92,6 +95,7 @@ namespace Content.Server.Entry
IoCManager.Resolve().Initialize();
_voteManager.Initialize();
+ _updateManager.Initialize();
}
}
@@ -145,6 +149,10 @@ namespace Content.Server.Entry
_voteManager.Update();
break;
}
+
+ case ModUpdateLevel.FramePostEngine:
+ _updateManager.Update();
+ break;
}
}
}
diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs
index c7b92b9537..ad64bf0bc4 100644
--- a/Content.Server/GameTicking/GameTicker.Player.cs
+++ b/Content.Server/GameTicking/GameTicker.Player.cs
@@ -26,11 +26,6 @@ namespace Content.Server.GameTicking
switch (args.NewStatus)
{
- case SessionStatus.Connecting:
- // Cancel shutdown update timer in progress.
- _updateShutdownCts?.Cancel();
- break;
-
case SessionStatus.Connected:
{
AddPlayerToDb(args.Session.UserId.UserId);
@@ -95,7 +90,6 @@ namespace Content.Server.GameTicking
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
- ServerEmptyUpdateRestartCheck();
_prefsManager.OnClientDisconnected(session);
break;
}
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 232cf59987..333ba99831 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -335,11 +335,9 @@ namespace Content.Server.GameTicking
if (DummyTicker)
return;
- if (_updateOnRoundEnd)
- {
- _baseServer.Shutdown(Loc.GetString("game-ticker-shutdown-server-update"));
+ // Handle restart for server update
+ if (_serverUpdates.RoundEnded())
return;
- }
_sawmill.Info("Restarting round!");
diff --git a/Content.Server/GameTicking/GameTicker.Updates.cs b/Content.Server/GameTicking/GameTicker.Updates.cs
deleted file mode 100644
index a75341702e..0000000000
--- a/Content.Server/GameTicking/GameTicker.Updates.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Linq;
-using System.Threading;
-using Robust.Shared.Enums;
-using Timer = Robust.Shared.Timing.Timer;
-
-namespace Content.Server.GameTicking
-{
- public sealed partial class GameTicker
- {
- private static readonly TimeSpan UpdateRestartDelay = TimeSpan.FromSeconds(20);
-
- [ViewVariables]
- private bool _updateOnRoundEnd;
- private CancellationTokenSource? _updateShutdownCts;
-
- private void InitializeUpdates()
- {
- _watchdogApi.UpdateReceived += WatchdogApiOnUpdateReceived;
- }
-
- private void WatchdogApiOnUpdateReceived()
- {
- _chatManager.DispatchServerAnnouncement(Loc.GetString("game-ticker-restart-round-server-update"));
- _updateOnRoundEnd = true;
- ServerEmptyUpdateRestartCheck();
- }
-
- ///
- /// Checks whether there are still players on the server,
- /// and if not starts a timer to automatically reboot the server if an update is available.
- ///
- private void ServerEmptyUpdateRestartCheck()
- {
- // Can't simple check the current connected player count since that doesn't update
- // before PlayerStatusChanged gets fired.
- // So in the disconnect handler we'd still see a single player otherwise.
- var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
- if (playersOnline || !_updateOnRoundEnd)
- {
- // Still somebody online.
- return;
- }
-
- if (_updateShutdownCts is {IsCancellationRequested: false})
- {
- // Do nothing because I guess we already have a timer running..?
- return;
- }
-
- _updateShutdownCts = new CancellationTokenSource();
-
- Timer.Spawn(UpdateRestartDelay, () =>
- {
- _baseServer.Shutdown(Loc.GetString("game-ticker-shutdown-server-update"));
- }, _updateShutdownCts.Token);
- }
- }
-}
diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs
index 2f4f73bc4a..95b3901b1e 100644
--- a/Content.Server/GameTicking/GameTicker.cs
+++ b/Content.Server/GameTicking/GameTicker.cs
@@ -5,6 +5,7 @@ using Content.Server.Database;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Preferences.Managers;
+using Content.Server.ServerUpdates;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
@@ -12,7 +13,6 @@ using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Robust.Server;
using Robust.Server.Maps;
-using Robust.Server.ServerStatus;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
#if EXCEPTION_TOLERANCE
@@ -55,7 +55,6 @@ namespace Content.Server.GameTicking
DebugTools.Assert(_prototypeManager.Index(FallbackOverflowJob).Name == Loc.GetString(FallbackOverflowJobName),
"Overflow role does not have the correct name!");
InitializeGameRules();
- InitializeUpdates();
_initialized = true;
}
@@ -101,7 +100,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IServerPreferencesManager _prefsManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
- [Dependency] private readonly IWatchdogApi _watchdogApi = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
@@ -116,5 +114,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly GhostSystem _ghosts = default!;
[Dependency] private readonly RoleBanManager _roleBanManager = default!;
+ [Dependency] private readonly ServerUpdateManager _serverUpdates = default!;
}
}
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 29a19a81ba..d2f12665e9 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -19,6 +19,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Objectives;
using Content.Server.Objectives.Interfaces;
using Content.Server.Preferences.Managers;
+using Content.Server.ServerUpdates;
using Content.Server.Voting.Managers;
using Content.Shared.Administration;
using Content.Shared.Administration.Logs;
@@ -42,6 +43,7 @@ namespace Content.Server.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Server/ServerUpdates/ServerUpdateManager.cs b/Content.Server/ServerUpdates/ServerUpdateManager.cs
new file mode 100644
index 0000000000..769c5d58d7
--- /dev/null
+++ b/Content.Server/ServerUpdates/ServerUpdateManager.cs
@@ -0,0 +1,109 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Shared.CCVar;
+using Robust.Server;
+using Robust.Server.Player;
+using Robust.Server.ServerStatus;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Timing;
+
+namespace Content.Server.ServerUpdates;
+
+///
+/// Responsible for restarting the server for update, when not disruptive.
+///
+public sealed class ServerUpdateManager
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IWatchdogApi _watchdog = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly IBaseServer _server = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ [ViewVariables]
+ private bool _updateOnRoundEnd;
+
+ private TimeSpan? _restartTime;
+
+ public void Initialize()
+ {
+ _watchdog.UpdateReceived += WatchdogOnUpdateReceived;
+ _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
+ }
+
+ public void Update()
+ {
+ if (_restartTime != null && _restartTime < _gameTiming.RealTime)
+ {
+ DoShutdown();
+ }
+ }
+
+ ///
+ /// Notify that the round just ended, which is a great time to restart if necessary!
+ ///
+ /// True if the server is going to restart.
+ public bool RoundEnded()
+ {
+ if (_updateOnRoundEnd)
+ {
+ DoShutdown();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ {
+ switch (e.NewStatus)
+ {
+ case SessionStatus.Connecting:
+ _restartTime = null;
+ break;
+ case SessionStatus.Disconnected:
+ ServerEmptyUpdateRestartCheck();
+ break;
+ }
+ }
+
+ private void WatchdogOnUpdateReceived()
+ {
+ _chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received"));
+ _updateOnRoundEnd = true;
+ ServerEmptyUpdateRestartCheck();
+ }
+
+ ///
+ /// Checks whether there are still players on the server,
+ /// and if not starts a timer to automatically reboot the server if an update is available.
+ ///
+ private void ServerEmptyUpdateRestartCheck()
+ {
+ // Can't simple check the current connected player count since that doesn't update
+ // before PlayerStatusChanged gets fired.
+ // So in the disconnect handler we'd still see a single player otherwise.
+ var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
+ if (playersOnline || !_updateOnRoundEnd)
+ {
+ // Still somebody online.
+ return;
+ }
+
+ if (_restartTime != null)
+ {
+ // Do nothing because I guess we already have a timer running..?
+ return;
+ }
+
+ var restartDelay = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.UpdateRestartDelay));
+ _restartTime = restartDelay + _gameTiming.RealTime;
+ }
+
+ private void DoShutdown()
+ {
+ _server.Shutdown(Loc.GetString("server-updates-shutdown"));
+ }
+}
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 6f4d1fc8fd..d28e6f5533 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -961,5 +961,15 @@ namespace Content.Shared.CCVar
///
public static readonly CVarDef DragDropDeadZone =
CVarDef.Create("control.drag_dead_zone", 12f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ /*
+ * UPDATE
+ */
+
+ ///
+ /// If a server update restart is pending, the delay after the last player leaves before we actually restart. In seconds.
+ ///
+ public static readonly CVarDef UpdateRestartDelay =
+ CVarDef.Create("update.restart_delay", 20f, CVar.SERVERONLY);
}
}
diff --git a/Resources/Locale/en-US/game-ticking/game-ticker.ftl b/Resources/Locale/en-US/game-ticking/game-ticker.ftl
index 56b27d2154..1dbe12299e 100644
--- a/Resources/Locale/en-US/game-ticking/game-ticker.ftl
+++ b/Resources/Locale/en-US/game-ticking/game-ticker.ftl
@@ -1,5 +1,3 @@
-game-ticker-restart-round-server-update = Update has been received, server will automatically restart for update at the end of this round.
-game-ticker-shutdown-server-update = Server is shutting down for update and will automatically restart.
game-ticker-restart-round = Restarting round...
game-ticker-start-round = The round is starting now...
game-ticker-start-round-cannot-start-game-mode-fallback = Failed to start {$failedGameMode} mode! Defaulting to {$fallbackMode}...
diff --git a/Resources/Locale/en-US/server-updates/server-updates.ftl b/Resources/Locale/en-US/server-updates/server-updates.ftl
new file mode 100644
index 0000000000..72047432bb
--- /dev/null
+++ b/Resources/Locale/en-US/server-updates/server-updates.ftl
@@ -0,0 +1,2 @@
+server-updates-received = Update has been received, server will automatically restart for update at the end of this round.
+server-updates-shutdown = Server is shutting down for update and will automatically restart.