Files
EscapistsReintroduced/tests/test_civilian_traffic.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

197 lines
18 KiB
Python

"""Execute traffic retirement, ownership and spawn scheduling with native adapters."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_CivilianTraffic.c'
def translate(s):
s=s.replace('protected ','').replace('foreach (','for (').replace('vector ', 'Vec ').replace('array<vector>', 'array<Vec>')
s=s.replace('array<Vec> observers = {};','Array<Vec> observers;').replace('array<IEntity> actors = {};','Array<IEntity*> actors;').replace('array<BaseCompartmentSlot> seats = {};','Array<BaseCompartmentSlot*> seats;')
s=s.replace('array<Vec> observers','Array<Vec> observers').replace('array<IEntity> actors','Array<IEntity*> actors')
for typ in ('ERR_CivilianCar','IEntity','BaseCompartmentSlot'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for name in ('traffic','m_Mode','seat'):
s=s.replace(name+'.',name+'->')
s=s.replace('Car.','Car->').replace('Driver.','Driver->').replace('Group.','Group->')
s=s.replace('string.Format', 'string::Format')
s=s.replace('GetGame().','GetGame()->').replace('GetFactionManager().','GetFactionManager()->').replace('Settings().','Settings()->')
for cls in ('ERR_CivilianTrafficRules','ERR_ConvoyRules','ERR_Actors','Replication','ERR_GameMode','ERR_Phase','ERR_ForceRules','vector','ERR_HQ'):
s=s.replace(cls+'.',('Geometry' if cls=='vector' else cls)+'::')
return s
rules=translate('\n'.join(body(p,x) for x in ('static bool Far(', 'static bool CleanupDue(')))
methods=translate('\n'.join(body(p,x) for x in ('protected void Drive(', 'void PlayerEntered(', 'protected bool Foreign(', 'protected bool SafeCleanup(', 'void Tick(', 'void Stop(')))
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(const char*,T...){return "retry";}};
struct Vec{float x=0,y=0,z=0;};
namespace Geometry{float DistanceXZ(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);}};
namespace ERR_ForceRules{bool FiniteVector(Vec p){return std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z);}}
struct ERR_CivilianTrafficRules{
'''+rules+r'''
};
struct Group{int faction=1,agents=1;bool captured=false,deleted=false;int current=0;int GetCurrentWaypoint(){return current;}int GetFaction(){return faction;}int GetAgentsCount(){return agents;}};
struct IEntity{Vec position;bool player=false,alive=true,fit=true,deleted=false,hidden=true;Group* group=nullptr;IEntity* parent=nullptr;Vec GetOrigin(){return position;}};
struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
struct Car:IEntity{bool captured=false,seatsKnown=true;Array<BaseCompartmentSlot*> seats;};
struct ERR_CivilianCar{struct Car* Car=nullptr;IEntity* Driver=nullptr;struct Group* Group=nullptr;bool Released=false,Ready=true,Finished=false;float Deadline=120000,HiddenSince=-1,ProgressAt=0;Vec ProgressPosition;int RouteStop=1,Retries=0;string Status;Array<int> Orders={1,2};void Report(string s){Status=s;}void Finish(string s){Finished=true;Status=s;}Array<Vec> Route={{},{},{3000,0,0}};int cleared=0;void Protect(bool){}void ClearOrders(){cleared++;}void Release(){if(Released)return;Released=true;ClearOrders();}};
namespace ERR_Actors{bool Player(IEntity* a){return a&&a->player;}Group* Group(IEntity* a){return a?a->group:nullptr;}bool CanFight(IEntity* a){return a&&a->alive&&a->fit;}bool Alive(IEntity* a){return a&&a->alive;}template<class T>void DeleteSpawned(T* e){if(e)e->deleted=true;}}
namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){for(int i=0;e&&i<32;i++,e=e->parent)if(e==root)return true;return false;}bool Captured(Car* c,Group* g){return (c&&c->captured)||(g&&g->captured);}bool CollectSeats(Car* c,Array<BaseCompartmentSlot*>& out){out=c->seats;return c->seatsKnown;}}
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace ERR_Phase{enum{PRISON=1,RADIO_SEARCH=2};}
struct Settings{int target=3;int Get(string){return target;}};
struct Mode{bool active=true;int phase=2;struct Settings settings;bool Active(){return active;}int Phase(){return phase;}struct Settings* Settings(){return &settings;}};
namespace ERR_GameMode{Mode* current=nullptr;Mode* Get(){return current;}}
struct FactionManager{int GetFactionByKey(string){return 1;}};
struct Game{FactionManager factions;FactionManager* GetFactionManager(){return &factions;}}game;
Game* GetGame(){return &game;}
namespace ERR_HQ{void Event(string,string){}}
struct Service{Mode* m_Mode;bool m_Stopped=false,known=true,hidden=true,prepare=true;float m_Next=0,m_NextSpawn=0;int m_Serial=0,spawns=0;Array<ERR_CivilianCar*> m_Cars;Array<Vec> observers={{0,0,0}};bool Observers(Array<Vec>& pos,Array<IEntity*>&){pos=observers;return known;}bool Hidden(Vec,Array<IEntity*>,IEntity* ignored=nullptr){return hidden;}bool Prepare(ERR_CivilianCar* t){if(prepare)t->Ready=true;return prepare;}int retries=0;bool issued=true;bool GiveRoute(ERR_CivilianCar*){retries++;return issued;}void Spawn(Array<Vec>,Array<IEntity*>,float){spawns++;}
'''+methods+r'''
};
struct Fixture{Mode mode;Group group;IEntity driver;Car car;ERR_CivilianCar traffic;Service service;Fixture(){ERR_GameMode::current=&mode;service.m_Mode=&mode;driver.group=&group;traffic.Group=&group;traffic.Car=&car;traffic.Driver=&driver;car.position=driver.position={1600,0,0};traffic.ProgressPosition=car.position;traffic.Finished=true;service.m_Cars={&traffic};mode.settings.target=0;}};
int main(){
assert(!ERR_CivilianTrafficRules::Far({999,0,0},{{0,0,0}},1000));
assert(ERR_CivilianTrafficRules::Far({1000,0,0},{{0,0,0}},1000));
assert(!ERR_CivilianTrafficRules::Far({1500,0,0},{{0,0,0},{1499,0,0}},1000));
assert(!ERR_CivilianTrafficRules::CleanupDue(59999,0)&&ERR_CivilianTrafficRules::CleanupDue(60000,0));
assert(!ERR_CivilianTrafficRules::CleanupDue(60000,-1)&&!ERR_CivilianTrafficRules::CleanupDue(0,1));
{Fixture f;f.service.Tick(0);assert(!f.car.deleted&&f.traffic.HiddenSince==0);f.service.Tick(55000);assert(!f.car.deleted);f.service.Tick(60000);assert(f.car.deleted&&f.driver.deleted&&f.group.deleted&&f.service.m_Cars.empty());}
for(int fault=0;fault<4;fault++){
Fixture f;f.service.Tick(0);
if(fault==0)f.car.position={1499,0,0};
if(fault==1)f.driver.position={1499,0,0};
if(fault==2)f.service.hidden=false;
if(fault==3)f.service.known=false;
f.service.Tick(60000);assert(!f.car.deleted&&f.traffic.HiddenSince==-1);f.car.position=f.driver.position={1600,0,0};f.service.hidden=f.service.known=true;
f.service.Tick(65000);f.service.Tick(120000);assert(!f.car.deleted);f.service.Tick(125000);assert(f.car.deleted);
}
{Fixture f;IEntity player;player.player=true;IEntity door;door.parent=&f.car;f.service.PlayerEntered(&player,&door);assert(f.traffic.Released);f.car.captured=false;f.service.Tick(999999);f.service.Stop();assert(!f.car.deleted&&!f.driver.deleted&&f.traffic.cleared==1);}
{Fixture f;IEntity ai;f.service.PlayerEntered(&ai,&f.car);assert(!f.traffic.Released);ai.player=true;IEntity unrelated;f.service.PlayerEntered(&ai,&unrelated);assert(!f.traffic.Released);Replication::server=false;f.service.PlayerEntered(&ai,&f.car);assert(!f.traffic.Released);Replication::server=true;}
for(int fault=0;fault<6;fault++){
Fixture f;Group foreign;IEntity passenger;BaseCompartmentSlot seat;seat.occupant=&passenger;
if(fault==0)f.car.captured=true;
if(fault==1)f.group.captured=true;
if(fault==2)f.driver.group=&foreign;
if(fault==3)f.group.faction=2;
if(fault==4)f.group.agents=2;
if(fault==5)f.car.seats={&seat};
f.service.Tick(0);assert(f.traffic.Released);f.service.Tick(100000);assert(!f.car.deleted);
}
{Fixture f;f.driver.alive=false;f.driver.group=nullptr;f.service.Tick(0);assert(!f.traffic.Released);f.service.Tick(60000);assert(f.driver.deleted&&f.car.deleted);}
{Fixture f;f.traffic.Ready=false;f.traffic.Finished=false;f.service.prepare=false;f.car.position=f.driver.position={100,0,0};f.service.Tick(120000);assert(f.traffic.Finished&&!f.car.deleted);}
{Fixture f;f.traffic.Finished=false;f.traffic.ProgressAt=0;f.service.Tick(179999);assert(!f.traffic.Finished);f.service.Tick(185000);assert(f.traffic.Finished&&!f.car.deleted);}
{Fixture f;f.service.m_Cars.clear();f.mode.settings.target=3;f.service.Tick(0);assert(f.service.spawns==1);f.service.Tick(29999);assert(f.service.spawns==1);f.service.Tick(35000);assert(f.service.spawns==2);f.service.m_Serial=64;f.service.Tick(70000);assert(f.service.spawns==2);}
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};
f.service.Tick(59999);assert(f.service.retries==0);
f.service.Tick(65000);assert(f.service.retries==1&&f.traffic.Retries==1&&!f.traffic.Finished);
f.service.Tick(120000);assert(f.service.retries==2&&f.traffic.Retries==2);
f.service.Tick(150000);assert(f.service.retries==2&&!f.traffic.Finished);
f.service.Tick(180000);assert(f.traffic.Finished&&!f.car.deleted&&f.service.retries==2);
}
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};f.group.current=2;
f.service.Tick(65000);assert(f.traffic.RouteStop==2&&f.service.retries==1); // Do not return to completed middle stop.
f.car.position={140,0,0};f.service.Tick(70000);assert(f.traffic.ProgressAt==70000&&!f.traffic.Finished);
}
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.service.issued=false;
f.service.Tick(65000);f.service.Tick(120000);f.service.Tick(180000);assert(f.traffic.Finished&&f.service.retries==2&&!f.car.deleted);
}
{Fixture f;f.traffic.Finished=false;f.traffic.Group=nullptr;f.service.Drive(&f.traffic,65000);assert(f.traffic.Finished&&f.service.retries==0);}
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};f.traffic.Orders[1]=0;f.group.current=0;f.service.Tick(65000);assert(f.traffic.RouteStop==1);}
for(int fault=0;fault<7;fault++){
Fixture f;f.service.m_Cars.clear();f.mode.settings.target=3;
if(fault==0)f.service.observers.clear();
if(fault==1)f.service.known=false;
if(fault==2)f.mode.phase=1;
if(fault==3)f.mode.active=false;
if(fault==4)f.service.m_Stopped=true;
if(fault==5)f.mode.settings.target=0;
if(fault==6)Replication::server=false;
f.service.Tick(0);assert(f.service.spawns==0);Replication::server=true;
}
}
''')
# Integration boundaries: no military admission or late observer-driven retargets.
s=p.read_text()
assert '.RegisterGroup(' not in s and '.Own(' not in s
assert 'm_Serial >= 64' in body(p,'protected bool Spawn(')
assert 'Observers(observers, actors)' in body(p,'protected bool Spawn(')
assert 'trace.ExcludeArray = excluded' in body(p,'protected bool Hidden(')
assert 'sample = -1; sample <= 1' in body(p,'protected bool Hidden(')
assert 'SetFactionsFriendly(civilian, other' in body(p,'protected bool Neutral(')
game_mode=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
assert 'ServerTickMeasured();' in body(game_mode,'protected void ServerTick(')
assert 'm_CivilianTraffic.Tick(now)' in body(game_mode,'protected void ServerTickMeasured(')
print('PASS: traffic distance/grace, all-observer veto, player claims, foreign custody, bounded spawn cadence, failure/stall retention and shutdown preserve nearby/claimed entities')
observers=body(p,'protected bool Observers(').replace('protected ','').replace('array<vector>', 'Array<Vec>&').replace('array<IEntity>', 'Array<IEntity*>&').replace('array<int> ids = {};','Array<int> ids;').replace('foreach (','for (')
observers=observers.replace('SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore))','editorCore').replace('SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id))','GetGame().GetPlayerManager().GetPlayerController(id)')
for typ in ('SCR_EditorManagerCore','SCR_EditorManagerEntity','SCR_PlayerController','IEntity'):
observers=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',observers)
for name in ('editors','editor','actor','controller','watched'):
observers=observers.replace(name+'.',name+'->')
observers=observers.replace('GetGame().','GetGame()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('ERR_ObserverReason.NONE','0').replace('SCR_EditorManagerEntity* editor;', 'SCR_EditorManagerEntity* editor=nullptr;').replace('IEntity* watched;', 'IEntity* watched=nullptr;')
run(r'''
#include <cassert>
#include <vector>
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Clear(){this->clear();}void Insert(T v){this->push_back(v);}};
struct Vec{float x=0;};struct IEntity{Vec p;Vec GetOrigin(){return p;}};
struct SCR_EditorManagerEntity{bool open=false;bool IsOpened(){return open;}};
struct SCR_EditorManagerCore{SCR_EditorManagerEntity* editor=nullptr;SCR_EditorManagerEntity* GetEditorManager(int){return editor;}};SCR_EditorManagerCore* editorCore=nullptr;
struct SCR_PlayerController{int state=0;IEntity* watched=nullptr;int ERR_ObserverState(){return state;}IEntity* ERR_ObservedEntity(){return watched;}};
struct Players{Array<int> ids={1};IEntity* actor=nullptr;SCR_PlayerController* controller=nullptr;void GetPlayers(Array<int>& out){out=ids;}IEntity* GetPlayerControlledEntity(int){return actor;}SCR_PlayerController* GetPlayerController(int){return controller;}};
struct Game{Players players;Players* GetPlayerManager(){return &players;}}game;Game* GetGame(){return &game;}
'''+observers+r'''
int main(){
Array<Vec> positions;Array<IEntity*> actors;IEntity body,watched;body.p.x=100;watched.p.x=300;SCR_PlayerController controller;game.players.controller=&controller;
assert(!Observers(positions,actors));game.players.actor=&body;assert(Observers(positions,actors)&&actors.size()==1&&positions[0].x==100);
controller.state=1;assert(!Observers(positions,actors));controller.watched=&watched;
assert(Observers(positions,actors)&&actors.size()==2&&positions[0].x==300&&positions[1].x==100);
game.players.actor=nullptr;assert(Observers(positions,actors)&&actors.size()==1&&actors[0]==&watched);
SCR_EditorManagerCore core;SCR_EditorManagerEntity editor;core.editor=&editor;editorCore=&core;editor.open=true;assert(!Observers(positions,actors));
editor.open=false;assert(Observers(positions,actors));game.players.controller=nullptr;assert(!Observers(positions,actors));
game.players.ids.clear();assert(Observers(positions,actors)&&positions.empty()&&actors.empty());
}
''')
print('PASS: actual observer collection includes spectator targets and controlled bodies; missing views/controllers and open editor cameras veto spawn/cleanup')
settings_path=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
settings='\n'.join(body(settings_path,x) for x in ('static bool Valid(', 'static int AddedDefault(', 'static bool Randomizable('))
command=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c','static string CoreSetting(')
run('#include <cassert>\n#include <string>\nusing string=std::string;\nstruct Settings{'+settings+'\n'+command+r'''
};
int main(){assert(Settings::AddedDefault("civilianTrafficCount")==3);assert(Settings::Valid("civilianTrafficCount",0));assert(Settings::Valid("civilianTrafficCount",6));assert(!Settings::Valid("civilianTrafficCount",-1));assert(!Settings::Valid("civilianTrafficCount",7));assert(!Settings::Randomizable("civilianTrafficCount"));assert(Settings::CoreSetting("mission","traffic")=="civilianTrafficCount");}
''')
print('PASS: civilian traffic defaults to three; menu command resolves to persisted 0..6 setting with zero disabling new cars')
exit_road=body(p,'protected bool ExitRoad(').replace('protected ','').replace('vector ', 'Vec ').replace('out Vec end','Vec& end').replace('array<vector>','std::vector<Vec>').replace('foreach (','for (').replace('ERR_SupplyConvoySystem.','Road::').replace('vector.DistanceXZ','Geometry::DistanceXZ')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Vec{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}Vec operator*(float f){return {x*f,y*f,z*f};}};
Vec Vector(float x,float y,float z){return {x,y,z};}
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
namespace Road{int calls=0,mode=0;bool ApproachDirected(Vec desired,Vec& end,Vec&,float){calls++;end=desired;if(mode==2)return false;if(mode==1)return desired.z>0;return true;}}
'''+exit_road+r'''
int main(){Vec end;assert(ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end));assert(end.x==-1800&&Road::calls==1);
Road::mode=1;Road::calls=0;assert(ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end));assert(end.z==1800&&Road::calls==2); // opposite endpoint is water, turn along coast
Road::mode=2;Road::calls=0;assert(!ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end)&&Road::calls==3);
}
''')
print('PASS: bounded exit-road selection can turn along a coastline when the opposite road is unavailable; no-route case is withheld')
protect=body(p,'void Protect(').replace('IEntity entity','IEntity* entity').replace('SCR_GarbageSystem garbage','SCR_GarbageSystem* garbage').replace('SCR_GarbageSystem.GetByEntityWorld','SCR_GarbageSystem::GetByEntityWorld').replace('garbage.','garbage->')
run(r'''
#include <cassert>
struct IEntity{bool held=false;};
struct SCR_GarbageSystem{inline static bool available=true;static SCR_GarbageSystem* GetByEntityWorld(IEntity*){static SCR_GarbageSystem instance;return available?&instance:nullptr;}void UpdateBlacklist(IEntity* entity,bool held){entity->held=held;}};
struct CarRecord{IEntity* Car=nullptr;IEntity* Driver=nullptr;
'''+protect+r'''
};
int main(){CarRecord record;record.Protect(true);IEntity car,driver;record.Car=&car;record.Driver=&driver;record.Protect(true);assert(car.held&&driver.held);record.Protect(false);assert(!car.held&&!driver.held);record.Car=nullptr;record.Protect(true);assert(driver.held);record.Protect(false);assert(!driver.held);SCR_GarbageSystem::available=false;record.Protect(true);assert(!driver.held);}
''')
assert 'Protect(false)' in body(p,'void Release(')
assert 'traffic.ClearOrders(); traffic.Protect(false);' in body(p,'void Tick(')
print('PASS: managed traffic pins both native garbage entries; release/retirement removes only those pins, including partial creation')