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.
103 lines
5.2 KiB
Python
103 lines
5.2 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 + '::')
|
|
for name in ('GetGame()', 'manager', 'm_Performance'):
|
|
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 Mode {
|
|
bool m_Stopped=false,m_NativeEndHandled=false,m_HadConnectedPlayers=false,m_EmptyRestartRequested=false;
|
|
int m_EmptyMissionStartedAt=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 five 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);
|
|
}
|
|
// Client, stopped, already-ended and unavailable population must never reload.
|
|
for(int fault=0;fault<4;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;
|
|
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 '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')
|