Files
EscapistsReintroduced/tests/test_mortar_refresh.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

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/core/Scripts/Game/ERR/10_AI'
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')