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.
145 lines
7.7 KiB
Python
145 lines
7.7 KiB
Python
"""Execute the real empty-server gate and tick wrapper with native boundaries stubbed."""
|
|
from policy_harness import ROOT, body, run
|
|
|
|
MODE = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
|
SPAWN = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c'
|
|
|
|
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 + '::')
|
|
source = source.replace('string.Format', 'EnforceString::Format')
|
|
for name in ('GetGame()', 'manager', 'm_Performance', 'm_Settings'):
|
|
source = source.replace(name + '.', name + '->')
|
|
return source
|
|
|
|
methods = '\n'.join(cpp(body(MODE, signature)) for signature in (
|
|
'void ObservePlayerDisconnect(', 'protected bool CheckEmptyServerRestart(',
|
|
'protected void ServerTick('))
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
|
namespace System {int now=0;int GetTickCount(){return now;}}
|
|
namespace EGameOverTypes {enum {LOOSE=1};}
|
|
struct SCR_GameModeEndData {static int Create(int reason){return reason;}};
|
|
struct PlayerManager {int connected=0;int GetPlayerCount(){return connected;}};
|
|
struct Game {PlayerManager* manager=nullptr;PlayerManager* GetPlayerManager(){return manager;}} game;
|
|
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_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;}
|
|
// Leave native completion pending to prove repeated callbacks cannot end twice.
|
|
void EndGameMode(int reason){assert(reason==EGameOverTypes::LOOSE);endCalls++;}
|
|
void ServerTickMeasured(){measuredTicks++;}
|
|
''' + methods + r'''
|
|
};
|
|
struct Fixture {
|
|
PlayerManager manager;Mode mode;
|
|
Fixture(){Replication::server=true;game.manager=&manager;}
|
|
void tick(int now,int count){System::now=now;manager.connected=count;mode.ServerTick();}
|
|
};
|
|
int main(){
|
|
const int fiveHours=5*60*60*1000;
|
|
// Each newly loaded, continuously empty mission gets its own five-hour window.
|
|
System::now=123456;
|
|
for(int round=0;round<3;round++){
|
|
Fixture f;int start=System::now;
|
|
f.tick(start,0);f.tick(start+60*60*1000,0);f.tick(start+fiveHours-1,0);
|
|
assert(f.mode.endCalls==0);
|
|
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 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.
|
|
System::now=0;
|
|
{Fixture f;f.tick(fiveHours-1,0);f.tick(fiveHours,1);f.tick(2*fiveHours,1);
|
|
assert(f.mode.endCalls==0&&f.mode.measuredTicks==3);
|
|
f.tick(2*fiveHours+1000,0);
|
|
assert(f.mode.endCalls==1&&f.mode.m_EndReason=="All players disconnected. Restarting the mission.");}
|
|
// One of several players leaves; the last disconnect ends on the next tick.
|
|
System::now=0;
|
|
{Fixture f;f.tick(0,2);f.mode.ObservePlayerDisconnect();f.tick(1000,1);
|
|
assert(f.mode.endCalls==0);f.mode.ObservePlayerDisconnect();f.tick(2000,0);
|
|
assert(f.mode.endCalls==1);}
|
|
// Join/disconnect entirely between ticks still triggers a restart.
|
|
{Fixture f;f.mode.ObservePlayerDisconnect();assert(f.mode.endCalls==0);
|
|
f.tick(3000,0);assert(f.mode.endCalls==1);}
|
|
// A connection arriving before the deferred check prevents the restart.
|
|
{Fixture f;f.mode.ObservePlayerDisconnect();f.tick(4000,1);assert(f.mode.endCalls==0);
|
|
f.tick(5000,0);assert(f.mode.endCalls==1);}
|
|
// Preparation is covered before Active() allows normal tactical/service ticks.
|
|
for(bool disconnected:{false,true}){
|
|
System::now=0;Fixture f;f.mode.phase=0;
|
|
if(disconnected)f.mode.ObservePlayerDisconnect();
|
|
f.tick(disconnected?1000:fiveHours,0);
|
|
assert(f.mode.endCalls==1&&f.mode.measuredTicks==0&&f.mode.performance.starts==0);
|
|
}
|
|
// 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_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: 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')
|