Files
EscapistsReintroduced/tests/test_hq_ammo_reporting.py
T
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

155 lines
12 KiB
Python

"""Actual local ammo-report policy and HQ scheduler bodies; no native inventory/radio claim."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
BASE=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT',ROOT))
P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c'
H=Path(os.environ.get('ERR_HQ_SOURCE',BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
A=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'))
M=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c'
def cpp(s):
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQMountedAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
for v in ('stock','mounted','report','unit','route'):
s=s.replace(v+'.',v+'->')
return s.replace('MountedAmmo.','MountedAmmo->').replace('AmmoState.','AmmoState->').replace('Task.','Task->')
policy=cpp(P.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
methods='\n'.join(cpp(body(H,x)) for x in ('protected void StatusReport(', 'protected void QueueReports('))
# The production hook must preserve the only existing delivery gate and use the policy per identity.
source=H.read_text()
assert 'unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());' in source
assert 'if (delivered == 1)' in source and 'm_Watch.Ingest(unit.PendingStatus, now)' in source
assert 'm_Watch' not in methods and 'm_Network.Route(unit->Radio)' in methods
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <limits>
using string=std::string;
template<class... T>string Format(T...){return "sample";}
struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v)&&v>-1e20&&v<1e20;}};
struct ERR_HQAmmoSnapshot{
bool MineComplete=false;int MineMembers=0,ReadyMines=0;
bool Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0;
int LauncherMuzzles=0,LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
'''+body(A,' bool Valid(')+r'''
};
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Weapons=1,Muzzles=1,Magazines=1,Rounds=100,Capacity=100,Chambers=0,LowMagazines=0;
'''+body(M,' bool Valid(')+r'''
};
struct ERR_HQStatusReport{ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="formation-1";float ObservedAt=0;};
'''+policy+r'''
struct Sampler{int observations=0,samples=0;bool valid=true;ERR_HQAmmoSnapshot stock;ERR_HQMountedAmmoSnapshot mounted;
void ObserveDeaths(){observations++;}
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;
auto r=new ERR_HQStatusReport;r->ObservedAt=now;r->MessageId=id;r->AmmoState=new ERR_HQAmmoSnapshot(stock);r->MountedAmmo=new ERR_HQMountedAmmoSnapshot(mounted);return r;}};
struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
struct Task{float ArrivedAt=0;};
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
struct ERR_HQUnit{bool ExecutionTurn=true;float NextContact=0;struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
struct HQ{bool m_Coordinated=false;void QueueExecution(ERR_HQUnit*,float){}void ContactReport(ERR_HQUnit*,float){}
Network m_Network;Journal m_Journal;int delivered=0;
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
void ObserveBody(ERR_HQUnit*,float){}
void Sweep(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&!u->visible&&m_Network.available)u->sweep=true;}
'''+methods+r'''
void Finish(ERR_HQUnit& u,bool success=true){assert(u.Uplink);if(u.PendingStatus&&success)delivered++;
u.Uplink=nullptr;u.PendingStatus=nullptr;u.contact=false;}
};
int main(){
// Mine scarcity/completeness alone never consumes the early status slot.
for(int state=0;state<3;state++){
HQ onlyMines;ERR_HQUnit squad;squad.NextStatus=180000;squad.StatusSampler.stock.MineMembers=4;
squad.StatusSampler.stock.ReadyMines=state==2?2:0;squad.StatusSampler.stock.MineComplete=state!=1;
onlyMines.QueueReports(&squad,0);assert(squad.contact&&!squad.PendingStatus&&onlyMines.m_Journal.writes==0);
onlyMines.Finish(squad);squad.visible=false;squad.NextStatus=1000;onlyMines.QueueReports(&squad,1000);
assert(squad.PendingStatus&&squad.PendingStatus->AmmoState->ReadyMines==(state==2?2:0)&&squad.PendingStatus->AmmoState->MineComplete==(state!=1)&&squad.PendingStatus->AmmoState->MineMembers==4);
assert(squad.Uplink->Payload.find("; carried mines=")!=string::npos);
if(state==1)assert(squad.Uplink->Payload.find("carried mines=unknown/incomplete")!=string::npos);
auto sent=squad.PendingStatus->AmmoState;squad.StatusSampler.stock.ReadyMines=77;squad.StatusSampler.stock.MineMembers=0;squad.StatusSampler.stock.MineComplete=!sent->MineComplete;
assert(sent->ReadyMines==(state==2?2:0)&&sent->MineComplete==(state!=1)&&sent->MineMembers==4);
}
// Exact inclusive threshold counts chambers, ignores launcher stock, requires the complete current living roster.
ERR_HQAmmoReporting p; p.Initialize(6,0);ERR_HQStatusReport r;ERR_HQAmmoSnapshot a;r.AmmoState=&a;
assert(!p.Urgent(&r,0));a.SmallArmsRounds=120;assert(p.Urgent(&r,0));
a.SmallArmsRounds=119;a.SmallArmsChambers=2;assert(!p.Urgent(&r,0));a.SmallArmsChambers=1;assert(p.Urgent(&r,0));
a.SmallArmsRounds=0;a.SmallArmsChambers=0;a.LauncherRounds=100;assert(p.Urgent(&r,0));
a.Complete=false;assert(!p.Urgent(&r,0));a.Complete=true;
a.SmallArmsMuzzles=0;assert(!p.Urgent(&r,0));a.SmallArmsMuzzles=4;
a.Members=3;assert(!p.Urgent(&r,0));r.Count=3;r.Fit=3;assert(p.Urgent(&r,0));
a.Members=0;r.Count=0;assert(!p.Urgent(&r,0));a.Members=33;r.Count=33;assert(!p.Urgent(&r,0));
a.Members=4;r.Count=4;a.SmallArmsRounds=-1;assert(!p.Urgent(&r,0));a.SmallArmsRounds=0;
r.ObservedAt=1;assert(!p.Urgent(&r,0));r.ObservedAt=0;assert(!p.Urgent(&r,1));
assert(!p.Urgent(nullptr,0));r.AmmoState=nullptr;assert(!p.Urgent(&r,0));r.AmmoState=&a;
assert(!p.Urgent(&r,std::numeric_limits<float>::quiet_NaN()));
// Each registration phase is bounded to a 0..25 s delay. Late evaluations scan once, never catch up in bursts.
for(int id=1;id<=128;id++){ERR_HQAmmoReporting phase;phase.Initialize(id,1000);float first=1000+(id%6)*5000;
assert(!phase.ScanDue(first-1)&&phase.ScanDue(first));phase.Scanned(first);
assert(!phase.ScanDue(first+29999)&&phase.ScanDue(first+30000));phase.Scanned(first+999999);
assert(!phase.ScanDue(first+999999));}
// A fresh complete low report uses the status-first shared uplink, never immediate delivered knowledge.
HQ h;ERR_HQUnit u;u.StatusSampler.stock.SmallArmsRounds=0;u.NextStatus=180000;
u.StatusSampler.mounted.Rounds=0;u.StatusSampler.mounted.LowMagazines=1;
h.QueueReports(&u,0);assert(u.PendingStatus&&!u.contact&&h.delivered==0&&u.PendingStatus->MessageId>0);
assert(u.Uplink->Payload.find("mounted ammunition=")!=string::npos);
auto mounted=u.PendingStatus->MountedAmmo;u.StatusSampler.mounted.Rounds=100;assert(mounted->Rounds==0);
auto pending=u.PendingStatus;h.QueueReports(&u,1000);assert(u.PendingStatus==pending&&u.StatusSampler.samples==1);
h.Finish(u,false);assert(h.delivered==0);h.QueueReports(&u,30000);assert(u.contact&&!u.PendingStatus);
h.Finish(u);h.QueueReports(&u,60000);assert(u.PendingStatus);h.Finish(u);assert(h.delivered==1);
// Adequate/incomplete early samples consume no frame and leave room for positive contact or a quiet sweep.
HQ healthy;ERR_HQUnit good;good.NextStatus=180000;
// Healthy mounted stock does not queue an early status; infantry remains independent.
healthy.QueueReports(&good,0);assert(good.contact&&!good.PendingStatus&&healthy.m_Journal.writes==0);
healthy.Finish(good);good.visible=false;good.StatusSampler.stock.Complete=false;
healthy.QueueReports(&good,30000);assert(good.sweep&&!good.Uplink);
// Complete low mounted stock can request the same status frame even with healthy carried ammunition.
HQ vehicle;ERR_HQUnit crew;crew.NextStatus=180000;crew.StatusSampler.mounted.Rounds=25;crew.StatusSampler.mounted.LowMagazines=1;
vehicle.QueueReports(&crew,0);assert(crew.PendingStatus&&!crew.contact&&vehicle.delivered==0);
assert(crew.PendingStatus->AmmoState->SmallArmsRounds==121&&crew.PendingStatus->MountedAmmo->Rounds==25);
auto installed=crew.PendingStatus->MountedAmmo;crew.StatusSampler.mounted.Rounds=0;assert(installed->Rounds==25);
vehicle.Finish(crew,false);vehicle.QueueReports(&crew,30000);assert(crew.contact&&!crew.PendingStatus);
vehicle.Finish(crew);vehicle.QueueReports(&crew,60000);assert(crew.PendingStatus);vehicle.Finish(crew);assert(vehicle.delivered==1);
// No mounted inference from incomplete/captured, unarmed, invalid or absent snapshots.
ERR_HQAmmoReporting mountedPolicy;ERR_HQStatusReport mr;ERR_HQAmmoSnapshot carried;mr.AmmoState=&carried;
ERR_HQMountedAmmoSnapshot vehicleStock;mr.MountedAmmo=&vehicleStock;vehicleStock.Rounds=25;vehicleStock.LowMagazines=1;
assert(mountedPolicy.Urgent(&mr,0));vehicleStock.Complete=false;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Complete=true;
vehicleStock.Weapons=0;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Weapons=1;
vehicleStock.LowMagazines=2;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.LowMagazines=1;
vehicleStock.Rounds=-1;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Rounds=25;
vehicleStock.LowMagazines=0;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.LowMagazines=1;
mr.Count=0;assert(!mountedPolicy.Urgent(&mr,0));mr.Count=4;mr.Fit=0;assert(!mountedPolicy.Urgent(&mr,0));mr.Fit=4;
mr.ObservedAt=-1;assert(!mountedPolicy.Urgent(&mr,0));mr.ObservedAt=0;
carried.Complete=false;assert(mountedPolicy.Urgent(&mr,0));carried.SmallArmsRounds=-1;assert(!mountedPolicy.Urgent(&mr,0));
carried.SmallArmsRounds=121;mr.AmmoState=nullptr;assert(mountedPolicy.Urgent(&mr,0));mr.MountedAmmo=nullptr;assert(!mountedPolicy.Urgent(&mr,0));
// No route/radio means no snapshot or invented radio receipt. Early no-route attempts are bounded.
HQ off;ERR_HQUnit absent;absent.NextStatus=180000;off.m_Network.available=false;
off.QueueReports(&absent,0);off.QueueReports(&absent,1000);assert(off.m_Network.routes==1&&absent.StatusSampler.samples==0);
absent.Radio=nullptr;off.QueueReports(&absent,30000);assert(absent.StatusSampler.samples==0);
absent.Radio=&absent;off.m_Network.available=true;absent.StatusSampler.stock.SmallArmsRounds=0;
off.QueueReports(&absent,30000);assert(absent.PendingStatus&&off.delivered==0);off.Finish(absent);
// Requested status bypasses the urgent cooldown and scan deadline, but waits for the existing visual frame.
off.QueueReports(&absent,31000);assert(absent.contact);absent.NextStatus=32000;
off.QueueReports(&absent,32000);assert(absent.contact&&!absent.PendingStatus);
off.Finish(absent);off.QueueReports(&absent,33000);assert(absent.PendingStatus&&absent.PendingStatus->ObservedAt==33000);
// Routine unavailable route remains immediately retryable independently of bounded optional scans.
HQ routine;ERR_HQUnit late;routine.m_Network.available=false;routine.QueueReports(&late,0);
routine.m_Network.available=true;routine.QueueReports(&late,1000);assert(late.PendingStatus);
// A minute cooldown prevents persistent low stock from starving five-second contacts over six-second routes.
HQ busy;ERR_HQUnit firing;firing.StatusSampler.stock.SmallArmsRounds=0;int contacts=0,status=0;float finish=-1;
for(int now=0;now<=600000;now+=1000){
if(firing.Uplink&&now>=finish){if(firing.PendingStatus)status++;else contacts++;busy.Finish(firing);}
bool occupied=firing.Uplink;busy.QueueReports(&firing,now);if(!occupied&&firing.Uplink)finish=now+6000;
}
assert(status==10&&contacts==90&&firing.StatusSampler.samples<=21);
}
''')
print('PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures requested check-ins and non-urgent detached carried-mine status')