Add configurable nearby garage vehicles and fix roadblocks and fuel pumps; record Workshop 0.0.131
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-30 18:56:57 +01:00
parent 3ec882dc4e
commit 2dcbe3107d
75 changed files with 7478 additions and 59 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
+106
View File
@@ -0,0 +1,106 @@
"""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')
+61
View File
@@ -0,0 +1,61 @@
"""Exercise production placement vetoes for players, spectators and editor cameras."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GarageVehicles.c'
s=body(p,'protected bool Observers(').replace('protected ','').replace('array<vector>', 'Array<Vec>').replace('array<IEntity>', 'Array<IEntity*>').replace('array<int> ids = {};','std::vector<int> ids;').replace('foreach (int id : ids)','for (int id : ids)').replace('null','nullptr')
s=s.replace('Array<Vec> positions','Array<Vec>& positions').replace('Array<IEntity*> actors','Array<IEntity*>& actors')
s=s.replace('ERR_ForceRules.', 'ERR_ForceRules::').replace('ERR_ObserverReason.NONE','ERR_ObserverReason::NONE')
for cls in ['SCR_PlayerController','SCR_EditorManagerEntity','SCR_EditorManagerCore','IEntity']:
s=s.replace(cls+'.',cls+'::')
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
s=s.replace('SCR_EditorManagerCore::GetInstance(SCR_EditorManagerCore)','SCR_EditorManagerCore::GetInstance()')
s=s.replace('SCR_EditorManagerEntity* editor;', 'SCR_EditorManagerEntity* editor = nullptr;')
for prefix in ['GetGame()','GetPlayerManager()','controller','body','watched','editors','editor']:
s=s.replace(prefix+'.',prefix+'->')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Vec{float x=0,z=0;};
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T t){this->push_back(t);}};
namespace ERR_ForceRules{bool FiniteVector(Vec v){return std::isfinite(v.x)&&std::isfinite(v.z);}}
namespace Geometry{float DistanceXZ(Vec a, Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
namespace ERR_ObserverReason{const int NONE=0;}
struct IEntity{Vec p;Vec GetOrigin(){return p;}};
struct SCR_PlayerController{
IEntity* body=nullptr;IEntity* watched=nullptr;int state=0;
IEntity* GetControlledEntity(){return body;}IEntity* ERR_ObservedEntity(){return watched;}int ERR_ObserverState(){return state;}
static SCR_PlayerController* Cast(SCR_PlayerController* p){return p;}
};
struct SCR_EditorManagerEntity{bool opened=false;bool IsOpened(){return opened;}};
SCR_EditorManagerEntity editor;
struct SCR_EditorManagerCore{
static SCR_EditorManagerCore* GetInstance(){static SCR_EditorManagerCore c;return &c;}
static SCR_EditorManagerCore* Cast(SCR_EditorManagerCore* p){return p;}
SCR_EditorManagerEntity* GetEditorManager(int){return &editor;}
};
struct Manager{
std::vector<SCR_PlayerController*> clients;
void GetPlayers(std::vector<int>& ids){for(unsigned i=0;i<clients.size();i++)ids.push_back(i);}
SCR_PlayerController* GetPlayerController(int id){return clients[id];}
};
struct Game{Manager players;Manager* GetPlayerManager(){return &players;}} game;
Game* GetGame(){return &game;}
''' + s + r'''
int main(){
Array<Vec> positions;Array<IEntity*> actors;
assert(Observers(positions,actors)&&positions.empty());
game.players.clients={nullptr};assert(!Observers(positions,actors));
SCR_PlayerController c;game.players.clients={&c};assert(!Observers(positions,actors));
IEntity player{{10,0}}, watched{{500,0}};c.body=&player;
assert(Observers(positions,actors)&&positions.size()==1&&actors[0]==&player);
editor.opened=true;assert(!Observers(positions,actors));editor.opened=false;
c.state=1;assert(!Observers(positions,actors));c.watched=&watched;
assert(Observers(positions,actors)&&positions.size()==2&&actors[1]==&watched);
c.body=nullptr;assert(Observers(positions,actors)&&positions.size()==1&&actors[0]==&watched);
watched.p={NAN,0};assert(!Observers(positions,actors));watched.p={500,0};
c.state=0;assert(!Observers(positions,actors));
}
''')
print('PASS: bodies and spectator targets are collected; unknown, nonfinite and editor views veto spawning/cleanup; no connected players yields an empty known view')
+38
View File
@@ -0,0 +1,38 @@
"""Run production garage filtering and vehicle-fit rules against unsafe inputs."""
from policy_harness import ROOT, body, run
p = ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GarageVehicles.c'
rules = '\n'.join(body(p, s) for s in ('static bool Garage(', 'static bool Envelope('))
rules = rules.replace('vector ', 'Vec ').replace('ERR_ForceRules.', 'ERR_ForceRules::')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cctype>
#include <limits>
#include <string>
struct string : std::string {
using std::string::string;
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
bool EndsWith(const std::string& suffix){return size()>=suffix.size() && compare(size()-suffix.size(),suffix.size(),suffix)==0;}
};
struct Vec {float v[3];float operator[](int i)const{return v[i];}};
namespace ERR_ForceRules {bool FiniteVector(Vec v){return std::isfinite(v[0])&&std::isfinite(v[1])&&std::isfinite(v[2]);}}
''' + rules + r'''
int main(){
assert(Garage("{GUID}Prefabs/Structures/Houses/Garage/Garage_E_01/Garage_E_01.et"));
assert(Garage("Prefabs/Structures/Houses/Village/HouseAddon_Garage_E_01/HouseAddon_Garage_E_01.et"));
assert(!Garage("Prefabs/Structures/Houses/Garage/Garage_E_01/Dst/Garage_E_01_Ruin.et"));
assert(!Garage("Prefabs/Structures/Houses/Garage/Garage_E_01/Furniture/Garage_E_01_furniture_01.et"));
assert(!Garage("Prefabs/Structures/BuildingParts/Doors/Door_Garage_E_01_A_LEFT_EXT_COV_B.et"));
assert(!Garage("Prefabs/Structures/Military/Houses/GarageMilitary_E_01/GarageMilitary_E_01.et"));
assert(Envelope({{-1,-0.2,-2.3}},{{1,1.8,2.3}}));
assert(!Envelope({{-1.06f,0,-2}},{{1,1.8,2}}));
assert(!Envelope({{-1,0,-2.5}},{{1,1.8,2}}));
assert(!Envelope({{-1,0,-2}},{{1,2.1,2}}));
assert(!Envelope({{-1,-0.4,-2}},{{1,1.8,2}}));
assert(!Envelope({{1,0,2}},{{-1,1.8,-2}}));
assert(!Envelope({{NAN,0,-2}},{{1,1.8,2}}));
assert(!Envelope({{-1,0,-2}},{{1,INFINITY,2}}));
}
''')
print('PASS: native garage families only; rubble/furniture/military garages rejected; oversized and invalid vehicle bounds rejected')
+4 -3
View File
@@ -14,7 +14,7 @@ def tr(s):
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
for n in ('m_Mode','mode','hq','unit','Group','agent','record','Recovery','Tactics','CloseContact','entity'):
s=re.sub(r'\b'+n+r'\.',n+'->',s)
s=s.replace('Forces().','Forces()->').replace('Players().','Players()->').replace('Command().','Command()->').replace('Hunters().','Hunters()->').replace('CivilianTraffic().','CivilianTraffic()->')
s=s.replace('Forces().','Forces()->').replace('Players().','Players()->').replace('Command().','Command()->').replace('Hunters().','Hunters()->').replace('CivilianTraffic().','CivilianTraffic()->').replace('GarageVehicles().','GarageVehicles()->')
for n in ('Replication','ERR_GameMode','ERR_ConvoyRules','SCR_EntityHelper'):s=s.replace(n+'.',n+'::')
s=s.replace('ERR_HQ* hq;','ERR_HQ* hq=nullptr;').replace('int depth;','int depth=0;')
return s
@@ -34,8 +34,8 @@ struct BaseCompartmentManagerComponent{};
struct Service;struct ERR_HQ;
struct Roster{IEntity*current=nullptr;void*CurrentActor(IEntity*a){return a&&a==current?this:nullptr;}};
struct ERR_ForceRecord{bool Pinned=true;};struct Ledger{ERR_ForceRecord r;ERR_ForceRecord*FindGroup(GroupType*){return &r;}};
struct Traffic{void PlayerEntered(IEntity*,IEntity*){}};
struct ERR_GameMode{Traffic* CivilianTraffic(){return nullptr;}inline static ERR_GameMode*current=nullptr;bool active=true;ERR_HQ*command=nullptr;Roster roster;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Roster*Players(){return &roster;}Ledger*Forces(){return &ledger;}};
struct Traffic{int calls=0;IEntity* actor=nullptr;IEntity* target=nullptr;void PlayerEntered(IEntity* a,IEntity* t){calls++;actor=a;target=t;}};
struct ERR_GameMode{Traffic garage;Traffic* GarageVehicles(){return &garage;}Traffic* CivilianTraffic(){return nullptr;}inline static ERR_GameMode*current=nullptr;bool active=true;ERR_HQ*command=nullptr;Roster roster;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Roster*Players(){return &roster;}Ledger*Forces(){return &ledger;}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace ERR_ConvoyRules{bool occupied=false;bool Captured(IEntity*,GroupType*){return occupied;}
'''+part+r'''
@@ -65,6 +65,7 @@ int main(){
// Native occupancy is not visible yet; super removes current identity to model
// disconnect/body loss before next one-second poll. Event must win that gap.
c.Enter();assert(c.unit.PlayerCaptureObserved&&c.unit.Released);assert(c.access.nativeCalls==1);
assert(c.mode.garage.calls==1&&c.mode.garage.actor==&c.player&&c.mode.garage.target==&c.turret);
assert(c.hq.unregistered==1&&c.hq.retained==0&&c.hq.recoveries==0);
assert(c.close.stops==1&&c.recovery.stops==1&&c.unit.Downlink==nullptr&&c.unit.orders==0&&c.unit.clears==1);
assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
+12 -5
View File
@@ -1,9 +1,12 @@
"""Execute the shipped checkpoint layout across road widths and rotations."""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,x) for x in ('protected void Layout(', 'protected void AddProp('))
s='\n'.join(body(p,x) for x in ('protected void Layout(', 'protected void AddProp(', 'protected vector GuardPosition('))
s=s.replace('protected ', '').replace('out ', '').replace('array<ref ERR_RoadblockProp> covers', 'Array<Prop*>& covers')
s=s.replace('ResourceName ', 'string ').replace('vector ', 'Vec ').replace('ERR_Actors.Ground','Ground').replace('new ERR_RoadblockProp','new Prop')
shoulder = re.search(r'm_Shoulder = ([^;]+);', body(p, 'protected bool Candidate('))[1]
shoulder = shoulder.replace('road.GetWidth()', 'width').replace('Math.Max', 'std::max')
run(r'''
#include <cassert>
#include <cmath>
@@ -27,24 +30,28 @@ int main(){
for(double width:{4.,6.,10.,16.})for(int degrees=0;degrees<360;degrees+=15){
Checkpoint l;double angle=degrees*3.141592653589793/180;
l.m_Forward={std::sin(angle),0,std::cos(angle)};l.m_Right={std::cos(angle),0,-std::sin(angle)};
l.m_Shoulder=std::max(width*.5+1.5,4.);Array<Prop*>props;l.Layout(props);assert(props.size()==18);
l.m_Shoulder='''+shoulder+r''';Array<Prop*>props;l.Layout(props);assert(props.size()==8);
std::map<string,int>counts;std::vector<double>barriers;
for(auto*p:props){counts[p->Prefab]++;
Vec delta=p->Position-l.m_Centre;double across=dot(delta,l.m_Right),along=dot(delta,l.m_Forward);
// Whole footprints (not just origins) preserve a four-metre straight passage.
double reach=std::abs(dot(p->Right,l.m_Right))*p->HalfWidth+std::abs(dot(p->Forward,l.m_Right))*p->HalfDepth;
assert(std::abs(across)-reach>=2.-1e-8);
// Bound the compact footprint, including both road shoulders.
assert(std::abs(across)+reach <= std::max(width*.5+1.2,4.5)+1e-6);
double depth=std::abs(dot(p->Right,l.m_Forward))*p->HalfWidth+std::abs(dot(p->Forward,l.m_Forward))*p->HalfDepth;
assert(std::abs(along)+depth <= 8.9+1e-6);
if(p->Prefab=="barrier")barriers.push_back(along);
if(p->Prefab=="sign")assert(dot(p->Forward,l.m_Forward)*along>0); // both approaches face outward
// Guard starting volumes cannot intersect any prop in the planned post.
for(int side:{-1,1})for(int row:{-1,1}){
Vec guard=l.m_Centre+l.m_Right*(side*(l.m_Shoulder+2.5))+l.m_Forward*(side*(row-1.5));
Vec guard=l.GuardPosition(side,row);
Vec rel=guard-p->Position;
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.6 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.6);
}
}
assert(counts["cover"]==8&&counts["barrier"]==4&&counts["obstacle"]==2&&counts["sign"]==2&&counts["table"]==1&&counts["shelter"]==1);
assert(barriers.size()==4&&std::abs(barriers[0]-barriers[2])>15.9);
assert(counts["cover"]==4&&counts["barrier"]==2&&counts["sign"]==2&&counts.size()==3);
assert(barriers.size()==2&&std::abs(barriers[0]-barriers[1])>11.9);
// No physical prop footprint intersects another (separating axes).
for(size_t i=0;i<props.size();i++)for(size_t j=i+1;j<props.size();j++){
auto*a=props[i];auto*b=props[j];bool separate=false;
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})
+63
View File
@@ -0,0 +1,63 @@
"""Execute station handover with native manager/world boundaries doubled."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSupportStations.c'
def cpp(signature):
s=body(p,signature)
for a,b in [('int restored;', 'int restored=0;'),('protected ',''),('override ',''),('SCR_SupportStationManagerComponent manager','Manager* manager'),('SCR_SupportStationManagerComponent original','Manager* original'),('array<SCR_BaseSupportStationComponent> stations','Array<Station*>& stations'),('array<SCR_BaseSupportStationComponent> registered','Array<Station*>* registered'),('foreach (SCR_BaseSupportStationComponent station : registered)','for (Station* station : *registered)'),('foreach (SCR_BaseSupportStationComponent station : m_ERRTerrainSupportStations)','for (Station* station : m_ERRTerrainSupportStations)'),('IEntity owner','Owner* owner'),('station.', 'station->'),('manager.', 'manager->'),('original.', 'original->'),('owner.', 'owner->'),('GetGame().', 'GetGame()->'),('EDamageState.DESTROYED','2'),('super.', 'Base::')]: s=s.replace(a,b)
return s
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
struct Station;struct Manager;struct Game;
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);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct World{} world,otherWorld;
struct Owner{World* world=&::world;World* GetWorld(){return world;}};
struct Base{int cleaned=0;void OnBeforeWorldCleanup(){cleaned++;}void ERR_TerrainStationRestored(Manager*){}};
template<class...T>void PrintFormat(T...){}
struct Game:Base{Array<Station*> m_ERRTerrainSupportStations;World* GetWorld(){return &world;}
void ERR_CaptureTerrainSupportStations(Manager*);
void ERR_RestoreTerrainSupportStations(Manager*);
void OnBeforeWorldCleanup();
} game;
Game* current=&game;Game* GetGame(){return current;}
struct Station:Base{Owner own;Owner* owner=&own;int m_eCurrentDamageState=0;bool ranged=true;
bool m_ERRItemStationInitialized=true;Manager* m_ERRItemStationManager=nullptr;
Owner* GetOwner(){return owner;}bool UsesRange(){return ranged;}
'''+cpp('bool ERR_CanRestoreTerrainStation(')+cpp('override void ERR_TerrainStationRestored(')+r'''
};
struct Map{std::vector<Array<Station*>*> lists;int Count(){return lists.size();}Array<Station*>* GetElement(int i){return lists[i];}};
struct Manager{Map m_mSupportStations;Array<Station*> accepted;void AddSupportStation(Station* s){if(!accepted.Contains(s))accepted.Insert(s);}
'''+cpp('void ERR_CollectTerrainStations(')+r'''
};
'''+cpp('void ERR_CaptureTerrainSupportStations(').replace('void ERR_', 'void Game::ERR_',1)+cpp('void ERR_RestoreTerrainSupportStations(').replace('void ERR_', 'void Game::ERR_',1)+cpp('override protected void OnBeforeWorldCleanup(').replace('void OnBefore','void Game::OnBefore',1)+r'''
int main(){
Station pump,item,destroyed,deleted,offWorld,nonRanged;
Manager old,replacement;
destroyed.m_eCurrentDamageState=2;deleted.owner=nullptr;offWorld.own.world=&otherWorld;nonRanged.ranged=false;
pump.m_ERRItemStationManager=&old;item.m_ERRItemStationManager=&old;
Array<Station*> fuel{&pump,&destroyed,nullptr,&deleted,&offWorld};
Array<Station*> repair{&item,&pump,&nonRanged};old.m_mSupportStations.lists={&fuel,&repair,nullptr};
game.ERR_CaptureTerrainSupportStations(&old);assert(game.m_ERRTerrainSupportStations.size()==6);
// The removed registry can disappear; the captured list is independent.
fuel.Clear();repair.Clear();
replacement.AddSupportStation(&item); // initialization races must not duplicate entries
game.ERR_RestoreTerrainSupportStations(&replacement);
assert(replacement.accepted.size()==2&&replacement.accepted.Contains(&pump)&&replacement.accepted.Contains(&item));
assert(pump.m_ERRItemStationManager==&replacement&&item.m_ERRItemStationManager==&replacement);
assert(game.m_ERRTerrainSupportStations.empty());
game.ERR_RestoreTerrainSupportStations(&replacement);assert(replacement.accepted.size()==2);
game.m_ERRTerrainSupportStations.Insert(&pump);game.ERR_RestoreTerrainSupportStations(nullptr);assert(game.m_ERRTerrainSupportStations.size()==1);
game.OnBeforeWorldCleanup();assert(game.m_ERRTerrainSupportStations.empty()&&game.cleaned==1);
game.m_ERRTerrainSupportStations.Insert(&pump);game.ERR_CaptureTerrainSupportStations(nullptr);assert(game.m_ERRTerrainSupportStations.empty());
current=nullptr;assert(!pump.ERR_CanRestoreTerrainStation());current=&game;
}
''')
core=p.parent
server=body(core/'ERR_TerrainTakeover.c','protected void ERR_StopOriginalTerrainMode(') if 'protected void ERR_StopOriginalTerrainMode(' in (core/'ERR_TerrainTakeover.c').read_text() else (core/'ERR_TerrainTakeover.c').read_text()
assert server.index('ERR_CaptureTerrainSupportStations(')<server.index('m_ERRTerrainOriginal.OnGameEnd()')
client=body(core/'ERR_TerrainTakeoverClient.c','void ERR_BeginClientTerrainTakeover(')
assert client.index('ERR_CaptureTerrainSupportStations(')<client.index('mode.OnGameEnd()')
init=body(p,'override void OnPostInit(')
assert 'ERR_TerrainTakeoverMode.Cast(owner) && GetInstance() == this' in init
print('PASS: support registrations survive takeover on both peers, deduplicate, skip invalid stations, update item cleanup and clear on world teardown')