Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
132 lines
9.4 KiB
Python
132 lines
9.4 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_path=Path(os.environ.get('ERR_CONVOY_PROBE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c'))
|
|
convoy=convoy_path.read_text()
|
|
assert convoy.index('all five native vehicles drove at least 20m')<convoy.index('m_Cover = new')
|
|
assert 'if (m_Step == 4) return m_Cover.Tick(hq, now);' in convoy
|
|
stop=body(convoy_path,'void Stop(')
|
|
assert 'if (m_Cover) m_Cover.Stop();' in stop and 'if (m_Intercept) m_Intercept.Stop();' in stop
|
|
smoke=Path(os.environ.get('ERR_NATIVE_SMOKE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')).read_text();assert 'if (m_ERRConvoyProbe) m_ERRConvoyProbe.Stop();' in smoke
|
|
runner=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_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')
|