Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
197 lines
18 KiB
Python
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')
|