Files
EscapistsReintroduced/tests/test_garage_vehicle_lifecycle.py
T
2026-10-30 18:56:57 +01:00

107 lines
8.5 KiB
Python

"""Execute production claim/retirement and per-update scheduling with engine doubles."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GarageVehicles.c'
def convert(s):
s=s.replace('protected ','').replace('null','nullptr').replace('foreach (','for (')
s=s.replace('array<vector>','Array<Vec>').replace('array<IEntity>','Array<IEntity*>').replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>')
s=s.replace('Array<Vec> observers,','Array<Vec>& observers,').replace('Array<IEntity*> actors,','Array<IEntity*>& actors,')
for cls in ['ERR_ParkedVehicle','SCR_GarbageSystem','IEntity','BaseCompartmentSlot']:
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
for cls in ['ERR_CivilianTrafficRules','ERR_ConvoyRules','ERR_Actors','Replication','SCR_GarbageSystem']:
s=s.replace(cls+'.',cls+'::')
for name in ['parked','seat','m_Mode','garbage','Site','Entity']:
s=s.replace(name+'.',name+'->')
s=s.replace('vector.Distance','Geometry::Distance')
s=s.replace('m_Parked[i].','m_Parked[i]->')
return s
methods=convert('\n'.join(body(p,x) for x in ['protected bool InUse(', 'protected void Claim(', 'void PlayerEntered(', 'protected void Maintain(', 'protected void Step(']))
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Vec{float x=0,z=0;};
namespace Geometry{float Distance(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Remove(int i){this->erase(this->begin()+i);}void Clear(){this->clear();}};
struct IEntity{Vec origin;bool player=false,hidden=true,deleted=false,seatsKnown=true;IEntity* occupant=nullptr;Vec GetOrigin(){return origin;}Vec GetYawPitchRoll(){return {};}};
struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
namespace Replication{bool authority=true;bool IsServer(){return authority;}}
namespace ERR_Actors{bool Player(IEntity* e){return e&&e->player;}void DeleteSpawned(IEntity* e){assert(!e->occupant);e->deleted=true;}}
namespace ERR_ConvoyRules{
bool PartOf(IEntity* e,IEntity* root){return e==root;}
bool CollectSeats(IEntity* e,Array<BaseCompartmentSlot*>& a){static BaseCompartmentSlot seat;seat.occupant=e->occupant;a.push_back(&seat);return e->seatsKnown;}
}
namespace ERR_CivilianTrafficRules{
bool Far(Vec p,Array<Vec>& o,float distance){for(auto q:o)if(Geometry::Distance(p,q)<distance)return false;return true;}
bool CleanupDue(float now,float since){return since>=0&&now-since>=60000;}
}
struct SCR_GarbageSystem{static SCR_GarbageSystem* GetByEntityWorld(IEntity*){static SCR_GarbageSystem g;return &g;}void UpdateBlacklist(IEntity*,bool){}};
struct SiteData{bool Consumed=false;};
struct ERR_ParkedVehicle{IEntity* Entity=nullptr;SiteData* Site=nullptr;Vec Origin;float HiddenSince=-1;};
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
struct Service{
Mode mode;Mode* m_Mode=&mode;bool m_Stopped=false,running=true,known=true;
int m_Claimed=0,m_MaintainCursor=0,m_Work=0,m_Pending=0,m_CleanupRadius=1400;
float clock=0,m_NextStep=0;int discovers=0,plans=0,pending=0,hiddenCalls=0;
Array<ERR_ParkedVehicle*> m_Parked;Array<Vec> views;Array<IEntity*> actors;
bool Running(){return running;}float Now(){return clock;}
bool Observers(Array<Vec>& o,Array<IEntity*>& a){o=views;a=actors;return known;}
bool Hidden(Vec,Vec,Array<IEntity*>& a,IEntity* e){hiddenCalls++;return e->hidden;}
void Discover(Array<Vec>&,float){discovers++;}void Plan(Array<Vec>&,Array<IEntity*>&,float){plans++;}
void Pending(Array<Vec>&,Array<IEntity*>&){pending++;}
''' + methods.replace('m_Pending = nullptr','m_Pending = 0') + r'''
};
int main(){
Array<Vec> far={{2000,0}},near={{100,0}};Array<IEntity*> actors;
IEntity car,player;player.player=true;SiteData site;ERR_ParkedVehicle record;record.Entity=&car;record.Site=&site;
Service s;s.m_Parked={&record};
s.Maintain(far,actors,100,true);assert(record.HiddenSince==100&&!car.deleted);
s.Maintain(far,actors,60099,true);assert(!car.deleted);
s.Maintain(near,actors,60100,true);assert(record.HiddenSince<0&&!car.deleted);
s.Maintain(far,actors,70000,true);car.hidden=false;s.Maintain(far,actors,140000,true);assert(!car.deleted&&record.HiddenSince<0);
car.hidden=true;s.Maintain(far,actors,150000,true);s.Maintain(far,actors,220000,false);assert(!car.deleted&&record.HiddenSince<0);
s.Maintain(far,actors,230000,true);s.Maintain(far,actors,290000,true);assert(car.deleted&&s.m_Parked.empty()&&!site.Consumed);
car.deleted=false;s.m_Parked={&record};s.PlayerEntered(&player,&car);assert(site.Consumed&&s.m_Claimed==1&&s.m_Parked.empty()&&s.mode.owned==1);
s.PlayerEntered(&player,&car);s.Maintain(far,actors,500000,true);assert(s.m_Claimed==1&&!car.deleted);
SiteData used;record.Site=&used;s.m_Parked={&record};car.origin={4,0};s.Maintain(far,actors,600000,true);assert(used.Consumed&&!car.deleted&&s.m_Parked.empty());
SiteData occupied;record.Site=&occupied;car.origin={0,0};car.occupant=&player;s.m_Parked={&record};s.Maintain(far,actors,700000,true);assert(occupied.Consumed&&!car.deleted);car.occupant=nullptr;
Service tick;tick.Step();assert(tick.discovers==1&&tick.plans==0);tick.Step();assert(tick.discovers==1); // no same-frame catch-up
tick.clock=250;tick.Step();assert(tick.plans==1);tick.clock=500;tick.Step();assert(tick.discovers==1&&tick.plans==1);
tick.m_Pending=1;tick.clock=750;tick.Step();assert(tick.pending==1&&tick.plans==1&&tick.discovers==1);
tick.known=false;tick.clock=1000;record.HiddenSince=100;tick.m_Parked={&record};tick.Step();assert(tick.m_Pending==0&&record.HiddenSince<0&&tick.pending==1);
tick.m_Stopped=true;tick.clock=1250;tick.Step();assert(tick.plans==1&&tick.discovers==1);
Service client;Replication::authority=false;client.Step();assert(client.discovers==0&&client.plans==0);
}
''')
print('PASS: permanent entry/movement/occupancy claims; far hidden grace cleanup; proximity/visibility/unknown-view resets; one staged operation per update, no same-frame catch-up, stop and client guards')
# The current round takes one snapshot of the saved menu values. No polling
# should silently apply next-round changes or restart disabled/client services.
start=body(p,'void Start(').replace('Replication.', 'Replication::').replace('Math.Max', 'std::max')
start=start.replace('m_Mode.Settings().Get(', 'settings.Get(').replace('m_Bikes.Find()', 'bikeLookups++')
start=start.replace('GetGame().GetCallqueue().CallLater(Step, 250, true)', 'queueCalls++')
run(r'''
#include <cassert>
#include <map>
#include <string>
#include <algorithm>
namespace Replication{bool authority=true;bool IsServer(){return authority;}}
struct Settings{std::map<std::string,int> values={{"garageVehicleCount",16},{"garageChancePercent",70},{"garageRadiusMetres",1000},{"garageSpawnIntervalSeconds",5}};int Get(std::string k){return values.at(k);}};
struct Service{
Settings settings;bool m_Started=false,m_Stopped=false;
int m_Requested=0,m_GarageChance=0,m_SpawnRadius=0,m_SpawnInterval=0,m_CleanupRadius=0,bikeLookups=0,queueCalls=0;
void Stop(){m_Stopped=true;}
''' + start + r'''
};
int main(){
Service normal;normal.Start();assert(normal.m_Requested==16&&normal.m_GarageChance==70&&normal.m_SpawnRadius==1000&&normal.m_SpawnInterval==5000&&normal.m_CleanupRadius==1400&&normal.queueCalls==1);
normal.settings.values["garageRadiusMetres"]=1500;normal.settings.values["garageSpawnIntervalSeconds"]=60;normal.Start();assert(normal.m_SpawnRadius==1000&&normal.m_SpawnInterval==5000&&normal.queueCalls==1);
Service custom;custom.settings.values={{"garageVehicleCount",24},{"garageChancePercent",100},{"garageRadiusMetres",1500},{"garageSpawnIntervalSeconds",60}};custom.Start();assert(custom.m_Requested==24&&custom.m_GarageChance==100&&custom.m_SpawnRadius==1500&&custom.m_SpawnInterval==60000&&custom.m_CleanupRadius==1900);
for(auto key:{"garageVehicleCount","garageChancePercent"}){Service disabled;disabled.settings.values[key]=0;disabled.Start();assert(disabled.m_Stopped&&disabled.queueCalls==0&&disabled.bikeLookups==0);}
Service small;small.settings.values["garageRadiusMetres"]=250;small.Start();assert(small.m_CleanupRadius==1400);
Service stopped;stopped.m_Stopped=true;stopped.Start();assert(stopped.queueCalls==0);
Service client;Replication::authority=false;client.Start();assert(!client.m_Started&&client.queueCalls==0&&client.bikeLookups==0);
}
''')
print('PASS: saved garage menu settings configure the round once; unit conversion and cleanup radius scale correctly; count/chance zero disable startup; clients and stopped rounds do no work')