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

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')