Files
EscapistsReintroduced/tests/test_aircraft_service.py
T

106 lines
12 KiB
Python

"""Actual finite service lifecycle methods; controller/physical APIs are fakes."""
from policy_harness import ROOT, body, run
import re
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_AircraftService.c'
def cpp(s):
s=s.replace('protected ','').replace('return null;', 'return nullptr;')
for a,b in [('ERR_AircraftServiceEntry entry','Entry* entry'),('ERR_AircraftServiceEntry known','Entry* known'),('new ERR_AircraftServiceEntry()','new Entry()'),('ERR_AircraftRouteEnvelope qualification','Qualification* qualification'),('new ERR_AircraftRouteFlight()','new Flight()'),('ERR_GameMode mode','Mode* mode'),('ERR_GameMode.Get()','Mode::Get()'),('ERR_AircraftFlightRules.','Rules::'),('ERR_AircraftFlightState.','State::'),('Replication.IsServer()','server'),('string reason','std::string reason'),('vector destination = "0 0 0"','Vec destination')]:s=s.replace(a,b)
s=re.sub(r'foreach \(Entry\* (entry|known) : m_Entries\)',r'for (Entry* \1 : m_Entries.values)',s)
for name in ('entry','known','qualification','GetGame()','m_Mode','m_World','s_Retainers','entry.Flight'):
s=s.replace(name+'.',name+'->')
s=s.replace('entry->Flight.','entry->Flight->').replace('entry->Reason.IsEmpty()','entry->Reason.empty()')
return s.replace('ERR_AircraftServiceEntry Find(', 'Entry* Find(').replace('ERR_AircraftServiceEntry entry)', 'Entry* entry)').replace('vector Destination(', 'Vec Destination(').replace('string Reason(', 'std::string Reason(')
keys=['protected bool Context(', 'protected ERR_AircraftServiceEntry Find(', 'bool Register(', 'bool Ready(', 'bool StartRoute(', 'protected void Finish(', 'bool Cancel(', 'void Tick(', 'void Stop(', 'protected void ReleaseRetainers(', 'bool Drained(', 'bool Stopping(', 'int RegisteredCount(', 'int AttemptedOrders(', 'bool Pending(', 'bool Retired(', 'int OrderToken(', 'float OrderDeadline(', 'int FlightState(', 'string Reason(', 'vector Destination(']
run(r'''
#include <cassert>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cmath>
bool server=true;
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
struct Vec {float x=0,y=0,z=0;};
struct World {float now=1000;float GetWorldTime(){return now;}}world;
struct Game {World* current=&world;World* GetWorld(){return current;}}game;Game* gamePointer=&game;Game* GetGame(){return gamePointer;}
struct Mode {bool active=true;World* world=&::world;static Mode* current;World* GetWorld(){return world;}bool Active(){return active;}static Mode* Get(){return current;}};Mode* Mode::current=nullptr;
struct Rules {static bool Deadline(float now,float deadline){return now>=0&&deadline>now&&deadline-now<=240000;}};
struct Entity {bool original=true,ready=true;int startMode=0,next=State::HOOKS;};
using Vehicle=Entity*;using SCR_AIGroup=Entity*;using IEntity=Entity*;using PilotCompartmentSlot=Entity*;
struct Qualification {bool claimed=false;Vec target={20,0,0};Vec Destination(){return target;}};
int starts=0,claims=0,ticks=0,cancels=0;
struct Flight {
Entity* aircraft=nullptr;int token=0,state=State::IDLE;float deadline=0;bool cancelled=false;std::string reason;
bool Finished(){return state>=State::COMPLETE&&state<=State::RELINQUISHED;}
bool StartRoute(Mode*,Vehicle a,SCR_AIGroup,IEntity,PilotCompartmentSlot,Qualification* q,int t,float d){
starts++;aircraft=a;
if(a->startMode==1||q->claimed)return false;
q->claimed=true;claims++;token=t;deadline=d;state=State::HOOKS;
if(a->startMode==2){state=State::FAILED;return false;}
if(a->startMode==3)return false;
return true;
}
void Cancel(std::string r){if(Finished()||cancelled)return;cancelled=true;cancels++;reason=r;state=State::LANDING;}
void Tick(){ticks++;if(!aircraft->original||!Mode::Get()||!Mode::Get()->active){state=State::RELINQUISHED;return;}if(world.now>=deadline){state=State::FAILED;return;}state=aircraft->next;}
int State(){return state;}int OrderToken(){return token;}std::string Reason(){return reason;}
};
struct Entry {Vehicle Aircraft=nullptr;SCR_AIGroup Group=nullptr;IEntity Pilot=nullptr;PilotCompartmentSlot Seat=nullptr;struct Flight* Flight=nullptr;int Token=0;float Deadline=0;Vec Destination;bool Pending=false,CancelRequested=false,Retired=false;std::string Reason;};
struct Entries {std::vector<Entry*> values;int Count(){return values.size();}void Insert(Entry* e){values.push_back(e);}};
struct Tokens {std::set<int> values;int Count(){return values.size();}bool Contains(int n){return values.count(n);}void Insert(int n){values.insert(n);}};
struct Service;
struct Registry {std::map<Entity*,Service*> values;Service* Get(Entity* a){auto i=values.find(a);return i==values.end()?nullptr:i->second;}void Set(Entity* a,Service* s){values[a]=s;}void Remove(Entity* a){values.erase(a);}}registry;
struct Service {
static const int MAX_AIRCRAFT=4,MAX_ORDERS=128;static Registry* s_Retainers;
Mode* m_Mode;World* m_World=&world;Entries m_Entries;Tokens m_UsedTokens;bool m_Stopping=false;
Service(Mode* m):m_Mode(m){}
bool Original(Entry* e){return Context()&&e&&e->Aircraft&&e->Aircraft->original;}
bool PhysicalReady(Entry* e){return Original(e)&&e->Aircraft->ready;}
''' + '\n'.join(cpp(body(P,k)) for k in keys) + r'''
};Registry* Service::s_Retainers=&registry;
struct Setup {
Mode mode;Entity a[5],g[5],p[5],seat[5];Service service;Qualification q;
Setup():service(&mode){registry.values.clear();server=true;gamePointer=&game;game.current=&world;world.now=1000;Mode::current=&mode;starts=claims=ticks=cancels=0;}
bool Register(int i=0){return service.Register(&a[i],&g[i],&p[i],&seat[i]);}
bool Start(int token=1){return service.StartRoute(&a[0],&q,token,200000);}
};
int main(){
{Setup x;for(int i=0;i<4;i++)assert(x.Register(i));assert(!x.Register(4)&&x.service.RegisteredCount()==4&&starts==0&&claims==0);}
{Setup x;assert(x.Register());assert(!x.Register());Service other(&x.mode);assert(!other.Register(&x.a[0],&x.g[0],&x.p[0],&x.seat[0]));assert(!x.service.Register(&x.a[1],&x.g[1],&x.p[0],&x.seat[1]));assert(!x.service.Register(&x.a[1],&x.g[0],&x.p[1],&x.seat[1]));assert(!x.service.Register(&x.a[1],&x.g[1],&x.p[1],&x.seat[0]));}
{Setup x;x.a[0].ready=false;assert(!x.Register()&&registry.values.empty());x.a[0].ready=true;x.mode.active=false;assert(!x.Register());}
{Setup x;assert(x.Register());Mode other;Mode::current=&other;assert(!x.Start()&&starts==0);Mode::current=&x.mode;World otherWorld;game.current=&otherWorld;assert(!x.Start()&&starts==0);game.current=&world;server=false;assert(!x.Start()&&starts==0);}
{Setup x;assert(x.Register());assert(!x.service.StartRoute(&x.a[0],nullptr,1,200000));assert(!x.Start(0));assert(!x.service.StartRoute(&x.a[0],&x.q,1,world.now));assert(!x.service.StartRoute(&x.a[0],&x.q,1,world.now+240001));assert(!x.service.StartRoute(&x.a[0],&x.q,1,NAN));assert(starts==0&&x.service.AttemptedOrders()==0);}
{Setup x;assert(x.Register());assert(x.Start());assert(starts==1&&claims==1&&x.service.Pending(&x.a[0]));assert(!x.Start()&&!x.Start(2)&&starts==1&&claims==1);assert(x.service.OrderToken(&x.a[0])==1&&x.service.OrderDeadline(&x.a[0])==200000);x.q.target.x=999;assert(x.service.Destination(&x.a[0]).x==20);}
// Cancellation retains the same controller/token/deadline and continues Tick.
{Setup x;assert(x.Register()&&x.Start());Entry* entry=x.service.Find(&x.a[0]);Flight* flight=entry->Flight;x.a[0].next=State::TRANSLATING;x.service.Tick();assert(!x.service.Cancel(&x.a[0],2,"foreign"));assert(x.service.Cancel(&x.a[0],1,"report lost"));assert(!x.service.Cancel(&x.a[0],1,"duplicate"));assert(cancels==1&&entry->Flight==flight&&x.service.Pending(&x.a[0])&&!x.service.Ready(&x.a[0]));x.a[0].next=State::LANDING;x.service.Tick();assert(ticks==2&&entry->Flight==flight&&x.service.OrderDeadline(&x.a[0])==200000);x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&!x.service.Retired(&x.a[0])&&x.service.Ready(&x.a[0]));assert(!x.Start());Qualification q;assert(x.service.StartRoute(&x.a[0],&q,2,210000)&&starts==2&&claims==2);}
// Stop is admission/cancellation, not fabricated completion or disposal.
{Setup x;assert(x.Register()&&x.Start());x.service.Stop();x.service.Stop();assert(x.service.Stopping()&&!x.service.Drained()&&cancels==1&&registry.Get(&x.a[0])==&x.service);assert(!x.Register(1)&&!x.Start(2));x.a[0].next=State::LANDING;x.service.Tick();assert(!x.service.Drained()&&registry.Get(&x.a[0])==&x.service);x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&registry.values.empty()&&!x.service.Ready(&x.a[0]));}
// Completion must not overrule current seat/health/contact/fuel readiness.
{Setup x;assert(x.Register()&&x.Start());x.a[0].next=State::COMPLETE;x.a[0].ready=false;x.service.Tick();assert(x.service.Drained()&&!x.service.Ready(&x.a[0]));x.a[0].ready=true;assert(x.service.Ready(&x.a[0]));x.a[0].original=false;x.service.Tick();assert(x.service.Retired(&x.a[0])&&!x.service.Ready(&x.a[0]));x.a[0].original=true;assert(!x.service.Ready(&x.a[0]));}
// Native failure/capture and mode teardown never claim a usable returned asset.
for(int state:{State::FAILED,State::RELINQUISHED}){Setup x;assert(x.Register()&&x.Start());x.a[0].next=state;x.service.Tick();assert(x.service.Drained()&&x.service.Retired(&x.a[0])&&!x.service.Ready(&x.a[0]));}
{Setup x;assert(x.Register()&&x.Start());x.mode.active=false;x.service.Tick();assert(x.service.Stopping()&&x.service.Drained()&&x.service.Retired(&x.a[0])&&registry.values.empty());}
// Expiry is owned by the original controller deadline, never renewed by Tick.
{Setup x;assert(x.Register()&&x.Start());world.now=200000;x.service.Tick();assert(x.service.FlightState(&x.a[0])==State::FAILED&&x.service.Retired(&x.a[0])&&x.service.Drained()&&x.service.OrderDeadline(&x.a[0])==200000&&!x.service.Ready(&x.a[0]));}
// Unclaimed/claimed/ambiguous Start failures burn the attempted token; a
// nonterminal partial lease remains retained and canceled until actual result.
for(int fail:{1,2,3}){Setup x;assert(x.Register());x.a[0].startMode=fail;assert(!x.Start());assert(starts==1&&x.service.AttemptedOrders()==1&&x.service.Retired(&x.a[0]));assert(!x.Start(2)&&starts==1);if(fail==3){assert(x.service.Pending(&x.a[0])&&cancels==1);assert(!x.service.Cancel(&x.a[0],1,"duplicate"));x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained());}else assert(x.service.Drained());}
{Setup x;assert(x.Register());x.q.claimed=true;assert(!x.Start()&&starts==1&&claims==0&&x.service.AttemptedOrders()==1);}
{Setup x;assert(x.Register());for(int i=1;i<=Service::MAX_ORDERS;i++){Qualification q;assert(x.service.StartRoute(&x.a[0],&q,i,200000));x.a[0].next=State::COMPLETE;x.service.Tick();}assert(x.service.AttemptedOrders()==128);Qualification q;assert(!x.service.StartRoute(&x.a[0],&q,129,200000)&&starts==128);}
}
''')
source=P.read_text()
# Native-boundary inventory: readiness invokes original occupancy/player/world
# checks plus actual fuel, rotor, engine and settled ground predicates.
original=body(P,'protected bool Original(');ready=body(P,'protected bool PhysicalReady(')
for required in ('entry.Seat.GetOccupant() != entry.Pilot','controller.GetPilotCompartmentSlot() != entry.Seat','CompartmentAccessComponent.GetVehicleIn(entry.Pilot) != entry.Aircraft','entry.Group.GetPlayerCount(true) > 0','GetPlayerControlledEntity(id)','entry.Aircraft.GetWorld() != m_World','entry.Pilot.GetWorld() != m_World'):
assert required in original
for required in ('!Original(entry)','!ERR_Actors.CanFight(entry.Pilot)','!fuel.HasFuel()','!damage.GetEngineFunctional()','simulation.RotorGetState(rotor) == RotorState.DESTROYED','!simulation.HasAnyGroundContact()','!physics.IsActive()','controller.IsEngineOn()'):
assert required in ready
assert source.count('new ERR_AircraftRouteFlight()')==1
assert '.Claim(' not in source and 'QualifyRoute(' not in source
for prohibited in ('Spawn(', 'Delete(', 'DeleteSpawned(', 'SetOrigin(', 'SetWorldTransform(', 'SetVelocity(', 'SetActive(', 'SetFuel(', 'SetHealth(', 'PrepareActions(', 'PrepareHelicopter(', 'CallLater(', 'GetPlayerCount() == 0'):
assert prohibited not in source
print('PASS: actual finite registration, unique claims/tokens, retention/cancel/stop, readiness gating, capture/expiry retirement and bounded order history; native boundaries faked')