105 lines
7.7 KiB
Python
105 lines
7.7 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 ForceBoundary {int clears=0;void Clear(){clears++;}};
|
|
struct Mode {
|
|
ForceBoundary m_Forces,m_ForceAgents;
|
|
int forceTicks=0;void TickForces(float){forceTicks++;}
|
|
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;
|
|
bool InitialAdmissionsExhausted(){return false;}
|
|
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&&m.forceTicks==2);
|
|
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&&m.m_Forces.clears==1&&m.m_ForceAgents.clears==1&&m.forceTicks==0);
|
|
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')
|