Add garage car variants and yard parking; record Workshop 0.0.142
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-06 11:32:33 +01:00
parent 9caed3f12d
commit 55c0ec775a
27 changed files with 6158 additions and 35 deletions
+5 -5
View File
@@ -78,7 +78,7 @@ print('PASS: permanent entry/movement/occupancy claims; far hidden grace cleanup
# 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('m_Mode.Settings().Get(', 'settings.Get(').replace('m_Bikes.Find()', 'bikeLookups++').replace('m_Cars.Find()', 'carLookups++')
start=start.replace('GetGame().GetCallqueue().CallLater(Step, 250, true)', 'queueCalls++')
run(r'''
#include <cassert>
@@ -89,15 +89,15 @@ 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;
int m_Requested=0,m_GarageChance=0,m_SpawnRadius=0,m_SpawnInterval=0,m_CleanupRadius=0,bikeLookups=0,carLookups=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 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.carLookups==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&&normal.carLookups==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);}
for(auto key:{"garageVehicleCount","garageChancePercent"}){Service disabled;disabled.settings.values[key]=0;disabled.Start();assert(disabled.m_Stopped&&disabled.queueCalls==0&&disabled.bikeLookups==0&&disabled.carLookups==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);
+64 -3
View File
@@ -1,7 +1,8 @@
"""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 = '\n'.join(body(p, s) for s in ('static bool Garage(', 'static bool Envelope(', 'static bool Yard(', 'static vector YardOffset('))
rules += '\n' + body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ParkedCarCatalog.c', 'static bool Candidate(')
rules = rules.replace('vector ', 'Vec ').replace('ERR_ForceRules.', 'ERR_ForceRules::')
run(r'''
#include <cassert>
@@ -13,9 +14,10 @@ run(r'''
struct string : std::string {
using std::string::string;
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
bool Contains(const std::string& needle){return find(needle)!=npos;}
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];}};
struct Vec {float v[3];float& operator[](int i){return v[i];}float operator[](int i)const{return v[i];}Vec operator+(Vec b)const{return {{v[0]+b[0],v[1]+b[1],v[2]+b[2]}};}Vec operator*(float n)const{return {{v[0]*n,v[1]*n,v[2]*n}};}};
namespace ERR_ForceRules {bool FiniteVector(Vec v){return std::isfinite(v[0])&&std::isfinite(v[1])&&std::isfinite(v[2]);}}
''' + rules + r'''
int main(){
@@ -25,6 +27,25 @@ int main(){
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(Yard("Prefabs/Structures/Houses/Village/House_Village_E_1I01/House_Village_E_1I01_beige_V2.et"));
assert(Yard("Prefabs/Structures/Houses/Village/House_Village_E_1I06/House_Village_E_1I06_white.et"));
assert(!Yard("Prefabs/Structures/Houses/Village/House_Village_E_1I06/House_Village_E_1I06_base.et"));
assert(!Yard("Prefabs/Structures/Houses/Village/House_Village_E_1I06/Dst/House_Village_E_1I06_ruin.et"));
assert(!Yard("Prefabs/Structures/Houses/Village/House_Village_E_1I06/Furniture/House_Village_E_1I06_furniture_01.et"));
Vec low={{-4,-1,-6}},high={{6,8,8}};
assert(YardOffset(low,high,0)[0]==-7 && YardOffset(low,high,0)[2]==1);
assert(YardOffset(low,high,1)[0]==9 && YardOffset(low,high,1)[1]==0);
assert(YardOffset(low,high,2)[2]==-9 && YardOffset(low,high,2)[0]==1);
assert(YardOffset(low,high,3)[2]==11);
assert(Candidate("{GUID}Prefabs/Vehicles/Wheeled/T601/T601_blue_twoTone.et",false));
assert(Candidate("Prefabs/Vehicles/Wheeled/V2101/V2101_lightBlue.et",false));
assert(Candidate("Prefabs/Vehicles/Wheeled/S105/S105_rally_08.et",true));
assert(!Candidate("Prefabs/Vehicles/Wheeled/S105/S105_rally_08.et",false));
assert(!Candidate("Prefabs/Vehicles/Wheeled/S105/S105_green.et",true));
assert(!Candidate("Prefabs/Vehicles/Wheeled/T601/T601_base.et",false));
assert(!Candidate("Prefabs/Vehicles/Wheeled/T601/T601_rally_01_fullkit.et",true));
assert(!Candidate("Prefabs/Vehicles/Wheeled/T601/VehParts/Wheels/T601_wheel_Rally_Blue.et",false));
assert(!Candidate("Prefabs/Vehicles/Wheeled/S105/S105_FIA.et",false));
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}}));
@@ -35,4 +56,44 @@ int main(){
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')
print('PASS: garage/yard family admission, bounded wall offsets, civilian and optional Rally catalog exclusions, vehicle-fit limits')
# Execute the actual yard ground veto with road/terrain doubles. A flat centre
# alone must not admit a sloping or submerged edge of the parking footprint.
ground = body(p, 'protected bool YardGround(')
ground = ground.replace('protected ', '').replace('vector ', 'Vec ')
ground = ground.replace('SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());', 'SCR_AIWorld* world = worldAvailable ? &nativeWorld : nullptr;')
ground = ground.replace('BaseRoad road; float distance;', 'BaseRoad* road = nullptr; float distance = 0;')
ground = ground.replace('world.', 'world->').replace('road.', 'road->').replace('GetRoadNetworkManager().', 'GetRoadNetworkManager()->')
ground = ground.replace('skipNavlinks: true', 'true')
for name in ['ERR_ForceRules', 'ERR_Actors', 'SCR_TerrainHelper', 'Math']:
ground = ground.replace(name + '.', name + '::')
run(r'''
#include <cassert>
#include <cmath>
struct Vec {float v[3];float operator[](int i)const{return v[i];}
Vec operator+(Vec b)const{return {{v[0]+b[0],v[1]+b[1],v[2]+b[2]}};}
Vec operator*(float n)const{return {{v[0]*n,v[1]*n,v[2]*n}};}};
bool worldAvailable=true,managerAvailable=true,hasRoad=true,wetEdge=false;
float roadDistance=20,edgeHeight=0;
struct BaseRoad{float width=10;float GetWidth(){return width;}} roadInstance;
struct Roads{void GetClosestRoad(Vec,BaseRoad*& road,float& distance,bool){road=hasRoad?&roadInstance:nullptr;distance=roadDistance;}} roads;
struct SCR_AIWorld{Roads* GetRoadNetworkManager(){return managerAvailable?&roads:nullptr;}} nativeWorld;
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}}
namespace SCR_TerrainHelper{float GetTerrainY(Vec p){return p[0]>.5f?edgeHeight:0;}}
namespace ERR_Actors{bool DryGround(Vec p){return !(wetEdge&&p[2]>2)&&std::isfinite(SCR_TerrainHelper::GetTerrainY(p));}}
namespace Math{float AbsFloat(float n){return std::abs(n);}}
''' + ground + r'''
int main(){Vec p={{0,0,0}},matrix[4]={{{1,0,0}},{{0,1,0}},{{0,0,1}},{{0,0,0}}};
assert(YardGround(p,matrix));
roadDistance=8.99f;assert(!YardGround(p,matrix));roadDistance=9;assert(YardGround(p,matrix));
hasRoad=false;assert(YardGround(p,matrix));hasRoad=true;
worldAvailable=false;assert(!YardGround(p,matrix));worldAvailable=true;
managerAvailable=false;assert(!YardGround(p,matrix));managerAvailable=true;
wetEdge=true;assert(!YardGround(p,matrix));wetEdge=false;
edgeHeight=.19f;assert(!YardGround(p,matrix));edgeHeight=.17f;assert(YardGround(p,matrix));
edgeHeight=NAN;assert(!YardGround(p,matrix));edgeHeight=0;
roadDistance=NAN;assert(!YardGround(p,matrix));
}
''')
print('PASS: yard road-width margin, missing road data, footprint water/slope and nonfinite-input vetoes')