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

130 lines
9.2 KiB
Python

"""Execute production cover-fixture event transitions with native boundaries mocked."""
from pathlib import Path
import os,re,subprocess,sys
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_CONVOY_COVER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyCoverProbe.c'))
s=P.read_text()
def cpp(s):
s=s.replace('protected ','').replace('ref ','').replace('foreach (','for (').replace('null','nullptr')
for t in ('ERR_NativeConvoyCoverReport','ERR_HQUnit','ERR_HQReport','ERR_HQRoute','ERR_ConvoyCombatOrder'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
for v in ('entry','unit','report','order','route','observer','m_Lead','m_Damage'):
s=re.sub(r'\b'+v+r'\.',v+'->',s)
for field in ('Report','PendingReport','Uplink','Utility','Combat','Entity'):
s=s.replace('->'+field+'.','->'+field+'->')
return s
methods='\n'.join(cpp(body(P,k)) for k in ('protected ERR_NativeConvoyCoverReport Find(', 'void Observed(', 'void Accepted(', 'void Delivered(', 'void Stop('))
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <string>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
struct Actor{float origin=7;float GetOrigin(){return origin;}};using IEntity=Actor*;
struct ERR_HQReport{int MessageId=1;IEntity Target=nullptr;ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
struct ERR_HQRoute{bool Failed=false,Downlink=false;float StartedAt=1;int Cursor=0;Array<int>Nodes={1,2};};
struct Utility{IEntity observer=nullptr;IEntity ERR_ResponseVisualObserver(ERR_HQReport*,float){return observer;}};
struct ERR_HQUnit{ERR_HQReport* PendingReport=nullptr;ERR_HQRoute* Uplink=nullptr;struct Utility* Utility=nullptr;};
struct ERR_NativeConvoyCoverReport{ERR_HQReport* Report=nullptr;ERR_HQRoute* Uplink=nullptr;bool Accepted=false;};
struct ERR_ConvoyCombatOrder{int Group=1,CommandId=1,ReportId=1;bool Hold=true;float ObservedAt=10;};
struct Combat{bool Held=false;int LastCommandId=1;};
struct Lead{int Group=1;struct Combat* Combat=nullptr;bool Waypoint=false;int RouteIndex=2;Actor* Entity=nullptr;};
struct Damage{bool enabled=true;int writes=0;void EnableDamageHandling(bool b){enabled=b;writes++;}};
struct Probe{
bool m_Stopped=false,m_Failed=false,m_Held=false,m_Resumed=false,m_Protected=false,m_DamageWasEnabled=true;
int m_RouteIndex=0,checks=0;float m_HeldAt=0,m_ResumedAt=0,m_HoldPosition=0,m_ResumePosition=0;
Lead* m_Lead=nullptr;IEntity m_Player=nullptr;Damage* m_Damage=nullptr;
Array<IEntity>m_Forces;Array<ERR_NativeConvoyCoverReport*>m_Reports;inline static Probe* s_Active=nullptr;
bool Check(bool ok,string){checks++;
if(!ok){m_Failed=true;Stop();}return ok;}
'''+methods+r'''
};
struct Setup{
Actor player,enemy,stranger;ERR_HQReport report;ERR_HQRoute uplink,downlink;Utility utility;ERR_HQUnit unit;
Combat combat;Lead lead;ERR_ConvoyCombatOrder order;Probe p;Damage damage;
Setup(){report.Target=&player;utility.observer=&enemy;unit.PendingReport=&report;unit.Uplink=&uplink;unit.Utility=&utility;
lead.Combat=&combat;lead.Entity=&enemy;p.m_Lead=&lead;p.m_Player=&player;p.m_Forces={&enemy};
p.m_Damage=&damage;p.m_Protected=true;damage.enabled=false;Probe::s_Active=&p;downlink.Downlink=true;downlink.Cursor=1;}
void observed(){p.Observed(&unit,20);}
void accepted(){uplink.Cursor=2;p.Accepted(&report,22);}
void hold(){combat.Held=true;lead.Waypoint=false;p.Delivered(&order,&downlink,true,30);}
void resume(){order.Hold=false;order.CommandId=2;combat.LastCommandId=2;combat.Held=false;lead.Waypoint=true;p.Delivered(&order,&downlink,false,60010);}
};
int main(){
// Observation snapshots the real pending report only for the existing prisoner and original observer.
Setup s;s.observed();assert(s.p.m_Reports.Count()==1);auto snapshot=s.p.m_Reports[0]->Report;
assert(snapshot!=&s.report&&snapshot->Target==&s.player);s.report.MessageId=9;assert(snapshot->MessageId==1);s.report.MessageId=1;
s.p.Accepted(&s.report,21);assert(!s.p.m_Reports[0]->Accepted); // radio still in transit
s.accepted();assert(s.p.m_Reports[0]->Accepted);s.hold();assert(s.p.m_Held&&!s.p.m_Failed&&s.p.m_RouteIndex==2&&s.p.m_HeldAt==30);
s.p.Delivered(&s.order,&s.downlink,false,40);assert(s.p.m_HeldAt==30); // repeated HOLD cannot replace first measurement
s.resume();assert(s.p.m_Resumed&&!s.p.m_Failed&&s.p.m_ResumedAt==60010);
s.p.Stop();assert(s.damage.enabled&&s.damage.writes==1&&Probe::s_Active==nullptr);s.p.Stop();assert(s.damage.writes==1);
int checks=s.p.checks;s.p.Delivered(&s.order,&s.downlink,false,61000);s.p.Observed(&s.unit,61000);assert(s.p.checks==checks&&s.p.m_Reports.Count()==1);
for(int exclusion=0;exclusion<5;exclusion++){
Setup x;
if(exclusion==0)x.report.Target=&x.stranger;
if(exclusion==1)x.unit.Uplink=nullptr;
if(exclusion==2)x.utility.observer=nullptr;
if(exclusion==3)x.utility.observer=&x.stranger;
if(exclusion==4)x.p.m_Reports.resize(64,nullptr);
int before=x.p.m_Reports.Count();x.observed();assert(x.p.m_Reports.Count()==before);
}
// Failed or unstarted uplinks cannot establish delivered evidence.
for(int invalid=0;invalid<3;invalid++){
Setup x;x.observed();x.uplink.Cursor=2;
if(invalid==0)x.uplink.Failed=true;
if(invalid==1)x.uplink.StartedAt=-1;
if(invalid==2)x.uplink.Cursor=1;
x.p.Accepted(&x.report,30);assert(!x.p.m_Reports[0]->Accepted);x.hold();assert(x.p.m_Failed&&!x.p.m_Held&&x.damage.enabled);
}
// Rejected production order is ignored; completed physical downlink requires exact cursor/direction.
for(int ignored=0;ignored<3;ignored++){
Setup x;x.observed();x.accepted();
if(ignored==0)x.order.Group=2;
if(ignored==1)x.combat.LastCommandId=2;
if(ignored==2)x.downlink.Failed=true;
x.hold();assert(!x.p.m_Held&&!x.p.m_Failed&&x.p.checks==0);
}
for(int invalid=0;invalid<3;invalid++){
Setup x;x.observed();x.accepted();
if(invalid==0)x.downlink.Downlink=false;
if(invalid==1)x.downlink.Cursor=0;
if(invalid==2)x.downlink.StartedAt=-1;
x.hold();assert(x.p.m_Failed&&!x.p.m_Held);
}
// A successful command cannot waive waypoint ownership, preserved route index or quiet period.
for(int invalid=0;invalid<4;invalid++){
Setup x;x.observed();x.accepted();x.hold();x.order.Hold=false;x.order.CommandId=2;x.combat.LastCommandId=2;
x.combat.Held=false;x.lead.Waypoint=true;bool hadWaypoint=false;float now=60010;
if(invalid==0)x.lead.RouteIndex=3;
if(invalid==1)hadWaypoint=true;
if(invalid==2)x.lead.Waypoint=false;
if(invalid==3)now=60009;
x.p.Delivered(&x.order,&x.downlink,hadWaypoint,now);assert(x.p.m_Failed&&!x.p.m_Resumed&&x.damage.enabled);
}
Setup previouslyDisabled;previouslyDisabled.p.m_DamageWasEnabled=false;previouslyDisabled.p.Stop();assert(!previouslyDisabled.damage.enabled&&previouslyDisabled.damage.writes==1);
}
''')
assert s.startswith('#ifdef WORKBENCH')
for signature,base,hook in [('protected override void Observe(', 'super.Observe(unit, now)', 'proof.Observed'),('protected override void Accept(', 'super.Accept(report, now)', 'proof.Accepted'),('protected override void DeliverConvoyOrder(', 'super.DeliverConvoyOrder(unit, now)', 'proof.Delivered')]:
method=body(P,signature);assert method.index(base)<method.index(hook)
assert 'entry.Report = unit.PendingReport.Snapshot()' in s
for forbidden in ('ApplyCombatOrder(', 'PendingReport = new', 'new ERR_HQRoute', 'SetAmmoCount(', 'TrySpawnPrefabToStorage(', 'SetOrigin('):assert forbidden not in s
convoy=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c').read_text()
assert convoy.index('all six native vehicles drove at least 20m')<convoy.index('m_Cover = new')
assert 'if (m_Step == 4) return m_Cover.Tick(hq, now);' in convoy
assert 'void Stop() { if (m_Cover) m_Cover.Stop(); }' in convoy
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text();assert 'if (m_ERRConvoyProbe) m_ERRConvoyProbe.Stop();' in smoke
runner=ROOT/'tools/headless_workbench.py'
for variant in ('victory','hq-body'):
result=subprocess.run([sys.executable,str(runner),'--smoke',variant,'--convoy-cover','--project','chat','--rounds','1','--timeout','900','--compat','/tmp/erri-unused-cover-compat','--profile','/tmp/erri-unused-cover-profile'],capture_output=True,text=True)
assert result.returncode==2 and '--convoy-cover requires --smoke convoy' in result.stderr,result.stderr
# Cover receives its own full budget after ordinary convoy proof; default budget stays bounded.
assert 'if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;' in smoke
assert 'if (ERR_NativeConvoyCoverProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000;' in smoke
result=subprocess.run([sys.executable,str(runner),'--smoke','convoy','--convoy-cover','--project','chat','--rounds','1','--timeout','899','--compat','/tmp/erri-unused-cover-compat','--profile','/tmp/erri-unused-cover-profile'],capture_output=True,text=True)
assert result.returncode==2 and '--convoy-cover requires timeout at least900 seconds' in result.stderr,result.stderr
assert "if a.convoy_cover: command += ['-errConvoyCover', '1']" in runner.read_text()
print('PASS: actual convoy cover observation/snapshot/uplink/HOLD/RESUME/cleanup transitions; native boundaries mocked, non-convoy runner opt-in refused')