Make empty-server restarts configurable in the mission menu

Add persistent last-disconnect and idle-interval controls with enabled/five-hour defaults. Apply saved settings immediately, reset idle time while occupied, and cover migration, routing, layout and restart behavior.
This commit is contained in:
2026-10-14 14:05:46 +02:00
parent 0048527fe2
commit e4ca37dfc1
13 changed files with 242 additions and 61 deletions
+50 -8
View File
@@ -8,7 +8,8 @@ def cpp(source):
source = source.replace('protected ', '').replace('PlayerManager manager', 'PlayerManager* manager')
for name in ('Replication', 'System', 'SCR_GameModeEndData', 'EGameOverTypes'):
source = source.replace(name + '.', name + '::')
for name in ('GetGame()', 'manager', 'm_Performance'):
source = source.replace('string.Format', 'EnforceString::Format')
for name in ('GetGame()', 'manager', 'm_Performance', 'm_Settings'):
source = source.replace(name + '.', name + '->')
return source
@@ -28,9 +29,17 @@ struct Game {PlayerManager* manager=nullptr;PlayerManager* GetPlayerManager(){re
Game* GetGame(){return &game;}
template<class... T> void PrintFormat(const char*,T...){}
struct Performance {int starts=0,ends=0;int BeginTick(){return ++starts;}void EndTick(int){ends++;}};
struct EnforceString {
static std::string Format(std::string pattern,int value){pattern.replace(pattern.find("%1"),2,std::to_string(value));return pattern;}
};
struct Settings {
int restart=1,hours=5;
int Get(std::string key){if(key=="restartOnEmpty")return restart;assert(key=="emptyRestartHours");return hours;}
};
struct Mode {
Settings settings;Settings* m_Settings=&settings;
bool m_Stopped=false,m_NativeEndHandled=false,m_HadConnectedPlayers=false,m_EmptyRestartRequested=false;
int m_EmptyMissionStartedAt=System::now,phase=1,endCalls=0,measuredTicks=0;
int m_EmptySince=System::now,phase=1,endCalls=0,measuredTicks=0;
std::string m_EndReason;
Performance performance;Performance* m_Performance=&performance;
bool Active(){return !m_Stopped&&!m_NativeEndHandled&&phase>0&&phase<5;}
@@ -55,7 +64,7 @@ int main(){
int before=f.mode.measuredTicks;
f.tick(start+fiveHours,0);
assert(f.mode.endCalls==1&&f.mode.measuredTicks==before);
assert(f.mode.m_EndReason=="The server has been empty for five hours. Restarting the mission.");
assert(f.mode.m_EndReason=="The server has been empty for 5 hours. Restarting the mission.");
f.tick(start+fiveHours+1000,0);assert(f.mode.endCalls==1);
}
// All native connections block a scheduled restart, even without a spawned life.
@@ -82,21 +91,54 @@ int main(){
f.tick(disconnected?1000:fiveHours,0);
assert(f.mode.endCalls==1&&f.mode.measuredTicks==0&&f.mode.performance.starts==0);
}
// Client, stopped, already-ended and unavailable population must never reload.
for(int fault=0;fault<4;fault++){
// Independent controls: no idle restart at any age when both are disabled.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=0;
f.tick(0,1);f.mode.ObservePlayerDisconnect();f.tick(1000,0);f.tick(169*3600000,0);
assert(f.mode.endCalls==0);}
// Last-disconnect restart remains available when the periodic interval is off.
System::now=0;
{Fixture f;f.mode.settings.hours=0;f.tick(1000,1);f.tick(2000,0);assert(f.mode.endCalls==1);}
// With immediate restart disabled, the full configured interval starts at
// the last disconnect even if the mission has been occupied for many hours.
for(int hours:{1,5,168}){
System::now=0;Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=hours;
f.tick(20*3600000,1);System::now+=100;f.mode.ObservePlayerDisconnect();int since=System::now;
f.tick(since,0);f.tick(since+hours*3600000-1,0);assert(f.mode.endCalls==0);
f.tick(since+hours*3600000,0);assert(f.mode.endCalls==1);
}
// A new connection resets the idle interval, including a short unaudited visit.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=1;
f.tick(3599999,1);System::now=3600000;f.mode.ObservePlayerDisconnect();
f.tick(7199999,0);assert(f.mode.endCalls==0);f.tick(7200000,0);assert(f.mode.endCalls==1);}
// Saved changes take effect on subsequent checks; disabling/enabling the
// interval does not forget elapsed empty time, and never restarts an occupied game.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.tick(2*3600000,0);assert(f.mode.endCalls==0);
f.mode.settings.hours=0;f.tick(6*3600000,0);assert(f.mode.endCalls==0);
f.mode.settings.hours=1;f.tick(6*3600000+1,1);assert(f.mode.endCalls==0);
f.tick(6*3600000+2,0);assert(f.mode.endCalls==0);
f.mode.settings.restart=1;f.tick(6*3600000+3,0);assert(f.mode.endCalls==1);}
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.tick(2*3600000,0);f.mode.settings.hours=1;
f.tick(2*3600000+1,0);assert(f.mode.endCalls==1);}
// Client, stopped, already-ended and unavailable population/settings never reload.
for(int fault=0;fault<5;fault++){
System::now=0;Fixture f;
if(fault==0)Replication::server=false;
if(fault==1)f.mode.m_Stopped=true;
if(fault==2)f.mode.m_NativeEndHandled=true;
if(fault==3)game.manager=nullptr;
if(fault==4)f.mode.m_Settings=nullptr;
f.mode.ObservePlayerDisconnect();f.tick(fiveHours,0);assert(f.mode.endCalls==0);
}
}
''')
init = body(MODE, 'protected override void EOnInit(')
assert init.index('if (!Replication.IsServer()) return;') < init.index('m_EmptyMissionStartedAt = System.GetTickCount();')
assert init.index('m_EmptyMissionStartedAt = System.GetTickCount();') < init.index('CallLater(ServerTick, 1000, true)')
assert init.index('if (!Replication.IsServer()) return;') < init.index('m_EmptySince = System.GetTickCount();')
assert init.index('m_EmptySince = System.GetTickCount();') < init.index('CallLater(ServerTick, 1000, true)')
assert 'mode.ObservePlayerDisconnect();' in body(SPAWN, 'override void OnPlayerDisconnected_S(')
assert 'Remove(ServerTick)' in body(MODE, 'protected void StopServices(')
print('PASS: last disconnect, pending admission/spectator population, five-hour boundary, repeated empty rounds, preparation, authority and duplicate-end guards')
print('PASS: configurable independent restarts, idle clock reset and live setting changes; last disconnect, pending admission/spectator population, five-hour boundary, repeated empty rounds, preparation, authority and duplicate-end guards')