Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
247 lines
23 KiB
Python
247 lines
23 KiB
Python
"""Execute production formation geometry and tactical lifecycle decisions."""
|
|
import re
|
|
from policy_harness import ROOT,body,run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c'
|
|
geometry='\n'.join(body(p,x) for x in ['static float ManeuverBudget(', 'static bool ExtendApproach(', 'static bool AdvanceWithoutVisualCover(', 'static bool DismountRange(', 'static bool Fresh(', 'static vector Offset(', 'static bool Stage(', 'static bool Foot('])
|
|
geometry=geometry.replace('array<vector> occupied','Array<Vec> occupied').replace('foreach (vector other : occupied)','for (Vec other : occupied)')
|
|
geometry=re.sub(r'\bvector (?=\w)','Vec ',geometry).replace('vector.','Geometry::').replace('Vector(','Vec(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')
|
|
stubs=r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
using string=std::string;
|
|
struct Vec {float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}float& operator[](int n){return n==0?x:n==1?y:z;}Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}Vec operator*(float n){return {x*n,y*n,z*n};}float Length(){return std::hypot(x,z);}void Normalize(){float length=Length();x/=length;z/=length;}string ToString(){return "position";}};
|
|
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
|
|
template<class T>using Array=std::vector<T>;
|
|
'''
|
|
run(stubs+geometry+r'''
|
|
int main(){
|
|
assert(DismountRange(140)&&DismountRange(450)&&!DismountRange(139)&&!DismountRange(451));
|
|
assert(ExtendApproach(1000,91000,91000));
|
|
assert(!ExtendApproach(1000,75999,91000));
|
|
assert(!ExtendApproach(1000,301000,301000));
|
|
assert(!ExtendApproach(1000,92000,91000));
|
|
assert(!ExtendApproach(92000,90000,91000));
|
|
assert(!AdvanceWithoutVisualCover(true,47000,16999));
|
|
assert(AdvanceWithoutVisualCover(true,47000,17000));
|
|
assert(!AdvanceWithoutVisualCover(false,47000,47000));
|
|
assert(ManeuverBudget(0)==90000&&ManeuverBudget(300)==180000&&ManeuverBudget(10000)==240000);
|
|
assert(Fresh(0,60000)&&!Fresh(0,60001)&&!Fresh(100,99)&&!Fresh(-1,0));
|
|
Vec c{300,0,500};
|
|
for(int a=0;a<360;a+=15){Vec f{std::sin(a*3.14159265f/180),0,std::cos(a*3.14159265f/180)};
|
|
Vec cover=Offset(c,f,0,0),left=Offset(c,f,1,0),right=Offset(c,f,2,0);
|
|
assert(Stage(cover,c,f,{}));assert(Stage(left,c,f,{cover}));assert(Stage(right,c,f,{cover,left}));
|
|
assert(!Stage(left,c,f,{left}));assert(!Stage(c,c,f,{}));assert(!Stage(c+f*300,c,f,{}));
|
|
for(int role=1;role<3;role++){assert(Foot(Offset(c,f,role,1),c,f,role));assert(Foot(Offset(c,f,role,2),c,f,role));assert(!Foot(Offset(c,f,3-role,2),c,f,role));}
|
|
}
|
|
}
|
|
''')
|
|
tick='\n'.join(body(p,x) for x in ['protected bool RecoverApproach(', 'protected bool UpdateCover(', 'bool Tick(ERR_HQ hq, float now)']).replace('protected ', '').replace('bool assigned;', 'bool assigned=false;').replace('m_ApproachRetries.ToString()', 'std::to_string(m_ApproachRetries)').replace('assigned.ToString()', 'std::to_string(assigned)')
|
|
for typ in ['ERR_HQ','IEntity','AIWaypoint']:
|
|
tick=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',tick)
|
|
for n in ['Radio','hq','Unit','Attack','search','coordination','m_GunTeam']:
|
|
tick=tick.replace(n+'.',n+'->')
|
|
tick=tick.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('vector reserved', 'Vec reserved').replace('AttackSpace().Claim', 'AttackSpace()->Claim')
|
|
tick=tick.replace('foreach (IEntity* crew : Unit->Members)','for (IEntity* crew : Unit->Members)')
|
|
tick=tick.replace('Unit->Entity.GetOrigin()','Unit->Entity->GetOrigin()')
|
|
tick=tick.replace('ERR_HunterAttackRules.', '').replace('Math.Min', 'std::min').replace('now + 30000, m_ApproachStarted + 300000','now + 30000.f, m_ApproachStarted + 300000.f')
|
|
tick=tick.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector))','SpawnSearch(Sector)')
|
|
tick=tick.replace('ERR_ConvoyMountedCrew.Hold','Hold').replace('ERR_HunterCombat.Active','Active').replace('vector.DistanceXZ','Geometry::DistanceXZ').replace('null','nullptr')
|
|
run(stubs+body(p,'static bool DismountRange(')+body(p,'static bool ExtendApproach(')+body(p,'static bool AdvanceWithoutVisualCover(')+body(p,'static float ManeuverBudget(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')+r'''
|
|
struct IEntity{Vec pos;Vec GetOrigin(){return pos;}};
|
|
struct AIWaypoint{void SetCompletionRadius(float){}};
|
|
struct Space{void Claim(void*,Vec,float){}};struct ERR_HQ{void CoordinatedHunterDelivery(string,int,bool,float){}Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}int Journal(){return 0;}};
|
|
struct RadioType{int MessageId=1;int result=0;int Advance(float,int){return result;}};
|
|
struct GroupType{bool active=false;};bool Active(GroupType*g){return g->active;}
|
|
int holds=0;void Hold(IEntity*,IEntity*){holds++;}
|
|
struct UnitType{string Source="hunter";struct GroupType*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;void*Downlink=nullptr;void CancelHQDownlink(){Downlink=nullptr;}bool CombatHeld=false;float CombatQuietUntil=0;int clears=0;void ClearOrder(){clears++;}};
|
|
struct AttackType{Vec ReportPosition;float Started=0;bool alive=true,ready=false;bool CoverAlive(){return alive;}bool CoverReady(){return ready;}};
|
|
struct GunTeam{int ticks=0;void Tick(float){ticks++;}void Pause(){}};
|
|
struct Tactics{
|
|
int m_ApproachRetries=0,m_CoverRetries=0,alternates=0;float m_BlindSince=-1;bool alternateWorks=true;
|
|
bool AlternateStage(float now){alternates++;if(!alternateWorks)return false;State=1;m_BlindSince=-1;m_ApproachProgress=now;Stage={500,0,-250};return true;}
|
|
GunTeam*m_GunTeam=nullptr;bool align=false,facing=false;bool TryAlign(){return align;}bool FacingSector(){return facing;}
|
|
UnitType*Unit=nullptr;AttackType*Attack=nullptr;RadioType*Radio=nullptr;int Role=1,State=0,Retries=0;float m_TravelDistance=0,Deadline=0,NextRetry=0,m_ApproachStarted=0,m_ApproachProgress=0;Vec m_ApproachPosition;bool m_UnsupportedSearch=false,Delivered=false,Stopped=false,living=true,aboard=true,moveWorks=true,boardWorks=true,searchWorks=true;int clears=0,moves=0,boards=0;Vec Stage,Approach,Sector,position;AIWaypoint search;
|
|
bool visual=false;bool VisualCover(){return visual;}bool Living(){return living;}void Stop(){Stopped=true;Radio=nullptr;Clear();if(Delivered)Unit->CombatHeld=false;}bool Move(Vec,float=20){moves++;return moveWorks;}void Clear(){clears++;}bool Board(bool){boards++;return boardWorks;}bool Aboard(bool required){return aboard==required;}Vec Position(){return position;}void VehicleUse(bool){}void Note(string){}void ObserveState(float){}void Add(AIWaypoint*){}AIWaypoint*SpawnSearch(Vec){return searchWorks?&search:nullptr;}
|
|
'''+tick+r'''
|
|
};
|
|
struct Fixture{IEntity car,crew;GroupType group;UnitType unit;AttackType attack;RadioType radio;Tactics t;ERR_HQ hq;
|
|
Fixture(){car.pos={0,0,-250};unit.Entity=&car;unit.Group=&group;unit.Members={&crew};t.Unit=&unit;t.Attack=&attack;t.Radio=&radio;t.Stage=car.pos;t.Approach={-170,0,-85};t.Sector={-105,0,35};t.position=car.pos;}
|
|
};
|
|
int main(){
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.Tick(&f.hq,1000);assert(f.t.State==3);
|
|
f.t.Tick(&f.hq,20999);assert(f.t.alternates==0);f.t.Tick(&f.hq,21000);assert(f.t.alternates==1&&f.t.State==1);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.visual=true;f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,120000);assert(f.t.State==3&&f.t.alternates==0);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.Tick(&f.hq,1000);
|
|
f.t.Tick(&f.hq,21000);f.t.Tick(&f.hq,22000);assert(f.t.alternates==1);
|
|
f.t.Tick(&f.hq,41000);f.t.Tick(&f.hq,61000);assert(f.t.alternates==3&&!f.t.Stopped);
|
|
assert(!f.t.Tick(&f.hq,81000)&&f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
|
|
f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,30999);assert(f.t.moves==1);
|
|
f.t.Tick(&f.hq,31000);assert(f.t.moves==2&&f.t.m_ApproachRetries==1);
|
|
f.t.Tick(&f.hq,61000);assert(f.t.alternates==1&&f.t.m_ApproachRetries==2);
|
|
assert(!f.t.Tick(&f.hq,91000)&&f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
|
|
f.t.Tick(&f.hq,1000);for(int i=1;i<=5;i++){f.car.pos.z+=20;f.t.Tick(&f.hq,1000+i*20000);}assert(f.t.m_ApproachRetries==0&&!f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.Stage={0,0,-500};f.t.visual=true;
|
|
assert(f.t.Tick(&f.hq,1000)&&f.t.State==3); // sees fight at 250m: stop, don't back away to stage
|
|
holds=0;
|
|
}
|
|
{Fixture f;assert(!f.t.Tick(&f.hq,1000));assert(!f.t.Delivered&&!f.unit.CombatHeld&&!f.unit.clears&&f.t.moves==0);f.radio.result=-1;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped&&f.unit.clears==0);}
|
|
{Fixture f;f.radio.result=1;f.t.moveWorks=false;assert(!f.t.Tick(&f.hq,1000)&&!f.t.Delivered&&f.unit.clears==0);}
|
|
{Fixture f;f.radio.result=1;assert(f.t.Tick(&f.hq,1000));assert(f.t.State==2&&f.t.boards==1&&f.unit.clears==1&&f.unit.CombatHeld);f.t.aboard=false;assert(f.t.Tick(&f.hq,2000)&&f.t.State==3);int moves=f.t.moves;f.t.Tick(&f.hq,5000);assert(f.t.moves==moves);f.attack.ready=true;f.t.Tick(&f.hq,6000);assert(f.t.State==4&&f.t.moves==moves+1);f.t.Tick(&f.hq,7000);assert(f.t.moves==moves+1);f.t.position=f.t.Approach;f.t.Tick(&f.hq,8000);assert(f.t.State==5);f.t.Tick(&f.hq,68000);assert(f.t.State==6);f.t.aboard=true;assert(!f.t.Tick(&f.hq,69000)&&f.t.Stopped&&!f.unit.CombatHeld);}
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.t.Tick(&f.hq,16999);assert(f.t.State==3);f.t.Tick(&f.hq,17000);assert(f.t.State==4&&f.t.moves==2);}
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.moves==2&&f.t.m_UnsupportedSearch);}
|
|
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==1);f.car.pos={0,0,-950};f.t.Tick(&f.hq,91000);assert(!f.t.Stopped&&f.t.Deadline==121000);f.t.Tick(&f.hq,121000);assert(f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);f.car.pos={0,0,-950};f.t.Tick(&f.hq,301000);assert(f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.attack.ready=true;f.t.Tick(&f.hq,2000);assert(f.t.State==4);f.attack.alive=false;f.t.Tick(&f.hq,3000);assert(f.t.State==3);}
|
|
{Fixture f;f.t.Role=0;f.radio.result=1;f.t.Tick(&f.hq,1000);assert(f.t.State==3&&f.t.boards==0&&holds==1);assert(!f.t.Tick(&f.hq,300000)&&f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==2); } // Road passes within infantry range before exact stage.
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.m_UnsupportedSearch);f.t.Tick(&f.hq,48000);assert(f.t.State==4); } // No repeated cover-loss reset.
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.moveWorks=false;f.t.Tick(&f.hq,47000);assert(f.t.State==6); } // Failed foot order still regroups.
|
|
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);assert(f.t.State==8&&f.t.boards==0);f.t.Tick(&f.hq,20999);assert(f.t.State==8);f.t.Tick(&f.hq,21000);assert(f.t.State==2&&f.t.boards==1);} // Alignment never blocks beyond 20s.
|
|
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.t.facing=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Stop aligning when facing.
|
|
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.group.active=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Combat preempts alignment.
|
|
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.living=false;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped);}
|
|
}
|
|
''')
|
|
print('PASS: separated rear vehicle approaches, opposite infantry sectors, radio-gated ownership, failed-order preservation, visual cover or bounded supported reconnaissance, bounded dismount/search/regroup and loss cleanup')
|
|
|
|
# Execute the ownership methods too: an interrupted flank must restore only its
|
|
# own vehicle and dispose only its own waypoints, including allocation failures.
|
|
own='\n'.join(body(p,x) for x in ['protected void Clear(', 'protected void Add(', 'protected void VehicleUse(', 'protected bool Board(', 'void Stop()'])
|
|
own=own.replace('protected ','').replace('foreach (','for (')
|
|
for typ in ['AIWaypoint','SCR_BoardingWaypoint','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','IEntity','ERR_HQ']:
|
|
own=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',own)
|
|
for name in ['Unit','utility','waypoint','m_Mode','coordination','pendingHQ','Radio','m_GunTeam']:
|
|
own=own.replace(name+'.',name+'->')
|
|
own=own.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('AttackSpace().Release', 'AttackSpace()->Release')
|
|
own=own.replace('Unit->Group.','Unit->Group->').replace('Unit->Entity.FindComponent(SCR_AIVehicleUsageComponent)','Unit->Entity->usage')
|
|
own=own.replace('SCR_AIVehicleUsageComponent.Cast','UsageCast').replace('SCR_BoardingWaypoint.Cast','BoardCast')
|
|
for cls in ['ERR_ConvoyRules','ERR_SupplyConvoyTactics','ERR_ConvoyMountedCrew','SCR_EntityHelper']:
|
|
own=own.replace(cls+'.',cls+'::')
|
|
own=own.replace('Unit->Recovery.SuspendBoarding()', 'Unit->Recovery->SuspendBoarding()')
|
|
own=own.replace('SCR_AIActionBase.PRIORITY_LEVEL_PLAYER','1000').replace('null','nullptr')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
|
|
struct AIWaypoint{bool boarding=true;int deleted=0;};
|
|
struct SCR_BoardingWaypoint:AIWaypoint{int priority=0;bool driver=false,turret=false,passenger=false;void SetPriorityLevel(int n){priority=n;}void SetAllowance(bool a,bool b,bool c){driver=a;turret=b;passenger=c;}};
|
|
SCR_BoardingWaypoint*BoardCast(AIWaypoint*w){return w&&w->boarding?static_cast<SCR_BoardingWaypoint*>(w):nullptr;}
|
|
struct SCR_AIVehicleUsageComponent{};auto UsageCast(SCR_AIVehicleUsageComponent*u){return u;}
|
|
struct SCR_AIGroupUtilityComponent{int adds=0,removes=0;void AddUsableVehicle(SCR_AIVehicleUsageComponent*){adds++;}void RemoveUsableVehicle(SCR_AIVehicleUsageComponent*){removes++;}};
|
|
struct Group{SCR_AIGroupUtilityComponent utility;Array<AIWaypoint*>waypoints;auto GetGroupUtilityComponent(){return &utility;}void RemoveWaypoint(AIWaypoint*w){auto&i=waypoints;i.erase(std::remove(i.begin(),i.end(),w),i.end());}void AddWaypoint(AIWaypoint*w){waypoints.Insert(w);}};
|
|
struct IEntity{SCR_AIVehicleUsageComponent usageValue;SCR_AIVehicleUsageComponent*usage=&usageValue;};
|
|
struct Recovery{void SuspendBoarding(){}};
|
|
struct UnitType{const char*Source="hunter";struct Recovery*Recovery=nullptr;struct Group*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;bool CombatHeld=true;float NextOrder=99;};
|
|
struct Mode{bool active=true;bool Active(){return active;}};
|
|
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}}
|
|
namespace ERR_ConvoyMountedCrew{int released=0;void Release(IEntity*){released++;}}
|
|
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint*w){w->deleted++;}}
|
|
namespace ERR_SupplyConvoyTactics{AIWaypoint*next=nullptr;bool requested=false;AIWaypoint*Board(IEntity*,bool enter){requested=enter;return next;}}
|
|
struct World{float GetWorldTime(){return 0;}};struct Game{World GetWorld(){return {};}};Game GetGame(){return {};}
|
|
struct RadioWire{int MessageId=1;};
|
|
struct Space{void Release(void*){}};struct ERR_HQ{void CoordinatedHunterDelivery(const char*,int,bool,float){}Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}};
|
|
struct ERR_HunterGunTeam{inline static bool usable=false;int stopped=0;ERR_HunterGunTeam(UnitType*){}bool Start(){return usable;}void Stop(){stopped++;}};
|
|
struct Tactics{ERR_HunterGunTeam*m_GunTeam=nullptr;void Note(const char*){}UnitType*Unit=nullptr;Mode*m_Mode=nullptr;int Role=0;bool m_VehicleDetached=false,Stopped=false,Delivered=false,living=true;RadioWire*Radio=nullptr;Array<AIWaypoint*>Orders;bool Living(){return living&&!Stopped;}
|
|
'''+own+r'''
|
|
};
|
|
int main(){
|
|
Group g;IEntity car,man;UnitType u;u.Group=&g;u.Entity=&car;u.Members.Insert(&man);Mode mode;Tactics t;t.Unit=&u;t.m_Mode=&mode;
|
|
AIWaypoint unrelated,old;g.AddWaypoint(&unrelated);t.Add(&old);
|
|
assert(!t.Board(false)&&t.Orders.size()==1&&old.deleted==0&&g.waypoints.size()==2);
|
|
AIWaypoint wrong;wrong.boarding=false;ERR_SupplyConvoyTactics::next=&wrong;
|
|
assert(!t.Board(false)&&wrong.deleted==1&&old.deleted==0);
|
|
SCR_BoardingWaypoint exit;ERR_SupplyConvoyTactics::next=&exit;
|
|
assert(t.Board(false)&&!ERR_SupplyConvoyTactics::requested&&old.deleted==1);
|
|
assert(exit.priority==1000&&exit.driver&&exit.turret&&exit.passenger&&unrelated.deleted==0);
|
|
t.VehicleUse(false);t.VehicleUse(false);assert(g.utility.removes==1&&t.m_VehicleDetached);
|
|
t.Delivered=true;t.Stop();assert(t.Stopped&&g.utility.adds==1&&exit.deleted==1&&unrelated.deleted==0&&g.waypoints.size()==1&&!u.CombatHeld&&u.NextOrder==0);
|
|
t.Stop();assert(g.utility.adds==1&&exit.deleted==1&&ERR_ConvoyMountedCrew::released==1);
|
|
ERR_HunterGunTeam::usable=true;Tactics split;split.Unit=&u;split.m_Mode=&mode;assert(split.Board(false)&&split.m_GunTeam);auto held=split.m_GunTeam;
|
|
assert(split.Board(false)&&split.m_GunTeam==held);split.Stop();assert(held->stopped==1&&!split.m_GunTeam);split.Stop();assert(held->stopped==1);ERR_HunterGunTeam::usable=false;
|
|
int added=g.utility.adds;
|
|
for(int scenario=0;scenario<3;scenario++){
|
|
Tactics x;x.Unit=&u;x.m_Mode=&mode;x.m_VehicleDetached=true;
|
|
ERR_ConvoyRules::captured=scenario==0;mode.active=scenario!=1;x.living=scenario!=2;
|
|
x.Stop();assert(g.utility.adds==added); // Never reclaim a captured/dead/teardown vehicle.
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: production dismount/boarding allocation, all crew allowances, waypoint ownership, one-time vehicle detach/restore and capture/teardown guards')
|
|
|
|
armed=(body(p,'static bool CoverCrew(').replace('ERR_HunterVehicle unit','UnitType* unit').replace('ERR_GameMode mode','int mode')+'\n'+body(p,'bool ArmedCover()')).replace('foreach (','for (').replace('BaseCompartmentSlot seat','BaseCompartmentSlot* seat').replace('bool gunner;','bool gunner=false;')
|
|
for name in ['Unit','unit','seat','m_GunTeam']:armed=armed.replace(name+'.',name+'->')
|
|
armed=armed.replace('ECompartmentType.TURRET','1').replace('ERR_Actors.CanFight','CanFight')
|
|
armed=armed.replace('ERR_HQMountedAmmoSnapshot ammo','ERR_HQMountedAmmoSnapshot* ammo').replace('ERR_HQMountedAmmoSampler.Inspect','Inspect').replace('ammo.','ammo->')
|
|
cover=body(p,'bool CoverAlive()').replace('foreach (','for (').replace('ERR_HunterTactics team','ERR_HunterTactics* team').replace('team.','team->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
struct Man{bool fit=true;};bool CanFight(Man*m){return m&&m->fit;}
|
|
struct BaseCompartmentSlot{Man*occupant=nullptr;int type=1;int GetType(){return type;}Man*GetOccupant(){return occupant;}};
|
|
struct Members{Man*original;bool Contains(Man*m){return m==original;}};
|
|
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Rounds=99,Chambers=1;}ammo;auto Inspect(int){return &ammo;}
|
|
struct UnitType{int Entity=0;struct Members Members;std::vector<BaseCompartmentSlot*>Seats;bool driver=true;bool CrewValid(int){return driver;}};
|
|
struct GunTeam{bool covered=true;bool Cover(){return covered;}};
|
|
struct ERR_HunterTactics{GunTeam*m_GunTeam=nullptr;UnitType*Unit;int Role=0,State=3,m_Mode=0;bool Delivered=true,living=true;bool Living(){return living;}
|
|
'''+armed+r'''
|
|
};struct Attack{std::vector<ERR_HunterTactics*>Teams;
|
|
'''+cover+r'''
|
|
};int main(){Man gunner,outsider;BaseCompartmentSlot slot;slot.occupant=&gunner;UnitType u;u.Members.original=&gunner;u.Seats={&slot};ERR_HunterTactics t;t.Unit=&u;Attack a;a.Teams={&t};assert(a.CoverAlive());gunner.fit=false;assert(!a.CoverAlive());gunner.fit=true;slot.occupant=&outsider;assert(!a.CoverAlive());slot.occupant=&gunner;u.driver=false;assert(!a.CoverAlive());u.driver=true;t.Delivered=false;assert(!a.CoverAlive());t.Delivered=true;t.living=false;assert(!a.CoverAlive());t.living=true;ammo.Rounds=0;ammo.Chambers=0;assert(!a.CoverAlive());ammo.Chambers=1;assert(a.CoverAlive());ammo.Complete=false;assert(!a.CoverAlive());GunTeam split;t.m_GunTeam=&split;t.Role=1;t.State=4;assert(a.CoverAlive());split.covered=false;assert(!a.CoverAlive());}
|
|
''')
|
|
print('PASS: covering support requires the original conscious turret gunner, valid mounted driver, live crew and delivered assignment')
|
|
|
|
# Execute production per-vehicle approach geometry: opposite arrivals stay on
|
|
# their own sides instead of sharing the cover vehicle's staging axis.
|
|
approach=body(p,'static vector Approach(').replace('vector ', 'Vec ')
|
|
run(stubs+approach+r'''
|
|
int main(){
|
|
Vec contact{100,0,100},fallback{0,0,1};
|
|
for(Vec origin : {Vec{100,0,-900},Vec{100,0,1100},Vec{-900,0,100},Vec{1100,0,100}}){
|
|
Vec forward=Approach(origin,contact,fallback);
|
|
Vec stage=contact-forward*260;
|
|
assert(Geometry::Dot(stage-contact,origin-contact)>0);
|
|
assert(Geometry::DistanceXZ(origin,stage)<Geometry::DistanceXZ(origin,contact));
|
|
assert(std::abs(forward.Length()-1)<0.0001);
|
|
}
|
|
assert(Approach(contact,contact,fallback).z==1);
|
|
}
|
|
''')
|
|
|
|
# Execute alternate-stage commit/rollback and reservations, not a duplicate planner.
|
|
alternate=body(p,'protected bool AlternateStage(').replace('protected ','').replace('array<vector>', 'Array<Vec>').replace('foreach (', 'for (').replace('vector ', 'Vec ').replace('ERR_HunterTactics other', 'Tactics* other')
|
|
for name in ['Unit','Attack','other']:
|
|
alternate=alternate.replace(name+'.',name+'->')
|
|
alternate=alternate.replace('Entity.GetOrigin()', 'Entity->GetOrigin()').replace('Math.Min', 'std::min').replace('ERR_HunterAttackRules.ManeuverBudget', 'ManeuverBudget').replace('m_ApproachStarted + 300000', 'm_ApproachStarted + 300000.f')
|
|
run(stubs.replace('template<class T>using Array=std::vector<T>;', 'template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}};')+r'''
|
|
struct Entity{Vec point{0,0,-600};Vec GetOrigin(){return point;}};
|
|
struct UnitType{struct Entity entity;struct Entity*Entity=&entity;};
|
|
struct Tactics;struct AttackType{Array<Tactics*>Teams;};
|
|
float ManeuverBudget(float){return 240000;}
|
|
struct Tactics{
|
|
UnitType unit;UnitType*Unit=&unit;AttackType attack;AttackType*Attack=&attack;
|
|
Vec Stage{0,0,-210},Approach{155,0,-240},Sector{105,0,35},m_ApproachPosition;
|
|
Array<Vec>m_RejectedStages,reservations;int State=3;float m_BlindSince=1000,m_ApproachProgress=0,m_ApproachStarted=1000,m_TravelDistance=1000,Deadline=100;
|
|
bool live=true,plan=true,move=true;bool Living(){return live;}
|
|
bool Plan(Array<Vec> occupied){reservations=occupied;Stage={200,0,-210};Approach={200,0,-85};Sector={105,0,35};return plan;}
|
|
bool Move(Vec,float){return move;}void Note(string){}
|
|
'''+alternate+r'''
|
|
};
|
|
int main(){
|
|
for(int failure=0;failure<2;failure++){
|
|
Tactics t;t.plan=failure!=0;t.move=failure!=1;
|
|
assert(!t.AlternateStage(21000));assert(t.Stage.x==0&&t.Approach.x==155&&t.State==3&&t.m_RejectedStages.empty());
|
|
}
|
|
Tactics t,other,dead;dead.live=false;other.Stage={-200,0,-210};t.attack.Teams={&t,&other,&dead};t.m_RejectedStages.Insert({-100,0,-300});
|
|
assert(t.AlternateStage(21000));assert(t.reservations.size()==4&&t.reservations.back().x==-200);
|
|
assert(t.State==1&&t.Stage.x==200&&t.m_BlindSince==-1&&t.m_ApproachProgress==21000&&t.m_RejectedStages.size()==2);
|
|
assert(t.Deadline==261000);assert(t.AlternateStage(200000)&&t.Deadline==301000); // retries cannot extend the total approach budget
|
|
}
|
|
''')
|
|
print('PASS: alternate positions exclude failed and occupied stages, roll back on failed planning/allocation, and preserve the total time budget')
|