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.
105 lines
8.9 KiB
Python
105 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute production bounded radio graph and hop-delivery state machine."""
|
|
from pathlib import Path
|
|
import os, re, subprocess, tempfile
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
source=Path(os.environ.get('ERR_NETWORK_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQNetwork.c')).read_text()
|
|
def extract(signature):
|
|
a=source.index(signature);b=source.index('{',a);end=b+1;depth=1
|
|
while depth:depth+=(source[end]=='{')-(source[end]=='}');end+=1
|
|
return source[a:end]
|
|
def trans(s):
|
|
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
|
|
s=s.replace('string.Format','Format').replace('string ', 'std::string ')
|
|
s=s.replace('Math.Round(distance).ToString()', 'std::to_string(int(distance))')
|
|
s=re.sub(r'\b(\w+)\.ToString\(\)',r'std::to_string(\1)',s)
|
|
for name in ['Math','vector','ERR_HQLocalHandoff']:s=s.replace(name+'.',name+'::')
|
|
for typ in ['IEntity','SCR_AIGroup','ERR_HQRoute','ERR_HQRadioNode','ERR_HQJournal']:
|
|
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
|
s=s.replace('array<int> queue = {}','Arr<int> queue').replace('array<int> cell','Arr<int>* cell').replace('array<int> neighbors','Arr<int>* neighbors')
|
|
s=s.replace('cell = {};','cell = new Arr<int>();')
|
|
s=re.sub(r'foreach\s*\(int (\w+) : neighbors\)',r'for (auto \1 : *neighbors)',s)
|
|
for name in ['carrier','caller','node','route','journal','cell']:
|
|
s=s.replace(name+'.',name+'->')
|
|
s=re.sub(r'(Nodes|m_Nodes)\[([^\]]+)\]\.',r'\1[\2]->',s)
|
|
s=s.replace('Carrier.GetOrigin()', 'Carrier->GetOrigin()')
|
|
s=re.sub(r'\b(int|float|bool) (\w+);',r'\1 \2{};',s)
|
|
return s
|
|
headers=r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <string>
|
|
#include <iostream>
|
|
struct vector{float v[3]{};float operator[](int i)const{return v[i];}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}};
|
|
vector V(float x,float z=0){return {{x,0,z}};}
|
|
namespace Math{int Floor(float x){return std::floor(x);}}
|
|
std::string Format(const char*,int x,int z){return std::to_string(x)+":"+std::to_string(z);}
|
|
template<class T>struct Arr:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}void InsertAt(T x,int i){this->insert(this->begin()+i,x);}};
|
|
template<class K,class V>struct Map:std::map<K,V>{void Clear(){this->clear();}bool Contains(K k){return this->count(k);}bool Find(K k,V& v){if(!Contains(k))return false;v=this->at(k);return true;}V Get(K k){if(!Contains(k))return {};return this->at(k);}void Insert(K k,V v){(*this)[k]=v;}};
|
|
struct SCR_AIGroup{bool enemy=true;};struct IEntity{vector pos;bool alive=true;int radio=1;bool captured=false;vector GetOrigin(){return pos;}};
|
|
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return Carrier&&Carrier->alive&&!Carrier->captured&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
|
|
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
|
|
'''
|
|
headers+='struct ERR_HQStatusSampler{};struct ERR_HQLocalHandoff{static bool Valid(SCR_AIGroup*,ERR_HQStatusSampler*,IEntity*,ERR_HQRadioNode*){return true;}};\n'
|
|
routefields='SCR_AIGroup* LocalSource=nullptr;IEntity* LocalWitness=nullptr;ERR_HQStatusSampler* LocalRoster=nullptr;Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{},Originated{};int Cursor{},MessageId{};float Due{},StartedAt=-1,OriginDue=-1;vector SentFrom;std::string Payload;'
|
|
networkfields='int m_LocalChecks=0;Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
|
route='struct ERR_HQRoute{'+routefields+'\n'+'\n'.join(trans(extract(x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
|
|
network='struct Network{'+networkfields+'\n'+'\n'.join(trans(extract(x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route('])+'};\n'
|
|
tests=r'''
|
|
int main(){
|
|
Network network;SCR_AIGroup group;IEntity c[5];for(int i=0;i<5;i++)c[i].pos=V(i*500);
|
|
c[0].radio=2;for(int i=0;i<5;i++)network.Add(&c[i],&group,37+i,c[i].radio);network.Build();
|
|
assert(network.Route(&c[0])->Nodes.Count()==1);assert(network.Route(&c[3])->Nodes.Count()==4);assert(!network.Route(&c[4]));
|
|
ERR_HQJournal journal;auto up=network.Route(&c[2]);assert(up->Nodes.Count()==3);
|
|
assert(up->Advance(0,&journal)==0);assert(!up->Sending&&!up->Originated&&up->OriginDue==3000);
|
|
assert(journal.events.Count()==1&&journal.events.back().find("CALL ")==0);
|
|
assert(up->Advance(2999,&journal)==0);assert(journal.events.Count()==1);
|
|
assert(up->Advance(3000,&journal)==0);assert(up->Sending&&up->Originated&&up->Due==5000);
|
|
assert(up->Advance(5000,&journal)==0);
|
|
c[2].alive=false;assert(up->Advance(7000,&journal)==0);c[0].alive=false;assert(up->Advance(9000,&journal)==-1);
|
|
assert(journal.events.back().find("LOST")!=std::string::npos); // final transmission needs its live sender
|
|
for(auto& carrier:c)carrier.alive=true;
|
|
auto good=network.Route(&c[2]);assert(good->Advance(0,&journal)==0);assert(good->Advance(3000,&journal)==0);assert(good->Advance(5000,&journal)==0);
|
|
c[2].alive=false;assert(good->Advance(7000,&journal)==0);assert(good->Advance(9000,&journal)==1);
|
|
assert(journal.events.back().find("HQ received")!=std::string::npos); // a completed earlier hop survives its sender
|
|
auto severed=network.Route(&c[0]);assert(severed->Advance(0,&journal)==0);assert(severed->Advance(3000,&journal)==0);c[0].radio=0;assert(severed->Advance(5000,&journal)==-1);c[0].radio=2;
|
|
for(auto& carrier:c)carrier.alive=true;
|
|
auto lost=network.Route(&c[2]);assert(lost->Advance(0,&journal)==0);assert(lost->Advance(3000,&journal)==0);c[1].radio=0;assert(lost->Advance(5000,&journal)==-1);assert(lost->Advance(7000,&journal)==-1);
|
|
c[1].radio=1;auto moving=network.Route(&c[2]);moving->Advance(0,&journal);moving->Advance(3000,&journal);c[1].pos=V(499);assert(moving->Advance(5000,&journal)==-1);c[1].pos=V(500);
|
|
// No originating transmission may survive radio loss, death/incapacitation, or capture during the hold.
|
|
for(int reason=0;reason<3;reason++){
|
|
auto interrupted=network.Route(&c[0]);ERR_HQJournal calls;assert(interrupted->Advance(10000,&calls)==0);
|
|
if(reason==0)c[0].radio=1;
|
|
if(reason==1)c[0].alive=false;
|
|
if(reason==2)c[0].captured=true;
|
|
assert(interrupted->Advance(11500,&calls)==-1);assert(!interrupted->Sending&&!interrupted->Originated);
|
|
assert(calls.events.Count()==2&&calls.events[0].find("CALL ")==0&&calls.events[1].find("LOST ")==0);
|
|
c[0].radio=2;c[0].alive=true;c[0].captured=false;
|
|
assert(interrupted->Advance(15000,&calls)==-1);assert(calls.events.Count()==2);
|
|
}
|
|
auto direct=network.Route(&c[0]);ERR_HQJournal directLog;
|
|
assert(direct->Advance(0,&directLog)==0);assert(direct->Advance(2999,&directLog)==0);
|
|
assert(direct->Advance(3000,&directLog)==0);assert(direct->Advance(4999,&directLog)==0);assert(direct->Advance(5000,&directLog)==1);
|
|
assert(directLog.events.Count()==4); // CALL, CALL COMPLETE, TRANSMIT, RECEIVE exactly once.
|
|
ERR_HQRoute empty;assert(empty.Advance(0,&journal)==-1);assert(!empty.Originated&&!empty.Sending);
|
|
int beforeDown=journal.events.Count();auto down=network.Route(&c[2],true);assert(down->Nodes[0]->Carrier==&c[0]);assert(down->Advance(0,&journal)==0);
|
|
assert(down->Advance(2000,&journal)==0);assert(down->Advance(4000,&journal)==0);assert(down->Advance(6000,&journal)==1);
|
|
assert(!down->Originated&&down->OriginDue==-1);for(int j=beforeDown;j<journal.events.Count();j++)assert(journal.events[j].find("CALL")!=0);
|
|
auto captured=network.Route(&c[2],true);captured->Advance(0,&journal);c[0].captured=true;assert(captured->Advance(2000,&journal)==-1);c[0].captured=false;
|
|
auto timeout=network.Route(&c[2]);timeout->Advance(0,&journal);assert(timeout->Advance(30001,&journal)==-1);
|
|
Network disconnected;disconnected.Add(&c[1],&group,1,1);disconnected.Build();assert(!disconnected.Route(&c[1]));
|
|
Network boundary;c[1].pos=V(501);boundary.Add(&c[0],&group,1,2);boundary.Add(&c[1],&group,2,1);boundary.Build();assert(!boundary.Route(&c[1]));
|
|
network.Clear();assert(!network.Route(&c[0]));
|
|
Network crowded;std::vector<IEntity> many(2050);for(auto& carrier:many){carrier.radio=2;crowded.Add(&carrier,&group,1,2);}assert(crowded.Overflow&&crowded.m_Nodes.Count()==2048);
|
|
crowded.Build();assert(crowded.Overflow); // bounded dense graph work
|
|
std::cout<<"PASS: real-carrier shortest-hop paths, 500m boundary, three-hop cap, no-manpack failure, three-second originating-call interruption, unchanged downlink hop timing, completed-hop survival, interrupted final uplink, radio loss, capture, motion, timeout, reset and network bounds\n";
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='eai-hq-network-') as tmp:
|
|
cpp=Path(tmp)/'test.cpp';exe=Path(tmp)/'test';cpp.write_text(headers+route+network+tests)
|
|
subprocess.run(['g++','-std=c++17','-Wall','-Wextra',str(cpp),'-o',str(exe)],check=True)
|
|
subprocess.run([str(exe)],check=True)
|