Restart empty missions after the last disconnect and every five hours

Use connected player counts to protect loading players and spectators, and elapsed real time for idle restarts. Reuse native mission termination and cover restart timing, authority and lifecycle guards with targeted regressions.
This commit is contained in:
2026-10-14 13:56:56 +02:00
parent bf85f62dca
commit 0048527fe2
6 changed files with 164 additions and 24 deletions
@@ -73,6 +73,9 @@ class ERR_GameMode : SCR_BaseGameMode
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
protected bool m_HadConnectedPlayers;
protected bool m_EmptyRestartRequested;
protected int m_EmptyMissionStartedAt;
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
protected bool m_Stopped;
protected bool m_NativeEndHandled;
@@ -449,6 +452,38 @@ class ERR_GameMode : SCR_BaseGameMode
protected string SettingsPath() { return ERR_Settings.PATH; }
// A connection can arrive and leave between server ticks, even before audit
// or spawning. Defer the population check until native disconnect cleanup ends.
void ObservePlayerDisconnect()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled) return;
m_HadConnectedPlayers = true;
}
protected bool CheckEmptyServerRestart()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled) return false;
if (m_EmptyRestartRequested) return true;
PlayerManager manager = GetGame().GetPlayerManager();
if (!manager) return false;
// Count all connections, including admission and spectators, not living bodies.
if (manager.GetPlayerCount() > 0)
{
m_HadConnectedPlayers = true;
return false;
}
// Real elapsed milliseconds: mission clock settings cannot accelerate this.
if (!m_HadConnectedPlayers && System.GetTickCount() - m_EmptyMissionStartedAt < 18000000) return false;
m_EmptyRestartRequested = true;
m_EndReason = "The server has been empty for five hours. Restarting the mission.";
if (m_HadConnectedPlayers) m_EndReason = "All players disconnected. Restarting the mission.";
PrintFormat("Reintroduced [round lifecycle]: %1", m_EndReason);
// Native termination also handles PREPARING, before Transition is available.
// OnGameModeEnd stops our services; vanilla owns the scenario reload.
EndGameMode(SCR_GameModeEndData.Create(EGameOverTypes.LOOSE));
return true;
}
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
@@ -457,6 +492,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(ApplyLocalViewDistance, 1000, true);
if (!Replication.IsServer()) return;
m_EmptyMissionStartedAt = System.GetTickCount();
m_Settings = new ERR_Settings(SettingsPath());
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
PublishViewDistance();
@@ -564,6 +600,7 @@ class ERR_GameMode : SCR_BaseGameMode
protected void ServerTick()
{
if (CheckEmptyServerRestart()) return;
if (!Replication.IsServer() || !Active()) return;
int started;
if (m_Performance) started = m_Performance.BeginTick();
@@ -618,9 +655,8 @@ class ERR_GameMode : SCR_BaseGameMode
}
}
else m_NoSurvivorsSince = -1;
// A played round must finish after its last reserved body expires, even
// with nobody connected. Keep tactical/observer work idle until players
// return; a fresh unplayed round still waits without a restart loop.
// Empty-server restart is checked by ServerTick before tactical work.
// Keep tactical/observer work idle when nobody is connected.
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return;
if (m_Performance) perfStamp = System.GetTickCount();
array<int> connected = {};
@@ -347,6 +347,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
ERR_GameMode mode = ERR_GameMode.Get();
if (mode)
{
mode.ObservePlayerDisconnect();
mode.InitialAdmissionStarted(playerId);
mode.Players().Disconnect(playerId);
}