Files
EscapistsReintroduced/tests/test_engineer_mine_site.py
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

143 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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,&parallel};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')