Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
143 lines
14 KiB
Python
143 lines
14 KiB
Python
"""Execute actual finite road mine-site geometry and bounded native selector."""
|
||
from pathlib import Path
|
||
import os,re
|
||
from policy_harness import ROOT,body,run
|
||
p=Path(os.environ.get('ERR_ENGINEER_MINE_SITE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c'))
|
||
source=p.read_text()
|
||
for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
|
||
assert forbidden not in source,forbidden
|
||
s=re.sub(r'//[^\n]*','',source).replace('class ERR_EngineerMineSite','struct ERR_EngineerMineSite')
|
||
s=s.replace('inout int','int&').replace('out vector','vector&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
||
s=s.replace('Array<vector> starts,','Array<vector>& starts,').replace('Array<vector> ends,','Array<vector>& ends,')
|
||
s=s.replace('Array<vector> candidates)', 'Array<vector>& candidates)').replace('FromPolyline(Array<vector> points,','FromPolyline(Array<vector>& points,')
|
||
s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
|
||
for name in ('SCR_AIWorld','BaseRoad','ERR_SettlementSafety'):
|
||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||
for name in ('world','road','settlements'):s=s.replace(name+'.',name+'->')
|
||
s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->')
|
||
for name in ('ERR_ForceRules','ERR_HQRoadIntercept','vector','Math','ERR_Actors','Replication'):s=s.replace(name+'.',name+'::')
|
||
s=s.replace('null','nullptr');s=re.sub(r'\bVector\(', 'vector(', s)
|
||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('\n}\n','\n};\n')
|
||
settlement=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SettlementSafety.c'
|
||
settle=settlement.read_text().replace('class ERR_SettlementSafety','struct ERR_SettlementSafety').replace('protected ','').replace('static const float','static constexpr float')
|
||
settle=settle.replace('MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent))','entity->descriptor').replace('entity.FindComponent(MapDescriptorComponent)','entity->descriptor')
|
||
settle=settle.replace('IEntity entity','Entity* entity').replace('MapDescriptorComponent descriptor','MapDescriptorComponent* descriptor').replace('descriptor.','descriptor->').replace('entity.','entity->')
|
||
settle=settle.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Vector(','vector(').replace('vector.','vector::').replace('string.Format','string::Format').replace('EMapDescriptorType.','EMapDescriptorType::').replace('EQueryEntitiesFlags.','EQueryEntitiesFlags::')
|
||
settle=settle.replace('CheckSettlement, FilterSettlement','[this](Entity* e){return CheckSettlement(e);}, [this](Entity* e){return FilterSettlement(e);}')
|
||
settle=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',settle).replace('\n}\n','\n};\n')
|
||
road=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||
better=body(road,'static bool Better(')
|
||
stand=body(road,'static bool Standable(').replace('array<vector>','Array<vector>').replace('foreach (','for (').replace('TraceBox body','TraceBox* body')
|
||
stand=stand.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('body.','body->').replace('null','nullptr')
|
||
for name in ('ERR_ForceRules','ChimeraWorldUtils','ERR_Actors','Math','TraceFlags'):stand=stand.replace(name+'.',name+'::')
|
||
clearance_checks=''
|
||
if 'static bool FarFromRoads(' in source:
|
||
clearance_checks=r'''
|
||
{Array<vector> starts={{0,10,0}},ends={{.009f,10,0}};int comparisons=0;
|
||
assert(!Site::FarFromRoads({15.001f,10,0},starts,ends,comparisons)&&comparisons==1);
|
||
comparisons=0;assert(Site::FarFromRoads({15.01f,10,0},starts,ends,comparisons));
|
||
comparisons=65536;assert(!Site::FarFromRoads({30,10,0},starts,ends,comparisons)&&comparisons==65536);
|
||
comparisons=65535;ends.Insert({0,10,1});starts.Insert({0,10,1});assert(!Site::FarFromRoads({30,10,0},starts,ends,comparisons)&&comparisons==65536);
|
||
starts={{0,10,0}};ends=starts;comparisons=0;assert(!Site::FarFromRoads({14.99f,10,0},starts,ends,comparisons));
|
||
comparisons=0;assert(Site::FarFromRoads({15,10,0},starts,ends,comparisons));
|
||
}
|
||
'''
|
||
run(r'''
|
||
#include <cassert>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <string>
|
||
struct string:std::string{using std::string::string;template<class...T>static string Format(const char*s,T...){return s;}};
|
||
#include <vector>
|
||
struct Math{static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(x);}static float Sqrt(float x){return std::sqrt(x);}};
|
||
struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}vector(const char*s){sscanf(s,"%f %f %f",&x,&y,&z);}float&operator[](int i){return i==0?x:i==1?y:z;}float operator[](int i)const{return i==0?x:i==1?y:z;}
|
||
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}
|
||
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static const vector Zero;};const vector vector::Zero{};
|
||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}void Remove(int i){this->erase(this->begin()+i);}bool IsEmpty(){return this->empty();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}explicit operator bool(){return true;}};
|
||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}};
|
||
struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
|
||
struct EMapDescriptorType{static constexpr int MDT_NAME_CITY=1,MDT_NAME_TOWN=2,MDT_NAME_VILLAGE=3,MDT_NAME_SETTLEMENT=4,MDT_BUILDING=5;};
|
||
struct EQueryEntitiesFlags{static constexpr int ALL=1;};
|
||
struct MapDescriptorComponent{int type=EMapDescriptorType::MDT_NAME_VILLAGE;int GetBaseType(){return type;}};
|
||
struct Entity{vector position;MapDescriptorComponent*descriptor=nullptr;vector GetOrigin(){return position;}};
|
||
struct NativeWorld{Array<Entity*> settlements;int queries=0;float now=1000;float GetWorldTime(){return now;}template<class A,class B>void QueryEntitiesByAABB(vector low,vector high,A check,B filter,int){queries++;assert(high.x-low.x==400&&high.y-low.y==10000&&high.z-low.z==400);for(auto*e:settlements)if(e&&filter(e)&&!check(e))break;}int traces=0;bool obstructed=false,blockNear=false,onlyTop=false;float TraceMove(TraceBox* t,void*){traces++;bool blocked=obstructed||(blockNear&&std::hypot(t->Start.x,t->Start.z)<50)||(onlyTop&&(std::abs(t->Start.x)>1||t->Start.z<50));delete t;return blocked?.5f:1.f;}};
|
||
struct BaseRoad{float width=4;int pointReads=0;Array<vector>points;float GetWidth(){return width;}void GetPoints(Array<vector>&p){pointReads++;p=points;}};
|
||
struct RoadManager{Array<BaseRoad*>roads;int queries=0;void GetRoadsInAABB(vector low,vector high,Array<BaseRoad*>&out){queries++;assert((high.x-low.x==120||high.x-low.x==160)&&high.z-low.z==high.x-low.x&&high.y-low.y==2000);out.clear();for(auto*r:roads){if(!r||r->points.size()<2){out.Insert(r);continue;}float minX=INFINITY,maxX=-INFINITY,minZ=INFINITY,maxZ=-INFINITY;bool invalid=false;for(auto p:r->points){if(!std::isfinite(p.x)||!std::isfinite(p.z))invalid=true;minX=std::min(minX,p.x);maxX=std::max(maxX,p.x);minZ=std::min(minZ,p.z);maxZ=std::max(maxZ,p.z);}if(invalid||(maxX>=low.x&&minX<=high.x&&maxZ>=low.z&&minZ<=high.z))out.Insert(r);}}};
|
||
struct SCR_AIWorld{RoadManager manager;bool available=true;RoadManager*GetRoadNetworkManager(){return available?&manager:nullptr;}};
|
||
struct Game{NativeWorld world;SCR_AIWorld ai;NativeWorld*GetWorld(){return &world;}SCR_AIWorld*GetAIWorld(){return &ai;}}game;Game*GetGame(){return &game;}
|
||
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||
struct ERR_Actors{static inline bool steep=false;static vector Ground(vector p){p.y=10;if(steep)p.y+=p.x;return p;}};
|
||
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||
struct ERR_HQRoadIntercept{'''+better+stand+r'''};
|
||
'''+settle+s+r'''
|
||
float roadDistance(vector p,const Array<vector>&points){
|
||
float best=INFINITY;
|
||
for(size_t i=1;i<points.size();i++){
|
||
vector a=points[i-1],b=points[i],d=b-a;d.y=0;float length2=vector::Dot(d,d);
|
||
if(length2<.0001f)continue;
|
||
vector off=p-a;off.y=0;float t=std::clamp(vector::Dot(off,d)/length2,0.f,1.f);
|
||
best=std::min(best,vector::DistanceXZ(p,a+d*t));
|
||
}return best;
|
||
}
|
||
int main(){
|
||
using Site=ERR_EngineerMineSite;vector post{0,10,0},out,reversed;
|
||
'''+clearance_checks+r'''
|
||
BaseRoad road;road.points={{-100,10,0},{100,10,0}};game.ai.manager.roads={&road};
|
||
assert(Site::Select(post,out));
|
||
// The previous production implementation compiles and fails exactly here.
|
||
assert(Site::InBand(post,out)&&roadDistance(out,road.points)>=15.f);
|
||
assert(game.ai.manager.queries==1&&game.world.queries>0);
|
||
std::reverse(road.points.begin(),road.points.end());assert(Site::Select(post,reversed));assert(vector::DistanceXZ(out,reversed)<.001f);
|
||
// A crossing or nearby parallel narrow road is still a clearance hazard.
|
||
BaseRoad cross,parallel;cross.width=2;cross.points={{0,10,-100},{0,10,100}};
|
||
game.ai.manager.roads={&road,&cross};assert(Site::Select(post,out));assert(roadDistance(out,road.points)>=15&&roadDistance(out,cross.points)>=15);
|
||
game.ai.manager.roads={&cross,&road};assert(Site::Select(post,reversed)&&vector::DistanceXZ(out,reversed)<.001f);
|
||
parallel.width=1;parallel.points={{-100,10,-16},{100,10,-16}};
|
||
game.ai.manager.roads={&road,¶llel};assert(Site::Select(post,out));assert(roadDistance(out,road.points)>=15&&roadDistance(out,parallel.points)>=15&&out.z>0);
|
||
// Dense parallel roads leave no safe flank in the25–60m annulus.
|
||
BaseRoad grid[9];game.ai.manager.roads={&road};for(int i=0;i<9;i++){grid[i].width=2;grid[i].points={{-100,10,float(-60+i*15)},{100,10,float(-60+i*15)}};game.ai.manager.roads.Insert(&grid[i]);}
|
||
assert(!Site::Select(post,out)&&out==vector::Zero);
|
||
// Clearance includes a narrow neighbor beyond the old60m query. Only the
|
||
// upper outer flank is physically clear, and its neighboring road is10m away.
|
||
BaseRoad edge,outer;edge.points={{-100,10,44.98f},{100,10,44.98f}};outer.width=2;outer.points={{-100,10,70},{100,10,70}};
|
||
game.ai.manager.roads={&edge};game.world.onlyTop=true;assert(Site::Select(post,out)&&out.z>59);
|
||
game.ai.manager.roads={&edge,&outer};assert(!Site::Select(post,out)&&out==vector::Zero);game.world.onlyTop=false;
|
||
// Actual shared settlement logic: named village within200m rejects a candidate;
|
||
// an opposite clear flank remains selectable. Ordinary buildings are excluded.
|
||
game.ai.manager.roads={&road};MapDescriptorComponent descriptor;Entity settlement;settlement.descriptor=&descriptor;settlement.position={0,10,-200};game.world.settlements={&settlement};
|
||
assert(Site::Select(post,out)&&out.z>0&&vector::DistanceXZ(out,settlement.position)>200);
|
||
settlement.position=post;assert(!Site::Select(post,out)&&out==vector::Zero);
|
||
descriptor.type=EMapDescriptorType::MDT_BUILDING;assert(Site::Select(post,out));
|
||
descriptor.type=EMapDescriptorType::MDT_NAME_CITY;assert(!Site::Select(post,out));
|
||
descriptor.type=EMapDescriptorType::MDT_NAME_TOWN;assert(!Site::Select(post,out));
|
||
descriptor.type=EMapDescriptorType::MDT_NAME_SETTLEMENT;assert(!Site::Select(post,out));game.world.settlements.Clear();
|
||
// Real bends retain clearance from every constituent segment, never chords.
|
||
BaseRoad bend;bend.points={{-70,10,30},{50,10,30},{50,10,-30},{-70,10,-30}};game.ai.manager.roads={&bend};
|
||
assert(Site::Select(post,out)&&Site::InBand(post,out)&&roadDistance(out,bend.points)>=15);
|
||
std::reverse(bend.points.begin(),bend.points.end());assert(Site::Select(post,reversed)&&vector::DistanceXZ(out,reversed)<.001f);
|
||
// Existing exact annulus bounds and deterministic bounded retention survive.
|
||
assert(Site::InBand(post,{25,10,0})&&Site::InBand(post,{60,10,0}));assert(!Site::InBand(post,{24.999f,10,0})&&!Site::InBand(post,{60.001f,10,0}));
|
||
Array<vector>forward,reverse;for(int i=0;i<200;i++)Site::Keep(post,{30+float(i)/100,10,0},forward);for(int i=199;i>=0;i--)Site::Keep(post,{30+float(i)/100,10,0},reverse);
|
||
assert(forward.Count()==128&&reverse.Count()==128);for(auto p:forward)assert(reverse.Contains(p));
|
||
// Elevated centerlines cannot be converted to terrain below a bridge.
|
||
game.ai.manager.roads={&road};road.points={{-100,20,0},{100,20,0}};int before=game.world.traces;
|
||
assert(!Site::Select(post,out)&&out==vector::Zero&&game.world.traces==before);road.points={{-100,10,0},{100,10,0}};
|
||
ChimeraWorldUtils::water=true;assert(!Site::Select(post,out)&&out==vector::Zero);ChimeraWorldUtils::water=false;
|
||
ERR_Actors::steep=true;assert(!Site::Select(post,out)&&out==vector::Zero);ERR_Actors::steep=false;
|
||
game.world.obstructed=true;before=game.world.traces;assert(!Site::Select(post,out)&&game.world.traces-before<=128);game.world.obstructed=false;
|
||
game.world.blockNear=true;assert(Site::Select(post,out)&&vector::DistanceXZ(out,post)>=50);game.world.blockNear=false;
|
||
// Hard bounds reject incomplete hazard inventories before physical checks.
|
||
game.ai.manager.roads.resize(129,&road);before=game.world.traces;assert(!Site::Select(post,out)&&game.world.traces==before);
|
||
BaseRoad dense;for(int i=0;i<1024;i++)dense.points.push_back({float(i),10,30});game.ai.manager.roads={&dense,&dense,&dense,&dense,&dense};before=game.world.traces;
|
||
assert(!Site::Select(post,out)&&game.world.traces==before);
|
||
BaseRoad bad;bad.width=1;bad.points={{0,10,0},{NAN,10,30}};game.ai.manager.roads={&road,&bad};assert(!Site::Select(post,out)&&out==vector::Zero);
|
||
bad.points={{0,10,0}};assert(!Site::Select(post,out));bad.points.resize(1025,{0,10,0});assert(!Site::Select(post,out));
|
||
game.ai.manager.roads={&road,nullptr};assert(!Site::Select(post,out));game.ai.manager.roads={&road};
|
||
Replication::server=false;before=game.ai.manager.queries;int settlementsBefore=game.world.queries;assert(!Site::Select(post,out)&&game.ai.manager.queries==before&&game.world.queries==settlementsBefore);Replication::server=true;
|
||
before=game.ai.manager.queries;assert(!Site::Select({NAN,10,0},out)&&out==vector::Zero&&game.ai.manager.queries==before);
|
||
}
|
||
|
||
''')
|
||
print('PASS: actual mine selector respects road and settlement flank safety; native boundaries doubled')
|