Files
EscapistsReintroduced/tests/test_hq_ammo_reporting.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

113 lines
8.0 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=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
def cpp(s):
s=s.replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
for v in ('stock','report','unit','route'):
s=s.replace(v+'.',v+'->')
return s.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 Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0;
int LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
'''+body(A,' bool Valid(')+r'''
};
struct ERR_HQStatusReport{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;
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);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_HQUnit{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{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(){
// 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;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;
h.QueueReports(&u,0);assert(u.PendingStatus&&!u.contact&&h.delivered==0&&u.PendingStatus->MessageId>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.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);
// 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 early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures and requested check-ins')