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

152 lines
14 KiB
Python

"""Actual Intel/HQ methods and readiness handoff; native expansion is an adapter."""
import re,os
from pathlib import Path
from policy_harness import ROOT, body, run
intel=ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelService.c'
hq=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
ready=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarReadiness.c'
system=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c'
def adapt(s):
s=s.replace('override ', '').replace('protected ', '').replace('null','nullptr')
s=re.sub(r'array<([^>]+)> (\w+) = \{\};',r'Array<\1> \2;',s)
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars'):
s=s.replace(n+'.',n+'->')
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
s=s.replace('home->Definition.', 'home->Definition->').replace('Patrol.IsEmpty()', 'Patrol->IsEmpty()').replace('unit->StatusSampler.Bind(', 'unit->StatusSampler.Bind(')
s=s.replace('ERR_SiteRuntime home;', 'ERR_SiteRuntime home=nullptr;')
s=s.replace('Forces().', 'Forces()->')
s=s.replace('Settings().','Settings()->').replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=s.replace('FindComponent(SCR_InventoryStorageManagerComponent)','FindComponent()').replace('FindComponent(SCR_AIGroupUtilityComponent)','FindComponent()')
s=s.replace('purpose: EStoragePurpose.PURPOSE_ANY','EStoragePurpose::PURPOSE_ANY').replace('cb: ', '').replace('Operation.', 'Operation->').replace('new ERR_IntelInsertCallback()', 'new Callback()')
s=s.replace('ERR_IntelRequest request = new ERR_IntelRequest();','ERR_IntelRequest request = new Request();')
s=s.replace('ERR_HQUnit unit = new ERR_HQUnit();','ERR_HQUnit unit = new Unit();')
s=re.sub(r'^.*(?:Print|m_Journal.Write)\([^\n]*$', ';',s,flags=re.M)
return s
register=adapt(body(intel,'void RegisterGroup('));tick=adapt(body(intel,'void Tick('));hqregister=adapt(body(hq,'override void RegisterGroup('));hqbind=adapt(body(hq,'protected void BindPhysical('))
observation=adapt(body(intel,'bool ERR_ProbeWasRolled('))
poll=body(ready,'ERR_MortarSupplyState Poll(')
# Execute exact early stopped/ready guards and final qualified handoff. Native
# population/inventory callback validations between them are not simulated.
prefix=poll[poll.index('{')+1:poll.index(' SCR_AIGroup group')]
suffix=poll[poll.index(' if (m_Battery.CountShells() != 27)'):poll.index(' group.ActivateAI();')]
handoff=adapt(prefix+suffix)+'return ERR_MortarSupplyState::READY;'
assert poll.index('if (m_Ready) return') < poll.index('mode.RegisterGroup(group)')
for condition in ('if (!operation.Finished)', 'if (!operation.Succeeded)', 'if (population == ERR_MortarSupplyState.FAILED)', 'if (population == ERR_MortarSupplyState.PENDING)'):
assert poll.index(condition)<poll.index('mode.RegisterGroup(group)')
assert poll.count('mode.RegisterGroup(group)')==1
prepare=body(system,'bool Prepare(')
assert 'mode.RegisterGroup(group)' not in prepare
assert prepare.index('mode.Own(group)')<prepare.index('m_HQ.RegisterGroup(group)')<prepare.index('m_HQ.ReserveGroup(group)')
site=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c','void Activate(')
assert site.index('m_State != ERR_PrepareResult.READY')<site.index('m_Mode.RegisterGroup(site.Group)')
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
#include <set>
using string=std::string;
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T a){this->push_back(a);}void Remove(int i){this->erase(this->begin()+i);}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
template<class T> struct Set:std::set<T>{bool Contains(T a){return this->count(a);}void Insert(T a){this->insert(a);}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int rolls=0;int RandomInt(int,int){rolls++;return 0;}}
namespace EStoragePurpose{const int PURPOSE_ANY=0;}
struct Callback{bool Finished=false,Succeeded=false;};
struct Inventory {int requests=0;bool accept=true,folder=false;bool TrySpawnPrefabToStorage(int,int,Callback*){requests++;return accept;}static Inventory* Cast(void* p){return static_cast<Inventory*>(p);}};
using SCR_InventoryStorageManagerComponent=Inventory;
struct Actor{bool alive=true,player=false;Inventory inventory;void* FindComponent(){return &inventory;}};
using IEntity=Actor*;
struct Agent{Actor* actor=nullptr;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}}
namespace ERR_Resources{int IntelFolder(){return 1;}}
namespace ERR_DiscoveryService{void Folders(Inventory* i,Array<IEntity>& a){a.clear();if(i->folder)a.Insert(nullptr);}}
struct Utility{static Utility* Cast(void* p){return static_cast<Utility*>(p);}};using SCR_AIGroupUtilityComponent=Utility;
struct Group{Array<AIAgent> members;Utility utility;int faction=1;void GetAgents(Array<AIAgent>& out){out=members;}void* FindComponent(){return &utility;}int GetFaction(){return faction;}};using SCR_AIGroup=Group*;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Definition{string Id="site-a";Array<int>* Patrol=nullptr;};
struct Site{struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
struct Sites{Site home;Site* FindGroup(SCR_AIGroup){return &home;}};using ERR_SiteService=Sites*;
struct ForceRecord{string SourceId="site-a";int InitialStrength=4;bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
struct Forces{ForceRecord record;bool available=false;ForceRecord* FindGroup(SCR_AIGroup){return available?&record:nullptr;}};
struct Mode{struct Sites sites;struct Forces forces;struct Sites* Sites(){return &sites;}struct Forces* Forces(){return &forces;}struct Settings settings;int publications=0;struct Settings* Settings(){return &settings;}int EnemyFaction(){return 1;}void RegisterGroup(SCR_AIGroup);};
struct Request{Callback* Operation=nullptr;IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Reroll{void Tick(){}};
struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled;bool m_Stopped=false;Reroll* m_Reroll=nullptr;void Unsettled(IEntity){}
'''+register+tick+observation+r'''
};
struct Clock{float GetWorldTime(){return 10;}Clock* GetWorld(){return this;}}clock_;Clock* GetGame(){return &clock_;}
struct Sampler{bool Bound(){return bindings>0;}int bindings=0,strength=0;SCR_AIGroup group=nullptr;string source;void Bind(SCR_AIGroup g,string id,int n,int faction){assert(faction==1);bindings++;group=g;source=id;strength=n;}};
struct GroupService{int registrations=0;Set<SCR_AIGroup> seen;Array<int> ids;Array<bool> fits;
void RegisterGroup(SCR_AIGroup g,int radioId=0,bool fitRadios=true){ids.Insert(radioId);fits.Insert(fitRadios);if(!seen.Contains(g)){seen.Insert(g);registrations++;}}};
struct Mortars{SCR_AIGroup owned=nullptr;bool FindGroup(SCR_AIGroup group){return group&&group==owned;}};
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;
'''+hqbind+hqregister+r'''
};
Intel* intel_;HQ* hq_;
void Mode::RegisterGroup(SCR_AIGroup group){forces.available=true;publications++;hq_->RegisterGroup(group);intel_->RegisterGroup(group);}
namespace ERR_MortarSupplyState{const int FAILED=-1,PENDING=0,READY=1;}
struct Battery{bool stopped=false;int shells=27;bool Stopped(){return stopped;}int CountShells(){return shells;}};
struct Ready{bool m_Stopped=false,m_Ready=false;Battery* m_Battery;int Finish(Mode* mode,SCR_AIGroup group){
'''+handoff+r'''
}};
int main(){
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;Mortars mortars;mortars.owned=&group;hq.m_Mortars=&mortars;
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==hq.m_Units[0]->Id&&hq.m_Units[0]->Id==1);
assert(!hq.m_Equipment.fits[0]); // Existing mortar readiness owns this group's radio allocation.
assert(!hq.m_Units[0]->StatusSampler.Bound()&&hq.m_Units[0]->ForceSourceId.empty()&&hq.m_Bodies.registrations==0);
Actor actors[4];Agent agents[4];for(int i=0;i<4;i++){agents[i].actor=&actors[i];group.members.Insert(&agents[i]);}
Battery battery;Ready ready{false,false,&battery};battery.shells=26;
assert(ready.Finish(&mode,&group)==0&&mode.publications==0&&Math::rolls==0);
battery.shells=27;assert(ready.Finish(&mode,&group)==1&&mode.publications==1&&Math::rolls==4&&intel.m_Requests.Count()==4);
assert(hq.m_Units.Count()==1&&hq.m_NextId==1);
assert(hq.m_Equipment.registrations==1&&hq.m_Bodies.registrations==1);
assert(hq.m_Units[0]->HomeGuard&&hq.m_Units[0]->HomeId=="site-a"&&hq.m_Units[0]->ForceSourceId=="site-a");
assert(hq.m_Units[0]->StatusSampler.bindings==1&&hq.m_Units[0]->StatusSampler.group==&group&&hq.m_Units[0]->StatusSampler.strength==4);
for(Actor& a:actors){assert(intel.ERR_ProbeWasRolled(&a));}assert(!intel.ERR_ProbeWasRolled(nullptr));Actor unrelated;assert(!intel.ERR_ProbeWasRolled(&unrelated));
Replication::server=false;assert(!intel.ERR_ProbeWasRolled(&actors[0]));Replication::server=true; // actual HQ method deduplicates readiness publication
for(int n=0;n<5;n++){assert(ready.Finish(&mode,&group)==1);intel.RegisterGroup(&group);hq.RegisterGroup(&group);}
assert(mode.publications==1&&Math::rolls==4&&hq.m_Units.Count()==1);
assert(hq.m_Units[0]->StatusSampler.bindings==1&&hq.m_Equipment.registrations==1&&hq.m_Bodies.registrations==1);
assert(hq.m_Units[0]->AmmoReporting.initializations==1);
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==1&&!hq.m_Equipment.fits[0]);
intel.Tick(1);for(Actor& a:actors)assert(a.inventory.requests==1);
for(int n=0;n<5;n++){intel.Tick(2+n);}for(Actor& a:actors)assert(a.inventory.requests==1); // accepted asynchronous spawn never reissued
for(Actor& a:actors){a.inventory.folder=true;}for(auto request:intel.m_Requests){request->Operation->Finished=true;request->Operation->Succeeded=true;}intel.Tick(10);assert(intel.m_Requests.Count()==0);
for(Actor& a:actors)a.inventory.folder=false; // real item subsequently picked up/dropped: assignment latch unchanged
intel.RegisterGroup(&group);intel.Tick(11);assert(Math::rolls==4&&intel.m_Requests.Count()==0);
actors[0].alive=false;intel.RegisterGroup(&group);intel.Tick(12);assert(Math::rolls==4);for(Actor& a:actors)assert(a.inventory.requests==1);
Ready stopped{true,false,&battery};assert(stopped.Finish(&mode,&group)==-1&&mode.publications==1);
battery.stopped=true;Ready cancelled{false,false,&battery};assert(cancelled.Finish(&mode,&group)==-1&&mode.publications==1);battery.stopped=false;
Actor killed;Agent killedAgent{&killed};Group deathGroup;deathGroup.members.Insert(&killedAgent);intel.RegisterGroup(&deathGroup);assert(Math::rolls==5);killed.alive=false;intel.Tick(13);assert(killed.inventory.requests==0&&intel.m_Requests.Count()==0);intel.RegisterGroup(&deathGroup);assert(Math::rolls==5);
Mode neverMode;neverMode.settings.chance=0;Intel never{&neverMode,{},{}};never.RegisterGroup(&group);int after=Math::rolls;neverMode.settings.chance=100;never.RegisterGroup(&group);assert(Math::rolls==after&&never.m_Requests.Count()==0); // no reroll of original negative decision
// Ordinary physical groups receive the stable HQ ID and opt in, with or without a mortar service.
Group ordinary,withoutMortars;hq.RegisterGroup(&ordinary);
assert(hq.m_Equipment.ids.Count()==2&&hq.m_Equipment.ids[1]==hq.m_Units[1]->Id&&hq.m_Equipment.ids[1]==2&&hq.m_Equipment.fits[1]);
hq.m_Mortars=nullptr;hq.RegisterGroup(&withoutMortars);
assert(hq.m_Equipment.ids.Count()==3&&hq.m_Equipment.ids[2]==hq.m_Units[2]->Id&&hq.m_Equipment.ids[2]==3&&hq.m_Equipment.fits[2]);
// A subsequent missing mortar registry cannot reroll an already bound group's opt-out.
hq.RegisterGroup(&group);assert(hq.m_Equipment.ids.Count()==3&&!hq.m_Equipment.fits[0]&&hq.m_NextId==3);
}
''')
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
# Observe only in Workbench, after readiness and before destructive fixture steps.
intel_source=intel.read_text()
observer_region=intel_source.split('#ifdef WORKBENCH',1)[1].split('#endif',1)[0]
assert 'bool ERR_ProbeWasRolled(IEntity actor)' in observer_region
assert 'ERR_Mortar' not in intel_source and 'ERR_HQ' not in intel_source
assert 'Insert(' not in observation and 'RandomInt' not in observation and 'TrySpawn' not in observation
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
step=smoke.split('else if (m_ERRSmokeStep == 120)',1)[1].split('else if (m_ERRSmokeStep == 121)',1)[0]
assert step.index('!battery.StartupActivated') < step.index('ERR_ProbeWasRolled(intelActor)') < step.index('DeleteEntityAndChildren(item)')
assert '!intelCrew.Contains(intelActor)' in step and 'intelCrew.Count() == 4' in step
assert 'RegisterGroup(' not in step and 'Settings().Set(' not in step and 'TrySpawn' not in step
print('PASS: Workbench-only read accessor rejects unknown/nonserver identities; Chat observes all distinct ready crew before existing shell-depletion mutation')