"""Actual HQ reservation collection and search decisions across interrupted retasks. Native radio/waypoint execution is not simulated; LostOrder's production ownership and the real coverage/bearing policies are executed against detached task doubles. """ from pathlib import Path import os, re from policy_harness import ROOT, body, run P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')) C=Path(os.environ.get('ERR_HQ_COVERAGE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c')) R=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c' def loop(text, signature): start=text.index(signature); opening=text.index('{',start); end=opening+1; depth=1 while depth: depth+=(text[end]=='{')-(text[end]=='}');end+=1 return text[start:end] def cpp(s): s=s.replace('ref ', '') s=s.replace('protected ','').replace('null','nullptr').replace('.IsEmpty()', '.empty()') for name in ('Math','ERR_HQPursuitRules','ERR_HQCoverageRules','vector'): s=s.replace(name+'.',name+'::') s=s.replace('out vector ', 'vector& ').replace('out int ', 'int& ') s=s.replace('array reserved','Array* reserved') for typ in ('ERR_HQUnit','ERR_HQTask','ERR_HQHuntWindow'): s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s) s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : m_Units\)',r'for (\1 \2 : m_Units)',s) s=s.replace('foreach (vector position : reserved)','for (vector position : *reserved)') for name in ('unit','other','task','planned','hunt','reserved'): s=s.replace(name+'.',name+'->') s=s.replace('PendingTask.', 'PendingTask->') s=s.replace('int total;','int total=0;').replace('bool found;','bool found=false;') return s.replace('other->Task.', 'other->Task->') assign=body(P,'protected void Assign(') collect=loop(assign[assign.index(' array reserved = {};'):], 'foreach (ERR_HQUnit unit : m_Units)') retask=body(P,'protected void Retask(') retask_collect=loop(retask,'foreach (ERR_HQUnit other : m_Units)') helpers='' if 'protected void ReserveTask(' in P.read_text(): helpers='\n'.join(body(P,s) for s in ('protected void ReserveTask(', 'protected void ReserveUnit(')) # All three planning paths must retain the same ownership rule. assert 'ReserveUnit(existing, reserved, first, second)' in body(P,'protected void Investigate(') methods='\n'.join(body(P,s) for s in ('protected int Assigned(', 'protected void FinishWatch(', 'protected void LostOrder(')) if 'protected float HuntCommitment(' in P.read_text(): methods=body(P,'protected float HuntCommitment(')+'\n'+methods if 'protected void ReconsiderHunt(' in P.read_text(): methods=body(P,'protected void ReconsiderHunt(')+'\n'+methods if 'protected void RememberCompletedHunt(' in P.read_text(): methods=body(P,'protected void RememberCompletedHunt(')+'\n'+methods unit_methods='\n'.join(body(P,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask(')) rules='\n'.join(body(R,s) for s in ('static bool Finite(', 'static bool PositionValid(', 'static bool BearingAllowed(')) coverage_rules='\n'.join(body(C,s) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(', 'static bool Point(')) coverage='\n'.join(body(C,s) for s in ('protected bool Reserved(', 'bool Select(')) run(r''' #include #include #include #include #include struct vector {float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;} 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 Clamp(float x,float a,float b){return std::clamp(x,a,b);} static float Floor(float x){return std::floor(x);} static float AbsFloat(float x){return std::abs(x);} static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}}; templatestruct Array:std::vector{int Count(){return this->size();}void Insert(T t){this->push_back(t);}}; struct ERR_HQPursuitRules { '''+cpp(rules)+r'''}; struct ERR_HQCoverageRules { '''+cpp(coverage_rules)+r'''}; struct Coverage {bool Recent(vector,float){return false;} '''+cpp(coverage)+r'''}; struct ERR_HQTask {int OperationId=0,ExecutionId=0;float ExpiresAt=120000;vector Destination;int ApproachBearing=0,BeliefId=1;bool ReleaseOnly=false; std::string SecuritySite,WatchSourceId;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}}; struct ERR_HQReport{};struct ERR_HQRoute{}; struct ERR_HQUnit {int Id=1;ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr; ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQRoute* Downlink=nullptr; void* PendingConvoyOrder=nullptr;bool PendingCheckin=false;void* Waypoint=nullptr; '''+cpp(unit_methods)+r'''}; struct ERR_HQHuntWindow {float CompletedUntil=0;float NextHunt=900000;}; struct Watch {void FinishInvestigation(std::string,int,int,float){}}; struct HQ {struct Operations{void Release(int,int,float){}}m_Operations;void ReserveAttackSpace(ERR_HQUnit*,float){}std::vectorm_Units;ERR_HQHuntWindow window;Watch m_Watch; ERR_HQHuntWindow* Hunt(int){return &window;} '''+cpp(helpers+methods)+r''' void Collect(Array* reserved,int& first,int& second){float now=1000; '''+cpp(collect)+r'''} void CollectForRetask(ERR_HQUnit* unit,Array* reserved,int& first,int& second){ '''+cpp(retask_collect)+r'''} }; int main(){ ERR_HQTask active,pending;active.Destination={250,0,250};pending.Destination={1250,0,250}; active.ApproachBearing=0;pending.ApproachBearing=1; ERR_HQReport delivered,replacement;ERR_HQRoute radio;int waypoint=1; ERR_HQUnit unit;unit.Task=&active;unit.Assignment=&delivered;unit.Waypoint=&waypoint; unit.PendingTask=&pending;unit.PendingOrder=&replacement;unit.Downlink=&radio; HQ hq;hq.m_Units={&unit};Coverage coverage;Array reserved;int first=-1,second=-1;vector selected; hq.Collect(&reserved,first,second); // The old search continues physically while replacement is only queued. // Previously only pending's cell was reserved, so this wrongly returned true. assert(!coverage.Select(active.Destination,25,active.Destination,1000,selected,&reserved)); assert(!coverage.Select(pending.Destination,25,pending.Destination,1000,selected,&reserved)); assert(reserved.Count()==2&&hq.Assigned()==1); assert(ERR_HQPursuitRules::BearingAllowed(0,first,second)); assert(ERR_HQPursuitRules::BearingAllowed(1,first,second)); assert(!ERR_HQPursuitRules::BearingAllowed(2,first,second)); // A broken radio transaction frees only pending's cell/sector, not the old task. hq.LostOrder(&unit,1000);assert(unit.Task==&active&&unit.Waypoint==&waypoint&&unit.Assignment==&delivered); assert(!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink&&hq.Assigned()==1); reserved.clear();first=second=-1;hq.Collect(&reserved,first,second); assert(reserved.Count()==1&&!coverage.Select(active.Destination,25,active.Destination,1001,selected,&reserved)); assert(coverage.Select(pending.Destination,25,pending.Destination,1001,selected,&reserved)); // After committed replacement, only the new physical task remains reserved. unit.Task=&pending;unit.Assignment=&replacement;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second); assert(reserved.Count()==1&&coverage.Select(active.Destination,25,active.Destination,1002,selected,&reserved)); assert(!coverage.Select(pending.Destination,25,pending.Destination,1002,selected,&reserved)); // Retasking excludes its own superseded destination but retains both peer states. unit.Task=&active;unit.PendingTask=&pending;unit.PendingOrder=&replacement;unit.Downlink=&radio; ERR_HQUnit candidate;hq.m_Units.push_back(&candidate);reserved.clear();first=second=-1; hq.CollectForRetask(&candidate,&reserved,first,second);assert(reserved.Count()==2); reserved.clear();first=second=-1;hq.CollectForRetask(&unit,&reserved,first,second);assert(reserved.empty()); // Retask cannot rotate into a third sector while its old approach still runs: // peer=0, own delivered=1, proposed replacement=2. Own cell is replaceable. ERR_HQTask ownTask;ownTask.Destination={2250,0,250};ownTask.ApproachBearing=1; unit.PendingTask=nullptr;unit.PendingOrder=nullptr;unit.Downlink=nullptr; candidate.Task=&ownTask;candidate.Assignment=&delivered; reserved.clear();first=second=-1;hq.CollectForRetask(&candidate,&reserved,first,second); assert(reserved.Count()==1&&!coverage.Select(active.Destination,25,active.Destination,1003,selected,&reserved)); assert(coverage.Select(ownTask.Destination,25,ownTask.Destination,1003,selected,&reserved)); assert(!ERR_HQPursuitRules::BearingAllowed(2,first,second)); assert(ERR_HQPursuitRules::BearingAllowed(0,first,second)&&ERR_HQPursuitRules::BearingAllowed(1,first,second)); assert(hq.Assigned()==2); candidate.Task=nullptr;candidate.Assignment=nullptr;unit.PendingOrder=&replacement; // A recall only removes the active order upon delivery; it reserves no new point. ERR_HQTask recall;recall.ReleaseOnly=true;recall.ApproachBearing=2;unit.PendingTask=&recall; reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1&&first==0&&second==-1); // Duplicate pointer ownership must not spend two entries for the same task. unit.PendingTask=&active;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1); // A site-security task has no approach sector but still occupies its cell. active.ApproachBearing=-1;unit.PendingTask=nullptr;unit.PendingOrder=nullptr; reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1&&first==-1&&second==-1); active.ApproachBearing=0;unit.PendingOrder=&replacement; // Third active/pending sector fails conservatively; it cannot overwrite an earlier sector. ERR_HQTask third;third.ApproachBearing=2;candidate.Task=&third;candidate.Assignment=&delivered; unit.PendingTask=&pending;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second); for(int sector=0;sector<4;sector++)assert(!ERR_HQPursuitRules::BearingAllowed(sector,first,second)); // Maximum eight squads, two snapshots apiece: 16 bounded reservation entries, // while the physical-force budget still counts exactly eight squads. ERR_HQUnit units[8];ERR_HQTask tasks[16];hq.m_Units.clear(); for(int i=0;i<8;i++){tasks[i*2].Destination={float(10000+i*1000),0,10000};tasks[i*2+1].Destination={float(10500+i*1000),0,10000}; units[i].Task=&tasks[i*2];units[i].Assignment=&delivered;units[i].PendingTask=&tasks[i*2+1];units[i].PendingOrder=&replacement;hq.m_Units.push_back(&units[i]);} reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(hq.Assigned()==8&&reserved.Count()==16); assert(coverage.Select({250,0,250},25,{250,0,250},2000,selected,&reserved)); reserved.Insert({20000,0,20000});assert(!coverage.Select({250,0,250},25,{250,0,250},2000,selected,&reserved)); } ''') print('PASS: actual HQ active/pending reservations, interruption, committed replacement, bounded coverage and unchanged force counts')