Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
152 lines
11 KiB
Python
152 lines
11 KiB
Python
"""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<vector> reserved','Array<vector>* 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<vector> 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 <cassert>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include <string>
|
|
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);}};
|
|
template<class T>struct Array:std::vector<T>{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::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow window;Watch m_Watch;
|
|
ERR_HQHuntWindow* Hunt(int){return &window;}
|
|
'''+cpp(helpers+methods)+r'''
|
|
void Collect(Array<vector>* reserved,int& first,int& second){float now=1000;
|
|
'''+cpp(collect)+r'''}
|
|
void CollectForRetask(ERR_HQUnit* unit,Array<vector>* 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<vector> 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')
|