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

93 lines
9.1 KiB
Python

"""Actual local handoff predicate/history/network route/origin delivery state machine."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
network=Path(os.environ.get('ERR_NETWORK_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQNetwork.c'))
helper=Path(os.environ.get('ERR_HANDOFF_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQLocalHandoff.c'))
# Reuse the existing native graph syntax translation, not its policy doubles.
base=Path(os.environ.get('ERR_NETWORK_HARNESS',ROOT/'tests/hq_network_regression.py')).read_text()
ns={'__file__':str(ROOT/'tests/hq_network_regression.py')}
exec(base[:base.index("headers=r'''")],ns)
base_trans=ns['trans']
def trans(s):
s=base_trans(s).replace('ref ','')
s=s.replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('ERR_EnemyEquipmentRules.','ERR_EnemyEquipmentRules::').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_HQLocalHandoff.','ERR_HQLocalHandoff::')
s=re.sub(r'\bERR_GameMode (\w+)',r'ERR_GameMode* \1',s)
s=re.sub(r'\bERR_HQStatusSampler (\w+)',r'ERR_HQStatusSampler* \1',s)
s=s.replace('array<IEntity> busy','Arr<IEntity*>* busy').replace('array<IEntity>','Arr<IEntity*>').replace('array<float>','Arr<float>')
s=s.replace('foreach (ERR_HQRadioNode* node : m_Nodes)','for (ERR_HQRadioNode* node : m_Nodes)')
for name in ('roster','receiver','witness','busy'):
s=s.replace(name+'.',name+'->')
s=s.replace('receiver->Carrier.GetOrigin()', 'receiver->Carrier->GetOrigin()')
return s
headers=r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <string>
#include <limits>
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]);}static float Distance(vector a,vector b){return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}};
vector V(float x,float z=0,float y=0){return {{x,y,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>{using std::vector<T>::vector;using std::vector<T>::operator=;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);}void Remove(int i){this->erase(this->begin()+i);}int Find(T x){auto it=std::find(this->begin(),this->end(),x);return it==this->end()?-1:int(it-this->begin());}bool Contains(T x){return Find(x)>=0;}};
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,military=true;int players=0;};
struct IEntity{vector pos;bool alive=true,captured=false,player=false;int radio=0;SCR_AIGroup* group=nullptr;vector GetOrigin(){return pos;}};
struct ERR_GameMode{static ERR_GameMode*Get(){static ERR_GameMode m;return &m;}};
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* a){return m&&g&&g->enemy&&g->military&&g->players==0&&a&&a->alive&&!a->captured&&!a->player&&a->group==g;}};
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}};
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return ERR_EnemyEquipmentRules::Eligible(ERR_GameMode::Get(),Group,Carrier)&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
struct ERR_HQStatusSampler{SCR_AIGroup*group=nullptr;Arr<IEntity*>members;bool attributed=true;bool OwnsRoster(SCR_AIGroup*g){return group&&group==g;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int i,IEntity*a){return attributed&&members[i]==a;}};
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
'''
helper_code=trans(helper.read_text()).replace('class ERR_HQLocalHandoff','struct ERR_HQLocalHandoff').replace('class ERR_HQObservationHistory','struct ERR_HQObservationHistory').replace('\n}\n','\n};\n')
route_fields='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;'
route='struct ERR_HQRoute{'+route_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
network_fields='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{};'
graph='struct Network{'+network_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route(', 'ERR_HQRoute LocalRoute('])+'};\n'
run(headers+helper_code+route+graph+r'''
struct Fixture{SCR_AIGroup source,receiver;IEntity witness,caller,other;ERR_HQStatusSampler roster;Network network;Arr<IEntity*>busy;
Fixture(){witness.group=&source;caller.group=&receiver;caller.radio=2;caller.pos=V(100);roster.group=&source;roster.members={&witness};network.Add(&caller,&receiver,2,2);network.Build();}
ERR_HQRoute* Local(){return network.LocalRoute(&source,&roster,&busy);}
};
int main(){
{Fixture f;auto route=f.Local();assert(route&&route->Nodes[0]->Carrier==&f.caller&&route->LocalSource==&f.source&&route->LocalWitness==&f.witness&&route->LocalRoster==&f.roster);
ERR_HQJournal journal;route->MessageId=17;route->Payload="unchanged sighting";assert(route->Advance(0,&journal)==0&&!route->Originated&&!route->Sending);
assert(route->Advance(2999,&journal)==0&&!route->Originated);assert(route->Advance(3000,&journal)==0&&route->Originated&&route->Sending);
// Local information is already handed over; later witness loss cannot erase it.
f.witness.alive=false;assert(route->Advance(5000,&journal)==1&&route->MessageId==17&&route->Payload=="unchanged sighting");}
for(int reason=0;reason<13;reason++){
Fixture f;auto r=f.Local();ERR_HQJournal j;assert(r&&r->Advance(0,&j)==0);
if(reason==0){f.witness.alive=false;}if(reason==1){f.witness.group=&f.receiver;}
if(reason==2){f.witness.player=true;}if(reason==3){f.roster.attributed=false;}
if(reason==4){f.caller.radio=0;}if(reason==5){f.caller.alive=false;}
if(reason==6){f.caller.captured=true;}if(reason==7){f.caller.group=&f.source;}
if(reason==8){f.caller.pos=V(100.01f);}if(reason==9){f.caller.pos=V(0,0,100.01f);}
if(reason==10){r->LocalSource=nullptr;}if(reason==11){r->LocalWitness=nullptr;}
if(reason==12){f.source.players=1;}
assert(r->Advance(1000,&j)==-1&&r->Failed&&!r->Originated&&!r->Sending);assert(r->Advance(5000,&j)==-1);
}
// Never trade an invalid first member for an unowned replacement; a different original may hand over.
{Fixture f;f.roster.members[0]=&f.other;assert(!f.Local());f.other.group=&f.source;f.roster.members.Insert(&f.witness);assert(f.Local());}
{Fixture f;f.busy.Insert(&f.caller);assert(!f.Local());f.busy.Clear();assert(f.Local());}
{Fixture f;f.caller.radio=1;f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,1);f.network.Build();assert(!f.Local());}
{Fixture f;f.roster.members.resize(33,&f.witness);assert(!f.Local());}
{Fixture f;f.network.m_LocalChecks=4096;assert(!f.Local());f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,2);f.network.Build();assert(f.Local());}
{Fixture f;f.network.Overflow=true;assert(!f.Local());}
// Direct routes remain unaffected and final radio loss is still terminal after local completion.
{Fixture f;auto direct=f.network.Route(&f.caller);f.witness.alive=false;ERR_HQJournal j;assert(direct->Advance(0,&j)==0);assert(direct->Advance(3000,&j)==0);assert(direct->Advance(5000,&j)==1);}
{Fixture f;auto r=f.Local();ERR_HQJournal j;r->Advance(0,&j);r->Advance(3000,&j);f.caller.radio=0;assert(r->Advance(5000,&j)==-1);}
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
assert(!history.CanQueue(&target,102,101)&&!history.CanQueue(&target,101,602));
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
}
''')
print('PASS: actual original-roster local handoff,100m3D bound,busy carrier/budget exclusion,origin interruption including deletedsource,normal radio delivery and per-source replay history')