Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
135 lines
13 KiB
Python
135 lines
13 KiB
Python
"""Actual paired road geometry and bounded native survey; native boundaries are doubled."""
|
|
from pathlib import Path
|
|
import os,re
|
|
from policy_harness import ROOT,body,run
|
|
p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
|
|
source=p.read_text()
|
|
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
|
source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
|
for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
|
|
assert forbidden not in source,forbidden
|
|
rules='\n'.join(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c', x) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static vector Direction(', 'static vector Objective('))
|
|
rules=rules.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRole.','ERR_HQPursuitRole::').replace('vector offset;','vector offset{};')
|
|
s=re.sub(r'//[^\n]*','',source).replace('ref ','').replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
|
|
s=s.replace('out vector','vector&').replace('out float','float&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
|
s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
|
|
for name in ('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement','ERR_InterceptVehicleFootprint'):
|
|
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
|
for name in ('belief','world','road','body','order','settlements','placement','footprint'):
|
|
s=s.replace(name+'.',name+'->')
|
|
s=s.replace('Footprint.', 'Footprint->')
|
|
s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->').replace('GetWorld().','GetWorld()->')
|
|
for name in ('ERR_ForceRules','ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','vector','Math','ERR_Actors','ChimeraWorldUtils','TraceFlags','Replication','ERR_HQRoadIntercept'):
|
|
s=s.replace(name+'.',name+'::')
|
|
s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
|
|
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').replace('\n}\n','\n};\n')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <limits>
|
|
#include <vector>
|
|
#include <string>
|
|
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};enum class ERR_HQMemoryKind{SIGHTING,DISTRESS,BODY};
|
|
struct SCR_AIGroup{};struct ERR_InterceptVehicleFootprint{bool valid=true;float radius=4;bool Valid(){return valid;}float Radius(){return radius;}};
|
|
struct Math{static float Max(float a,float b){return std::max(a,b);}static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(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;}
|
|
float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float n=Length();return{x/n,y/n,z/n};}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{};vector Vector(float a,float b,float c){return{a,b,c};}
|
|
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);}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 ERR_HQPursuitRules{''' + rules + r'''};
|
|
struct ERR_HQBelief{ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;int SampleCount=2,MessageId=1,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};float ObservedAt=5000,LastDeliveredAt=7000,Confidence=.9,Radius=40;std::string TravelMode="on foot";vector PredictedPosition={1000,10,1000},Velocity={0,0,4};bool HasVelocity=true;};
|
|
struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
|
|
struct NativeWorld{int traces=0;float fraction=1;float TraceMove(TraceBox* t,void*){traces++;assert(t->Flags==3&&t->Mins.y==-.9f);return fraction;}};
|
|
struct BaseRoad{static inline int reads=0;float width=6;Array<vector>points={{900,10,1300},{1100,10,1300}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){reads++;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==200&&high.z-low.z==200&&high.y-low.y==2000);out=roads;}};
|
|
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 float rightHeight=10;static vector Ground(vector p){p.y=p.x>1000?rightHeight:10;return p;}};
|
|
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
|
|
|
struct ERR_SettlementSafety{static inline bool clear=true,rightClear=true;static inline int calls=0;bool OutsideSettlements(vector p){calls++;return clear&&(p.x<=1000||rightClear);}};
|
|
struct ERR_ConvoyPlacement{static inline bool clear=true,rightClear=true,water=false;static inline int calls=0;static inline float heading=0;bool ClearFootprint(vector p,float h,ERR_InterceptVehicleFootprint*){return Clear(p,h);}bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear&&!water&&(p.x<=1000||rightClear);}};
|
|
''' + s + r'''
|
|
int main(){
|
|
using Rules=ERR_HQMountedInterceptRules;
|
|
ERR_HQBelief belief;Array<vector> reserved;vector a={1,2,3},b={4,5,6};float score=0,ha=0,hb=0;
|
|
vector origin={1000,10,1000},velocity={0,0,4},seed={1000,10,1300};
|
|
auto pair=[&](Array<vector> p){return Rules::PairFromPolyline(p,origin,velocity,seed,reserved,a,b,score,ha,hb);};
|
|
auto empty=[&](){assert(a==vector::Zero&&b==vector::Zero);};
|
|
// Real arc interpolation over a90-degree bend, not straight interpolation
|
|
// between the road's endpoints (which would put both escorts off the road).
|
|
assert(pair({{900,10,1300},{1000,10,1300},{1000,10,1400}}));
|
|
assert(a==vector(970,10,1300)&&b==vector(1000,10,1330));
|
|
assert(std::abs(ha-90)<.001&&std::abs(hb)<.001);
|
|
assert(vector::DistanceXZ(a,b)>42&&vector::DistanceXZ(a,b)<43);
|
|
// Arc span alone is insufficient on a bend-back/hairpin.
|
|
assert(!pair({{970,10,1300},{1000,10,1300},{1000,10,1301},{970,10,1301}}));empty();
|
|
assert(!pair({{980,10,1300},{1019.9f,10,1300}}));empty();
|
|
assert(pair({{980,10,1300},{1020,10,1300}})&&vector::DistanceXZ(a,b)==40);
|
|
// A road aligned with travel must not push a partner behind the300m floor.
|
|
assert(pair({{1000,10,1250},{1000,10,1450}}));
|
|
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
|
assert(pair({{1000,10,1450},{1000,10,1250}}));
|
|
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
|
// Actual sloped segment retains height for subsequent bridge validation.
|
|
assert(pair({{900,5,1300},{1100,15,1300}}));
|
|
assert(a==vector(970,8.5,1300)&&b==vector(1030,11.5,1300));
|
|
// Degenerate segments are harmless but a wholly degenerate road has no span.
|
|
assert(pair({{900,10,1300},{900,10,1300},{1100,10,1300}}));
|
|
assert(!pair({{1000,10,1300},{1000,10,1300}}));empty();
|
|
assert(!pair({{900,10,1300},{NAN,10,1300},{1100,10,1300}}));empty();
|
|
assert(!pair(Array<vector>(1025,{1000,10,1300})));empty();
|
|
assert(!pair({}));empty();assert(!pair({{1000,10,1300}}));empty();
|
|
auto good=Array<vector>{{900,10,1300},{1100,10,1300}};
|
|
velocity={0,0,0};assert(!pair(good));empty();velocity={0,0,4};
|
|
seed.x=NAN;assert(!pair(good));empty();seed={1000,10,1300};
|
|
origin.x=NAN;assert(!pair(good));empty();origin={1000,10,1000};
|
|
reserved={{970,10,1380}};assert(pair(good));
|
|
reserved={{970,10,1379.99f}};assert(!pair(good));empty();
|
|
reserved={{NAN,10,1380}};assert(!pair(good));empty();
|
|
reserved=Array<vector>(129,{10000,10,10000});assert(!pair(good));empty();reserved.clear();
|
|
for(float z:{1290.f,1901.f,700.f}){seed.z=z;assert(!pair({{900,10,z},{1100,10,z}}));empty();}seed.z=1300;
|
|
BaseRoad road;road.points=good;game.ai.manager.roads={&road};
|
|
int q=game.ai.manager.queries;
|
|
assert(Rules::SelectPair(&belief,10000,reserved,a,b));assert(game.ai.manager.queries==q+1);
|
|
assert(a==vector(970,10,1300)&&b==vector(1030,10,1300));
|
|
assert(ERR_SettlementSafety::calls==2&&ERR_ConvoyPlacement::calls==2);
|
|
auto select=[&](){a={1,2,3};b={4,5,6};bool ok=Rules::SelectPair(&belief,10000,reserved,a,b);if(!ok)empty();return ok;};
|
|
// Second position cannot escape settlement, water/collision or bridge checks.
|
|
int pc=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_SettlementSafety::rightClear=true;
|
|
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+2);ERR_ConvoyPlacement::rightClear=true;
|
|
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::water=true;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+1);ERR_ConvoyPlacement::water=false;
|
|
pc=ERR_ConvoyPlacement::calls;ERR_Actors::rightHeight=7;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_Actors::rightHeight=10;
|
|
road.points={{900,20,1300},{1100,20,1300}};assert(!select());road.points=good;
|
|
// Reject a pair assembled from two short unrelated roads, even when their
|
|
// endpoints could have formed a geometrically spaced chord.
|
|
BaseRoad left,right;left.points={{960,10,1300},{980,10,1300}};right.points={{1020,10,1300},{1040,10,1300}};game.ai.manager.roads={&left,&right};assert(!select());
|
|
game.ai.manager.roads={&road};road.width=5.99;assert(!select());road.width=NAN;assert(!select());road.width=6;
|
|
// All pre-query gates remain cheap; one permitted pair call is one query.
|
|
q=game.ai.manager.queries;belief.HasVelocity=false;assert(!select());belief.HasVelocity=true;
|
|
Replication::server=false;assert(!select());Replication::server=true;
|
|
reserved.resize(129);assert(!select());reserved.clear();
|
|
assert(game.ai.manager.queries==q);
|
|
game.ai.available=false;assert(!select());game.ai.available=true;assert(game.ai.manager.queries==q);
|
|
// Road count bound refuses before any point arrays are read.
|
|
game.ai.manager.roads=Array<BaseRoad*>(129,&road);int reads=BaseRoad::reads;assert(!select());assert(BaseRoad::reads==reads);
|
|
game.ai.manager.roads=Array<BaseRoad*>(128,&road);assert(select());assert(BaseRoad::reads==reads+128);
|
|
// Total4096 segment allowance includes irrelevant/degenerate vertices; an
|
|
// earlier valid pair cannot leak through once later roads exhaust the cap.
|
|
BaseRoad big,tail;big.points=Array<vector>(1024,{900,10,1300});big.points.back()={1100,10,1300};tail.points=Array<vector>(5,{900,10,1300});tail.points.back()={1100,10,1300};
|
|
game.ai.manager.roads={&big,&big,&big,&big,&tail};assert(select());
|
|
tail.points.insert(tail.points.begin(),{900,10,1300});pc=ERR_ConvoyPlacement::calls;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);
|
|
// Pair failure still permits unchanged singleton selection in a separate
|
|
// call; this explicitly consumes the caller's second survey budget.
|
|
road.points={{995,10,1300},{1005,10,1300}};game.ai.manager.roads={&road};q=game.ai.manager.queries;
|
|
assert(!select());vector singleton;assert(Rules::Select(&belief,10000,reserved,singleton));assert(singleton==vector(1000,10,1300)&&game.ai.manager.queries==q+2);
|
|
}
|
|
|
|
''')
|
|
print('PASS: actual paired arc geometry,40m/80m spacing, separate original polylines, both-position validation and128road/4096segment budget; no native driving proof')
|