Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
183 lines
19 KiB
Diff
183 lines
19 KiB
Diff
--- a/tests/test_engineer_mine_site.py
|
||
+++ b/tests/test_engineer_mine_site.py
|
||
@@ -7,80 +7,136 @@
|
||
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('out vector','vector&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
||
+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'):
|
||
+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'):s=s.replace(name+'.',name+'->')
|
||
+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 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;}};
|
||
+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 NativeWorld{int traces=0;bool obstructed=false,blockNear=false;float TraceMove(TraceBox* t,void*){traces++;bool blocked=obstructed||(blockNear&&std::hypot(t->Start.x,t->Start.z)<50);delete t;return blocked?.5f:1.f;}};
|
||
-struct BaseRoad{float width=4;Array<vector>points;float GetWidth(){return width;}void GetPoints(Array<vector>&p){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.z-low.z==120&&high.y-low.y==2000);out=roads;}};
|
||
+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'''};
|
||
-'''+s+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;Array<vector>candidates;
|
||
- 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})&&!Site::InBand(post,{NAN,10,0}));
|
||
- Array<vector>straight={{-100,10,0},{100,10,0}};assert(Site::FromPolyline(straight,post,candidates));
|
||
- assert(candidates.size()==4);for(auto p:candidates)assert(Site::InBand(post,p)&&p.z==0);
|
||
- // Real hairpin segments only: no fabricated chord through the road's bend.
|
||
- Array<vector>bend={{-50,10,30},{50,10,30},{50,10,-30},{-50,10,-30}};candidates.clear();
|
||
- assert(Site::FromPolyline(bend,post,candidates));for(auto p:candidates)assert(std::abs(p.z)==30||p.x==50);
|
||
- Array<vector>bad={{0,10,30},{NAN,10,30}},shortRoad={{0,10,30}},oversized(1025,{0,10,30});
|
||
- assert(!Site::FromPolyline(bad,post,candidates)&&!Site::FromPolyline(shortRoad,post,candidates)&&!Site::FromPolyline(oversized,post,candidates));
|
||
- Array<vector>zero={{0,10,30},{0,10,30}};candidates.clear();assert(Site::FromPolyline(zero,post,candidates)&&candidates.empty());
|
||
- // Candidate cap and tie policy are independent of road/point enumeration order.
|
||
- 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.size()==128&&reverse.size()==128);for(auto p:forward)assert(reverse.Contains(p));
|
||
- BaseRoad road;road.points=straight;game.ai.manager.roads={&road};assert(Site::Select(post,out));
|
||
- assert(out.x<0&&std::abs(std::abs(out.x)-25.01f)<.001f&&out.y==10);
|
||
- std::reverse(road.points.begin(),road.points.end());vector reversed;assert(Site::Select(post,reversed));assert(vector::DistanceXZ(out,reversed)<.001f);
|
||
- // Block nearest points: a retained farther point on that same road may qualify.
|
||
- game.world.blockNear=true;assert(Site::Select(post,out)&&std::abs(out.x)>59);game.world.blockNear=false;
|
||
- // Bridges cannot ground a mine onto terrain below their elevated 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=straight;
|
||
+ 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.ai.manager.roads.resize(129,&road);assert(!Site::Select(post,out));game.ai.manager.roads={&road};
|
||
- 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);
|
||
- game.ai.manager.roads={&road};road.width=3;assert(!Site::Select(post,out));road.width=4;
|
||
- Replication::server=false;before=game.ai.manager.queries;assert(!Site::Select(post,out)&&before==game.ai.manager.queries);Replication::server=true;
|
||
- assert(!Site::Select({NAN,10,0},out)&&out==vector::Zero);
|
||
- // Choosing among roads remains stable with reversed native enumeration order.
|
||
- BaseRoad a,b;a.points={{-100,10,30},{100,10,30}};b.points={{-100,10,-30},{100,10,-30}};
|
||
- game.ai.manager.roads={&a,&b};assert(Site::Select(post,out)&&out.z==-30);
|
||
- game.ai.manager.roads={&b,&a};assert(Site::Select(post,reversed)&&out==reversed);
|
||
+ 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 road mine-site annulus/bend geometry, deterministic bounded candidates, server/query limits, and native ground/water/obstruction rejection; no runtime placement claim')
|
||
+print('PASS: actual mine selector respects road and settlement flank safety; native boundaries doubled')
|