Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
457 lines
35 KiB
Python
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')
|