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