#!/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 queue = {}','Arr queue').replace('array cell','Arr* cell').replace('array neighbors','Arr* neighbors') s=s.replace('cell = {};','cell = new Arr();') 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 #include #include #include #include #include #include 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);} templatestruct Arr:std::vector{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);}}; templatestruct Map:std::map{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 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 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 m_Nodes;Arr m_Parents,m_Depths;Map m_Indices;Map*> 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;jAdvance(0,&journal);c[0].captured=true;assert(captured->Advance(2000,&journal)==-1);c[0].captured=false; // The sender must stay valid on EVERY observed tick of the two-second hop, // not only at completion. Recovery before Due cannot revive an interrupted hop. for(bool downlink:{false,true})for(int reason=0;reason<3;reason++){ auto interrupted=network.Route(downlink?&c[1]:&c[0],downlink);ERR_HQJournal log; assert(interrupted->Advance(0,&log)==0); float sendingAt=3000; if(downlink)sendingAt=2000; assert(interrupted->Advance(sendingAt,&log)==0&&interrupted->Sending); if(reason==0)c[0].alive=false; if(reason==1)c[0].captured=true; if(reason==2)c[0].radio=0; assert(interrupted->Advance(sendingAt+1000,&log)==-1); assert(interrupted->Failed&&log.events.back().find("LOST ")==0); int terminalEvents=log.events.Count(); c[0].alive=true;c[0].captured=false;c[0].radio=2; assert(interrupted->Advance(sendingAt+2000,&log)==-1&&log.events.Count()==terminalEvents); } // Receiver interruption and leaving/re-entering the link also cancel the // same physical short-range hop in either direction, before its due time. for(bool downlink:{false,true})for(int reason=0;reason<4;reason++){ auto interrupted=network.Route(downlink?&c[1]:&c[2],downlink);ERR_HQJournal log; assert(interrupted->Advance(0,&log)==0); float sendingAt=downlink?2000:3000; assert(interrupted->Advance(sendingAt,&log)==0&&interrupted->Sending); assert(interrupted->Nodes[interrupted->Cursor+1]->Carrier==&c[1]); if(reason==0)c[1].alive=false; if(reason==1)c[1].captured=true; if(reason==2)c[1].radio=0; if(reason==3)c[1].pos=V(3000); assert(interrupted->Advance(sendingAt+1000,&log)==-1); int terminalEvents=log.events.Count(); c[1].alive=true;c[1].captured=false;c[1].radio=1;c[1].pos=V(500); assert(interrupted->Advance(sendingAt+2000,&log)==-1&&log.events.Count()==terminalEvents); } // HQ's first downlink leg has no physical sender; its original receiving // manpack must still survive while waiting, rather than use a late recovery. auto firstDown=network.Route(&c[1],true);ERR_HQJournal firstDownLog; assert(firstDown->Advance(0,&firstDownLog)==0&&firstDown->Cursor==-1); c[0].radio=1;assert(firstDown->Advance(1000,&firstDownLog)==-1); c[0].radio=2;assert(firstDown->Advance(2000,&firstDownLog)==-1); // Healthy receiver-bearing hops keep the cursor and RECEIVE log unchanged // during intermediate polling, with both 500m boundary links still valid. for(bool downlink:{false,true}){ auto healthy=network.Route(downlink?&c[1]:&c[2],downlink);ERR_HQJournal log; assert(healthy->Advance(0,&log)==0); float sendingAt=downlink?2000:3000; assert(healthy->Advance(sendingAt,&log)==0&&healthy->Sending); int cursor=healthy->Cursor,events=log.events.Count(); assert(healthy->Advance(sendingAt+1000,&log)==0&&healthy->Cursor==cursor&&log.events.Count()==events); assert(healthy->Advance(sendingAt+2000,&log)==(downlink?1:0)&&healthy->Cursor==cursor+1); } // Healthy intermediate polling does not transmit early or duplicate log entries. auto waiting=network.Route(&c[0]);ERR_HQJournal waitingLog; assert(waiting->Advance(0,&waitingLog)==0&&waiting->Advance(3000,&waitingLog)==0); int transmittingEvents=waitingLog.events.Count(); assert(waiting->Advance(4000,&waitingLog)==0&&waiting->Sending); assert(waitingLog.events.Count()==transmittingEvents); assert(waiting->Advance(5000,&waitingLog)==1&&waitingLog.events.Count()==transmittingEvents+1); 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 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)