Files
EscapistsReintroduced/tests/test_command_maintenance.py
T

101 lines
7.4 KiB
Python

"""Execute actual Core tick/stop/default maintenance methods with fake engine APIs.
Checks scheduling/authority boundaries, not native aircraft, network or timing.
"""
from policy_harness import ROOT,body,run
MODE=ROOT/'addons/core/Scripts/Game/ERR/ERR_GameMode.c'
ACTORS=ROOT/'addons/core/Scripts/Game/ERR/ERR_Actors.c'
def cpp(s):
s=s.replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
s=s.replace('ERR_Phase.DEFEAT','5').replace('ERR_Phase.PRISON','1').replace('ERR_MissionRules.Terminal(m_Phase)','(m_Phase>=5)')
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('foreach (int id : connected)','for (int id : connected)')
s=s.replace('SCR_PlayerController controller = SCR_PlayerController.Cast(', 'Controller* controller = Controller::Cast(')
s=s.replace('foreach (IEntity entity : m_OwnedEntities)','for (Entity* entity : m_OwnedEntities)')
s=s.replace('SCR_AIGroup.Cast(entity)','(entity && entity->group)').replace('ERR_Actors.DeleteSpawned(entity)','Delete(entity)')
s=s.replace('ERR_ObserverReason.NONE','0')
for var in ('m_Extraction','m_RadioDefence','m_Recovery','m_Command','m_Roster','controller','GetWorld()','GetGame()','GetPlayerManager()','GetCallqueue()'):
s=s.replace(var+'.',var+'->')
for name in ('StartScenario','ServerTick','BindLocalObserver'):s=s.replace('Remove('+name+')','Remove("'+name+'")')
return s
run(r'''
#include <cassert>
#include <functional>
#include <string>
#include <vector>
bool server=true;int bumps=0;void Bump(){bumps++;}
std::vector<std::string> events;
struct World {float now=1234;float GetWorldTime(){return now;}}world;
struct Controller {int updates=0,resets=0;static Controller* Cast(Controller* p){return p;}void ERR_UpdateObserverServer(){updates++;events.push_back("observer");}void ERR_SetObserver(int){resets++;}};
struct Players {int count=0;Controller controller;int GetPlayerCount(){return count;}void GetPlayers(std::vector<int>& out){if(count)out.push_back(1);}Controller* GetPlayerController(int){return &controller;}}players;
struct Queue {std::vector<std::string> removed;void Remove(std::string s){removed.push_back(s);}}queue;
struct Game {Players* GetPlayerManager(){return &players;}Queue* GetCallqueue(){return &queue;}}game;Game* GetGame(){return &game;}
struct Hook {int calls=0;std::function<void()> callback;std::string name;void Invoke(float=0){calls++;events.push_back(name);if(callback)callback();}};
struct Work {int calls=0;std::string name;std::function<void()> callback;void Tick(float=0){calls++;events.push_back(name);if(callback)callback();}};
struct Roster {bool survivor=true;bool HasSurvivor(){return survivor;}}roster;
struct Entity {bool group=false;};int deleted=0;void Delete(Entity*){deleted++;}
struct Command {
virtual '''+body(ACTORS,'void Maintain(')+r'''
virtual void Tick(float){events.push_back("tactics");}
virtual void Stop(){events.push_back("stop");}
virtual ~Command()=default;
};
struct Retained : Command {
int maintenance=0,tactics=0,stops=0;float time=-1;bool pending=true;
std::function<void()> callback;
void Maintain(float now) override {maintenance++;time=now;events.push_back("maintain");if(callback)callback();}
void Tick(float) override {tactics++;events.push_back("tactics");}
void Stop() override {stops++;pending=false;events.push_back("stop");}
};
struct Mode {
bool m_Stopped=false,m_NativeEndHandled=false,m_StartedPlayers=false;int m_Phase=1;
float m_NoSurvivorsSince=-1;std::string m_EndReason;
Work extraction{0,"extraction",{}},radio{0,"radio",{}},recovery{0,"recovery",{}};
Work* m_Extraction=&extraction;Work* m_RadioDefence=&radio;Work* m_Recovery=&recovery;
Command* m_Command=nullptr;Roster* m_Roster=&roster;
Hook m_OnTick{0,{},"event"},m_OnStop{0,{},"onstop"};std::vector<Entity*> m_OwnedEntities;
World* GetWorld(){return &world;}bool RefreshRadioFeedback(){return false;}
void Transition(int phase){m_Phase=phase;StopServices();}
'''+cpp(body(MODE,'bool Active('))+cpp(body(MODE,'protected void ServerTick('))+cpp(body(MODE,'protected void StopServices('))+r'''
};
void reset(){server=true;events.clear();world.now=1234;players.count=0;players.controller=Controller();queue.removed.clear();roster.survivor=true;deleted=0;}
int main(){
// Existing defaults remain inert; the no-player tactical pause is preserved.
{reset();Mode m;Command c;m.m_Command=&c;m.ServerTick();assert((events==std::vector<std::string>{"extraction","radio","recovery"}));assert(m.m_OnTick.calls==0);}
// Retained work receives exactly one maintenance call on an empty server,
// including later ticks, without tactical decisions, observer updates or OnTick.
{reset();Mode m;Retained c;m.m_Command=&c;m.m_NoSurvivorsSince=100;
m.ServerTick();world.now=2234;m.ServerTick();assert(c.maintenance==2&&c.time==2234&&c.tactics==0&&c.pending);
assert(players.controller.updates==0&&m.m_OnTick.calls==0&&m.m_NoSurvivorsSince==-1);}
// Populated ticks have exactly one maintenance call before ordinary decisions.
{reset();players.count=1;Mode m;Retained c;m.m_Command=&c;m.ServerTick();assert(c.maintenance==1&&c.tactics==1);
assert((events==std::vector<std::string>{"extraction","radio","recovery","maintain","observer","tactics","event"}));}
// No command service is valid both with and without connected players.
{reset();Mode m;m.ServerTick();players.count=1;m.ServerTick();assert(m.m_OnTick.calls==1);}
// Authority, preparation, terminal phase, native end and stopped service all
// refuse maintenance as well as tactics. No stale clock/actor admission exists.
for(int fault=0;fault<5;fault++){reset();Mode m;Retained c;m.m_Command=&c;
if(fault==0){server=false;} if(fault==1){m.m_Phase=0;} if(fault==2){m.m_Phase=5;}
if(fault==3){m.m_NativeEndHandled=true;} if(fault==4){m.m_Stopped=true;}
m.ServerTick();assert(c.maintenance==0&&c.tactics==0&&events.empty());}
// A preceding service can terminalize the round: later maintenance must stop.
for(int earlier=0;earlier<2;earlier++){reset();Mode m;Retained c;m.m_Command=&c;
auto end=[&](){m.Transition(5);};if(earlier==0)m.extraction.callback=end;else m.recovery.callback=end;
m.ServerTick();assert(c.maintenance==0&&c.tactics==0&&c.stops==1&&!m.Active());}
// Maintenance may itself end the round. Its return cannot resurrect tactics.
{reset();players.count=1;Mode m;Retained c;m.m_Command=&c;c.callback=[&](){m.Transition(5);};m.ServerTick();
assert(c.maintenance==1&&c.tactics==0&&c.stops==1&&m.m_OnTick.calls==0&&players.controller.updates==0);}
// Existing defeat path still runs after maintenance with players present.
{reset();players.count=1;roster.survivor=false;Mode m;Retained c;m.m_Command=&c;m.m_StartedPlayers=true;m.m_NoSurvivorsSince=0;world.now=30000;
m.ServerTick();assert(c.maintenance==1&&c.stops==1&&c.tactics==0&&m.m_Phase==5&&!m.Active());}
// Stop removes the existing callback before the command receives terminal Stop;
// no maintenance drain loop or new scheduling has been introduced in Core.
{reset();Mode m;Retained c;m.m_Command=&c;Entity group{true},other{false};m.m_OwnedEntities={&group,&other};
m.StopServices();m.StopServices();m.ServerTick();assert(c.stops==1&&c.maintenance==0&&!c.pending&&deleted==1);
assert((queue.removed==std::vector<std::string>{"StartScenario","ServerTick","BindLocalObserver"}));
assert((events==std::vector<std::string>{"stop","onstop"}));}
}
''')
print('PASS: actual Core maintenance/default/tick/stop methods; empty/populated authority, terminal reentry, no tactical resume or new scheduler; native boundaries substituted')