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.
145 lines
14 KiB
Python
145 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'):
|
|
code=code.replace(name+'.',name+'::')
|
|
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 ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
|
|
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}};
|
|
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')
|