Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

457 lines
35 KiB
Python

"""Execute production execution/arrival/report methods against native doubles.
This proves the local state machine and evidence boundaries, not engine movement
or projectile behavior. The native coordinated fixture exercises those separately.
"""
import re
from policy_harness import ROOT, body, run
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c'
task_path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c'
def cpp(text):
text = text.replace('protected ', '').replace('ref ', '').replace('null', 'nullptr')
text = text.replace('out vector ', 'Vec& ').replace('out int ', 'int& ')
text = text.replace('vector.Zero', 'Vec{}').replace('vector ', 'Vec ')
text = text.replace('array<ref ERR_HQVisualContact>', 'Array<ERR_HQVisualContact*>')
text = text.replace('array<ERR_HQVisualContact>', 'Array<ERR_HQVisualContact*>')
text = text.replace('.IsEmpty()', '.empty()')
for typ in ('ERR_HQExecutionReport', 'ERR_HQTask', 'ERR_HQUnit', 'IEntity', 'ERR_HQVisualContact'):
text = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', text)
for name in ('unit', 'report', 'copy', 'actor', 'm_Task', 'm_Unit', 'StatusSampler', 'sample', 'sighting'):
text = text.replace(name + '.', name + '->')
text = text.replace('foreach (', 'for (')
text = text.replace('!samples || samples.empty()', 'samples.empty()')
text = re.sub(r'ERR_HQVisualContact\* (selected|fallback);', r'ERR_HQVisualContact* \1=nullptr;', text)
for name in ('Math', 'vector', 'ERR_HQPursuitRules', 'ERR_HQExecutionRules', 'ERR_Actors',
'CompartmentAccessComponent', 'ERR_HQObservation', 'ERR_GameMode',
'ERR_HQExecutionState', 'ERR_HQOperationRole'):
text = text.replace(name + '.', name + '::')
text = text.replace('samples[0].', 'samples[0]->')
text = text.replace('GetGame().GetWorld().GetWorldTime()', 'worldTime')
text = text.replace('int fit; int near; Vec witness;', 'int fit=0; int near=0; Vec witness;')
text = text.replace('float total;', 'float total=0;')
return text
enums = body(path, 'enum ERR_HQExecutionState') + ';\n' + body(task_path, 'enum ERR_HQOperationRole') + ';'
enums = enums.replace('enum ', 'enum class ')
rules = cpp(body(path, 'class ERR_HQExecutionRules')).replace('class ERR_HQExecutionRules', 'struct ERR_HQExecutionRules') + ';'
report = cpp(body(path, 'class ERR_HQExecutionReport')).replace('class ERR_HQExecutionReport', 'struct ERR_HQExecutionReport') + ';'
# Enforce initializes primitive members; reproduce that in the native doubles.
report = re.sub(r'\b(int|float|bool) (\w+);', r'\1 \2=0;', report)
methods = '\n'.join(cpp(body(path, signature)) for signature in (
'protected void Change(', 'void Receive(', 'void Stop(float', 'bool NeedsReport(',
'void Reported(', 'ERR_HQExecutionReport Snapshot(int', 'protected void MoveFailed(',
'protected bool Physical(', 'protected ERR_HQVisualContact SelectContact(', 'void Update(',
))
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
using string=std::string;
struct Vec{float x=0,y=0,z=0;};
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
namespace Math{template<class T> T Max(T a,T b){return std::max(a,b);}template<class T> T Min(T a,T b){return std::min(a,b);}}
namespace ERR_HQPursuitRules{bool Finite(float f){return std::isfinite(f);}bool PositionValid(Vec p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;};
'''+enums+'\n'+rules+'\n'+report+r'''
struct ERR_HQTask{int OperationId=1,OperationRevision=1,ExecutionId=1,BeliefId=7,BeliefRevision=2,HypothesisId=3;
ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;float ExpiresAt=1000000,SupportUntil=0;Vec Destination={100,0,100},SupportPosition;};
struct IEntity{Vec position;bool fit=true,attributable=true,vehicle=false;Vec GetOrigin(){return position;}};
namespace ERR_Actors{bool CanFight(IEntity* a){return a&&a->fit;}}
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity* a){return a->vehicle;}}
struct Sampler{std::vector<IEntity*> members;bool owned=true;bool OwnsRoster(int){return owned;}
int InitialStrength(){return members.size();}IEntity* InitialMember(int s){return members.at(s);}
bool CanAttribute(int s,IEntity* a,bool){return a==members.at(s)&&a->attributable;}};
struct ERR_HQVisualContact{Vec Position;float ObservedAt=0;};
namespace ERR_HQObservation{Array<ERR_HQVisualContact*> visible;void Gather(bool,float,Array<ERR_HQVisualContact*>& out){out=visible;}}
namespace ERR_GameMode{int Get(){return 1;}}
struct ERR_HQUnit{ERR_HQTask* Task=nullptr;Sampler* StatusSampler=nullptr;int Group=1;bool Utility=true,eligible=true;bool Eligible(int){return eligible;}};
float worldTime=0;
struct ERR_HQExecution{
ERR_HQTask* m_Task=nullptr;ERR_HQUnit* m_Unit=nullptr;
ERR_HQExecutionState m_State=ERR_HQExecutionState::QUEUED,m_Phase=ERR_HQExecutionState::QUEUED;
int m_Sequence=0,m_Acknowledged=0,m_ShotCount=0,m_ShotLimit=0,m_SuppressAttempts=0;float m_NextSuppressAttempt=0,m_SuppressSeenAt=-1;
float m_ObservedAt=0,m_NextSample=0,m_NextHeartbeat=0,m_ProgressAt=0,m_BestDistance=-1,m_Remaining=0,m_ArrivedAt=-1,m_QuietAt=-1;
Vec m_Position,m_QuietPosition,m_ContactPosition,m_SuppressPosition;float m_ContactAt=-1,m_AmmoAt=-1,m_SupportUntil=0,m_SearchedRadius=0;
bool m_Contact=false,m_MovePaused=false,m_Bound=false,m_SuppressAttempted=false,m_SupportReady=false,m_PhysicallyArrived=false,m_HasPhysicalSample=false,m_Suppress=false;
string m_Detail;int pauses=0,resumes=0,suppressionRequests=0;
void RevokeSupport(){m_SupportReady=false;m_SupportUntil=0;}
void CancelSuppress(){m_Suppress=false;RevokeSupport();}
void Unbind(){CancelSuppress();m_Bound=false;}
void Bind(ERR_HQUnit* u){m_Unit=u;m_Bound=true;}
void PauseMove(){if(!m_MovePaused)pauses++;m_MovePaused=true;}
void ResumeMove(){if(m_MovePaused)resumes++;m_MovePaused=false;}
void Suppress(ERR_HQUnit*,int,float){suppressionRequests++;}
'''+methods+r'''
};
struct Fixture{ERR_HQTask task;Sampler sampler;ERR_HQUnit unit;IEntity a,b,c;ERR_HQExecution execution;
Fixture(){a.position=b.position=c.position={0,0,0};sampler.members={&a,&b,&c};unit.StatusSampler=&sampler;unit.Task=&task;execution.Receive(&task,0);ERR_HQObservation::visible.clear();}
void Tick(float now){worldTime=now;execution.Update(&unit,now);}
ERR_HQExecutionReport* Report(float now){return execution.Snapshot(1,99,now);}
void Arrive(){a.position=b.position=task.Destination;}
};
int main(){
{Fixture f;assert(!f.execution.NeedsReport()&&!f.Report(0));f.Tick(0);assert(f.execution.NeedsReport()&&f.Report(0));}
// FIX uses a current native visual in its assigned sector even when an
// unrelated perception entry comes first. Existing fire keeps its own volume.
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={100,0,100};
ERR_HQVisualContact unrelated{{500,0,100},1000},nearSector{{101,0,100},1000},nearVolume{{119,0,100},1000};
Array<ERR_HQVisualContact*> samples{&unrelated,&nearSector,&nearVolume};
assert(f.execution.SelectContact(samples,1000)==&nearSector);
f.execution.m_Suppress=true;f.execution.m_SuppressPosition={120,0,100};
assert(f.execution.SelectContact(samples,1000)==&nearVolume);
nearVolume.ObservedAt=499;assert(f.execution.SelectContact(samples,1000)==&nearSector);
nearVolume.ObservedAt=1001;assert(f.execution.SelectContact(samples,1000)==&nearSector);
nearVolume.ObservedAt=1000;nearVolume.Position={201,0,100};f.execution.m_SuppressPosition={199,0,100};
assert(f.execution.SelectContact(samples,1000)==&nearSector); // original100m sector still binds
nearSector.ObservedAt=499;assert(f.execution.SelectContact(samples,1000)==&unrelated); // contact still reported; Suppress gate rejects it
unrelated.ObservedAt=499;nearVolume.ObservedAt=499;assert(!f.execution.SelectContact(samples,1000));
f.task.OperationRole=ERR_HQOperationRole::OBSERVE;assert(f.execution.SelectContact(samples,1000)==&unrelated);}
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={100,0,100};f.Arrive();
ERR_HQVisualContact unrelated{{500,0,100},0},assigned{{110,0,100},0};ERR_HQObservation::visible={&unrelated,&assigned};
f.Tick(0);assert(f.Report(0)->HasContact&&f.Report(0)->ContactPosition.x==110);}
assert(!ERR_HQExecutionRules::Arrived(0,0)&&!ERR_HQExecutionRules::Arrived(3,1)&&ERR_HQExecutionRules::Arrived(3,2));
assert(!ERR_HQExecutionRules::Arrived(33,33)&&!ERR_HQExecutionRules::Arrived(3,4));
assert(!ERR_HQExecutionRules::SupportFresh(20001,0)&&ERR_HQExecutionRules::SupportFresh(10000,0)&&!ERR_HQExecutionRules::SupportFresh(10000,10000));
assert(!ERR_HQExecutionRules::AmmoReserve(60,3)&&ERR_HQExecutionRules::AmmoReserve(61,3));
for(int gate=0;gate<4;gate++)assert(!ERR_HQExecutionRules::ShotProof(gate!=0,gate!=1,gate!=2,gate!=3,0,500,0,8));
assert(ERR_HQExecutionRules::ShotProof(true,true,true,true,0,500,0,8));
assert(!ERR_HQExecutionRules::ShotProof(true,true,true,true,0,2501,0,8));
assert(!ERR_HQExecutionRules::ShotProof(true,true,true,true,0,500,8,8));
// Waypoint completion is deliberately not an input: one nearby member and
// group-origin-at-target do not establish physical arrival for three soldiers.
{Fixture f;f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::TRAVELLING);
f.a.position=f.task.Destination;f.Tick(2000);assert(f.Report(2000)->State==ERR_HQExecutionState::TRAVELLING);
f.b.position=f.task.Destination;f.b.attributable=false;f.Tick(4000);assert(f.execution.m_ArrivedAt==4000); // majority of two original eligible members
f.a.vehicle=true;f.Tick(6000);assert(f.execution.m_ArrivedAt<0);
f.sampler.owned=false;f.Tick(8000);assert(f.Report(8000)->State==ERR_HQExecutionState::FAILED);}
// Contact-changed and mean distance below 40m cannot impersonate the member arrival quorum.
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::OBSERVE;f.a.position={110,0,100};f.b.position=f.c.position={150,0,100};
ERR_HQVisualContact contact;contact.Position={120,0,100};ERR_HQObservation::visible.push_back(&contact);f.Tick(0);
auto r=f.Report(0);assert(r->State==ERR_HQExecutionState::CONTACT_CHANGED&&r->DistanceRemaining<40&&!r->PhysicallyArrived);}
// Quiet, stationary, actual witness footprint only; neither 500m grid nor whole search radius.
{Fixture f;f.Arrive();f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::ARRIVED);
f.Tick(2000);f.Tick(31999);assert(f.Report(31999)->SearchedRadius==0);
f.Tick(34000);auto r=f.Report(34000);assert(r->State==ERR_HQExecutionState::COMPLETED&&r->SearchedRadius==10&&r->Position.x==100);}
// Moving the witness resets the inspected patch's dwell; contact resets it and pauses search.
{Fixture f;f.Arrive();f.Tick(0);f.Tick(2000);f.a.position.x+=6;f.b.position.x+=6;f.Tick(30000);
assert(f.Report(30000)->SearchedRadius==0);ERR_HQVisualContact contact;contact.Position={120,0,100};contact.ObservedAt=32000;
ERR_HQObservation::visible.push_back(&contact);f.Tick(32000);assert(f.Report(32000)->HasContact&&f.execution.pauses==1);
ERR_HQObservation::visible.clear();f.Tick(34000);f.Tick(36000);f.Tick(64000);assert(f.Report(64000)->SearchedRadius==0);
f.Tick(66000);assert(f.Report(66000)->SearchedRadius==10&&f.execution.resumes==1);}
// A native failure cannot masquerade as arrived; actual progress can recover.
// Unchanged sustained SEARCH contact refreshes after15 seconds, including
// after radio acknowledgment, without producing a new report each local tick.
{Fixture f;ERR_HQVisualContact contact{{120,0,100},0};ERR_HQObservation::visible={&contact};f.Tick(0);
auto first=f.Report(0);assert(first->HasContact&&first->State==ERR_HQExecutionState::CONTACT_CHANGED);
f.execution.Reported(first);assert(!f.execution.NeedsReport());
for(int now=2000;now<=14000;now+=2000){contact.ObservedAt=now;f.Tick(now);assert(!f.execution.NeedsReport());}
contact.ObservedAt=16000;f.Tick(16000);auto fresh=f.Report(16000);
assert(fresh&&fresh->ObservedAt==16000&&fresh->ContactObservedAt==16000&&fresh->ReportSequence==first->ReportSequence+1);
f.execution.Reported(fresh);for(int now=18000;now<=30000;now+=2000){contact.ObservedAt=now;f.Tick(now);assert(!f.execution.NeedsReport());}
contact.ObservedAt=32000;f.Tick(32000);assert(f.Report(32000)->ReportSequence==fresh->ReportSequence+1);}
{Fixture f;f.Tick(0);worldTime=1000;f.execution.MoveFailed(1,nullptr,true,f.task.Destination);
assert(f.Report(1000)->State==ERR_HQExecutionState::BLOCKED);f.Tick(2000);assert(f.Report(2000)->State==ERR_HQExecutionState::BLOCKED);
f.a.position=f.b.position=f.c.position={20,0,20};f.Tick(4000);assert(f.Report(4000)->State==ERR_HQExecutionState::TRAVELLING);}
{Fixture f;f.Tick(0);f.Tick(60000);assert(f.Report(60000)->State==ERR_HQExecutionState::BLOCKED&&f.Report(60000)->SearchedRadius==0);
f.Tick(180000);assert(f.Report(180000)->State==ERR_HQExecutionState::FAILED&&f.Report(180000)->SearchedRadius==0);}
// Physical hold completion is not negative search evidence.
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::BLOCK;f.Arrive();f.Tick(0);f.Tick(45000);
assert(f.Report(45000)->State==ERR_HQExecutionState::COMPLETED&&f.Report(45000)->SearchedRadius==0);}
// Unsupported bounds halt locally even when radios are unavailable.
// Owned suppression suspends only the old hold. Sampling cannot re-add the
// higher-priority Defend until fire ends; cancellation resumes the same order,
// while release/supersession never resurrects its paused waypoint.
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={160,0,100};f.Arrive();
ERR_HQVisualContact contact{{160,0,100},0};ERR_HQObservation::visible={&contact};f.Tick(0);
f.execution.m_Suppress=true;f.execution.PauseMove();contact.ObservedAt=2000;f.Tick(2000);
assert(f.execution.m_MovePaused&&f.execution.resumes==0);
f.execution.CancelSuppress();contact.ObservedAt=4000;f.Tick(4000);assert(!f.execution.m_MovePaused&&f.execution.resumes==1);
f.execution.m_Suppress=true;f.execution.PauseMove();ERR_HQObservation::visible.clear();f.Tick(6000);
assert(f.execution.m_Suppress&&f.execution.m_MovePaused&&f.execution.resumes==1&&!f.Report(6000)->HasContact&&!f.Report(6000)->SupportReady);
f.execution.m_Suppress=true;f.execution.PauseMove();f.execution.Stop(7000,true);
assert(!f.execution.m_Suppress&&f.execution.m_MovePaused&&f.execution.resumes==1);}
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FLANK;f.task.SupportUntil=6000;f.Tick(0);f.Tick(6000);
assert(f.execution.pauses==1&&f.Report(6000)->State==ERR_HQExecutionState::BLOCKED);f.Tick(60000);
assert(f.Report(60000)->State==ERR_HQExecutionState::FAILED);}
// A short bound must move inside its five-metre radius, then completes immediately.
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FLANK;f.task.SupportUntil=20000;
f.a.position=f.b.position={80,0,100};f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::TRAVELLING);
f.a.position=f.b.position={96,0,100};f.Tick(2000);
assert(f.Report(2000)->State==ERR_HQExecutionState::COMPLETED&&f.Report(2000)->SearchedRadius==0);}
// Snapshot timestamps don't refresh while queued; delayed ack can't consume a newer event.
{Fixture f;f.Tick(0);auto old=f.Report(999);assert(old->ObservedAt==0);f.Tick(60000);f.execution.Reported(old);
assert(f.execution.NeedsReport());auto latest=f.Report(60000);latest->ExecutionId=99;f.execution.Reported(latest);assert(f.execution.NeedsReport());
latest->ExecutionId=1;f.execution.Reported(latest);assert(!f.execution.NeedsReport());f.execution.Stop(61000,true);assert(f.Report(61000)->State==ERR_HQExecutionState::SUPERSEDED);}
}
''')
print('PASS: actual execution lifecycle, original physical arrivals, failed movement, quiet footprint, local support expiry and delivered report acknowledgments')
source = path.read_text()
shot = body(path, 'void Shot(')
assert 'm_Unit.Utility.GetExecutedAction() == m_Suppress' in shot
assert 'SCR_AISuppressBehavior.Cast(utility.GetExecutedAction())' in shot
assert 'behavior.GetGroupActivityContext() == m_Suppress' in shot
assert 'CanAttribute(slot, actor, false)' in shot
assert 'OnProjectileShot' in source and 'RemoveScriptHandler' in source
assert 'SetFailReason(EAIActionFailReason.CANCELLED)' in source
suppression = body(path, 'protected void Suppress(')
assert 'new ERR_HQSuppressActivity(unit.Utility, null, volume)' in suppression
assert suppression.index('PauseMove();') < suppression.index('new ERR_HQSuppressActivity')
assert 'SetPriority(PRIORITY_ACTIVITY_ATTACK_CLUSTER + 1)' in source
assert 'SetPriority(SCR_AIActionBase.PRIORITY_BEHAVIOR_ATTACK_SELECTED + 1)' in source
assert 'SetPriorityLevel' not in suppression and 'PRIORITY_LEVEL_PLAYER' not in source
assert 'ResumeMove' not in body(path, 'void Stop(float') and 'ResumeMove' not in body(path, 'protected void Unbind(')
assert 'QueryEntitiesByAABB' in body(path, 'protected bool ClearFriendlyLaneFrom(')
assert 'ClearFriendlyLaneFrom(actor.GetOrigin(), m_SuppressPosition)' in shot
assert 'm_Memory' not in source and 'GetPlayers' not in source
assert 'GetCurrentWaypoint' not in source and 'OnWaypointCompleted' not in source
print('PASS: native support wiring requires own activity, soldier suppression, actual projectile event and immutable roster; no waypoint-completion arrival shortcut')
lod = body(path, 'class ERR_HQExecutionLOD').replace('class ERR_HQExecutionLOD', 'struct ERR_HQExecutionLOD') + ';'
lod = lod.replace('null', 'nullptr').replace('SCR_AIUtils.', 'SCR_AIUtils::').replace('int Previous;', 'int Previous=0;')
for typ in ('AIAgent', 'IEntity', 'ERR_HQUnit'):
lod = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', lod)
for name in ('Agent', 'unit', 'Group', 'StatusSampler'):
lod = lod.replace(name + '.', name + '->')
run(r'''
#include <cassert>
struct AIAgent{int lod=-1,wakes=0;int GetPermanentLOD(){return lod;}void SetPermanentLOD(int v){lod=v;}void ActivateAI(){wakes++;}};
struct Group:AIAgent{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
struct IEntity{AIAgent* agent=nullptr;bool original=true;};
namespace SCR_AIUtils{AIAgent* GetAIAgent(IEntity* actor){return actor->agent;}}
struct Sampler{bool owned=true;bool OwnsRoster(Group*){return owned;}bool CanAttribute(int slot,IEntity* actor,bool){return slot==0&&actor->original;}};
struct ERR_HQUnit{struct Group* Group;Sampler* StatusSampler;};
'''+lod+r'''
int main(){Group group;Sampler sampler;ERR_HQUnit unit{&group,&sampler};
{ERR_HQExecutionLOD lease;group.lod=2;lease.Begin(&group);assert(group.lod==0&&group.wakes==1);lease.Restore(&unit);assert(group.lod==2);}
AIAgent agent;IEntity actor{&agent,true};
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);assert(agent.lod==0&&agent.wakes==1);lease.Restore(&unit);assert(agent.lod==-1);}
// An intervening native override, player takeover or transfer belongs to its new owner.
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);agent.lod=3;lease.Restore(&unit);assert(agent.lod==3);}
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);group.players=1;lease.Restore(&unit);assert(agent.lod==0);group.players=0;lease.Restore(&unit);assert(agent.lod==3);}
{ERR_HQExecutionLOD lease;agent.lod=-1;lease.Begin(&agent,&actor,0);actor.original=false;lease.Restore(&unit);assert(agent.lod==0);actor.original=true;lease.Restore(&unit);assert(agent.lod==-1);}
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);AIAgent replacement;actor.agent=&replacement;lease.Restore(&unit);assert(agent.lod==0&&replacement.lod==-1);}
}
''')
assert 'm_LODs.Clear()' in body(path, 'protected void Unbind(')
assert 'groupLOD.Begin(unit.Group)' in body(path, 'protected void Bind(')
assert 'CanAttribute(slot, actor, false)' in body(path, 'protected void Bind(')
assert 'DeactivateAI' not in body(path, 'class ERR_HQExecutionLOD')
print('PASS: actual finite native LOD leases restore prior overrides and protect transferred agents, player ownership and intervening overrides')
movement = '\n'.join(cpp(body(path, signature)).replace('Group.', 'Group->') for signature in (
'protected void PauseMove(', 'protected void ResumeMove(',
))
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
struct ERR_HQTask{};struct Waypoint{};
struct Group{std::vector<Waypoint*> waypoints;int removed=0,added=0;
void RemoveWaypoint(Waypoint*w){waypoints.erase(std::remove(waypoints.begin(),waypoints.end(),w),waypoints.end());removed++;}
void AddWaypointAt(Waypoint*w,int i){waypoints.insert(waypoints.begin()+i,w);added++;}};
struct ERR_HQUnit{struct Group*Group;struct Waypoint*Waypoint;ERR_HQTask*Task;};
struct Execution{bool m_MovePaused=false;ERR_HQUnit*m_Unit=nullptr;ERR_HQTask*m_Task=nullptr;
'''+movement+r'''
};
int main(){Group group;Waypoint hold,unrelated,replacement;ERR_HQTask task,newTask;ERR_HQUnit unit{&group,&hold,&task};
Execution e;e.m_Unit=&unit;e.m_Task=&task;group.waypoints={&hold,&unrelated};
e.PauseMove();e.PauseMove();assert(group.removed==1&&group.waypoints.size()==1&&group.waypoints[0]==&unrelated);
e.ResumeMove();e.ResumeMove();assert(group.added==1&&group.waypoints.size()==2&&group.waypoints[0]==&hold);
e.PauseMove();unit.Task=&newTask;unit.Waypoint=&replacement;e.ResumeMove();
assert(group.added==1&&group.waypoints.size()==1&&group.waypoints[0]==&unrelated); // superseding owner is untouched
unit.Task=&task;unit.Waypoint=nullptr;e.ResumeMove();assert(group.added==1); // released/deleted hold cannot return
e.m_Unit=nullptr;e.ResumeMove();assert(group.added==1);
}
''')
print('PASS: actual waypoint pause/resume owns only its finite order, preserves unrelated routes and cannot resurrect a superseded or deleted hold')
# The installed native source is the authority for action arbitration. This is
# an optional engine compatibility check in portable CI, and a real assertion
# against the local game constants during native qualification.
from pathlib import Path
import hashlib
import struct
import sys
import zlib
native_pak = Path.home() / '.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak'
if native_pak.is_file():
sys.path.insert(0, str(ROOT / 'tools'))
from pak_inventory import Limits, parse_tree
wanted = {
'scripts/Game/AI/Behavior/SCR_AIAction.c',
'scripts/Game/AI/Behavior/SCR_AISuppressActivity.c',
'scripts/Game/AI/Behavior/SCR_AIBehavior.c',
'scripts/Game/generated/AI/AIActionBase.c',
'scripts/Game/AI/Reaction/SCR_AIGoalReaction.c',
}
native = {}
with native_pak.open('rb') as stream:
assert stream.read(12)[:4] == b'FORM'
chunks = {}
for _ in range(3):
tag, size = struct.unpack('>4sI', stream.read(8))
chunks[tag] = stream.tell(), size
stream.seek(size, 1)
start, size = chunks[b'FILE']
stream.seek(start)
tree = stream.read(size)
start, size = chunks[b'DATA']
entries, _, _ = parse_tree(tree, start, start + size, Limits())
for entry in entries:
if entry['path'] not in wanted:
continue
stream.seek(entry['offset'])
content = stream.read(entry['stored_bytes'])
if entry['compression'] == 'zlib':
content = zlib.decompress(content)
native[entry['path']] = content.decode('utf-8')
assert set(native) == wanted, 'installed native priority definitions changed'
action = native['scripts/Game/AI/Behavior/SCR_AIAction.c']
constants = {}
for name, expression in re.findall(r'const static float\s+(PRIORITY_\w+)\s*=\s*([^;]+);', action):
tokens = [part.strip() for part in expression.split('+')]
constants[name] = sum(constants[token] if token in constants else float(token) for token in tokens)
priority = re.search(r'SetPriority\((PRIORITY_ACTIVITY_\w+) \+ (\d+)\)', source)
assert priority
chosen = constants[priority[1]] + int(priority[2])
assert constants['PRIORITY_LEVEL_NORMAL'] == 0
assert constants['PRIORITY_ACTIVITY_SUPPRESS'] < constants['PRIORITY_ACTIVITY_DEFEND'] < chosen
assert constants['PRIORITY_ACTIVITY_ATTACK_CLUSTER'] < chosen < constants['PRIORITY_LEVEL_PLAYER']
soldier = constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] + 1
assert constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] < soldier < constants['PRIORITY_BEHAVIOR_PROVIDE_AMMO']
assert soldier < constants['PRIORITY_BEHAVIOR_MEDIC_HEAL'] < constants['PRIORITY_BEHAVIOR_MOVE_FROM_DANGER']
assert chosen < constants['PRIORITY_ACTIVITY_HEAL'] < constants['PRIORITY_ACTIVITY_COMBAT_WITH_VEHICLES']
assert constants['PRIORITY_BEHAVIOR_SUPPRESS'] < constants['PRIORITY_BEHAVIOR_ATTACK_NOT_SELECTED']
assert constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] < constants['PRIORITY_BEHAVIOR_MOVE_FROM_DANGER']
assert 'priorityLevel = PRIORITY_LEVEL_NORMAL' in native['scripts/Game/AI/Behavior/SCR_AISuppressActivity.c']
assert 'proto external void SetPriority(float priority)' in native['scripts/Game/generated/AI/AIActionBase.c']
assert 'event float EvaluatePriorityLevel() { return 0.0; }' in native['scripts/Game/generated/AI/AIActionBase.c']
reaction = native['scripts/Game/AI/Reaction/SCR_AIGoalReaction.c'].split('class SCR_AIGoalReaction_Suppress', 1)[1].split('\n};', 1)[0]
assert 'utility.AddAction(behavior)' in reaction and '.Fail(' not in reaction and 'SetPriority' not in reaction
assert 'override void PerformReaction(notnull SCR_AIUtilityComponent utility, SCR_AIMessageBase message)' in reaction
for field in ('msg.m_RelatedGroupActivity', 'msg.m_Volume', 'msg.m_fDuration_s', 'msg.m_fFireRate', 'msg.m_fPriorityLevel'):
assert field in reaction
behavior = native['scripts/Game/AI/Behavior/SCR_AIBehavior.c']
assert 'SCR_AIActivityBase GetGroupActivityContext()' in behavior and 'GetRelatedGroupActivity()' in behavior
print(f'PASS: installed native priorities authorize group suppression{chosen:g} over ordinary combat70, below native healing80; tagged soldier suppression91 yields ammo100, retreat110, medics111 and immediate danger (action SHA256={hashlib.sha256(action.encode()).hexdigest()})')
else:
print('SKIP: native action priority compatibility requires installed Arma Reforger data007.pak')
# Execute the production suppress/shot methods, rather than granting readiness in
# the double. The callback must belong to the exact activity and original actor.
def suppress_cpp(text):
text = cpp(text)
for typ in ('SCR_AISuppressActivity', 'ERR_HQSuppressActivity', 'SCR_AISuppressBehavior',
'SCR_AIUtilityComponent', 'AIAgent', 'BaseWeaponComponent',
'SCR_AISuppressionVolumeBox', 'ERR_HQExecutionShooter'):
text = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', text)
text = text.replace('SCR_AIUtilityComponent* utility;', 'SCR_AIUtilityComponent* utility=nullptr;')
text = text.replace('SCR_AIUtilityComponent.Cast(agent.FindComponent(SCR_AIUtilityComponent))', 'agent->utility')
text = text.replace('SCR_AISuppressBehavior.Cast(utility.GetExecutedAction())', 'utility->GetExecutedAction()')
text = text.replace('SCR_AIUtils.GetAIAgent(actor)', 'actor->agent')
for name in ('m_Suppress', 'Utility', 'behavior', 'weapon', 'shooter'):
text = text.replace(name + '.', name + '->')
for name in ('EWeaponType', 'EAIActionState', 'EAIActionFailReason'):
text = text.replace(name + '.', name + '::')
text = text.replace('m_Shooters.Insert(shooter)', 'm_Shooters.push_back(shooter)')
text = text.replace('"4 0 4"', 'Vec{4,0,4}').replace('"4 2 4"', 'Vec{4,2,4}')
return text
combat_methods = '\n'.join(suppress_cpp(body(path, signature)) for signature in (
'protected void RevokeSupport(', 'protected void CancelSuppress(', 'void Shot(', 'protected void Suppress(',
))
run(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 operator+(Vec v){return{x+v.x,y+v.y,z+v.z};}Vec operator-(Vec v){return{x-v.x,y-v.y,z-v.z};}};
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
namespace Math{template<class T> T Min(T a,T b){return std::min(a,b);}}
namespace ERR_HQPursuitRules{bool Finite(float v){return std::isfinite(v);}}
'''+enums+'\n'+rules+r'''
enum class EWeaponType{WT_RIFLE,WT_MACHINEGUN,WT_SNIPERRIFLE,WT_ROCKETLAUNCHER};
enum class EAIActionState{RUNNING,FAILED,COMPLETED};enum class EAIActionFailReason{CANCELLED};
struct BaseWeaponComponent{EWeaponType type=EWeaponType::WT_RIFLE;EWeaponType GetWeaponType(){return type;}};
struct SCR_AISuppressionVolumeBox{Vec min,max;SCR_AISuppressionVolumeBox(Vec a,Vec b):min(a),max(b){}};
struct GroupUtility;
struct SCR_AISuppressActivity{EAIActionState state=EAIActionState::RUNNING;int cancels=0;Vec target;
SCR_AISuppressActivity(GroupUtility*,void*,SCR_AISuppressionVolumeBox*v):target(v->min+Vec{4,0,4}){}
void SetFailReason(EAIActionFailReason){}void Fail(){state=EAIActionState::FAILED;cancels++;}EAIActionState GetActionState(){return state;}};
struct ERR_HQSuppressActivity:SCR_AISuppressActivity{using SCR_AISuppressActivity::SCR_AISuppressActivity;};
struct SCR_AISuppressBehavior{SCR_AISuppressActivity* context=nullptr;SCR_AISuppressActivity* GetGroupActivityContext(){return context;}};
struct SCR_AIUtilityComponent{SCR_AISuppressBehavior* executed=nullptr;SCR_AISuppressBehavior* GetExecutedAction(){return executed;}};
struct AIAgent{SCR_AIUtilityComponent* utility=nullptr;};
struct IEntity{AIAgent*agent=nullptr;bool fit=true,original=true;Vec GetOrigin(){return{4,0,7};}};
namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->fit;}}
struct Sampler{std::vector<IEntity*> members;int InitialStrength(){return members.size();}
IEntity* InitialMember(int i){return members.at(i);}bool CanAttribute(int s,IEntity*a,bool){return a->original&&members.at(s)==a;}};
struct GroupUtility{SCR_AISuppressActivity* executed=nullptr;int actions=0;SCR_AISuppressActivity* GetExecutedAction(){return executed;}
void AddAction(SCR_AISuppressActivity* action){executed=action;actions++;}};
struct ERR_HQTask{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::FIX;Vec SupportPosition={100,0,0};float ExpiresAt=90000;};
struct ERR_HQUnit{Sampler*StatusSampler;GroupUtility*Utility;};
struct ERR_HQExecutionShooter{void Bind(void*,IEntity*,int){}};
float worldTime=0;
struct Combat{
ERR_HQTask*m_Task;ERR_HQUnit*m_Unit;SCR_AISuppressActivity*m_Suppress=nullptr;std::vector<ERR_HQExecutionShooter*>m_Shooters;
bool m_AmmoReady=false,m_Contact=true,m_SupportReady=false,m_SuppressAttempted=false;
int m_Sequence=0,m_ShotCount=0,m_ShotLimit=0,m_SuppressAttempts=0,pauses=0;
float m_AmmoAt=-1,m_ContactAt=0,m_ObservedAt=0,m_SupportUntil=0,m_SuppressDeadline=0,m_SuppressSeenAt=-1,m_NextSuppressAttempt=0;
Vec m_ContactPosition={100,0,0},m_SuppressPosition,m_Position;bool actualAmmo=true,clearLane=true;
bool Ammo(ERR_HQUnit*,int,float now){m_AmmoAt=now;return actualAmmo;}
bool ClearFriendlyLane(Vec){return clearLane;}bool ClearFriendlyLaneFrom(Vec from,Vec){assert(from.x==4&&from.z==7);return clearLane;}void PauseMove(){pauses++;}
'''+combat_methods+r'''
};
struct Fixture{ERR_HQTask task;Sampler sampler;GroupUtility utility;ERR_HQUnit unit{&sampler,&utility};
SCR_AIUtilityComponent soldier;AIAgent agent{&soldier};IEntity actor{&agent,true,true};BaseWeaponComponent rifle;SCR_AISuppressBehavior behavior;Combat e;
Fixture(){sampler.members={&actor};e.m_Unit=&unit;e.m_Task=&task;}
void Tick(float now,bool visible=true){worldTime=now;e.m_Contact=visible;e.m_ContactAt=visible?now:-1;e.Suppress(&unit,1,now);behavior.context=e.m_Suppress;soldier.executed=&behavior;}
void Fire(float now){worldTime=now;e.Shot(&actor,0,&rifle);}
};
int main(){
assert(ERR_HQExecutionRules::AreaFresh(0,8000)&&!ERR_HQExecutionRules::AreaFresh(0,8001));
assert(!ERR_HQExecutionRules::AreaFresh(-1,0)&&!ERR_HQExecutionRules::AreaFresh(1,0));
assert(ERR_HQExecutionRules::RetrySuppress(1,4000,4000)&&!ERR_HQExecutionRules::RetrySuppress(2,0,5000));
// Blind/stale, distant, unsafe or depleted initial inputs never create fire.
{Fixture f;f.Tick(0,false);assert(!f.e.m_Suppress);f.e.actualAmmo=false;f.Tick(0);assert(!f.e.m_Suppress);
f.e.actualAmmo=true;f.e.clearLane=false;f.Tick(0);assert(!f.e.m_Suppress);f.e.clearLane=true;
f.e.m_ContactPosition={30,0,0};f.Tick(0);assert(!f.e.m_Suppress);f.e.m_ContactPosition={201,0,0};f.Tick(0);assert(!f.e.m_Suppress);}
// Real, exact activity shots are required. Merely starting the behavior is not readiness.
{Fixture f;f.Tick(0);auto first=f.e.m_Suppress;assert(first&&!f.e.m_SupportReady&&f.e.pauses==1);
f.Fire(100);assert(f.e.m_ShotCount==1&&f.e.m_SupportReady&&f.e.m_SupportUntil==20100);
f.Tick(2000,false);assert(f.e.m_Suppress==first&&!f.e.m_SupportReady&&first->target.x==100);
f.Fire(2100);assert(f.e.m_ShotCount==2&&!f.e.m_SupportReady); // blind shots consume original budget
f.Tick(8000,false);assert(f.e.m_Suppress==first);f.Tick(8001,false);assert(!f.e.m_Suppress&&first->cancels==1);
f.Tick(10000);assert(!f.e.m_Suppress);f.Tick(12001);assert(f.e.m_Suppress&&f.utility.actions==2&&f.e.m_ShotCount==2);
f.Tick(21000,false);assert(!f.e.m_Suppress);f.Tick(26000);assert(!f.e.m_Suppress&&f.utility.actions==2);}
// Exact behavior context, original members and small arms: unrelated shots cannot prove support.
{Fixture f;f.Tick(0);f.behavior.context=nullptr;f.Fire(100);assert(f.e.m_ShotCount==0&&!f.e.m_SupportReady);
f.behavior.context=f.e.m_Suppress;f.actor.original=false;f.Fire(100);assert(f.e.m_ShotCount==0);
f.actor.original=true;f.rifle.type=EWeaponType::WT_ROCKETLAUNCHER;f.Fire(100);assert(f.e.m_ShotCount==0);
f.rifle.type=EWeaponType::WT_RIFLE;f.utility.executed=nullptr;f.Fire(100);assert(f.e.m_ShotCount==0);}
// Actual new visual outside the original volume cancels it; it never follows hidden actor motion.
{Fixture f;f.Tick(0);auto first=f.e.m_Suppress;f.e.m_ContactPosition={109,0,0};f.Tick(2000);
assert(!f.e.m_Suppress&&first->target.x==100);f.Tick(4000);assert(!f.e.m_Suppress);
f.Tick(6000);assert(f.e.m_Suppress&&f.e.m_Suppress->target.x==109);}
// The shared cap survives retries and blind bursts; safe cancellation is immediate.
{Fixture f;f.Tick(0);f.Fire(100);f.Tick(2000,false);for(int i=0;i<7;i++)f.Fire(2100+i);
assert(f.e.m_ShotCount==8&&!f.e.m_Suppress&&!f.e.m_SupportReady);f.Tick(3000);assert(!f.e.m_Suppress);}
{Fixture f;f.Tick(0);f.e.clearLane=false;f.Fire(100);assert(f.e.m_ShotCount==1&&!f.e.m_Suppress&&!f.e.m_SupportReady);
f.e.clearLane=true;f.Tick(2000);assert(!f.e.m_Suppress);f.Tick(4100);assert(f.e.m_Suppress&&f.e.m_ShotCount==1);}
{Fixture f;f.Tick(0);f.Fire(2600);assert(f.e.m_ShotCount==1&&!f.e.m_Suppress);}
{Fixture f;f.Tick(0);f.e.actualAmmo=false;f.Tick(2000);assert(!f.e.m_Suppress&&!f.e.m_SupportReady);}
{Fixture f;f.Tick(0);f.Fire(100);f.utility.executed=nullptr;f.Tick(2000);assert(!f.e.m_Suppress&&!f.e.m_SupportReady);}
}
''')
print('PASS: actual suppression and projectile methods retain a fixed eight-second last-seen area, deny blind support, require exact owned behavior, preserve finite ammo/shot budgets and bound retries')