170 lines
16 KiB
Python
170 lines
16 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','ERR_LocationKind','ERR_ObjectiveDefence'):
|
|
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','m_Scouts','m_Recon','m_Airfield','m_Hunters','m_Towns'):
|
|
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.replace('Evidence().','Evidence()->').replace('Cargo().','Cargo()->')
|
|
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,'protected bool CreateBattery(')
|
|
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 Runtime(const char*){return chance;}};
|
|
namespace ERR_LocationKind {enum {RADIO=1};}
|
|
namespace ERR_ObjectiveDefence {bool Objective(int kind){return kind==1;}}
|
|
struct Definition{int Kind=3;string Id="site-a";Array<int>* Patrol=nullptr;};
|
|
struct Site{SCR_AIGroup Group=nullptr;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 EvidenceService{void Capture(IEntity){}void Retire(IEntity){}};
|
|
struct Mode{EvidenceService evidence;EvidenceService* Evidence(){return &evidence;}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;}bool IsMissionEnemy(int f){return f==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 Scouts{SCR_AIGroup owned=nullptr;bool Contains(SCR_AIGroup group){return group&&group==owned;}};
|
|
struct CargoFlight{SCR_AIGroup crew=nullptr;SCR_AIGroup Crew(){return crew;}};struct Airfield{CargoFlight* flight=nullptr;CargoFlight* Cargo(){return flight;}};
|
|
struct Recon{SCR_AIGroup owned=nullptr;bool Owns(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 ReservedForSupport=false;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{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;Scouts* m_Hunters=nullptr;Scouts* m_Towns=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;mode.sites.home.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]->ReservedForSupport);
|
|
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);
|
|
Group radioGuards;mode.sites.home.Group=&radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
|
|
assert(hq.m_Units[3]->ReservedForSupport && hq.m_Units[3]->HomeGuard);
|
|
hq.RegisterGroup(&radioGuards);assert(hq.m_Units.Count()==4&&hq.m_Units[3]->ReservedForSupport);
|
|
// Same home, separate mobile formation: only the permanent defenders are reserved.
|
|
Group mobile;hq.RegisterGroup(&mobile);
|
|
assert(!hq.m_Units[4]->ReservedForSupport && !hq.m_Units[4]->HomeGuard && hq.m_Units[4]->HomeId==hq.m_Units[3]->HomeId);
|
|
Group hunter;Scouts hunters;hunters.owned=&hunter;hq.m_Hunters=&hunters;hq.RegisterGroup(&hunter);
|
|
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()); // every hunter selects physical manpack fitting
|
|
Group ordinaryAgain;hq.RegisterGroup(&ordinaryAgain);assert(hq.m_Equipment.ids.back()==7); // ordinary fitting remains distributed
|
|
Group town;Scouts towns;towns.owned=&town;hq.m_Towns=&towns;hq.RegisterGroup(&town);
|
|
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()&&!hq.m_Units.back()->ReservedForSupport&&!hq.m_Units.back()->HomeGuard);
|
|
}
|
|
''')
|
|
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')
|