147 lines
14 KiB
Python
147 lines
14 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('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'):coordinator=coordinator.replace(cls+'.',cls+'::')
|
|
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;
|
|
struct SCR_AICombatComponent{void ERR_BindEnemySkill(ERR_GameMode*,Group*){} void ERR_RefreshEnemySkill(){}};
|
|
struct ERR_EnemyOptics{ERR_EnemyOptics(IEntity,Group*){}void Check(ERR_GameMode*){}};
|
|
struct ERR_EnemyEquipmentEntry{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')
|