Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
225 lines
14 KiB
Python
225 lines
14 KiB
Python
"""Execute the actual local terrain selector against deterministic native boundary doubles."""
|
|
from policy_harness import ROOT, body, run
|
|
import re
|
|
|
|
P = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQTacticalGround.c'
|
|
A = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoordinated.c'
|
|
|
|
|
|
def cpp(s):
|
|
s = s.replace('protected static float s_Window', 'inline static float s_Window')
|
|
s = s.replace('protected static int s_Samples', 'inline static int s_Samples')
|
|
s = s.replace('protected ', '').replace('out vector ', 'vector& ')
|
|
s = s.replace('class ERR_HQTacticalGround', 'struct ERR_HQTacticalGround')
|
|
for name in ('ERR_HQMemoryRules', 'ERR_HQTacticalGroundRules', 'ERR_HQOperationRole',
|
|
'ERR_Actors', 'ChimeraWorldUtils', 'TraceFlags', 'Math', 'vector'):
|
|
s = s.replace(name + '.', name + '::')
|
|
s = s.replace('AIPathfindingComponent.Cast(group.FindComponent(AIPathfindingComponent))',
|
|
'group->path')
|
|
s = s.replace('ChimeraCharacter.Cast(parent)', 'parent->character')
|
|
s = s.replace('Vehicle.Cast(parent)', 'parent->vehicle')
|
|
for kind in ('IEntity', 'Physics', 'TraceParam', 'TraceBox', 'SCR_AIGroup',
|
|
'ERR_HQTask', 'ERR_AttackSpace', 'AIPathfindingComponent',
|
|
'NavmeshWorldComponent', 'ERR_HQUnit', 'ERR_HQWatchEntry',
|
|
'ERR_HQBelief', 'ERR_HQOperationSlot', 'ERR_HQTacticalGround'):
|
|
s = re.sub(r'\b' + kind + r' (\w+)', kind + r'* \1', s)
|
|
for name in ('parent', 'physics', 'trace', 'volume', 'path', 'navmesh', 'task',
|
|
'occupied', 'unit', 'own', 'belief', 'slot', 'Report', 'Task', 'survey'):
|
|
s = s.replace(name + '.', name + '->')
|
|
s = s.replace('GetGame().GetWorld().', 'GetGame()->GetWorld()->')
|
|
s = s.replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
|
|
s = s.replace('TraceMove(trace, StaticObstacle)',
|
|
'TraceMove(trace, [this](IEntity* e){return StaticObstacle(e);})')
|
|
s = s.replace('TraceMove(volume, StaticObstacle)',
|
|
'TraceMove(volume, [this](IEntity* e){return StaticObstacle(e);})')
|
|
s = re.sub(r'"(-?[0-9.]+) (-?[0-9.]+) (-?[0-9.]+)"', r'Vector(\1,\2,\3)', s)
|
|
s = re.sub(r'\bbool (found|sampled|standingClear|lowCovered|approachCovered);', r'bool \1=false;', s)
|
|
s = s.replace('int requested;', 'int requested=0;').replace('float bestScore;', 'float bestScore=0;')
|
|
return s
|
|
|
|
|
|
rules = cpp(body(P, 'class ERR_HQTacticalGroundRules')) + ';'
|
|
selector = cpp(body(P, 'class ERR_HQTacticalGround\n')) + ';'
|
|
adapter = cpp(body(A, 'protected bool CoordinatedGround(')).replace(', m_AttackSpace,', ', &m_AttackSpace,')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <string>
|
|
using string=std::string;
|
|
struct vector {
|
|
float x=0,y=0,z=0;float& operator[](int i){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};}
|
|
float Length()const{return std::sqrt(x*x+y*y+z*z);}
|
|
vector Normalized()const{return *this*(1/Length());}
|
|
void Normalize(){*this=Normalized();}
|
|
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
|
};
|
|
vector Vector(float x,float y,float z){return{x,y,z};}
|
|
struct Math{static float AbsFloat(float v){return std::abs(v);}static float Min(float a,float b){return std::min(a,b);}};
|
|
struct ERR_HQMemoryRules{
|
|
static bool Finite(float v){return std::isfinite(v)&&std::abs(v)<=1e12;}
|
|
static bool PositionValid(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z)&&std::abs(p.x)<=1e6&&std::abs(p.y)<=1e6&&std::abs(p.z)<=1e6;}
|
|
static bool DeliveryFresh(float at,float now){return Finite(at)&&Finite(now)&&at>=0&&now>=at&&now-at<=120000;}
|
|
};
|
|
enum class ERR_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
|
|
struct ERR_HQTask{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::OBSERVE;vector Destination{100,.1,100},SupportPosition{100,.1,0};float SupportUntil=30000;int OperationId=1,BeliefId=1,BeliefRevision=2,ExecutionId=1;};
|
|
struct Physics{bool dynamic=false;bool IsDynamic(){return dynamic;}};
|
|
struct IEntity{bool character=false,vehicle=false;Physics*physics=nullptr;IEntity*parent=nullptr;Physics*GetPhysics(){return physics;}IEntity*GetParent(){return parent;}};
|
|
struct TraceParam{vector Start,End;int Flags=0;};
|
|
struct TraceBox:TraceParam{vector Mins,Maxs;};
|
|
struct TraceFlags{enum{WORLD=1,ENTS=2};};
|
|
struct NativeWorld{
|
|
int scene=0,rays=0,boxes=0;bool cliff=false,water=false;
|
|
float Surface(vector p){return cliff&&p.x>100?4.1:.1;}
|
|
float TraceMove(TraceBox*,const std::function<bool(IEntity*)>&){boxes++;return 1;}
|
|
float TraceMove(TraceParam*t,const std::function<bool(IEntity*)>&){
|
|
rays++;
|
|
if(scene==3)return std::numeric_limits<float>::quiet_NaN();
|
|
if(scene==4)return 0;
|
|
if(scene==1||scene==2){
|
|
if(t->Start.z<84&&std::abs(t->Start.x-100)<1)return .01; // blind wall north
|
|
if(t->Start.x<84&&std::abs(t->Start.z-100)<13){
|
|
if(t->Start.y<1.4)return .025; // low wall west with clear standing view
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
} world;
|
|
struct Game{NativeWorld*GetWorld(){return &world;}} game;
|
|
Game*GetGame(){return &game;}
|
|
struct ERR_Actors{static vector Ground(vector p){p.y=world.Surface(p);return p;}};
|
|
struct ChimeraWorldUtils{static bool TryGetWaterSurfaceSimple(NativeWorld*,vector p){return world.water&&p.x>101;}};
|
|
struct NavmeshWorldComponent{
|
|
bool loaded=true;int requests=0;bool IsTileLoaded(vector){return loaded;}
|
|
bool IsTileRequested(vector){return false;}void LoadTileIn(vector){requests++;}
|
|
};
|
|
struct AIPathfindingComponent{
|
|
bool allow=true;vector delta;NavmeshWorldComponent nav;int projections=0;
|
|
NavmeshWorldComponent*GetNavmeshComponent(){return &nav;}
|
|
bool GetClosestPositionOnNavmesh(vector p,vector,vector&out){projections++;out=p+delta;return allow;}
|
|
};
|
|
struct SCR_AIGroup{AIPathfindingComponent*path=nullptr;};
|
|
struct ERR_AttackSpace{bool allow=true;int checks=0;bool Free(SCR_AIGroup*,vector,float){checks++;return allow;}};
|
|
'''+rules+selector+r'''
|
|
struct ERR_HQWatchEntry{bool Closed=false;int Fit=4;float ObservedAt=1000;vector Position{100,.1,200};};
|
|
struct ERR_HQBelief{int BeliefRevision=2;vector Position{100,.1,0};};
|
|
struct ERR_HQExecutionReport{int ExecutionId=1;float ObservedAt=2000;vector Position{100,.1,100};};
|
|
struct ERR_HQOperationSlot{ERR_HQExecutionReport*Report=nullptr;};
|
|
struct ERR_HQUnit{SCR_AIGroup*Group=nullptr;void*Utility=this;bool HomeGuard=false,ReservedForSupport=false;int LogisticsReservation=0,Id=1;string ForceSourceId="one";ERR_HQTask*Task=nullptr;};
|
|
struct Watch{ERR_HQWatchEntry*entry=nullptr;ERR_HQWatchEntry*Find(string){return entry;}};
|
|
struct Memory{ERR_HQBelief*belief=nullptr;ERR_HQBelief*GetById(int){return belief;}};
|
|
struct Operations{ERR_HQOperationSlot*slot=nullptr;ERR_HQOperationSlot*Find(int){return slot;}};
|
|
struct Adapter{Watch m_Watch;Memory m_Memory;Operations m_Operations;ERR_AttackSpace m_AttackSpace;
|
|
'''+adapter+r'''
|
|
};
|
|
int main(){
|
|
using R=ERR_HQTacticalGroundRules;using Role=ERR_HQOperationRole;
|
|
vector anchor{100,.1,100},from{100,.1,200},threat{100,.1,0},selected;
|
|
float nan=std::numeric_limits<float>::quiet_NaN();
|
|
// At an otherwise identical point the observer prefers a usable low wall,
|
|
// while a withdrawing squad prefers concealment over a clear firing view.
|
|
assert(R::Score(Role::OBSERVE,100,0,0,true,true,true,false)>R::Score(Role::OBSERVE,100,0,0,true,true,false,false));
|
|
assert(R::Score(Role::OBSERVE,100,0,0,true,true,false,false)>R::Score(Role::OBSERVE,100,0,0,true,false,true,false));
|
|
assert(R::Score(Role::WITHDRAW,100,0,0,true,false,true,true)>R::Score(Role::WITHDRAW,100,0,0,true,true,true,true));
|
|
assert(R::Score(Role::FLANK,20,0,0,true,false,true,true)>R::Score(Role::FLANK,20,0,0,true,true,false,false));
|
|
assert(R::Score(Role::RESERVE,100,0,0,false,true,true,true)==R::Score(Role::RESERVE,100,0,0,false,false,false,false));
|
|
for(Role role:{Role::OBSERVE,Role::FIX,Role::FLANK,Role::BLOCK,Role::SEARCH,Role::RESERVE,Role::WITHDRAW}){
|
|
for(int i=0;i<9;i++){vector p=R::Candidate(anchor,threat,role,i);assert(vector::DistanceXZ(anchor,p)<=R::Radius(role));}
|
|
}
|
|
assert(R::Candidate(anchor,threat,Role::SEARCH,8).x==anchor.x); // no survey of neighbouring search patches
|
|
assert(!R::Admissible(Role::SEARCH,anchor,from,{102.01,.1,100},0,1000));
|
|
assert(!R::Admissible(Role::OBSERVE,anchor,from,{100,5,100},0,1000));
|
|
assert(!R::Admissible(Role::OBSERVE,anchor,from,{nan,0,100},0,1000));
|
|
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120.1,.1,100},30000,1000));
|
|
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},9000,1000));
|
|
assert(R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},10000,1000));
|
|
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},nan,1000));
|
|
|
|
ERR_HQTacticalGround survey;AIPathfindingComponent path;SCR_AIGroup group;group.path=&path;
|
|
ERR_AttackSpace space;ERR_HQTask task;
|
|
world.scene=1;
|
|
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
|
|
assert(selected.x<84&&std::abs(selected.z-100)<1); // takes the low wall, not the blind wall or exposed anchor
|
|
assert(world.rays<=27&&world.boxes==9);
|
|
task.OperationRole=Role::RESERVE;
|
|
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
|
|
assert(selected.z<84&&std::abs(selected.x-100)<1); // reserve uses the blind side for protection
|
|
task.OperationRole=Role::WITHDRAW;
|
|
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
|
|
int geometry=world.rays+world.boxes;
|
|
assert(!survey.Select(&group,&task,from,threat,&space,1000,selected));
|
|
assert(world.rays+world.boxes==geometry&&geometry<=108); // shared hard work budget
|
|
assert(!survey.Select(&group,&task,from,threat,&space,1999,selected));
|
|
assert(survey.Select(&group,&task,from,threat,&space,2000,selected));
|
|
|
|
// Flat open ground retains the original destination. It is still a candidate
|
|
// only; no contact/arrival/route claim is returned or manufactured.
|
|
world.scene=0;task.OperationRole=Role::OBSERVE;
|
|
assert(survey.Select(&group,&task,from,threat,&space,3000,selected));
|
|
assert(vector::DistanceXZ(anchor,selected)<.01);
|
|
// Tile loading stays asynchronous and bounded; absence never accepts origin.
|
|
path.nav.loaded=false;int before=world.rays+world.boxes;
|
|
assert(!survey.Select(&group,&task,from,threat,&space,4000,selected));
|
|
assert(path.nav.requests==2&&world.rays+world.boxes==before);
|
|
path.nav.loaded=true;path.allow=false;
|
|
assert(!survey.Select(&group,&task,from,threat,&space,5000,selected));path.allow=true;
|
|
space.allow=false;assert(!survey.Select(&group,&task,from,threat,&space,6000,selected));space.allow=true;
|
|
task.OperationRole=Role::SEARCH;path.delta={1.5,0,0};before=path.projections;
|
|
assert(survey.Select(&group,&task,from,threat,&space,7000,selected));
|
|
assert(path.projections==before+1&&selected.x==101.5); // only the already-selected local footprint
|
|
path.delta={2.1,0,0};assert(!survey.Select(&group,&task,from,threat,&space,8000,selected));
|
|
path.delta={0,2.1,0};assert(!survey.Select(&group,&task,from,threat,&space,9000,selected));
|
|
path.delta={1.5,0,0};world.water=true;
|
|
assert(!survey.Select(&group,&task,from,threat,&space,10000,selected)); // water rechecked after projection
|
|
path.delta={0,0,0};world.water=false;
|
|
world.cliff=true;assert(!survey.Standing(anchor));world.cliff=false;
|
|
task.OperationRole=Role::OBSERVE;world.scene=3;
|
|
assert(!survey.Select(&group,&task,from,threat,&space,11000,selected)); // corrupt trace cannot invent cover
|
|
world.scene=0;
|
|
task.OperationRole=Role::FLANK;task.Destination={120,.1,100};task.SupportUntil=14000;
|
|
assert(!survey.Select(&group,&task,anchor,threat,&space,12000,selected));
|
|
task.SupportUntil=21000;
|
|
assert(survey.Select(&group,&task,anchor,threat,&space,13000,selected));
|
|
assert(vector::DistanceXZ(anchor,selected)<18); // picks an inward point fitting the remaining lease
|
|
assert(task.SupportUntil==21000&&task.Destination.x==120); // no lease extension or task mutation inside survey
|
|
|
|
// Characters, cars, weapons parented to them and moving props cannot create
|
|
// a terrain advantage in a survey. Only static map geometry is admitted.
|
|
survey.m_FilterChecks=0;survey.m_FilterExhausted=false;
|
|
IEntity wall,character,car,weapon,prop;Physics moving;moving.dynamic=true;
|
|
character.character=true;car.vehicle=true;weapon.parent=&character;prop.physics=&moving;
|
|
assert(survey.StaticObstacle(&wall));assert(!survey.StaticObstacle(&character));
|
|
assert(!survey.StaticObstacle(&car)&&!survey.StaticObstacle(&weapon)&&!survey.StaticObstacle(&prop));
|
|
wall.parent=&wall;assert(!survey.StaticObstacle(&wall));wall.parent=nullptr;
|
|
survey.m_FilterChecks=512;assert(survey.StaticObstacle(&wall)&&survey.m_FilterExhausted);
|
|
|
|
// The adapter uses radio-delivered positions, including a newer execution
|
|
// report. An older roster position would incorrectly reject this short bound.
|
|
Adapter hq;ERR_HQUnit unit;unit.Group=&group;ERR_HQWatchEntry own;ERR_HQBelief belief;
|
|
hq.m_Watch.entry=&own;hq.m_Memory.belief=&belief;ERR_HQTask previous;unit.Task=&previous;
|
|
ERR_HQOperationSlot slot;ERR_HQExecutionReport report;slot.Report=&report;hq.m_Operations.slot=&slot;
|
|
task.SupportUntil=26000;assert(hq.CoordinatedGround(&unit,&task,15000));
|
|
assert(vector::DistanceXZ(report.Position,task.Destination)<=20);
|
|
vector saved=task.Destination;unit.HomeGuard=true;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.HomeGuard=false;
|
|
unit.ReservedForSupport=true;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.ReservedForSupport=false;
|
|
unit.LogisticsReservation=1;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.LogisticsReservation=0;
|
|
belief.BeliefRevision=3;assert(!hq.CoordinatedGround(&unit,&task,16000));belief.BeliefRevision=2;
|
|
own.Closed=true;assert(!hq.CoordinatedGround(&unit,&task,16000));own.Closed=false;
|
|
own.Fit=0;assert(!hq.CoordinatedGround(&unit,&task,16000));own.Fit=4;
|
|
own.ObservedAt=0;assert(!hq.CoordinatedGround(&unit,&task,120001));
|
|
assert(vector::DistanceXZ(saved,task.Destination)==0); // rejected surveys never replace an existing destination
|
|
}
|
|
''')
|
|
|
|
source = P.read_text()
|
|
for forbidden in ('GetOrigin(', 'GetPlayerManager(', 'GetControlledEntity(', 'Gather(',
|
|
'CreateEntity(', 'Spawn(', 'SetOrigin(', 'AddWaypoint(', '.Target'):
|
|
assert forbidden not in source
|
|
assert 'CoordinatedGround(' not in body(A, 'protected void ShadowCoordinated(')
|
|
print('PASS: actual terrain survey chooses usable cover/concealment, preserves search and bound envelopes, honours finite support, delivered evidence, ownership, async tiles and shared geometry budget')
|