Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
256 lines
23 KiB
Python
256 lines
23 KiB
Python
"""Actual radio task delivery/ownership methods with native waypoint and memory 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'))
|
|
ROAD=Path(os.environ.get('ERR_HQ_ROAD_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
|
|
def cpp(s):
|
|
s=s.replace('EngineerMine.', 'EngineerMine->')
|
|
s=s.replace('ref ', '')
|
|
s=s.replace('protected ','').replace('null','nullptr').replace('string.Format','Format')
|
|
for name in ('ERR_SupplyConvoyTactics','Math','ERR_GameMode','SCR_EntityHelper','ERR_HQPursuitRules','AIWaypoint','ERR_Actors','ERR_Resources','ERR_ForceDuty','ERR_HQRoadIntercept','ERR_ForceRules','ERR_HQPursuitRole','ERR_HQMemoryKind','ERR_HQOperationRole'):
|
|
s=s.replace(name+'.',name+'::')
|
|
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_GameMode','ERR_HQReport','ERR_HQBelief','ERR_ForceRecord','ERR_HQHuntWindow','AIWaypoint'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
|
for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','waypoint','m_Mode','lostActive','lostPending'):
|
|
s=s.replace(name+'.',name+'->')
|
|
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
|
|
s=s.replace('PendingOrder.', 'PendingOrder->').replace('Assignment.', 'Assignment->')
|
|
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
|
|
s=s.replace('Downlink.', 'Downlink->').replace('Forces().','Forces()->').replace('ERR_ForceRecord* record;','ERR_ForceRecord* record=nullptr;')
|
|
return s
|
|
units='\n'.join(cpp(body(P,x)) for x in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
|
|
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool SecurityAssigned(', 'protected bool AlarmCommitted(', 'protected void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
|
|
if 'protected float HuntCommitment(' in P.read_text(): methods=cpp(body(P,'protected float HuntCommitment('))+'\n'+methods
|
|
if 'protected void ReconsiderHunt(' in P.read_text(): methods=cpp(body(P,'protected void ReconsiderHunt('))+'\n'+methods
|
|
if 'protected void RememberCompletedHunt(' in P.read_text(): methods=cpp(body(P,'protected void RememberCompletedHunt('))+'\n'+methods
|
|
if 'protected void ReleaseLostForce(' in P.read_text(): methods=cpp(body(P,'protected void ReleaseLostForce('))+'\n'+methods
|
|
missing=cpp(body(P,'if (!unit.Group && !unit.Uplink)')).replace('m_Units.Remove(i)', 'm_Units.erase(m_Units.begin()+i)')
|
|
cleanup=cpp(body(P,' foreach (ERR_HQUnit unit : m_Units)\n {\n if (!unit.Assignment && !unit.PendingOrder) continue;\n bool security = SecurityAssigned(unit);'))
|
|
R=Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
|
|
rules='\n'.join(cpp(body(R,x)) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role('))
|
|
eligible=cpp(body(ROAD,'static bool Eligible('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
using string=std::string;
|
|
template<class... T>string Format(T...){return "order";}
|
|
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:(i==1?y:z);}float Length(){return std::sqrt(x*x+y*y+z*z);}};
|
|
enum class ERR_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
|
|
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};
|
|
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
|
|
struct ERR_HQMemoryKind{static const int SIGHTING=1,ALARM=4;};
|
|
struct ERR_HQPursuitRules{
|
|
'''+rules+r'''
|
|
};
|
|
struct Math{static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}};
|
|
struct ERR_HQTask{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;int OperationId=0,ExecutionId=0;string SecuritySite;bool Maneuver=false,RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
|
|
int BeliefId=1,BeliefRevision=2;ERR_HQPursuitRole Role=ERR_HQPursuitRole::SEARCH;vector Destination{40,0,50};float SearchRadiusMetres=60,QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
|
|
struct ERR_HQReport{string TravelMode="on foot";int MessageId=10;float ObservedAt=0;};
|
|
struct ERR_HQRoute{int MessageId=20;};
|
|
struct AIWaypoint{bool commanded=false;int resource=0,deletes=0,configured=0;float radius=30;void SetCompletionRadius(float r){assert(!configured);radius=r;configured++;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
|
struct ERR_SupplyConvoyTactics{static void Command(AIWaypoint* w){w->commanded=true;}};
|
|
struct SCR_AIGroup{int removals=0,additions=0;AIWaypoint* last=nullptr;void RemoveWaypoint(AIWaypoint* w){removals++;last=w;}void AddWaypointAt(AIWaypoint* w,int at){assert(at==0);additions++;last=w;}};
|
|
struct ERR_ForceRecord{bool Eliminated=false;int Duty=2,HomeDuty=1;vector DutyTarget,HomePosition;};
|
|
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
|
|
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;Ledger ledger;static ERR_GameMode* Get(){return current;}Ledger* Forces(){return &ledger;}};
|
|
struct SCR_EntityHelper{static void DeleteEntityAndChildren(AIWaypoint* p){p->deletes++;}};
|
|
struct ERR_Actors{static inline bool refuse=false;static inline int spawns=0;static AIWaypoint* Spawn(int resource,vector){spawns++;if(refuse)return nullptr;auto w=new AIWaypoint;w->resource=resource;return w;}static vector Ground(vector p){return p;}};
|
|
struct ERR_Resources{static int MoveWaypoint(){return 3;}static int SearchWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
|
|
struct ERR_ForceDuty{static const int SEARCH=2;};
|
|
struct MineJob {int stops=0;void Stop(){stops++;}};
|
|
struct TestWorld{operator bool(){return true;}float GetWorldTime(){return 0;}};struct TestGame{operator bool(){return true;}TestWorld GetWorld(){return {};}};TestGame GetGame(){return {};}struct ExecutionDouble{void Update(void*,float){}operator bool(){return false;}void Stop(float,bool){}void Receive(ERR_HQTask*,float){}};
|
|
struct ERR_HQUnit{ExecutionDouble Execution;MineJob* EngineerMine=nullptr;int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
|
|
ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;
|
|
ERR_HQRoute* Downlink=nullptr;ERR_HQRoute* Uplink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
|
|
bool Eligible(ERR_GameMode*){return eligible&&Group;}
|
|
'''+units+r'''
|
|
};
|
|
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,Kind=1,SampleCount=2,MessageId=10;float Confidence=.8f,Radius=60,ObservedAt=0,LastDeliveredAt=0;string TravelMode="on foot";vector Velocity{0,0,3};bool HasVelocity=true;};
|
|
struct ERR_HQRoadIntercept{
|
|
'''+eligible+r'''
|
|
};
|
|
struct Memory{ERR_HQBelief* belief=nullptr;ERR_HQBelief* GetById(int id){return belief&&belief->BeliefId==id?belief:nullptr;}};
|
|
struct Watch{bool empty=false;int finished=0;bool ArmedSearchBlocked(string,float){return empty;}bool FinishInvestigation(string,int,int,float){finished++;return true;}};
|
|
struct Journal{int writes=0;void Write(string,string,int=0){writes++;}};
|
|
struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
|
|
struct HQ{bool CoordinatedDelivery(ERR_HQUnit*,ERR_HQTask*,float){return false;}struct Operations{void Release(int,int,float){}}m_Operations;ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt,otherHunt;bool watchValid=true;
|
|
bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int id){return id==2?&otherHunt:&hunt;}
|
|
'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}\nvoid RemoveMissing(float now){for(int i=int(m_Units.size())-1;i>=0;i--){ERR_HQUnit* unit=m_Units[i];'+missing+'}}'+r'''
|
|
};
|
|
struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypoint previous;ERR_HQTask active,next;ERR_HQReport oldReport,newReport;ERR_HQRoute route;ERR_HQBelief belief;
|
|
Fixture(){ERR_GameMode::current=&mode;ERR_Actors::refuse=false;ERR_Actors::spawns=0;hq.m_Mode=&mode;hq.m_Units={&unit};hq.m_Memory.belief=&belief;
|
|
unit.Group=&group;unit.Task=&active;unit.Assignment=&oldReport;unit.Waypoint=&previous;unit.PendingTask=&next;unit.PendingOrder=&newReport;unit.Downlink=&route;active.BeliefId=9;}
|
|
void Retained(){assert(unit.Task==&active&&unit.Assignment==&oldReport&&unit.Waypoint==&previous&&previous.deletes==0&&group.removals==0&&!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink);}
|
|
};
|
|
int main(){
|
|
// The copied radius is used before installation even if current uncertainty grows.
|
|
for(float radius:{45.f,60.f,110.f}){Fixture f;f.next.SearchRadiusMetres=radius;f.belief.Radius=500;
|
|
float deadline=f.next.ExpiresAt;f.hq.DeliverOrder(&f.unit,1000);
|
|
assert(f.group.additions==1&&f.unit.Waypoint->radius==radius&&f.unit.Waypoint->configured==1);
|
|
assert(f.next.SearchRadiusMetres==radius&&f.next.ExpiresAt==deadline);}
|
|
// Malformed snapshot cannot spawn or replace the delivered previous assignment.
|
|
for(float radius:{0.f,44.99f,110.01f,-1.f,std::numeric_limits<float>::quiet_NaN(),std::numeric_limits<float>::infinity()}){
|
|
Fixture f;f.next.SearchRadiusMetres=radius;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
// Missing patrol, road/security defend, interception and containment keep native defaults.
|
|
for(int kind=0;kind<5;kind++){Fixture f;f.next.SearchRadiusMetres=std::numeric_limits<float>::quiet_NaN();
|
|
if(kind==0)f.next.WatchSourceId="missing-original";
|
|
if(kind==1)f.next.RoadIntercept=true;
|
|
if(kind==2)f.next.SecuritySite="site";
|
|
if(kind==3)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
|
|
if(kind==4){f.next.Role=ERR_HQPursuitRole::CONTAIN;f.belief.Confidence=.9f;}
|
|
f.hq.DeliverOrder(&f.unit,1000);assert(f.group.additions==1&&f.unit.Waypoint->radius==(kind==1?70:30)&&f.unit.Waypoint->configured==(kind==1?1:0));}
|
|
|
|
// Alarm-only deliveries retain their weak SEARCH/mobile policy even if a queued task is tampered with.
|
|
for(int invalid=0;invalid<5;invalid++){Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;
|
|
ERR_HQUnit other;ERR_HQTask otherTask;ERR_HQReport alarm;alarm.TravelMode="heard vehicle alarm";
|
|
if(invalid==0)f.unit.HomeGuard=true;
|
|
if(invalid==1)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
|
|
if(invalid==2)f.next.RoadIntercept=true;
|
|
if(invalid==3){other.PendingTask=&otherTask;other.PendingOrder=&alarm;f.hq.m_Units.push_back(&other);}
|
|
if(invalid==4){other.Task=&otherTask;other.Assignment=&alarm;f.hq.m_Units.push_back(&other);}
|
|
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
{Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;f.newReport.TravelMode="heard vehicle alarm";
|
|
assert(f.hq.AlarmCommitted()&&!f.hq.AlarmCommitted(&f.unit));
|
|
f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.unit.Waypoint->resource==1&&f.hq.AlarmCommitted());
|
|
f.unit.ReleaseAssignment();assert(!f.hq.AlarmCommitted());}
|
|
// A changed belief revision or absent/stale/uncertain track cannot create a native destination.
|
|
for(int invalid=0;invalid<5;invalid++){Fixture f;
|
|
if(invalid==0){f.belief.BeliefRevision=3;}if(invalid==1){f.hq.m_Memory.belief=nullptr;}
|
|
if(invalid==2){f.belief.ObservedAt=2000;}if(invalid==3){f.belief.Confidence=.1f;}if(invalid==4){f.belief.Radius=2001;}
|
|
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
// Exact absolute expiry boundary, combat, physical reservation eligibility and known-empty ammo remain gates.
|
|
for(int invalid=0;invalid<4;invalid++){Fixture f;
|
|
if(invalid==0){f.next.ExpiresAt=1000;}if(invalid==1){f.unit.fighting=true;}if(invalid==2){f.unit.eligible=false;}if(invalid==3){f.hq.m_Watch.empty=true;}
|
|
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
// Fresh delivered flank orders continue through contact; stale orders preserve the old assignment.
|
|
{Fixture f;f.unit.fighting=true;f.next.Maneuver=true;f.hq.DeliverOrder(&f.unit,1000);
|
|
assert(f.group.additions==1&&f.unit.Waypoint->commanded);}
|
|
{Fixture f;f.next.Maneuver=true;f.next.ExpiresAt=120000;f.hq.DeliverOrder(&f.unit,60001);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Waypoint->commanded);}
|
|
// Native spawn refusal must leave the delivered old waypoint and duty intact.
|
|
{Fixture f;ERR_Actors::refuse=true;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==1&&f.mode.ledger.record.Duty==2);}
|
|
// Commit swaps the old task only after native success. Duplicate delivery has no pending lineage and cannot spawn twice.
|
|
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.previous.deletes==1&&f.group.removals==1&&f.group.additions==1);
|
|
assert(f.unit.Waypoint->resource==1);
|
|
assert(f.unit.Task==&f.next&&f.unit.Assignment==&f.newReport&&f.unit.Waypoint!=&f.previous&&!f.unit.PendingTask&&!f.unit.Downlink);
|
|
auto retained=f.unit.Waypoint;f.hq.DeliverOrder(&f.unit,1001);assert(ERR_Actors::spawns==1&&f.unit.Waypoint==retained&&f.group.additions==1);
|
|
assert(f.mode.ledger.record.DutyTarget.x==f.next.Destination.x);f.unit.ReleaseAssignment();assert(retained->deletes==1&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
|
|
// Actual assignment-pass expiry releases only the expired reservation, preserving the other lineage.
|
|
{Fixture f;f.active.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);
|
|
assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
|
|
assert(f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);f.hq.Cleanup(1001,1,4);assert(f.previous.deletes==1&&f.unit.PendingTask==&f.next);}
|
|
{Fixture f;f.active.BeliefId=1;f.next.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);f.Retained();}
|
|
{Fixture f;f.unit.PlanEligible=false;f.hq.Cleanup(1000,1,4);assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
|
|
// Fixed site defence survives a full pursuit budget, including pending downlinks.
|
|
for(bool pending:{false,true}){Fixture f;f.active.BeliefId=1;
|
|
if(pending){f.next.SecuritySite="radio";f.next.ExpiresAt=600000;}
|
|
else {f.unit.CancelPending();f.active.SecuritySite="radio";f.active.ExpiresAt=600000;}
|
|
f.hq.Cleanup(1000,5,1);assert(f.unit.Task==&f.active&&f.unit.Waypoint==&f.previous);
|
|
if(pending)assert(f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
|
|
f.unit.PlanEligible=false;f.hq.Cleanup(1001,5,1);assert(!f.unit.Task&&!f.unit.PendingTask);}
|
|
// Loss of the only physical pursuit squad releases its unfulfilled long hold.
|
|
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
|
|
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==25000);assert(!f.unit.Task&&!f.unit.Assignment&&f.previous.deletes==1);}
|
|
// Native group deletion takes the earlier Tick cleanup path, before Assign.
|
|
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.Group=nullptr;
|
|
f.hq.RemoveMissing(10000);assert(f.hq.m_Units.empty()&&f.hq.hunt.NextHunt==25000&&!f.unit.Task&&f.previous.deletes==1);}
|
|
// Finishing an ordinary delivered search retains its completed-hunt cooldown.
|
|
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=190000;
|
|
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Assignment);}
|
|
// Simultaneous expiry and unavailable-force detection cannot erase a completed
|
|
// hunt's quiet interval, including after loss of a pending same-belief retask.
|
|
for(int removed=0;removed<2;removed++)for(int pending=0;pending<3;pending++)for(int shorter=0;shorter<2;shorter++){
|
|
Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=shorter?12000:190000;f.hq.otherHunt.NextHunt=780000;
|
|
if(pending==0)f.unit.CancelPending();else {f.next.BeliefId=pending==1?1:2;f.next.ExpiresAt=600000;}
|
|
f.unit.PlanEligible=false;if(removed){f.unit.Group=nullptr;f.hq.RemoveMissing(10000);}else f.hq.Cleanup(10000,1,4);
|
|
assert(f.hq.hunt.NextHunt==(shorter?12000:190000));assert(f.hq.otherHunt.NextHunt==(pending==2?25000:780000));
|
|
assert(!f.unit.Task&&!f.unit.PendingTask&&f.previous.deletes==1);
|
|
}
|
|
// A completed squad's floor survives a different lost squad in either cleanup
|
|
// order, whether the completed squad expires normally or is also unavailable.
|
|
for(int reverse=0;reverse<2;reverse++)for(int completedLost=0;completedLost<2;completedLost++){
|
|
Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.unit.PlanEligible=!completedLost;f.hq.hunt.NextHunt=780000;
|
|
ERR_HQUnit lost;SCR_AIGroup lostGroup;ERR_HQTask lostTask;ERR_HQReport lostReport;lost.Group=&lostGroup;lost.Task=&lostTask;lost.Assignment=&lostReport;lost.PlanEligible=false;lostTask.BeliefId=1;lostTask.ExpiresAt=600000;
|
|
f.hq.m_Units.push_back(&lost);if(reverse)std::reverse(f.hq.m_Units.begin(),f.hq.m_Units.end());f.hq.Cleanup(10000,2,4);assert(f.hq.hunt.NextHunt==190000);
|
|
}
|
|
// A replacement can arrive before Assign processes the original expiry.
|
|
// Native commit must preserve completion history before clearing old ownership.
|
|
{Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
|
f.hq.DeliverOrder(&f.unit,10000);assert(f.unit.Task==&f.next&&f.hq.hunt.CompletedUntil==190000);
|
|
f.unit.PlanEligible=false;f.hq.Cleanup(11000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Task);}
|
|
// Only expired delivered pursuit work creates completion history.
|
|
for(int kind=0;kind<5;kind++){Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;
|
|
if(kind==0){f.active.ExpiresAt=10001;}if(kind==1){f.active.ReleaseOnly=true;}if(kind==2){f.active.SecuritySite="security";}if(kind==3){f.active.WatchSourceId="watch";}if(kind==4){f.unit.Assignment=nullptr;}
|
|
f.hq.ReleaseUnit(&f.unit,10000);assert(f.hq.hunt.CompletedUntil==0&&f.hq.otherHunt.CompletedUntil==0);}
|
|
// Capacity trimming is deliberately unchanged: it is not evidence of force loss.
|
|
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
|
f.hq.Cleanup(10000,2,1);assert(f.hq.hunt.NextHunt==780000&&!f.unit.Assignment);}
|
|
// Surviving active and pending same-belief commitments both retain their own
|
|
// horizon; foreign replacements cannot hide active work or prolong this hunt.
|
|
for(int scenario=0;scenario<5;scenario++){Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
|
|
ERR_HQUnit peer;SCR_AIGroup peerGroup;ERR_HQTask surviving,replacement;ERR_HQReport activeReport,pendingReport;peer.Group=&peerGroup;peer.Task=&surviving;peer.Assignment=&activeReport;peer.PendingTask=&replacement;peer.PendingOrder=&pendingReport;
|
|
surviving.BeliefId=1;surviving.ExpiresAt=120000;replacement.BeliefId=1;replacement.ExpiresAt=180000;
|
|
if(scenario==1)replacement.BeliefId=2;
|
|
if(scenario==2){surviving.SecuritySite="outpost";replacement.ReleaseOnly=true;}
|
|
if(scenario==3){peer.Assignment=nullptr;peer.PendingOrder=nullptr;}
|
|
if(scenario==4)f.hq.hunt.NextHunt=12000;
|
|
f.hq.m_Units.push_back(&peer);f.hq.Cleanup(10000,2,4);float expected=scenario==0?360000:scenario==1?300000:scenario==4?12000:25000;
|
|
assert(f.hq.hunt.NextHunt==expected);assert(peer.Task==&surviving);}
|
|
// Expired undelivered retasks use the same bounded rollback as lost radio.
|
|
// Both original and replacement expire here; no unfulfilled hunt owns 780s.
|
|
{Fixture f;f.active.ExpiresAt=120000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
|
f.hq.Cleanup(600000,1,4);assert(f.hq.hunt.NextHunt==615000);
|
|
assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
|
|
// A still-live containment order also needs current authorization at delivery.
|
|
// The fixed task deadline is not permission to use an older/weaker movement belief.
|
|
for(int invalid=0;invalid<6;invalid++){Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;
|
|
f.next.QueuedAt=30000;f.next.ExpiresAt=120000;float now=45000;
|
|
if(invalid==0){now=45001;}
|
|
if(invalid==1){f.belief.Radius=100.01f;}
|
|
if(invalid==2){f.belief.Confidence=.799f;}
|
|
if(invalid==3){f.belief.HasVelocity=false;}
|
|
if(invalid==4){f.belief.Velocity={0,0,.49f};}
|
|
if(invalid==5){f.belief.Velocity={0,0,71};}
|
|
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
{Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;f.next.QueuedAt=30000;f.next.ExpiresAt=120000;
|
|
f.belief.Radius=100;f.belief.Confidence=.8f;f.belief.Velocity={0,0,.5f};
|
|
f.hq.DeliverOrder(&f.unit,45000);assert(ERR_Actors::spawns==1&&f.unit.Task==&f.next&&f.previous.deletes==1);
|
|
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);}
|
|
// Radio latency may age a precision interception beyond its 45-second authorization.
|
|
// Eligibility uses the actual road helper and pursuit rules, never a test predicate.
|
|
for(int invalid=0;invalid<9;invalid++){Fixture f;f.next.RoadIntercept=true;float now=1000;
|
|
if(invalid==0)now=45001;
|
|
if(invalid==1)f.belief.LastDeliveredAt=1001;
|
|
if(invalid==2)f.belief.SampleCount=1;
|
|
if(invalid==3)f.belief.Velocity={0,0,1.99f};
|
|
if(invalid==4)f.belief.HasVelocity=false;
|
|
if(invalid==5)f.belief.TravelMode="aircraft";
|
|
if(invalid==6)f.belief.Kind=2;
|
|
if(invalid==7)f.belief.MessageId=0;
|
|
if(invalid==8){f.belief.ObservedAt=1;f.belief.LastDeliveredAt=0;}
|
|
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
{Fixture f;f.next.RoadIntercept=true;f.hq.DeliverOrder(&f.unit,45000);
|
|
assert(ERR_Actors::spawns==1&&f.unit.Waypoint&&f.unit.Waypoint->resource==2&&f.unit.Task==&f.next&&f.previous.deletes==1);
|
|
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);
|
|
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
|
|
// Security orders carry an inspected-site hypothesis, not a fabricated enemy track.
|
|
{Fixture f;f.next.SecuritySite="mortar-1";f.next.BeliefId=999;
|
|
f.hq.DeliverOrder(&f.unit,1000);assert(f.unit.Task==&f.next&&f.unit.Waypoint->resource==2);
|
|
f.hq.Cleanup(1001,1,4);assert(f.unit.Task==&f.next);
|
|
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint);}
|
|
// Cancelling a mismatched recall never releases a replacement task which arrived first.
|
|
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=nullptr;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
|
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=&f.active;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&ERR_Actors::spawns==0);}
|
|
}
|
|
''')
|
|
print('PASS: actual delivered-task lineage, expiry, old-order retention, native commit, release, road precision expiry and duplicate delivery')
|
|
|
|
delivery=body(P,'protected void DeliverOrder(')
|
|
assert delivery.index('waypoint.SetCompletionRadius(task.SearchRadiusMetres)') < delivery.index('unit.Release();') < delivery.index('unit.Group.AddWaypointAt')
|