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

257 lines
18 KiB
Python

"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
from policy_harness import ROOT,body,run
import re,os
from pathlib import Path
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c')))
BASE=Path(os.environ.get('ERR_CONVOY_SETTINGS_ROOT', ROOT))
A=BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c'
def cpp(s):
for old,new in [('protected ',''),('override ',''),('null','nullptr'),('Replication.','Replication::'),
('ERR_GameMode.','ERR_GameMode::'),('ERR_ConvoyRoadPlan.','ERR_ConvoyRoadPlan::'),
('ERR_Settings.','ERR_Settings::'),('Math.','Math::'),('array<string>','Array<string>&')]:s=s.replace(old,new)
for typ in ('ERR_GameMode','ERR_ConvoyReceipt','ERR_ConvoyTask','ERR_SiteRuntime','ERR_HQUnit','ERR_AdminProvider'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('m_ConvoyStartup','m_Mode','m_Convoys','m_ConvoyPool','m_ConvoyTask','m_HQ','site','unit','receipt','mode','provider'):
s=s.replace(var+'.',var+'->')
s=s.replace('site->Definition.', 'site->Definition->')
for name in ('GetGame()','GetWorld()','Forces()','Sites()','Settings()'):s=s.replace(name+'.',name+'->')
s=s.replace('foreach (ERR_HQUnit* unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
s=s.replace('foreach (ERR_AdminProvider* provider : m_Providers)','for (ERR_AdminProvider* provider : m_Providers)')
s=s.replace('foreach (string key : ERR_Settings::Keys())','for (string key : ERR_Settings::Keys())')
s=s.replace('mode->Settings()->Get(key).ToString()','string(std::to_string(mode->Settings()->Get(key)))')
s=s.replace('bool handled;','bool handled=false;')
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};')
s=s.replace('out int ', 'int& ').replace('ERR_CommandArguments.Integer', 'ERR_CommandArguments::Integer')
s=s.replace('value.ToString()', 'string(std::to_string(value))')
s=s.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf(')
return s
hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool PrepareConvoy(', 'bool StartConvoy(',
'bool StartScenarioConvoy(', 'string ConvoyStatus('))
provider=cpp(body(P,'protected void ConvoyAutoSetting('))+cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
integer=cpp(body(A,'static bool Integer('))
validator=cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid('))
extension=cpp(body(A,'protected bool Extension('))
# Execute the exact complete auth/normalization/extension-dispatch region.
# Unrelated settings/help parsers after this boundary are covered by their own suites.
admin=body(A,'void Execute(')
admin=cpp(admin[:admin.index(' array<string> parts = {};')]+'\n }')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#include <map>
template<class T>struct Array;
struct string:std::string{
using std::string::string;string(const std::string& v):std::string(v){}
bool IsEmpty()const{return empty();}int Length()const{return size();}
string Substring(int begin,int count)const{return substr(begin,count);}int ToInt()const{return std::stoi(*this);}
void Split(const char* separator,Array<string>& parts,bool skipEmpty)const;
int IndexOf(const std::string& s)const{auto p=find(s);return p==npos?-1:int(p);}
string Trim()const{auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";
return substr(a,find_last_not_of(" \t\r\n")-a+1);}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
};
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T n){this->push_back(n);}
int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)const{
parts.Clear();size_t begin=0;const size_t width=std::char_traits<char>::length(separator);assert(width>0);
for(;;){auto end=find(separator,begin);string part=substr(begin,end==npos?npos:end-begin);
if(!skipEmpty||!part.empty())parts.Insert(part);
if(end==npos)break;
begin=end+width;
}
}
struct ERR_CommandArguments{
'''+integer+r'''
};
struct vector{float x=0,y=0,z=0;};
struct Math{static int Max(int a,int b){return std::max(a,b);}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct Group{};struct Record{};
struct Ledger{std::map<Group*,Record*>records;Record* FindGroup(Group* g){return records.count(g)?records[g]:nullptr;}};
struct ERR_HQUnit{struct Group* Group=nullptr;};
struct Definition{vector Position{12,3,45};};
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;};
struct Sites{ERR_SiteRuntime* site=nullptr;int finds=0;string last;
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
struct ERR_Settings{
int automatic=1,stinger=15,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){return key=="stingerChanceOneIn"?stinger:automatic;}
'''+validator+r'''
bool Set(string key,int value){writes++;if(saveFails||!Valid(key,value))return false;
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
assert(key=="stingerChanceOneIn");stinger=value;return true;}
};
struct ERR_GameMode{
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
struct Sites sites;ERR_Settings settings;bool sitesReady=true;
static ERR_GameMode* Get(){return current;}bool Active(){return active;}Ledger* Forces(){return &ledger;}
struct Sites* Sites(){return sitesReady?&sites:nullptr;}ERR_Settings* Settings(){return settingsReady?&settings:nullptr;}
string Status(){return "mission";}bool AbandonRound(){return false;}
};
struct Clock{float GetWorldTime(){return 123000;}Clock* GetWorld(){return this;}}clock_;
Clock* GetGame(){return &clock_;}
struct ERR_ForceRoute{};
struct ERR_ConvoyRoadPlan{
inline static bool allowed=true;inline static int calls=0;inline static vector lastSeed;
static bool Build(vector seed,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){
calls++;lastSeed=seed;assert(positions.IsEmpty()&&angles.IsEmpty()&&!route);
for(int i=0;i<6;i++){positions.Insert({float(i),0,0});angles.Insert({0,0,0});}
route=new ERR_ForceRoute();return allowed; // a rejected survey may still write all out parameters
}
};
struct ERR_ConvoyReceipt{bool Consumed=false;};
struct Pool{bool allowed=true;int calls=0,created=0;string source;ERR_ConvoyReceipt receipt;
ERR_ConvoyReceipt* Reserve(string id){calls++;source=id;if(!allowed)return nullptr;
if(created==0){created++;}return &receipt;}
};
struct ERR_ConvoyTask{bool Stopped=false;int retained=0;};
struct Service{
int calls=0,statusCalls=0;bool preflight=true,partial=false;ERR_ConvoyReceipt* last=nullptr;float lastTime=0;
ERR_ConvoyTask task;
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* receipt,Array<vector>& positions,Array<vector>& angles,
ERR_ForceRoute* route,float now){calls++;last=receipt;lastTime=now;
assert(positions.Count()==6&&angles.Count()==6&&route&&!receipt->Consumed);
if(!preflight)return nullptr;
receipt->Consumed=true;task.Stopped=partial;task.retained=partial?3:6;return &task;
}
string Status(){statusCalls++;return task.Stopped?"stopped; retained vehicles=3":"convoy service status";}
};
struct ERR_ConvoyStartup{string Status(){return "startup snapshot";}};
struct HQ{
ERR_ConvoyStartup* m_ConvoyStartup=nullptr;
ERR_GameMode* m_Mode;Pool* m_ConvoyPool;Service* m_Convoys;ERR_ConvoyTask* m_ConvoyTask=nullptr;
Array<vector>m_ConvoyPositions,m_ConvoyAngles;ERR_ForceRoute* m_ConvoyRoute=nullptr;string m_ConvoyFailure;
Array<ERR_HQUnit*>m_Units;
HQ(ERR_GameMode* mode,Pool* pool,Service* service):m_Mode(mode),m_ConvoyPool(pool),m_Convoys(service){}
string Status(){return "hq status";}void MemoryStatus(Array<string>& r){r.Insert("memory");}
'''+hq+r'''
};
struct ERR_AdminProvider{virtual bool Execute(ERR_GameMode*,string,Array<string>&)=0;virtual ~ERR_AdminProvider()=default;};
struct Provider:ERR_AdminProvider{
HQ* m_HQ;Provider(HQ* hq):m_HQ(hq){}int JournalSequence(){return 0;}
bool ReadJournal(int,Array<string>& r,int& cursor){cursor=0;r.Insert("journal");return true;}
'''+provider+r'''
};
struct Admin{Array<ERR_AdminProvider*>m_Providers;
'''+extension+admin+r'''
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;Pool pool;Service service;HQ hq(&mode,&pool,&service);
Provider provider(&hq);Admin admin;admin.m_Providers.Insert(&provider);Array<string> replies;
Definition definition;ERR_SiteRuntime site;site.Definition=&definition;mode.sites.site=&site;
// Core authenticated dispatch: no provider call, route scan or reservation without authority/server.
replies.Insert("old");admin.Execute(&mode,false,"hq convoy start",replies);
assert(replies.Count()==1&&replies[0]=="Administrator access required."&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
Replication::server=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies.IsEmpty()&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());Replication::server=true;
admin.Execute(nullptr,true,"hq convoy start",replies);assert(replies.IsEmpty()&&pool.calls==0);
admin.Execute(&mode,true,string(161,'x'),replies);assert(replies[0]=="Command is too long."&&pool.calls==0);
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies[0]=="Mission settings are not ready."&&pool.calls==0);mode.settingsReady=true;
mode.active=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&ERR_ConvoyRoadPlan::calls==0);mode.active=true;
ERR_GameMode::current=nullptr;assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());ERR_GameMode::current=&mode;
// Read-only status and unsupported commands cannot dispatch.
admin.Execute(&mode,true,"hq convoy show",replies);assert(replies.Count()==1&&pool.calls==0);
assert(!provider.Execute(&mode,"hq convoy start extra",replies)&&pool.calls==0);
Provider unavailable(nullptr);replies.Clear();assert(unavailable.Execute(&mode,"hq convoy start",replies));
assert(replies[0]=="HQ unavailable.");
// Stinger commands pass the same actual Core authority and provider dispatch boundary.
admin.Execute(&mode,false,"hq stinger set 0",replies);assert(mode.settings.writes==0&&mode.settings.stinger==15);
Replication::server=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies.IsEmpty());Replication::server=true;
mode.settingsReady=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
admin.Execute(&mode,true," HQ STINGER SET 0 ",replies);
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
admin.Execute(&mode,true,"hq stinger show",replies);
assert(mode.settings.writes==1&&replies[0].find("stingerChanceOneIn=0")!=string::npos);
admin.Execute(&mode,true,"hq stinger help",replies);
assert(mode.settings.writes==1&&replies[0].find("2..10000 one in N")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
int writes=mode.settings.writes;
admin.Execute(&mode,true,"hq stinger set 1 extra",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 1junk",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 10001",replies);assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);
replies.Clear();assert(unavailable.Execute(&mode,"hq stinger set 1",replies)&&replies[0]=="HQ unavailable.");
// New automatic switch is saved only: it does not scan, reserve or launch now.
writes=mode.settings.writes;
admin.Execute(&mode,false,"hq convoy auto set 0",replies);assert(mode.settings.writes==writes&&mode.settings.automatic==1);
Replication::server=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
assert(replies.IsEmpty()&&mode.settings.writes==writes);Replication::server=true;
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
assert(mode.settings.writes==writes&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
admin.Execute(&mode,true," HQ CONVOY AUTO SET 0 ",replies);
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0&&ERR_ConvoyRoadPlan::calls==0);
assert(replies[0].find("next round only")!=string::npos&&replies[0].find("manual convoy start unchanged")!=string::npos);
admin.Execute(&mode,true,"hq convoy auto show",replies);assert(replies[0].find("automaticConvoyEnabled=0 saved for next round")!=string::npos);
admin.Execute(&mode,true,"hq convoy auto help",replies);assert(replies[0].find("1 enabled (default)")!=string::npos);
writes=mode.settings.writes;
for(string bad:{"hq convoy auto set", "hq convoy auto set 1 extra", "hq convoy auto show extra", "hq convoy auto set -1", "hq convoy auto set 1.0", "hq convoy auto set 1junk"}){
admin.Execute(&mode,true,bad,replies);assert(mode.settings.writes==writes&&mode.settings.automatic==0);
}
admin.Execute(&mode,true,"hq convoy auto set 2",replies);assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq convoy auto set 1",replies);
assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
replies.Clear();assert(unavailable.Execute(&mode,"hq convoy auto set 1",replies)&&replies[0]=="HQ unavailable.");
// Disabled automatic startup does not disable manual canonical dispatch below.
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0);
// Canonical authored site and preparation failures refuse without consuming the finite receipt.
mode.sitesReady=false;assert(!hq.StartScenarioConvoy());mode.sitesReady=true;
mode.sites.site=nullptr;assert(!hq.StartScenarioConvoy());mode.sites.site=&site;
site.Definition=nullptr;assert(!hq.StartScenarioConvoy());site.Definition=&definition;
ERR_ConvoyRoadPlan::allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&service.calls==0&&replies[0].find("No sufficiently long")!=string::npos);
assert(!hq.m_ConvoyRoute&&hq.m_ConvoyPositions.IsEmpty()&&hq.m_ConvoyAngles.IsEmpty());
assert(!hq.StartConvoy()&&pool.calls==0); // rejected builder output cannot become a deployment later
ERR_ConvoyRoadPlan::allowed=true;
// An asynchronously pending existing group blocks all new convoy reservations.
Group original;ERR_HQUnit unit;unit.Group=&original;hq.m_Units.Insert(&unit);
admin.Execute(&mode,true," HQ CONVOY START ",replies);
assert(pool.calls==0&&service.calls==0&&mode.sites.last=="everon-supply-1");
assert(ERR_ConvoyRoadPlan::lastSeed.x==12&&ERR_ConvoyRoadPlan::lastSeed.z==45);
assert(replies[0].find("Existing force admission is still pending")!=string::npos);
Record record;mode.ledger.records[&original]=&record;
// Pool refusal remains authoritative; the separate pool suite verifies the 59/60-body arithmetic.
pool.allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==1&&pool.source=="everon-supply-convoy-1"&&service.calls==0&&!hq.m_ConvoyTask);
assert(replies[0].find("physical force budget is unavailable")!=string::npos);pool.allowed=true;
// Preflight failure retains one unconsumed canonical receipt; safe retry does not allocate a new allowance.
service.preflight=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.created==1&&!pool.receipt.Consumed&&service.calls==1&&!hq.m_ConvoyTask);
assert(replies[0].find("Deployment preflight failed; no convoy created")!=string::npos);
service.preflight=true;admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.created==1&&pool.receipt.Consumed&&hq.m_ConvoyTask==&service.task);
assert(service.last==&pool.receipt&&service.lastTime==123000&&!service.task.Stopped);
int scans=ERR_ConvoyRoadPlan::calls,reserves=pool.calls;
for(int i=0;i<5;i++)admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.calls==reserves&&ERR_ConvoyRoadPlan::calls==scans);
// Partial native failure returns a stopped retained task. HQ must keep it, never retry/refund it.
Pool partialPool;Service partialService;partialService.partial=true;HQ partial(&mode,&partialPool,&partialService);
assert(!partial.StartScenarioConvoy()&&partial.m_ConvoyTask==&partialService.task);
assert(partialPool.receipt.Consumed&&partialService.task.retained==3&&partialService.calls==1);
assert(partial.ConvoyStatus().find("retained vehicles=3")!=string::npos);
scans=ERR_ConvoyRoadPlan::calls;
for(int i=0;i<5;i++)assert(!partial.StartScenarioConvoy()&&!partial.StartConvoy());
assert(partialService.calls==1&&partialPool.calls==1&&ERR_ConvoyRoadPlan::calls==scans);
// A preexisting consumed receipt is rejected even if the wrapper has no task handle.
Pool consumedPool;consumedPool.receipt.Consumed=true;consumedPool.created=1;
Service consumedService;HQ consumed(&mode,&consumedPool,&consumedService);
assert(!consumed.StartScenarioConvoy()&&!consumed.m_ConvoyTask&&consumedService.calls==0);
assert(consumed.ConvoyStatus().find("finite convoy allowance")!=string::npos);
}
''')
for sig in ('bool PrepareConvoy(', 'bool StartConvoy(', 'bool StartScenarioConvoy('):
source=body(H,sig)
for forbidden in ('ERR_Actors.Spawn(', 'DeleteEntity', 'DespawnMembers(', 'SpawnMembers('):assert forbidden not in source
assert body(H,'bool StartConvoy(').count('everon-supply-convoy-1')==1
assert 'm_Convoys.Stop()' in body(H,'override void Stop(')
print('PASS: actual authenticated convoy dispatch, active/ready admission gates, canonical receipt, safe unconsumed preflight retry, pool refusal, retained partial failure and consumed no-retry')