Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
112 lines
7.7 KiB
Python
112 lines
7.7 KiB
Python
"""Execute the production infantry combat state machine against waypoint doubles."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryCombat.c'
|
|
s=p.read_text()
|
|
s=s.replace('class ERR_InfantryCombat', 'struct ERR_InfantryCombat')
|
|
s=s.replace('protected ', '').replace('ref ', '').replace('null','nullptr')
|
|
s=s.replace('EAIGroupCombatMode.FIRE_AT_WILL','1').replace('.IsEmpty()', '.empty()')
|
|
for typ in ('AIWaypoint','ERR_HQTask','ERR_HQUnit'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
s=s.replace('bool Held;', 'bool Held=false;').replace('float QuietUntil;', 'float QuietUntil=0;')
|
|
s=s.replace('m_PausedWaypoint;', 'm_PausedWaypoint=nullptr;').replace('m_PausedTask;', 'm_PausedTask=nullptr;')
|
|
for name in ('unit','task','Task','Group','Utility'):
|
|
s=s.replace(name+'.',name+'->')
|
|
s=s.rstrip()+';'
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <algorithm>
|
|
struct AIWaypoint{};
|
|
struct ERR_HQTask {int OperationId=0;bool Maneuver=false,ReleaseOnly=false,RoadIntercept=false;std::string SecuritySite;float ExpiresAt=100000;};
|
|
struct Group {std::vector<AIWaypoint*> queue;int adds=0,removes=0;
|
|
void RemoveWaypoint(AIWaypoint* w){removes++;queue.erase(std::remove(queue.begin(),queue.end(),w),queue.end());}
|
|
void AddWaypointAt(AIWaypoint* w,int slot){assert(slot==0);adds++;queue.insert(queue.begin(),w);}};
|
|
struct Utility{int mode=0,changes=0;void SetCombatMode(int m){mode=m;changes++;}};
|
|
struct ERR_HQUnit{AIWaypoint* Waypoint=nullptr;ERR_HQTask* Task=nullptr;void* Assignment=nullptr;struct Group* Group;struct Utility* Utility;};
|
|
'''+s+r'''
|
|
struct Fixture {Group group;Utility utility;AIWaypoint search,home;ERR_HQTask task;ERR_HQUnit unit;ERR_InfantryCombat combat;
|
|
Fixture(){unit.Group=&group;unit.Utility=&utility;unit.Task=&task;unit.Assignment=&task;unit.Waypoint=&search;group.queue={&search,&home};}
|
|
};
|
|
int main(){
|
|
{Fixture f;assert(f.combat.Update(&f.unit,true,false,1000)==0);assert(f.group.removes==0);
|
|
assert(f.combat.Update(&f.unit,true,true,2000)==1);assert(f.combat.Held&&f.combat.Paused());
|
|
assert(f.group.queue.size()==1&&f.group.queue[0]==&f.home&&f.utility.mode==1);
|
|
assert(f.unit.Task==&f.task&&f.unit.Waypoint==&f.search&&f.unit.Assignment);
|
|
assert(f.combat.Update(&f.unit,true,true,4000)==0&&f.group.removes==1&&f.utility.changes==1);
|
|
assert(f.combat.Update(&f.unit,true,false,18999)==0&&f.combat.Held);
|
|
assert(f.combat.Update(&f.unit,true,false,19000)==2&&!f.combat.Held&&!f.combat.Paused());
|
|
assert(f.group.adds==1&&f.group.queue.size()==2&&f.group.queue[0]==&f.search);
|
|
assert(f.combat.Update(&f.unit,true,false,20000)==0&&f.group.adds==1);}
|
|
// Explicit HQ flank movement survives contact; expiration returns to normal combat.
|
|
{Fixture f;f.task.Maneuver=true;assert(f.combat.Update(&f.unit,true,true,2000)==0&&!f.combat.Held&&f.group.removes==0);f.task.ExpiresAt=3000;assert(f.combat.Update(&f.unit,true,true,3000)==1&&f.combat.Paused());}
|
|
// Never resurrect expired, released, replaced, or newly defensive tasks.
|
|
for(int invalid=0;invalid<6;invalid++){Fixture f;ERR_HQTask replacement;AIWaypoint other;
|
|
f.combat.Update(&f.unit,true,true,0);
|
|
if(invalid==0)f.task.ExpiresAt=15000;
|
|
if(invalid==1)f.unit.Task=nullptr;
|
|
if(invalid==2)f.unit.Waypoint=&other;
|
|
if(invalid==3)f.unit.Task=&replacement;
|
|
if(invalid==4)f.unit.Assignment=nullptr;
|
|
if(invalid==5)f.task.RoadIntercept=true;
|
|
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
|
|
// Existing defensive posts and recalls keep their queue and fire policy.
|
|
for(int defence=0;defence<3;defence++){Fixture f;
|
|
if(defence==0){f.task.SecuritySite="radio";}if(defence==1){f.task.RoadIntercept=true;}if(defence==2){f.task.ReleaseOnly=true;}
|
|
assert(f.combat.Update(&f.unit,true,true,0)==1&&!f.combat.Paused());
|
|
assert(f.group.removes==0&&f.utility.changes==0);
|
|
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
|
|
// Reservations/player/vehicle control cannot insert or remove waypoints.
|
|
{Fixture f;assert(f.combat.Update(&f.unit,false,true,0)==0&&f.group.removes==0);
|
|
f.combat.Update(&f.unit,true,true,0);
|
|
assert(f.combat.Update(&f.unit,false,false,20000)==0&&f.group.adds==0);
|
|
assert(f.combat.Update(&f.unit,true,false,20000)==2&&f.group.adds==1);}
|
|
}
|
|
''')
|
|
print('PASS: actual infantry search suspension, quiet grace, finite resume, expiry/replacement and defensive/ownership boundaries')
|
|
# Guard the production integration: local pause occurs before radio delivery, and
|
|
# only task downlinks are cancelled; status/vehicle messages remain independent.
|
|
hq=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
|
tick=body(hq,'override void Tick(')
|
|
assert tick.index('UpdateInfantryCombat(unit, now)') < tick.index('unit.Downlink.Advance')
|
|
assert 'unit.InfantryCombat.Held || ERR_HunterCombat.Active(unit.Group)' in body(hq,'protected bool Fighting(')
|
|
assert 'if (unit.PendingTask && !unit.PendingTask.Maneuver) LostOrder(unit, now)' in body(hq,'protected void UpdateInfantryCombat(')
|
|
print('PASS: infantry combat gate precedes radio delivery and preserves non-task transports')
|
|
|
|
update=body(hq,'protected void UpdateInfantryCombat(').replace('protected ', '')
|
|
update=update.replace('ERR_HQUnit unit','ERR_HQUnit* unit').replace('unit.', 'unit->')
|
|
update=update.replace('PendingTask.', 'PendingTask->')
|
|
update=update.replace('InfantryCombat.', 'InfantryCombat->').replace('Group.', 'Group->')
|
|
update=update.replace('array<AIAgent> reacting', 'std::vector<AIAgent*> reacting')
|
|
update=update.replace('foreach (AIAgent actor : reacting)', 'for (AIAgent* actor : reacting)').replace('actor.', 'actor->')
|
|
update=update.replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active').replace('ERR_Actors.CanFight', 'ERR_Actors::CanFight').replace('string.Format', 'Format')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
using string=std::string;
|
|
template<class... T>string Format(T...){return "entry";}
|
|
struct AIAgent{bool fit=true;int activations=0;AIAgent* GetControlledEntity(){return this;}void ActivateAI(){activations++;}};
|
|
struct Group{int activations=0;std::vector<AIAgent*> members;void ActivateAI(){activations++;}void GetAgents(std::vector<AIAgent*>& a){a=members;}};
|
|
struct ERR_Actors{static bool CanFight(AIAgent* a){return a->fit;}};
|
|
struct ERR_HunterCombat{static bool Active(Group*){return true;}};
|
|
struct ERR_HQUnit;
|
|
struct State{int change=1;bool paused=true;int Update(ERR_HQUnit*,bool eligible,bool,float){return eligible?change:0;}bool Paused(){return paused;}};
|
|
struct Task{bool Maneuver=false;};
|
|
struct ERR_HQUnit{int Id=7;string ForceSourceId="town";struct Group* Group;State* InfantryCombat;Task* PendingTask=nullptr;bool eligible=true;bool Eligible(int){return eligible;}};
|
|
struct Journal{int logs=0;void Write(string,string){logs++;}};
|
|
struct HQ{int m_Mode=1,cancelled=0;Journal m_Journal;void LostOrder(ERR_HQUnit* u,float){cancelled++;u->PendingTask=nullptr;}
|
|
'''+update+r'''
|
|
};
|
|
int main(){HQ h;Group g;State state;ERR_HQUnit u;u.Group=&g;u.InfantryCombat=&state;
|
|
AIAgent fit,casualty;casualty.fit=false;g.members={&fit,&casualty,nullptr};Task pending;u.PendingTask=&pending;
|
|
h.UpdateInfantryCombat(&u,1000);assert(g.activations==1&&fit.activations==1&&casualty.activations==0&&h.cancelled==1&&h.m_Journal.logs==1);
|
|
state.change=0;h.UpdateInfantryCombat(&u,2000);assert(g.activations==1&&h.cancelled==1);
|
|
state.change=1;u.PendingTask=nullptr;h.UpdateInfantryCombat(&u,3000);assert(h.cancelled==1&&fit.activations==2);
|
|
u.eligible=false;h.UpdateInfantryCombat(&u,4000);assert(g.activations==2&&h.m_Journal.logs==2);
|
|
u.eligible=true;pending.Maneuver=true;u.PendingTask=&pending;h.UpdateInfantryCombat(&u,4500);assert(u.PendingTask==&pending&&h.cancelled==1);state.change=2;h.UpdateInfantryCombat(&u,5000);assert(g.activations==3&&h.m_Journal.logs==4);
|
|
}
|
|
''')
|
|
print('PASS: actual HQ contact transition wakes only fit squad members, cancels only task orders and logs changes once')
|