Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

152 lines
15 KiB
Python

"""Actual one-time group radio state machine against native inventory API doubles."""
from pathlib import Path
import os,re
from policy_harness import ROOT,run
P=Path(os.environ.get('ERR_RADIO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyRadio.c'))
s=P.read_text()
assert 'GetOrigin' not in s and 'CallLater' not in s and 'Math.Random' not in s
callback=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c').read_text().split('class ERR_EnemyEquipmentCallback',1)[1].split('class ERR_EnemyEquipmentEntry',1)[0]
code='class ERR_EnemyEquipmentCallback'+callback+s
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('override ','').replace('class ','struct ')
code=code.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
code=code.replace('item.FindComponent(BaseRadioComponent)','item->baseRadio').replace('SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent))','item->radio')
code=code.replace('ref array<','Array<').replace('array<','Array<').replace('ref ERR_EnemyEquipmentCallback','ERR_EnemyEquipmentCallback*')
code=code.replace('static const ResourceName','inline static const ResourceName').replace('void ERR_EnemyRadio(','ERR_EnemyRadio(')
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
code=code.replace('foreach (','for (').replace('null','nullptr')
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Replication','SCR_Global','ERR_GameMode','EStoragePurpose','Math'):
code=code.replace(name+'.',name+'::')
code=code.replace('ERR_HQ hq','ERR_HQ* hq').replace('hq.Hunters().','hq->Hunters()->').replace('hq.Hunters()','hq->Hunters()')
for name in ('actor','inventory','item','radio','agent','m_Group','m_Operation','mode'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup|ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
code=code.replace('\n}\n','\n};\n')
equipment=Path(os.environ.get('ERR_EQUIPMENT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c')).read_text()
coordinator=equipment[equipment.index('class ERR_EnemyEquipmentGroup'):]
coordinator=re.sub(r'//[^\n]*','',coordinator).replace('protected ','').replace('ref ','').replace('class ','struct ')
coordinator=coordinator.replace('array<ERR_EnemyEquipmentGroup>','Array<ERR_EnemyEquipmentGroup*>').replace('array<ERR_EnemyEquipmentEntry>','Array<ERR_EnemyEquipmentEntry*>').replace('array<AIAgent>','Array<AIAgent>')
coordinator=coordinator.replace('SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent))','actor->combat').replace('SCR_AICombatComponent.Cast(entry.Actor.FindComponent(SCR_AICombatComponent))','entry->Actor->combat')
for cls in ('ERR_GameMode','ERR_EnemyRadio','ERR_EnemyEquipmentGroup','ERR_EnemyEquipmentEntry'):
coordinator=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',coordinator)
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
coordinator=coordinator.replace(obj+'.',obj+'->')
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_').replace('Mines.ReadyForResupply','Mines->ReadyForResupply').replace('Optics.Check','Optics->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Check','Radio->Check').replace('Radio.Register','Radio->Register')
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog'):coordinator=coordinator.replace(cls+'.',cls+'::')
coordinator=coordinator.replace('string.Format', 'Format')
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
coordinator=re.sub(r'\b(bool|int|float|SCR_AIGroup|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',coordinator).replace('\n}\n','\n};\n')
hook_stubs=r'''
float worldTime=0;
template<class... T>string Format(string,T...){return "";}
namespace ERR_EquipmentLog{void Round(ERR_GameMode*){}void Write(string,string){}}
struct AutoGrant{void Bind(IEntity,SCR_AIGroup){}};
struct SCR_AICombatComponent{void ERR_BindEnemySkill(ERR_GameMode*,Group*){} void ERR_RefreshEnemySkill(){}};
struct ERR_EnemyOptics{ERR_EnemyOptics(IEntity,Group*){}void Check(ERR_GameMode*){}};
struct MineGrant{bool ReadyForResupply(bool replacement=false){return false;}};
struct ERR_EnemyEquipmentEntry{AutoGrant Automatic;MineGrant*Mines=nullptr;IEntity Actor=nullptr;struct Group*Group=nullptr;SCR_AICombatComponent*Combat=nullptr;ERR_EnemyOptics*Optics=nullptr;bool Released=false;int SkillChecks=0;
void Check(ERR_GameMode*){}void Stop(ERR_GameMode*){Released=true;}};
'''
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;using ResourceName=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 t){this->push_back(t);}void Clear(){this->clear();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
struct Entity;struct Group;struct Inventory;struct Agent;struct Radio;struct SCR_AICombatComponent;
using IEntity=Entity*;using SCR_AIGroup=Group*;using InventoryStorageManagerComponent=Inventory*;using AIAgent=Agent*;using SCR_RadioComponent=Radio*;
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnFailed(){}virtual void OnAllComplete(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
struct EStoragePurpose{static const int PURPOSE_ANY=0;};
struct Radio{bool manpack=false;bool ERR_IsHQManpack(){return manpack;}};
struct Entity{SCR_AICombatComponent*combat=nullptr;Group*group=nullptr;Inventory*inventory=nullptr;bool eligible=true,baseRadio=false;Radio*radio=nullptr;};
struct Agent{IEntity actor=nullptr;IEntity GetControlledEntity(){return actor;}};
struct Group{Array<Agent*>agents;int players=0,faction=1;bool slave=false,expanded=true;
void GetAgents(Array<Agent*>&out){out=agents;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}
bool IsSlave(){return slave;}bool IsExpandComplete(){return expanded;}};
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
struct Math{static int Max(int a,int b){return std::max(a,b);}};
struct ERR_HQ{static ERR_HQ* Get(){return nullptr;} ERR_HQ* Hunters(){return this;} int VehicleRadioCapability(IEntity){return 0;} inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
void GetItems(Array<IEntity>&out){out=items;}
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
bool CanInsertResource(ResourceName p,int purpose){assert(purpose==0);return Kind(p)==2?manpackFits:basicFits;}
bool TrySpawnPrefabToStorage(ResourceName p,void*target,int slot,int purpose,ScriptedInventoryOperationCallback*cb){assert(!target&&slot==-1&&purpose==0);requests.Insert(Kind(p));last=cb;
if(callback==1){cb->OnComplete();}if(callback==2){cb->OnFailed();}return accept;}};
'''+code+hook_stubs+coordinator+r'''
struct Fixture{ERR_GameMode mode;Group group,other;Entity actors[3],basic,manpack;Agent agents[3];Inventory inv[3];Radio pack;
Fixture(){ERR_GameMode::current=&mode;pack.manpack=true;basic.baseRadio=true;manpack.baseRadio=true;manpack.radio=&pack;
for(int i=0;i<3;i++){actors[i].group=&group;actors[i].inventory=&inv[i];agents[i].actor=&actors[i];group.agents.Insert(&agents[i]);}}
void Bind(ERR_EnemyRadio&s){for(auto&a:actors)s.Register(&a);}
int Requests(){int n=0;for(auto&i:inv)n+=i.requests.Count();return n;}
};
int main(){
// Existing basic/manpack sets are terminal, including later loss. Basic sets aren't upgraded on ordinary groups.
for(int kind=1;kind<=2;kind++){Fixture f;ERR_EnemyRadio s(&f.group,kind==1?2:1);f.Bind(s);f.inv[0].items.Insert(kind==1?&f.basic:&f.manpack);
assert(s.Check(&f.mode)&&f.Requests()==0);f.inv[0].items.clear();for(int i=0;i<8;i++)s.Check(&f.mode);assert(f.Requests()==0);}
// Frozen relay policy: only positive ids congruent to one modulo four select a manpack.
for(int id=0;id<=9;id++){Fixture f;ERR_EnemyRadio s(&f.group,id);f.Bind(s);s.Check(&f.mode);
assert(s.m_RelaySelected==(id>0&&id%4==1));if(id==0){assert(f.Requests()==0);continue;}
assert(f.Requests()==1&&f.inv[0].requests[0]==(id%4==1?2:1));}
// A pending request owns the sole group grant slot; no second carrier or fallback even after timeout.
{Fixture f;ERR_EnemyRadio s(&f.group,1);f.Bind(s);for(int i=0;i<12;i++)s.Check(&f.mode);
assert(f.Requests()==1&&s.m_Stopped&&s.m_Submitted&&s.m_Checks==5);f.inv[0].last->OnFailed();s.Check(&f.mode);assert(f.Requests()==1);}
// Explicit sync/async failure allows a different original carrier once, bounded before check five.
for(int sync=0;sync<2;sync++){Fixture f;for(auto&i:f.inv)i.callback=sync?2:0;ERR_EnemyRadio s(&f.group,2);f.Bind(s);
for(int t=0;t<6;t++){s.Check(&f.mode);if(!sync){for(auto&i:f.inv)if(i.last)i.last->OnAllFailed();}}
assert(f.Requests()==3);for(auto&i:f.inv)assert(i.requests.Count()==1);}
// Success is terminal even if the physical item was removed before inspection; callback first result wins.
for(int sync=0;sync<2;sync++){Fixture f;f.inv[0].callback=sync?1:0;ERR_EnemyRadio s(&f.group,2);f.Bind(s);s.Check(&f.mode);
if(!sync){f.inv[0].last->OnAllComplete();}f.inv[0].last->OnFailed();assert(s.Check(&f.mode));assert(f.Requests()==1);}
// Native false plus synchronous success must not issue another copy.
{Fixture f;f.inv[0].accept=false;f.inv[0].callback=1;ERR_EnemyRadio s(&f.group,2);f.Bind(s);s.Check(&f.mode);assert(s.Check(&f.mode));assert(f.Requests()==1);}
// Existing basic capability is never replenished while optional manpack fitting is pending/refused.
{Fixture f;f.inv[1].items={&f.basic};for(auto&i:f.inv)i.manpackFits=false;ERR_EnemyRadio s(&f.group,1);f.Bind(s);s.Check(&f.mode);
f.inv[1].items.clear();for(int t=0;t<8;t++)s.Check(&f.mode);assert(f.Requests()==0&&s.m_BasicDone);}
// No fitting manpack: one basic fallback may fit; no parallel optional upgrade while it is pending.
{Fixture f;for(auto&i:f.inv)i.manpackFits=false;ERR_EnemyRadio s(&f.group,1);f.Bind(s);s.Check(&f.mode);assert(f.inv[0].requests[0]==1);
for(auto&i:f.inv){i.manpackFits=true;}s.Check(&f.mode);assert(f.Requests()==1);f.inv[0].last->OnComplete();s.Check(&f.mode);
assert(f.Requests()==2&&f.inv[0].requests[1]==2);}
// Unknown inventory/oversize inspection blocks all grants, but a detected existing manpack is enough.
{Fixture f;f.actors[2].inventory=nullptr;ERR_EnemyRadio s(&f.group,2);f.Bind(s);for(int t=0;t<5;t++)s.Check(&f.mode);assert(f.Requests()==0);}
{Fixture f;for(int i=0;i<257;i++)f.inv[1].items.Insert(&f.basic);ERR_EnemyRadio s(&f.group,2);f.Bind(s);s.Check(&f.mode);assert(f.Requests()==0);}
{Fixture f;f.actors[2].inventory=nullptr;f.inv[0].items={&f.manpack};ERR_EnemyRadio s(&f.group,1);f.Bind(s);assert(s.Check(&f.mode)&&f.Requests()==0);}
// Native roster expansion is bounded; final fifth check never issues.
{Fixture f;f.group.expanded=false;ERR_EnemyRadio s(&f.group,2);f.Bind(s);for(int i=0;i<4;i++)s.Check(&f.mode);f.group.expanded=true;
assert(s.Check(&f.mode)&&f.Requests()==0);}
// Changed/dead/captured/removed original crew terminates the whole startup allocation.
for(int change=0;change<4;change++){Fixture f;ERR_EnemyRadio s(&f.group,2);f.Bind(s);s.Check(&f.mode);
if(change==0){f.actors[0].group=&f.other;}if(change==1){f.actors[0].eligible=false;}
if(change==2){f.group.agents.pop_back();}if(change==3){f.group.agents[0]=&f.agents[1];}
assert(s.Check(&f.mode));f.inv[0].last->OnFailed();f.actors[0].group=&f.group;f.actors[0].eligible=true;s.Check(&f.mode);assert(f.Requests()==1);}
// An actor not bound through the coordinator's canonical entry cannot receive a grant in another group.
{Fixture f;ERR_EnemyRadio s(&f.group,2);for(int t=0;t<8;t++)s.Check(&f.mode);assert(f.Requests()==0);}
// Stop retains quarantine and registrations cannot revive it.
{Fixture f;ERR_EnemyRadio s(&f.group,2);f.Bind(s);s.Check(&f.mode);s.Stop();f.inv[0].last->OnFailed();s.Register(&f.actors[0]);s.Check(&f.mode);assert(f.Requests()==1);}
for(int bad=0;bad<6;bad++){Fixture f;ERR_EnemyRadio s(&f.group,2);f.Bind(s);
if(bad==0){Replication::server=false;}if(bad==1){SCR_Global::editor=true;}if(bad==2){f.mode.active=false;}
if(bad==3){f.group.slave=true;}if(bad==4){f.group.players=1;}if(bad==5){f.group.faction=2;}
assert(s.Check(&f.mode)&&f.Requests()==0);Replication::server=true;SCR_Global::editor=false;}
// Actual coordinator binds only its first exact actor entry; a transferred pending carrier cannot receive a second allocation.
{Fixture f;ERR_EnemyEquipment coordinator(&f.mode);coordinator.RegisterGroup(&f.group,1);coordinator.RegisterGroup(&f.group,2);
assert(coordinator.m_Groups.Count()==1&&coordinator.m_Groups[0]->Radio->m_RelaySelected);
coordinator.Tick(3000);assert(f.Requests()==1&&coordinator.m_Entries.Count()==3);
coordinator.Tick(3001);assert(f.Requests()==1&&coordinator.m_Groups[0]->Radio->m_Checks==1);
f.actors[0].group=&f.other;f.group.agents.erase(f.group.agents.begin());f.other.agents.Insert(&f.agents[0]);
coordinator.RegisterGroup(&f.other,2);coordinator.Tick(6000);
assert(coordinator.m_Groups[0]->Radio->m_Stopped&&coordinator.m_Groups[1]->Radio->m_Allowed.IsEmpty());
f.inv[0].last->OnFailed();for(int t=9000;t<=21000;t+=3000)coordinator.Tick(t);assert(f.Requests()==1);
coordinator.UnregisterGroup(&f.other);assert(coordinator.m_Groups[1]->Radio->m_Stopped);coordinator.Stop();}
// Mortar allocation owner explicitly opts out; later duplicate registration cannot turn fitting on or reroll.
{Fixture f;ERR_EnemyEquipment coordinator(&f.mode);coordinator.RegisterGroup(&f.group,1,false);coordinator.RegisterGroup(&f.group,1,true);
coordinator.Tick(3000);assert(!coordinator.m_Groups[0]->Radio&&f.Requests()==0);}
{Fixture f;ERR_EnemyEquipment coordinator(&f.mode);coordinator.RegisterGroup(&f.group,2);coordinator.Tick(3000);
auto radio=coordinator.m_Groups[0]->Radio;coordinator.Stop();f.inv[0].last->OnFailed();coordinator.Tick(6000);assert(radio->m_Stopped&&f.Requests()==1);}
assert(ERR_EnemyRadio::Capability(nullptr)==-1);
}
''')
print('PASS: actual group radio startup preservation, finite native grants, callback quarantine, frozen selection/actor ownership and bounded observation')