Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
165 lines
26 KiB
Python
165 lines
26 KiB
Python
"""Execute actual mounted HQ dispatch methods; radio/physical service are adversarial doubles."""
|
|
from policy_harness import ROOT, body, run
|
|
from pathlib import Path
|
|
import os,re
|
|
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
|
names=['protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
|
def cpp(s):
|
|
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
|
for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice']:
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
|
for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string']:
|
|
s=s.replace(typ+'.',typ+'::')
|
|
for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute']:
|
|
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
|
for name in ['Unit','Task','Vehicle','Route','StatusSampler','MountedAmmo','Combat','Receipt']:
|
|
s=s.replace('->'+name+'.','->'+name+'->')
|
|
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
|
|
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
|
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
|
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
|
s=s.replace('Array<vector> reserved,','Array<vector>& reserved,')
|
|
return s
|
|
methods='\n'.join(cpp(body(P,n)) for n in names if n in P.read_text())
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <sstream>
|
|
struct string:std::string{using std::string::string;string(std::string s):std::string(s){} template<class...T>static string Format(const char*fmt,T...v){std::ostringstream out;out<<fmt;((out<<"|"<<v),...);return out.str();}};
|
|
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
|
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
|
struct Group{};struct Entity{};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
|
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
|
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
|
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
|
struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";};
|
|
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
|
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
|
struct ERR_ConvoyInterceptOrder{int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
|
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
|
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
|
struct ERR_HQRoute{int result=0,calls=0,MessageId=0;string Payload;int Advance(float,Journal*){calls++;return result;}};
|
|
struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:nullptr);}};
|
|
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
|
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
|
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
|
struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
|
|
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
|
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
|
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
|
int Count(){return tasks.Count();}ERR_ConvoyTask*At(int n){return tasks[n];}
|
|
bool InterceptCurrent(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*){return current;}
|
|
bool ReserveIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){reserves++;return reserve;}
|
|
bool ApplyIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){applies++;return apply;}
|
|
void ReleaseIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){releases++;if(onRelease)onRelease();}
|
|
};
|
|
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
|
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
|
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
|
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
|
struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
|
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
|
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
|
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
|
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
|
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
|
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
|
struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Vehicle=nullptr;ERR_HQRoute*Route=nullptr;vector Destination;float Distance=0;};
|
|
struct HQ:Base{
|
|
ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
|
|
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
|
bool m_MountedInterceptReceipt=false,m_MountedInterceptPartnerReceipt=false;int m_MountedTokenCount=0;
|
|
int m_MountedSurveyCursor=0;
|
|
Array<int>m_MountedAttemptBeliefs,m_MountedAttemptRevisions;Array<ERR_HQUnit*>m_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network;
|
|
bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
|
|
'''+methods+r'''
|
|
};
|
|
struct Fixture{Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
|
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
|
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
|
};
|
|
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
|
PairFixture(){unit2=unit;unit2.Group=&group2;unit2.Radio=&radio2;unit2.StatusSampler=&sampler2;unit2.ForceSourceId="partner";entry2=entry;entry2.SourceId="partner";entry2.Position={200,0};watch.second=&entry2;vehicle2=vehicle;vehicle2.Index=3;vehicle2.Group=&group2;vehicle2.Entity=&entity2;task.Vehicles[3]=&vehicle2;h.m_Units.Insert(&unit2);network.routes[&radio2]=&route2;ERR_HQMountedInterceptRules::pairGeometry=true;}
|
|
void pair(){plan();assert(h.m_MountedIntercept&&h.m_MountedInterceptPartner);assert(h.m_MountedInterceptTask==&task);assert(unit.LogisticsReservation==400001&&unit2.LogisticsReservation==400002);assert(service.pairReserves==1&&service.reserves==0&&service.pairApplies==0);}
|
|
};
|
|
int main(){
|
|
{Fixture f;assert(f.h.Status()=="base HQ status");f.queued();string queued=f.h.Status();assert(queued.find("base HQ status")!=string::npos&&queued.find("|0|queued|20|100,0")!=string::npos);f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.Status().find("|0|delivered|20|100,0")!=string::npos);f.h.Stop();assert(f.h.Status()=="base HQ status");}
|
|
{Fixture f;f.queued();assert(f.h.Assigned()==1&&f.h.HuntingAssigned()==1);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);assert(r.Count()==1&&r[0]==f.belief.destination);f.h.PlanMountedIntercept(4000);assert(f.service.reserves==1);f.h.TickMountedIntercept(4000);assert(f.route.calls==1&&f.service.applies==0);f.route.result=1;f.h.TickMountedIntercept(5000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.belief.BeliefRevision++;f.h.TickMountedIntercept(6000);assert(f.h.m_MountedInterceptDelivered&&f.service.releases==0);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&f.unit.LogisticsReservation==0);}
|
|
// Task-wide logistics and queued sibling downlinks cannot consume a revision or starve a free farther convoy.
|
|
for(int kind=0;kind<5;kind++){Fixture f,far;far.unit.ForceSourceId=far.entry.SourceId="crew2";far.receipt.SourceId="convoy2";far.entry.Position={-200,0};f.watch.second=&far.entry;f.service.tasks.Insert(&far.task);f.h.m_Units.Insert(&far.unit);ERR_HQUnit sibling; sibling.Group=&f.otherGroup;f.unused.Group=&f.otherGroup;f.h.m_Units.Insert(&sibling);if(kind==0)f.unused.SupplyToken=123;if(kind==1)f.unused.SupplyHeld=true;if(kind==2)sibling.PendingConvoyOrder=true;if(kind==3)sibling.Downlink=true;if(kind==4)sibling.LogisticsReservation=123;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&far.unit&&!f.unit.LogisticsReservation&&far.unit.LogisticsReservation==400001&&f.service.reserves==1);}
|
|
// A disconnected nearest escort cannot starve the farther reachable original escort.
|
|
{Fixture f;Group group2;Entity radio2,entity2;ERR_HQUnit u2=f.unit;u2.Group=&group2;u2.Radio=&radio2;u2.ForceSourceId="crew2";ERR_HQWatchEntry entry2=f.entry;entry2.SourceId="crew2";entry2.Position={-200,0};f.watch.second=&entry2;ERR_ConvoyVehicle vehicle2=f.vehicle;vehicle2.Group=&group2;vehicle2.Entity=&entity2;vehicle2.Index=3;f.task.Vehicles[3]=&vehicle2;f.h.m_Units.Insert(&u2);f.network.denied=&f.radio;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&u2&&f.h.m_MountedIntercept->VehicleIndex==3&&f.network.calls>=2&&!f.unit.LogisticsReservation&&u2.LogisticsReservation==400001);}
|
|
// Invalid world clock must preserve the exact claim for the next finite tick.
|
|
{Fixture f;f.queued();f.h.TickMountedIntercept(std::nanf(""));assert(f.h.m_MountedIntercept&&f.unit.LogisticsReservation==400001&&f.service.releases==0&&f.route.calls==0);}
|
|
// No own vehicle/fresh crew evidence means no native road surveys.
|
|
for(int kind=0;kind<6;kind++){Fixture f;if(kind==0)f.service.tasks.clear();if(kind==1)f.task.Stopped=true;if(kind==2)f.unit.LogisticsReservation=1;if(kind==3)f.entry.Closed=true;if(kind==4)f.sampler.bound=false;if(kind==5)f.combat.Held=true;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&f.service.reserves==0);}
|
|
// Interrupted pending claims never apply physical orders, and the same delivered revision cannot retry.
|
|
{Fixture f;f.queued();f.route.result=-1;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.applies==0);f.plan();assert(f.service.reserves==1);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==2&&f.unit.LogisticsReservation==400002);}
|
|
{Fixture f;f.queued();f.belief.BeliefRevision++;f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.route.calls==0&&f.service.applies==0&&!f.h.m_MountedIntercept);}
|
|
// Physical validation changes after radio delivery cannot trigger a stale order.
|
|
for(int kind=0;kind<7;kind++){Fixture f;f.queued();f.route.result=1;if(kind==0)f.ammo.Complete=false;if(kind==1)f.ammo.valid=false;if(kind==2)f.sampler.bound=false;if(kind==3)f.entry.Fit--;if(kind==4)f.entry.LastDeliveredAt=9000;if(kind==5)f.unit.contact=true;if(kind==6)f.service.apply=false;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation);assert(f.service.applies==(kind==6?1:0));}
|
|
// Conditional release and recursive unregister preserve unrelated reservations.
|
|
{Fixture f;f.queued();f.unit.LogisticsReservation=123;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept);f.h.ReleaseUnit(&f.unit,4000);};f.h.TickMountedIntercept(4000);assert(f.unit.LogisticsReservation==123&&f.service.releases==1&&f.h.releaseUnits==1);}
|
|
{Fixture f;f.queued();f.h.Stop();assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1&&f.h.stops==1&&f.service.applies==0);}
|
|
// Ordinary assigned and pursuit capacities reserve space before any road query or radio action.
|
|
for(int kind=0;kind<3;kind++){Fixture f;if(kind==0)f.mode.settings.cap=0;if(kind==1)f.h.assigned=3;if(kind==2)f.h.hunting=3;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&ERR_HQMountedInterceptRules::selects==before);}
|
|
{Fixture f;f.queued();f.h.assigned=3;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&f.service.applies==0);}
|
|
// Admission is based on detached delivered position, not physical entity telemetry; stock/crew/roles guard it.
|
|
for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
|
{Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);}
|
|
{Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
|
{Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);assert(f.h.m_MountedAttemptBeliefs.empty());int after=ERR_HQMountedInterceptRules::selects;f.h.PlanMountedIntercept(8000);assert(ERR_HQMountedInterceptRules::selects-after==1);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.service.reserves==1);}
|
|
// Cursor normalization handles track expiry/removal without out-of-range access.
|
|
{Fixture f;ERR_HQBelief second;second.BeliefId=2;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second};f.plan();assert(!f.h.m_MountedIntercept);f.h.m_MountedSurveyCursor=31;f.memory.entries={&f.belief};f.belief.select=true;f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==1);}
|
|
// Skipped ineligible/already-attempted tracks do not spend the two-road-query budget.
|
|
{Fixture f;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.eligible=false;f.h.m_MountedAttemptBeliefs.Insert(2);f.h.m_MountedAttemptRevisions.Insert(second.BeliefRevision);f.memory.entries={&f.belief,&second,&third};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==1&&f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3);}
|
|
// No eligible reports and no reports both produce zero native road queries.
|
|
{Fixture f;f.belief.eligible=false;int before=ERR_HQMountedInterceptRules::selects;f.plan();f.memory.entries.clear();f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
|
// Full bounded memory: every pass has at most two surveys, no failed revision is consumed.
|
|
{Fixture f;ERR_HQBelief beliefs[64];f.memory.entries.clear();for(int i=0;i<64;i++){beliefs[i].BeliefId=i+1;beliefs[i].select=false;f.memory.entries.Insert(&beliefs[i]);}for(int pass=0;pass<32;pass++){int before=ERR_HQMountedInterceptRules::selects;float now=3000+pass*5000;f.entry.ObservedAt=f.entry.LastDeliveredAt=now;f.h.PlanMountedIntercept(now);assert(ERR_HQMountedInterceptRules::selects-before==2&&f.h.m_MountedSurveyCursor==((pass+1)*2)%64&&f.h.m_MountedAttemptBeliefs.empty());}assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
|
|
|
// Paired admission counts and reserves both original slots; one receipt never detours either.
|
|
{PairFixture f;f.pair();assert(f.h.Assigned()==2&&f.h.HuntingAssigned()==2);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);f.h.ReserveUnit(&f.unit2,r,a,b);assert(r.Count()==2&&vector::DistanceXZ(r[0],r[1])>=40);assert(f.h.Status().find("Paired")!=string::npos);
|
|
f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptReceipt&&!f.h.m_MountedInterceptPartnerReceipt&&!f.h.m_MountedInterceptDelivered&&f.service.pairApplies==0&&f.service.applies==0);f.h.TickMountedIntercept(4500);assert(f.route.calls==1&&f.route2.calls==2&&f.h.m_MountedInterceptRoute==&f.route&&f.h.m_MountedInterceptPartnerRoute==&f.route2);
|
|
f.route2.result=1;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&f.h.m_MountedInterceptDelivered&&f.h.m_MountedIntercept->ExpiresAt==121000&&f.h.m_MountedInterceptPartner->ExpiresAt==121000);f.h.TickMountedIntercept(6000);assert(f.service.pairApplies==1);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
|
// Either receipt may arrive first; interrupted/expired/revised/changed ownership never creates half a pair.
|
|
for(int kind=0;kind<9;kind++){PairFixture f;f.pair();f.route2.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptPartnerReceipt&&f.service.pairApplies==0);float now=5000;
|
|
if(kind==0)f.route.result=-1;
|
|
if(kind==1)now=121000;
|
|
if(kind==2)f.belief.BeliefRevision++;
|
|
if(kind==3)f.unit2.LogisticsReservation=999;
|
|
if(kind==4)f.unit2.contact=true;
|
|
if(kind==5)f.entry2.Fit--;
|
|
if(kind==6)f.sampler2.bound=false;
|
|
if(kind==7)f.h.assigned=2;
|
|
if(kind==8)now=34000;
|
|
if(kind!=0)f.route.result=1;
|
|
f.h.TickMountedIntercept(now);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairApplies==0&&f.service.applies==0&&!f.unit.LogisticsReservation);assert(f.unit2.LogisticsReservation==(kind==3?999:0));}
|
|
// Native preparation failure settles BOTH claims. No retry or singleton replacement after commitment.
|
|
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.apply=false;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0);}
|
|
{PairFixture f;f.service.reserve=false;f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.h.m_MountedTokenCount==2);f.service.reserve=true;f.plan();assert(f.service.pairReserves==1);f.belief.BeliefRevision++;f.plan();assert(f.h.m_MountedIntercept->Token==400003&&f.h.m_MountedInterceptPartner->Token==400004);}
|
|
// Stop/reentrant unregister detach both owned handles before external release; unrelated claims survive.
|
|
{PairFixture f;f.pair();f.unit2.LogisticsReservation=999;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner);f.h.ReleaseUnit(&f.unit2,5000);};f.h.Stop();f.h.Stop();assert(f.service.releases==2&&!f.unit.LogisticsReservation&&f.unit2.LogisticsReservation==999);}
|
|
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.onApply=[&]{f.h.Stop();};f.h.TickMountedIntercept(5000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptDelivered&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);}
|
|
// Two free slots and two IDs are required for pair. Singleton fallback occurs only before claims.
|
|
for(int kind=0;kind<5;kind++){PairFixture f;if(kind==0)f.h.assigned=2;if(kind==1)f.h.hunting=2;if(kind==2)f.h.m_MountedTokenCount=255;if(kind==3)f.network.denied=&f.radio2;if(kind==4)ERR_HQMountedInterceptRules::pairGeometry=false;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.reserves==1&&f.service.pairReserves==0);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before<=2);}
|
|
{PairFixture f;f.h.m_MountedTokenCount=256;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects==before);}
|
|
// A valid reversed assignment must win over greedy singleton fallback (same bounded road survey).
|
|
{PairFixture f;f.mode.settings.radius=800;f.belief.destination={0,0};f.entry.Position={0,0};f.entry2.Position={-800,0};int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedInterceptPartner&&f.h.m_MountedInterceptUnit==&f.unit2&&f.h.m_MountedInterceptPartnerUnit==&f.unit);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==1);}
|
|
// Stop during reservation cannot resurrect a job after external callback returns.
|
|
{PairFixture f;f.service.onReserve=[&]{f.h.Stop();};f.plan();assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
|
// No mixed-convoy pair; both geometric failures still cost at most two queries, no attempts/tokens.
|
|
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
|
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
|
|
|
}
|
|
''')
|
|
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|