Files
EscapistsReintroduced/tests/test_mortar_refresh.py
T

243 lines
20 KiB
Python

"""Execute production refresh/adoption methods against adversarial radio/solver outcomes.
The real scheduler, refresh, cancellation and projectile-bookkeeping bodies run.
Native route/ballistic/crew-readiness operations, battery Update and initial-order
progression are boundary adapters; this is not native delivery, death or firing.
"""
import re
from policy_harness import ROOT, body, run
r=ROOT/'addons/ai/Scripts/Game/ERR'
def cpp(path, signature):
code=body(r/path,signature).replace('protected ','')
for typ in ['ERR_MortarBattery','ERR_MortarSystem','ERR_HQReport','ERR_HQRoute','ERR_MortarRefresh','ERR_MortarAimSolution']:
code=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',code)
for name in ['battery','report','current','incoming','update','route','commanded','updated','system','m_HQ','newest','selected','target','m_Transfer','Activity']:
code=code.replace(name+'.',name+'->')
# Members are owning/weak script references, both represented as pointers.
for name in ['Route','Report','Refresh','AimSolver']:
code=code.replace('.'+name+'.','.'+name+'->').replace('->'+name+'.','->'+name+'->')
code=code.replace('Route.Advance','Route->Advance')
for name in ['ERR_MortarPrediction','ERR_MortarRefreshRules','ERR_MortarAimRules','ERR_MortarBattery']:
code=code.replace(name+'.',name+'::')
code=code.replace('null','nullptr').replace('IEntity gun;','IEntity gun=nullptr;').replace('ResourceName ammo;','std::string ammo;')
code=code.replace('string reason','std::string reason')
# Only journal formatting is inert; authorization/solver mutations are real bodies.
code=re.sub(r'^.*Audit\([^\n]*$', ';', code, flags=re.M)
code=code.replace('vector.Distance','vector::Distance').replace('Math.AbsFloat','std::fabs')
code=code.replace('array<ref ERR_HQReport> reports = {};','Items<ERR_HQReport*> reports;')
code=re.sub(r'foreach \((ERR_MortarBattery|ERR_HQReport)\* (\w+) : (\w+)\)',r'for (\1* \2 : \3.values)',code)
code=re.sub(r'(ERR_MortarBattery|ERR_HQReport)\* (newest|selected|target);',r'\1* \2=nullptr;',code)
code=code.replace('Replication.IsServer()', 'Replication::IsServer()').replace('float.MAX','1e30f')
return code
constructor=cpp('ERR_MortarRefresh.c','void ERR_MortarRefresh(').replace('void ERR_MortarRefresh(', 'ERR_MortarRefresh(')
newer=cpp('ERR_MortarRefresh.c','static bool Newer(')
owns=cpp('ERR_MortarRefresh.c','bool Owns(')
delivered=cpp('ERR_MortarRefresh.c','bool DeliveredFor(')
fresh=cpp('ERR_MortarPrediction.c','static bool Fresh(')
equivalent=cpp('ERR_MortarAimSolution.c','static bool Equivalent(')
matches=cpp('ERR_MortarAimSolution.c','bool Matches(')
can=cpp('ERR_MortarBattery.c','bool CanRefresh(')
begin=cpp('ERR_MortarBattery.c','bool BeginRefresh(')
reject=cpp('ERR_MortarBattery.c','void RejectRefresh(')
advance=cpp('ERR_MortarSystem.c','protected void AdvanceRefresh(')
adopt=cpp('ERR_MortarFiring.c','bool ERR_AdoptReport(')
cancel=cpp('ERR_MortarBattery.c','void Cancel(')
projectile=cpp('ERR_MortarBattery.c','void ObserveProjectile(')
queue_method=cpp('ERR_MortarSystem.c','protected void QueueRefreshes(')
tick=cpp('ERR_MortarSystem.c','void Tick(')
# Supply owners before methods that dereference them, without replacing method logic.
can=can.replace('bool CanRefresh(', 'bool ERR_MortarBattery::CanRefresh(')
begin=begin.replace('bool BeginRefresh(', 'bool ERR_MortarBattery::BeginRefresh(')
reject=reject.replace('void RejectRefresh(', 'void ERR_MortarBattery::RejectRefresh(')
advance=advance.replace('void AdvanceRefresh(', 'void ERR_MortarSystem::AdvanceRefresh(')
adopt=adopt.replace('bool ERR_AdoptReport(', 'bool Node::ERR_AdoptReport(')
cancel=cancel.replace('void Cancel(', 'void ERR_MortarBattery::Cancel(').replace('std::string reason = "cancel requested"','std::string reason')
projectile=projectile.replace('void ObserveProjectile(', 'void ERR_MortarBattery::ObserveProjectile(')
queue_method=queue_method.replace('void QueueRefreshes(', 'void ERR_MortarSystem::QueueRefreshes(')
tick=tick.replace('void Tick(', 'void ERR_MortarSystem::Tick(')
run(r'''
#include <cassert>
#include <cmath>
#include <set>
#include <string>
#include <vector>
struct vector {float x=0,y=0,z=0;static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}static float DistanceXZ(vector a,vector b){a.y=0;b.y=0;return Distance(a,b);}};
using IEntity=void*;
template<class T>struct Items {std::vector<T> values;void Insert(T t){values.push_back(t);}};
struct Replication {static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_HQReport {IEntity Target=this;int MessageId=1;float ObservedAt=0;bool Distress=false;vector Position{};std::string TravelMode="foot";std::string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;
'''+fresh+r'''};
struct ERR_MortarRefreshRules {
'''+newer+r'''};
struct ERR_MortarAimRules {
'''+equivalent+r'''};
struct ERR_MortarBattery;struct ERR_MortarSystem;struct ERR_MortarRefresh;struct ERR_MortarAimSolution;struct Node;
struct ERR_HQRoute {int MessageId=9,state=0,calls=0;int Advance(float,int){calls++;return state;}};
struct HQ {Items<ERR_HQReport*> contacts;ERR_HQRoute* nextRoute=nullptr;int downlinks=0;int Journal(){return 0;}void SupportContacts(Items<ERR_HQReport*>& out,float){out=contacts;}ERR_HQRoute* Downlink(IEntity,const char*,std::string,int){downlinks++;return nextRoute;}};
struct Ledger {std::set<int> claimed;bool Available(int id,float,float){return !claimed.count(id);}bool Claim(int id,float,float){return claimed.insert(id).second;}};
struct FakeActivity {int completions=0;void Complete(){completions++;}};
struct Transfer {int invalidated=0;void Invalidate(){invalidated++;}};
struct ERR_MortarBattery {
static inline std::string SHELL="HE";
IEntity Gun=this,Group=this;int OrderId=1,OrderShots=0,TotalShots=0,RadioId=0;float OrderedAt=500,AimedAt=1000,AimValidatedAt=1000,NextOrder=0;vector Position{};
bool m_Stopped=false,CommandOpen=true,RefreshRejected=false,Rejected=false,Released=false,operational=true,StartupActivated=true;
FakeActivity originalActivity;FakeActivity* Activity=&originalActivity;Transfer transfer;Transfer* m_Transfer=&transfer;
ERR_HQRoute* RadioOrder=nullptr;ERR_HQReport* PendingRadioReport=nullptr;ERR_HQReport* PendingSupplyReport=nullptr;
std::string LastFailure;ERR_HQReport* Report=nullptr;ERR_MortarRefresh* Refresh=nullptr;ERR_MortarAimSolution* AimSolution=nullptr;Node* AimSolver=nullptr;Ledger m_ReportLedger;
bool Operational(){return operational;}
bool CanRefresh(ERR_HQReport*,float);bool BeginRefresh(ERR_HQReport*,ERR_HQRoute*,float);void RejectRefresh(std::string);
void Cancel(std::string reason="cancel requested");void ObserveProjectile(IEntity,IEntity,bool);void FlushTraces(){}void DetachActivity(){}
void Update(float){}bool SupplyPending(){return false;}int CountShells(){return 27;}
bool CanRequest(ERR_HQReport*,float){return false;}bool BeginRequest(ERR_HQReport*,float){assert(false);return false;}
};
struct ERR_MortarRefresh {
IEntity Gun=nullptr,Group=nullptr;int OrderId=0,RadioId=0;ERR_HQReport* Report=nullptr;ERR_HQRoute* Route=nullptr;
'''+constructor+owns+delivered+r'''};
struct ERR_MortarAimSolution {
IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
'''+matches+r'''};
struct ERR_MortarSystem {
HQ hq;HQ* m_HQ=&hq;int failures=0;
Items<ERR_MortarBattery*> m_Batteries;bool m_Stopped=false;float m_NextDecision=1e9f,m_NextRefreshDecision=0;
bool Withhold(ERR_MortarBattery* b,const char* why){b->Rejected=true;b->LastFailure=why;failures++;return false;}
void AdvanceRefresh(ERR_MortarBattery*,float);
void QueueRefreshes(float);void Tick(float);void AdvanceOrder(ERR_MortarBattery*,float){}bool OutsideSettlements(vector){return true;}
};
struct Node {
static constexpr int PORT_ARTILLERY_ENTITY=1,PORT_AMMO_PREFAB=2;
IEntity portGun=nullptr;std::string portAmmo=ERR_MortarBattery::SHELL;ERR_MortarAimSolution* calculated=nullptr;int solves=0;
void GetVariableIn(int,IEntity& out){out=portGun;}void GetVariableIn(int,std::string& out){out=portAmmo;}
ERR_MortarAimSolution* ERR_Solve(ERR_MortarBattery*,ERR_MortarSystem*,ERR_HQReport*,float){solves++;return calculated;}
bool ERR_AdoptReport(ERR_MortarBattery*,ERR_MortarSystem*,ERR_HQReport*,float);
};
'''+can+begin+reject+adopt+advance+cancel+projectile+queue_method+tick+r'''
struct F {
ERR_MortarBattery b;ERR_HQReport old,next;ERR_HQRoute route;ERR_MortarSystem system;ERR_MortarAimSolution original,solved;Node node;
F(){next=old;next.MessageId=2;next.ObservedAt=20000;b.Report=&old;original.Gun=b.Gun;original.Report=&old;solved=original;node.portGun=b.Gun;node.calculated=&solved;b.AimSolver=&node;system.m_Batteries.Insert(&b);}
bool queue(){return b.BeginRefresh(&next,&route,25000);}
void aim(){b.AimSolution=&original;}
void delivery(float now=26000){route.state=1;system.AdvanceRefresh(&b,now);}
};
int main(){
F good;assert(good.queue());assert(!good.queue());assert(good.b.m_ReportLedger.claimed.count(2));
good.system.AdvanceRefresh(&good.b,25000);assert(good.b.Report==&good.old&&good.route.calls==1); // no instant HQ knowledge
good.delivery();assert(!good.b.Refresh&&!good.b.RefreshRejected&&good.b.Report->MessageId==2);
assert(good.b.Report->ObservedAt==20000&&good.b.OrderId==1&&good.b.OrderShots==0&&good.b.OrderedAt==500&&good.b.Activity);
assert(!good.queue());good.b.Report=&good.old;assert(!good.queue()); // claimed refresh cannot become replay after original restored
for(int fault=0;fault<10;fault++){
F f;
if(fault==0)f.next.Target=&f;
if(fault==1)f.next.ObservedAt=0;
if(fault==2)f.next.ObservedAt=-1;
if(fault==3)f.next.ObservedAt=25001;
if(fault==4)f.next.MessageId=1;
if(fault==5)f.next.MessageId=0;
if(fault==6)f.next.Distress=true;
if(fault==7)f.next.TravelMode="aircraft";
if(fault==8)f.b.CommandOpen=false;
if(fault==9)f.b.operational=false;
assert(!f.queue()&&f.b.m_ReportLedger.claimed.empty());
}
F absent;assert(!absent.b.BeginRefresh(&absent.next,nullptr,25000)&&absent.b.m_ReportLedger.claimed.empty());
F expired;assert(expired.queue());expired.delivery(80001);assert(expired.b.RefreshRejected&&expired.b.Report==&expired.old);
F failed;assert(failed.queue());failed.route.state=-1;failed.system.AdvanceRefresh(&failed.b,26000);assert(failed.b.RefreshRejected&&!failed.b.Refresh&&failed.b.Report==&failed.old);
F changed;assert(changed.queue());changed.b.Gun=&changed.old;changed.delivery();assert(changed.b.RefreshRejected);
F group;assert(group.queue());group.b.Group=&group.old;group.delivery();assert(group.b.RefreshRejected);
F stale;assert(stale.queue());stale.b.OrderId++;stale.delivery();assert(!stale.b.Refresh&&!stale.b.RefreshRejected&&stale.b.Report==&stale.old&&stale.route.calls==0);
F closed;assert(closed.queue());closed.b.CommandOpen=false;closed.delivery();assert(!closed.b.Refresh&&!closed.b.RefreshRejected&&closed.route.calls==0);
F committed;assert(committed.queue());committed.b.Released=true;committed.delivery();
assert(committed.b.Refresh&&!committed.b.Refresh->Route&&committed.b.Report==&committed.old&&committed.b.Released);
committed.b.Released=false;committed.b.OrderShots=1;committed.system.AdvanceRefresh(&committed.b,27000);
assert(!committed.b.Refresh&&committed.b.Report->MessageId==2&&committed.b.OrderShots==1&&committed.b.OrderId==1);
F failInFlight;assert(failInFlight.queue());failInFlight.b.Released=true;failInFlight.route.state=-1;failInFlight.system.AdvanceRefresh(&failInFlight.b,26000);
assert(failInFlight.b.RefreshRejected&&failInFlight.b.Released&&failInFlight.b.CommandOpen&&failInFlight.b.Report==&failInFlight.old); // callback ownership retained; future load gate closes
F validAim;validAim.aim();assert(validAim.queue());validAim.delivery();
assert(validAim.node.solves==1&&!validAim.b.RefreshRejected&&validAim.b.Report->MessageId==2&&validAim.original.Report==validAim.b.Report);
assert(validAim.b.AimedAt==1000&&validAim.b.AimValidatedAt==26000&&validAim.b.AimSolution==&validAim.original&&validAim.original.Impact.x==500);
for(int fault=0;fault<10;fault++){
F f;f.aim();assert(f.queue());
if(fault==0)f.solved.Impact.x+=.101f;
if(fault==1)f.solved.Charge++;
if(fault==2)f.solved.AimTarget.y+=.101f;
if(fault==3)f.solved.GunOrigin.y+=.051f;
if(fault==4)f.solved.FlightTime+=.051f;
if(fault==5)f.node.portGun=&f.old;
if(fault==6)f.node.portAmmo="other";
if(fault==7)f.b.AimSolver=nullptr;
if(fault==8)f.node.calculated=nullptr;
if(fault==9)f.b.AimedAt=27000;
f.delivery();assert(f.b.RefreshRejected&&f.b.Report==&f.old&&f.original.Report==&f.old&&f.b.AimValidatedAt==1000);
}
F notDelivered;notDelivered.aim();assert(notDelivered.queue());assert(!notDelivered.node.ERR_AdoptReport(&notDelivered.b,&notDelivered.system,notDelivered.b.Refresh->Report,26000));assert(notDelivered.node.solves==0);
F creep;creep.aim();creep.solved.Impact.x+=.06f;assert(creep.queue());creep.delivery();assert(!creep.b.RefreshRejected);
creep.next.MessageId=3;creep.next.ObservedAt=22000;creep.solved.Impact.x+=.06f;assert(creep.queue());creep.delivery(27000);
assert(creep.b.RefreshRejected&&creep.original.Impact.x==500&&creep.b.Report->MessageId==2&&creep.b.AimedAt==1000);
// Execute the actual HQ tick, refresh scheduler, cancellation and projectile
// bookkeeping together. Native route/crew/ballistic operations remain stubs.
F repeated;repeated.system.hq.contacts.Insert(&repeated.next);repeated.system.hq.nextRoute=&repeated.route;
repeated.system.Tick(25000);assert(repeated.system.hq.downlinks==1&&repeated.b.Refresh&&repeated.route.calls==0);
repeated.system.Tick(25001);repeated.system.Tick(30000);assert(repeated.system.hq.downlinks==1&&repeated.route.calls==2);
repeated.route.state=1;repeated.system.Tick(30001);assert(!repeated.b.Refresh&&repeated.b.Report->MessageId==2);
repeated.system.Tick(35000);repeated.system.hq.contacts.Insert(&repeated.old);repeated.system.Tick(40000);
assert(repeated.system.hq.downlinks==1&&repeated.b.OrderId==1&&repeated.b.Activity==&repeated.b.originalActivity);
assert(repeated.b.m_ReportLedger.claimed.size()==1&&repeated.b.Report->ObservedAt==20000);
F snapshot;assert(snapshot.queue());snapshot.next.MessageId=99;snapshot.next.ObservedAt=24999;
snapshot.delivery();assert(snapshot.b.Report->MessageId==2&&snapshot.b.Report->ObservedAt==20000); // in-flight packet is immutable
F newest;ERR_HQReport later=newest.next;later.MessageId=3;later.ObservedAt=22000;
newest.system.hq.contacts.Insert(&newest.next);newest.system.hq.contacts.Insert(&later);newest.system.hq.nextRoute=&newest.route;
newest.system.Tick(25000);assert(newest.b.Refresh->Report->MessageId==3&&newest.b.m_ReportLedger.claimed.count(3)&&!newest.b.m_ReportLedger.claimed.count(2));
F interrupted;assert(interrupted.queue());interrupted.b.Released=true;
interrupted.system.Tick(25000);interrupted.route.state=-1;interrupted.system.Tick(25001);
assert(interrupted.b.RefreshRejected&&interrupted.b.Released&&interrupted.b.CommandOpen&&interrupted.b.Report==&interrupted.old);
interrupted.system.Tick(25002);assert(interrupted.b.RefreshRejected&&interrupted.b.Released&&interrupted.route.calls==2);
interrupted.b.ObserveProjectile(&interrupted.next,nullptr,false); // actual physical-event bookkeeping, not native launch proof
assert(!interrupted.b.Released&&interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.Report==&interrupted.old);
interrupted.system.Tick(25003);assert(!interrupted.b.CommandOpen&&!interrupted.b.Activity&&!interrupted.b.Report&&!interrupted.b.RefreshRejected);
assert(interrupted.b.originalActivity.completions==1&&interrupted.b.transfer.invalidated==1&&interrupted.b.NextOrder==35003);
assert(interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.m_ReportLedger.claimed.count(2));
F noRoute;noRoute.system.hq.contacts.Insert(&noRoute.next);noRoute.b.Released=true;
noRoute.system.Tick(25000);assert(noRoute.system.hq.downlinks==1&&noRoute.b.RefreshRejected&&noRoute.b.Released);
noRoute.system.Tick(30000);assert(noRoute.system.hq.downlinks==1&&noRoute.b.CommandOpen); // latch cannot reset while committed
F deliveredThenRadioDies;assert(deliveredThenRadioDies.queue());deliveredThenRadioDies.b.Released=true;deliveredThenRadioDies.route.state=1;
deliveredThenRadioDies.system.Tick(26000);assert(deliveredThenRadioDies.b.Refresh&&!deliveredThenRadioDies.b.Refresh->Route&&deliveredThenRadioDies.route.calls==1);
deliveredThenRadioDies.route.state=-1; // loss AFTER complete delivery cannot erase the packet already received
deliveredThenRadioDies.b.ObserveProjectile(&deliveredThenRadioDies.next,nullptr,false);
deliveredThenRadioDies.system.Tick(27000);assert(!deliveredThenRadioDies.b.RefreshRejected&&deliveredThenRadioDies.b.Report->MessageId==2&&deliveredThenRadioDies.route.calls==1);
assert(deliveredThenRadioDies.b.AimedAt==-1&&deliveredThenRadioDies.b.AimValidatedAt==-1&&deliveredThenRadioDies.b.OrderShots==1);
F expiresInFlight;assert(expiresInFlight.queue());expiresInFlight.b.Released=true;expiresInFlight.route.state=1;
expiresInFlight.system.Tick(26000);expiresInFlight.system.Tick(80001);
assert(expiresInFlight.b.RefreshRejected&&expiresInFlight.b.Released&&expiresInFlight.b.Report==&expiresInFlight.old);
expiresInFlight.b.ObserveProjectile(&expiresInFlight.next,nullptr,false);expiresInFlight.system.Tick(80002);
assert(!expiresInFlight.b.CommandOpen&&expiresInFlight.b.TotalShots==1&&expiresInFlight.b.OrderShots==1);
F crewLost;assert(crewLost.queue());crewLost.b.operational=false;crewLost.delivery();
assert(crewLost.b.RefreshRejected&&crewLost.route.calls==0&&crewLost.b.Report==&crewLost.old); // readiness loss refuses before delivery
F cancelled;assert(cancelled.queue());ERR_MortarRefresh* cancelledPacket=cancelled.b.Refresh;
cancelled.b.Cancel("explicit cancellation");cancelled.route.state=1;cancelled.system.Tick(26000);
assert(!cancelled.b.Refresh&&!cancelled.b.Report&&cancelled.route.calls==0&&cancelled.b.m_ReportLedger.claimed.count(2));
cancelled.b.OrderId=2;cancelled.b.CommandOpen=true;cancelled.b.Activity=&cancelled.b.originalActivity;cancelled.b.Report=&cancelled.next;
cancelled.b.Refresh=cancelledPacket;cancelled.system.AdvanceRefresh(&cancelled.b,27000); // delayed old-order transaction cannot hit replacement
assert(!cancelled.b.Refresh&&!cancelled.b.RefreshRejected&&cancelled.b.CommandOpen&&cancelled.b.OrderId==2&&cancelled.b.Report==&cancelled.next&&cancelled.route.calls==0);
F stopped;assert(stopped.queue());stopped.system.m_Stopped=true;stopped.system.Tick(26000);assert(stopped.route.calls==0&&stopped.b.Refresh);
stopped.system.m_Stopped=false;Replication::server=false;stopped.system.Tick(26000);assert(stopped.route.calls==0);Replication::server=true;
}
''')
# Boundaries not modeled as native C++ operations: real HQ route construction,
# projectile latch, immutable report timestamps, existing cadence and cleanup.
system=(r/'ERR_MortarSystem.c').read_text(); battery=(r/'ERR_MortarBattery.c').read_text()
queue=body(r/'ERR_MortarSystem.c','protected void QueueRefreshes(')
assert 'm_HQ.SupportContacts(reports, now)' in queue and 'm_HQ.Downlink(' in queue
assert 'new SCR_AIStaticArtilleryActivity' not in queue+begin+advance+adopt
assert 'ObservedAt =' not in queue+begin+advance+adopt
assert 'OrderId++' not in queue+begin+advance+adopt
assert 'OrderedAt =' not in queue+begin+advance+adopt
assert 'battery.RefreshRejected' in body(r/'ERR_MortarSystem.c','bool AllowShot(')
assert '(battery.Rejected || battery.RefreshRejected) && !battery.Released' in system
assert 'Refresh = null;' in body(r/'ERR_MortarBattery.c','void Cancel(')
assert 'AimValidatedAt = -1;' in body(r/'ERR_MortarBattery.c','void ObserveProjectile(')
assert 'battery.AimedAt = now;' not in body(r/'ERR_MortarFiring.c','bool ERR_AdoptReport(')
assert 'battery.AimSolution = updated' not in adopt
print('PASS: actual refresh scheduler/delivery/cancellation/projectile methods reject interrupted or replayed reports, retain committed ownership, preserve immutable snapshots/aim geometry and isolate stopped or replaced orders; native radio/crew behavior remains separate')