Files
EscapistsReintroduced/tests/test_hq_convoy_dispatch.py
T

184 lines
12 KiB
Python

"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
from policy_harness import ROOT,body,run
import re
H=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAdminProvider.c'
A=ROOT/'addons/core/Scripts/Game/ERR/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_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;')
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,'override bool Execute('))
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>
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();}
int IndexOf(const char* 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();}};
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{static Array<string> Keys(){return {}; }int Get(string){return 1;}};
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 HQ{
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.");
// 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')