231 lines
16 KiB
Python
231 lines
16 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=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
|
|
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(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;')
|
|
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 StingerSetting('))+cpp(body(P,'override bool Execute('))
|
|
integer=cpp(body(A,'static bool Integer('))
|
|
validator=cpp(body(ROOT/'addons/core/Scripts/Game/ERR/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 stinger=15,writes=0;bool saveFails=false;
|
|
static Array<string> Keys(){return {}; }int Get(string key){return key=="stingerChanceOneIn"?stinger:1;}
|
|
'''+validator+r'''
|
|
bool Set(string key,int value){writes++;if(saveFails||!Valid(key,value))return false;
|
|
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 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.");
|
|
// 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.");
|
|
// 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')
|