Reduce redundant AI equipment, radio and observation checks
continuous-integration/drone/push Build is passing

Preserve live ownership and detection rules while skipping redundant work. Record published Workshop version 0.0.150 and add operation-count regressions.
This commit is contained in:
2026-11-09 17:05:25 +01:00
parent 47cd4fa2bc
commit 4b737c825f
12 changed files with 335 additions and 18 deletions
+16 -3
View File
@@ -12,7 +12,7 @@ for cls in ('ERR_GameMode','SCR_AIGroup','ERR_EnemyRadio','ERR_EnemyEquipmentGro
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.Check','Radio->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Register','Radio->Register')
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog'):coordinator=coordinator.replace(cls+'.',cls+'::')
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog','ERR_Actors'):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')
@@ -31,10 +31,11 @@ struct SCR_AIGroup;
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
namespace ERR_EquipmentLog{void Round(ERR_GameMode*){}void Write(string,string){}}
struct SCR_AICombatComponent{int binds=0,refreshes=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}void ERR_RefreshEnemySkill(){refreshes++;}};
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true,unconscious=false;};
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true,unconscious=false,alive=true,player=false,captured=false;};
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
namespace ERR_EnemyEquipmentRules{bool Owned(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* actor){return Owned(m,g,actor)&&!actor->unconscious;}}
namespace ERR_Actors{bool CanFight(IEntity* actor){return actor&&actor->alive&&!actor->unconscious;}}
namespace ERR_EnemyEquipmentRules{int ownedChecks=0;bool Owned(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){++ownedChecks;return actor&&actor->eligible&&actor->alive&&!actor->player&&!actor->captured;}bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* actor){return Owned(m,g,actor)&&ERR_Actors::CanFight(actor);}}
struct ERR_EnemyOptics{
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
ERR_EnemyOptics(IEntity* a,SCR_AIGroup* g):actor(a),group(g){}
@@ -66,7 +67,9 @@ int main(){
service.m_Stopped=true;assert(!service.MineSupplyReady(&group,&soldier));service.m_Stopped=false;
service.m_Entries.resize(2049,nullptr);assert(!service.MineSupplyReady(&group,&soldier));service.m_Entries.resize(1);
service.Tick(3001);assert(entry->checks==1&&entry->Optics->checks==1&&combat.refreshes==1);
ERR_EnemyEquipmentRules::ownedChecks=0;
service.Tick(6000);assert(entry->checks==2&&entry->Optics->checks==2&&combat.binds==1&&combat.refreshes==2);
assert(ERR_EnemyEquipmentRules::ownedChecks==1); // Immediate Eligible must not repeat successful Owned.
service.UnregisterGroup(&group);service.Tick(9000);assert(service.m_Entries[0]->Released&&service.m_Entries[0]->checks==2&&entry->Optics->checks==2&&entry->Optics->stops==1);
service.RegisterGroup(&group);service.Tick(12000);assert(service.m_Entries.Count()==1&&combat.binds==1&&combat.refreshes==2);
service.Stop();service.RegisterGroup(&group);service.Tick(50000);assert(service.m_Groups.Count()==0&&service.m_Stopped);
@@ -88,6 +91,16 @@ int main(){
auto capturedOptics=capture.m_Entries[0]->Optics;capturedBody.eligible=false;capture.Tick(6000);
assert(capturedOptics->checks==1&&capturedOptics->stops==1&&capture.m_Entries[0]->Released);
capturedBody.eligible=true;capture.Tick(9000);assert(capturedOptics->checks==1);
// Every pass still performs ownership vetoes before skill or equipment work.
for(int reason=0;reason<3;reason++){
ERR_EnemyEquipment ownership(&mode);SCR_AIGroup ownedGroup;IEntity ownedBody;AIAgent ownedAgent;ownedAgent.actor=&ownedBody;
ownedGroup.agents.Insert(&ownedAgent);ownership.RegisterGroup(&ownedGroup);ownership.Tick(3000);auto ownedEntry=ownership.m_Entries[0];
if(reason==0)ownedBody.alive=false;else if(reason==1)ownedBody.player=true;else ownedBody.captured=true;
ERR_EnemyEquipmentRules::ownedChecks=0;ownership.Tick(6000);
assert(ownedEntry->Released&&ownedEntry->stops==1&&ownedEntry->checks==1&&ERR_EnemyEquipmentRules::ownedChecks==1);
ownedBody.alive=true;ownedBody.player=false;ownedBody.captured=false;ownership.Tick(9000);
assert(ownedEntry->checks==1&&ownedEntry->stops==1); // Terminal release never restarts completed grants.
}
}
''')
print('PASS: actual equipment coordinator registration dedupe, startup delay, five member scans, finite component retries, unregister/stop terminal behavior and 256-group bound')
+22 -1
View File
@@ -24,10 +24,17 @@ rules=convert(rules.replace('class ERR_EnemyEquipmentRules','struct ERR_EnemyEqu
# Declare helper before rules, then define its real method bodies after rules.
helper=helper[helper.index(' static ERR_ConvoyTask TaskForGroup'):helper.rfind('}')]
helper=convert(helper).replace('static ERR_ConvoyTask* TaskForGroup','ERR_ConvoyTask* ERR_EnemyGunnerSkill::TaskForGroup').replace('static bool CapturedGroup','bool ERR_EnemyGunnerSkill::CapturedGroup').replace('static bool OriginalGunner','bool ERR_EnemyGunnerSkill::OriginalGunner').replace('static EAISkill Profile','EAISkill ERR_EnemyGunnerSkill::Profile')
# Count entry into the real query; changing only the floor models a future policy.
helper,count=re.subn(r'(bool ERR_EnemyGunnerSkill::OriginalGunner\([^)]*\)\s*\{)',r'\1 originalGunnerCalls++;',helper)
assert count==1
assert rules.count('return EAISkill::CYLON;')==1
rules=rules.replace('return EAISkill::CYLON;','return useRequestedFloor ? skill : EAISkill::CYLON;')
run(r'''
#include <cassert>
#include <vector>
enum class EAISkill{ROOKIE=20,REGULAR=50,EXPERT=80,CYLON=100};
bool useRequestedFloor=false;
int originalGunnerCalls=0;
enum class ERR_ForceKind{CONVOY,GUARD};
enum class ECompartmentType{TURRET,CARGO};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}};
@@ -97,7 +104,9 @@ int main(){
}
ERR_ConvoyRules::capture=true;assert(!ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));ERR_ConvoyRules::capture=false;
// Current turret specialization must not overwrite the underlying requested skill.
originalGunnerCalls=0;
SCR_AICombatComponent combat;combat.owner=&actor;combat.ERR_BindEnemySkill(&mode,&group);assert(combat.skill==EAISkill::CYLON);
assert(originalGunnerCalls==0);
combat.ResetAISkill();assert(combat.skill==EAISkill::CYLON);
combat.SetAISkill(static_cast<EAISkill>(90));assert(combat.skill==EAISkill::CYLON);
actor.vehicle=nullptr;slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
@@ -114,6 +123,18 @@ int main(){
hq.task=nullptr;combat.SetAISkill(EAISkill::REGULAR);combat.ERR_BindEnemySkill(&mode,&group);assert(combat.skill==EAISkill::CYLON);
combat.SetAISkill(static_cast<EAISkill>(90));assert(combat.skill==EAISkill::CYLON);
combat.ERR_ReleaseEnemySkill(&mode);assert(combat.skill==EAISkill::REGULAR);
assert(originalGunnerCalls==0); // Both refresh and setter skip irrelevant identity scans.
// A future lower floor still queries real turret identity and applies its profile.
useRequestedFloor=true;hq.task=&task;slot.occupant=&actor;actor.vehicle=&vehicle;
SCR_AICombatComponent lower;lower.owner=&actor;
lower.ERR_BindEnemySkill(&mode,&group);assert(lower.skill==EAISkill::CYLON&&originalGunnerCalls==1);
lower.SetAISkill(EAISkill::ROOKIE);assert(lower.skill==EAISkill::CYLON&&originalGunnerCalls==2);
slot.occupant=nullptr;lower.ERR_RefreshEnemySkill();assert(lower.skill==EAISkill::ROOKIE&&originalGunnerCalls==3);
lower.SetAISkill(EAISkill::REGULAR);assert(lower.skill==EAISkill::REGULAR&&originalGunnerCalls==4);
lower.SetAISkill(EAISkill::CYLON);lower.ERR_RefreshEnemySkill();assert(lower.skill==EAISkill::CYLON&&originalGunnerCalls==4);
ERR_ConvoyRules::capture=true;int guardedWrites=lower.writes;lower.ERR_RefreshEnemySkill();
assert(lower.writes==guardedWrites&&originalGunnerCalls==4);ERR_ConvoyRules::capture=false;
lower.ERR_ReleaseEnemySkill(&mode);assert(lower.skill==EAISkill::REGULAR);
}
''')
print('PASS: actual original turret identity, native/canonical guards, scoped maximum-skill profile, reset, dismount/reboard, requested-skill preservation and restoration')
print('PASS: actual original turret identity and ownership guards; zero irrelevant query calls at maximum floor; lower-floor query/profile fallback; reset, dismount/reboard and restoration')
+2 -1
View File
@@ -31,7 +31,7 @@ for cls in ('ERR_GameMode','ERR_EnemyRadio','ERR_EnemyEquipmentGroup','ERR_Enemy
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+'::')
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog','ERR_Actors'):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')
@@ -70,6 +70,7 @@ struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){retur
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 ERR_Actors{static bool CanFight(IEntity a){return a!=nullptr;}};
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;}
+42 -5
View File
@@ -2,6 +2,8 @@
from policy_harness import ROOT, run
from pathlib import Path
import re, os
expected_empty=int(os.environ.get('ERR_EXPECT_EMPTY_LOOKUPS','0'))
expected_far=int(os.environ.get('ERR_EXPECT_FAR_TRANSFORMS','0'))
path=Path(os.environ.get('ERR_BODY_SOURCE', str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQBodyObserver.c')))
s=path.read_text()
assert 'GroupLookup' not in s and 'Spawn(' not in s and 'Accept(' not in s and 'm_Contacts' not in s
@@ -31,19 +33,21 @@ code=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2=0;',code)
code=re.sub(r'\b(IEntity|SCR_AIGroup) (\w+);',r'\1 \2=nullptr;',code)
code=code.replace('ERR_HQBodyScan* state;','ERR_HQBodyScan* state=nullptr;').replace('ERR_HQBodyScan* selected;','ERR_HQBodyScan* selected=nullptr;')
code=code.replace('\n}\n','\n};\n').replace('class ', 'struct ')
run(r'''
run(f"#define EXPECT_EMPTY {expected_empty}\n#define EXPECT_FAR {expected_far}\n"+r'''
#include <cassert>
#include <cmath>
#include <vector>
#include <algorithm>
#include <string>
#include <map>
#include <iostream>
using string=std::string;
int normalizations=0;
struct vector {float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}
vector operator-(vector v)const{return {x-v.x,y-v.y,z-v.z};}
vector operator+(vector v)const{return {x+v.x,y+v.y,z+v.z};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
vector Normalized()const{normalizations++;float n=Length();return {x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
@@ -51,8 +55,8 @@ template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;
void Remove(int n){this->erase(this->begin()+n);}
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct ECharacterLifeState{static constexpr int DEAD=1;};
struct Entity {vector position,forward{0,0,1};bool dead=false,awake=true,inVehicle=false,character=true;int reads=0;
vector GetOrigin(){reads++;return position;}void GetWorldTransform(vector v[4]){v[2]=forward;}
struct Entity {vector position,forward{0,0,1};bool dead=false,awake=true,inVehicle=false,character=true;int reads=0,transformReads=0;
vector GetOrigin(){reads++;return position;}void GetWorldTransform(vector v[4]){transformReads++;v[2]=forward;}
Entity* GetCharacterController(){return character?this:nullptr;}
int GetLifeState(){return dead?1:0;}};
using IEntity=Entity*;Entity* CastCharacter(Entity* p){return p&&p->character?p:nullptr;}
@@ -61,7 +65,7 @@ struct Group {Array<AIAgent> agents;int faction=1,players=0;
int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}
void GetAgents(Array<AIAgent>& out){out=agents;}};using SCR_AIGroup=Group*;
struct ForceRecord {bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
struct Ledger {ForceRecord force;ERR_ForceRecord FindGroup(Group*){return &force;}};
struct Ledger {ForceRecord force;int finds=0;ERR_ForceRecord FindGroup(Group*){finds++;return &force;}};
struct GameMode {Ledger ledger;static GameMode* current;
static GameMode* Get(){return current;}int EnemyFaction(){return 1;}Ledger* Forces(){return &ledger;}};
GameMode* GameMode::current=nullptr;
@@ -156,6 +160,39 @@ int main(){
service.m_Groups[0]->LastAsked=0;service.m_Groups[1]->NextScan=1;
assert(!service.Observe(&contenders[1],1000,report));
service.Observe(&contenders[1],6000,report);assert(service.m_Groups[1]->NextScan==21000);
// Independently count the empty-body and impossible-distance paths in actual source.
service.Stop();Entity healthy;Agent healthyAgent;healthyAgent.actor=&healthy;Group healthyGroup;healthyGroup.agents={&healthyAgent};
auto* healthyRoster=Roster(&healthyGroup);service.RegisterGroup(&healthyGroup,healthyRoster);
mode.ledger.finds=0;int beforeEmptyTraces=game.world.traces;
for(int i=0;i<100;i++){assert(!service.Observe(&healthyGroup,0,report));assert(!report);}
assert(service.m_Now==0&&service.m_NextBudget==5000&&service.m_Traces==12); // budget still refreshes
assert(service.m_Groups[0]->LastAsked==-1&&service.m_Groups[0]->NextScan==0);
assert(game.world.traces==beforeEmptyTraces&&mode.ledger.finds==EXPECT_EMPTY);
std::cout<<"BODY_WORK empty_calls=100 ledger_lookups="<<mode.ledger.finds<<"\n";
// A later death becomes a candidate on the normal5s refresh; no stale empty cache.
healthy.dead=true;service.TickBudget(5000);assert(service.m_Bodies.Count()==1);
int beforeOwnership=mode.ledger.finds;mode.ledger.force.Physical=false;
assert(!service.Observe(&healthyGroup,5000,report)&&mode.ledger.finds==beforeOwnership+1);
assert(service.m_Groups[0]->LastAsked==-1);mode.ledger.force.Physical=true;
healthyGroup.players=1;assert(!service.Observe(&healthyGroup,5000,report));healthyGroup.players=0;
healthyGroup.faction=2;assert(!service.Observe(&healthyGroup,5000,report));healthyGroup.faction=1;
service.Stop();service.m_Traces=12;observer.position={0,0,0};observer.forward={0,0,1};observer.awake=true;observer.inVehicle=false;
Entity remote;remote.dead=true;remote.position={0,0,1000};observer.transformReads=0;normalizations=0;
int beforeFarTraces=game.world.traces;
for(int i=0;i<128;i++)assert(!service.Visible(&observer,&remote,point));
assert(observer.transformReads==EXPECT_FAR&&normalizations==EXPECT_FAR*2);
assert(game.world.traces==beforeFarTraces&&service.m_Traces==12);
std::cout<<"BODY_WORK far_pairs=128 transforms="<<observer.transformReads<<" normalizations="<<normalizations<<" traces=0\n";
// Exact edge60m is still visible and12m rear remains allowed; outside60 cannot trace.
game.world.hit=nullptr;game.world.fraction=1;remote.position={0,0,60};
assert(service.Visible(&observer,&remote,point));remote.position={0,0,60.01f};
assert(!service.Visible(&observer,&remote,point));remote.position={0,0,-12};assert(service.Visible(&observer,&remote,point));
remote.position={0,0,-12.01f};assert(!service.Visible(&observer,&remote,point));
remote.position={0,0,NAN};assert(!service.Visible(&observer,&remote,point));
remote.position={0,0,INFINITY};assert(!service.Visible(&observer,&remote,point));
// Backward request cannot refresh candidates or observe released/dead identities anew.
service.m_Now=10000;int beforeStale=mode.ledger.finds;
assert(!service.Observe(&healthyGroup,9999,report)&&mode.ledger.finds==beforeStale);
// Registry cap is finite, logs once, and deleted identities free future slots.
service.Stop();logs=0;Entity actors[4128];Agent agents[4128];Group groups[129];
for(int g=0;g<129;g++){for(int i=0;i<32;i++){int n=g*32+i;agents[n].actor=&actors[n];groups[g].agents.Insert(&agents[n]);}service.RegisterGroup(&groups[g],Roster(&groups[g]));}
+78
View File
@@ -0,0 +1,78 @@
"""Execute actual HQ/vehicle radio queries; prove max-capability short circuit."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
source=Path(os.environ.get('ERR_HQ_REPORT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQReport.c'))
hq=body(source,'static int Capability(')
hq=hq.replace('IEntity character','Entity* character').replace('ERR_Actors.CanFight','CanFight')
hq=hq.replace('InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));','Storage* inventory = character->storage;')
hq=hq.replace('array<IEntity> items = {};','std::vector<Entity*> items;').replace('inventory.GetItems','inventory->GetItems').replace('ERR_HQ hq = ERR_HQ.Get();','HQ* hq = HQ::current;').replace('hq.Hunters()','hq->Hunters()').replace('Hunters().','Hunters()->')
hq=hq.replace('foreach (IEntity item : items)','for (Entity* item : items)').replace('item.FindComponent(BaseRadioComponent)','HasBaseRadio(item)').replace('SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));','Radio* gadget = GetRadio(item);').replace('gadget.','gadget->').replace('Math.Max','std::max').replace('int result;','int result=0;')
vehicle=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c','int VehicleRadioCapability(')
vehicle=vehicle.replace('IEntity actor','Entity* actor').replace('foreach (ERR_HunterVehicle unit : m_Hunters)','for (Unit* unit : m_Hunters)').replace('InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));','Storage* storage = unit->Entity->storage;')
vehicle=vehicle.replace('array<IEntity> items = {};','std::vector<Entity*> items;').replace('foreach (IEntity item : items)','for (Entity* item : items)').replace('item.FindComponent(BaseRadioComponent)','HasBaseRadio(item)').replace('SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));','Radio* radio = GetRadio(item);')
for obj in ('m_Mode','unit','storage','radio'):vehicle=vehicle.replace(obj+'.',obj+'->')
vehicle=vehicle.replace('ERR_Actors.Alive','Alive').replace('CompartmentAccessComponent.GetVehicleIn','VehicleIn').replace('ERR_ConvoyRules.Captured','Captured')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <iostream>
struct Radio{bool pack=false;bool ERR_IsHQManpack(){return pack;}};
struct Storage;
struct Entity{bool alive=true,fit=true,baseRadio=false,captured=false;Entity* vehicle=nullptr;Storage* storage=nullptr;Radio* radio=nullptr;int componentQueries=0;};
struct Storage{int enumerations=0;std::vector<Entity*>items;void GetItems(std::vector<Entity*>&out){enumerations++;out=items;}};
struct Members:std::vector<Entity*>{bool Contains(Entity*p){return std::find(begin(),end(),p)!=end();}};
struct Mode{bool active=true;bool Active(){return active;}};
struct Unit{struct Entity* Entity=nullptr;void* Group=nullptr;bool Released=false,valid=true;struct Members Members;bool CrewValid(Mode*){return valid;}};
bool Alive(Entity*e){return e&&e->alive;}bool CanFight(Entity*e){return Alive(e)&&e->fit;}
Entity* VehicleIn(Entity*e){return e->vehicle;}bool Captured(Entity*e,void*){return e->captured;}
bool HasBaseRadio(Entity*e){e->componentQueries++;return e->baseRadio;}Radio* GetRadio(Entity*e){e->componentQueries++;return e->radio;}
struct Service{Mode* m_Mode=nullptr;bool m_Stopped=false;std::vector<Unit*>m_Hunters;
'''+vehicle+r'''
};
struct HQ{static inline HQ*current=nullptr;Service* service=nullptr;Service*Hunters(){return service;}};
struct Query{
'''+hq+r'''
};
int main(){
HQ h;Service service;Mode mode;HQ::current=&h;h.service=&service;service.m_Mode=&mode;
Entity actor,car,manpack,basic,junk;Storage carried,mounted;Radio packRadio,basicRadio;packRadio.pack=true;
actor.storage=&carried;car.storage=&mounted;manpack.baseRadio=true;manpack.radio=&packRadio;basic.baseRadio=true;basic.radio=&basicRadio;
Unit unit;unit.Entity=&car;unit.Members.push_back(&actor);service.m_Hunters={&unit};actor.vehicle=&car;mounted.items={&manpack};
assert(Query::Capability(nullptr)==0);actor.fit=false;assert(Query::Capability(&actor)==0&&carried.enumerations==0&&mounted.enumerations==0);actor.fit=true;
actor.alive=false;assert(Query::Capability(&actor)==0);actor.alive=true;
actor.storage=nullptr;assert(Query::Capability(&actor)==0&&mounted.enumerations==0);actor.storage=&carried;
// The real vehicle helper still validates ownership and physical inventory.
//128 carried objects must not be enumerated or scanned when it returns max2.
carried.items.assign(128,&junk);
assert(Query::Capability(&actor)==2);
std::cout<<"OPCOUNTS actor_inventory="<<carried.enumerations<<" carried_item_components="<<junk.componentQueries<<" vehicle_inventory="<<mounted.enumerations<<std::endl;
assert(carried.enumerations==0&&junk.componentQueries==0&&mounted.enumerations==1);
// Every following call observes current vehicle state; no added cache.
carried.items={&basic};
for(int reason=0;reason<7;reason++){
if(reason==0)actor.vehicle=nullptr;
if(reason==1)car.captured=true;
if(reason==2)car.alive=false;
if(reason==3)unit.Released=true;
if(reason==4)unit.valid=false;
if(reason==5)mounted.items.clear();
if(reason==6)unit.Members.clear();
int before=carried.enumerations;assert(Query::Capability(&actor)==1&&carried.enumerations==before+1);
actor.vehicle=&car;car.captured=false;car.alive=true;unit.Released=false;unit.valid=true;mounted.items={&manpack};unit.Members.clear();unit.Members.push_back(&actor);
before=carried.enumerations;assert(Query::Capability(&actor)==2&&carried.enumerations==before);
}
// Lack of HQ/service/active mission never invents a mounted link; ordinary
//personal inventory classification keeps its existing0/1/2 behavior.
mode.active=false;assert(Query::Capability(&actor)==1);mode.active=true;
service.m_Stopped=true;assert(Query::Capability(&actor)==1);service.m_Stopped=false;
h.service=nullptr;
for(int cap=0;cap<=2;cap++){
carried.items={nullptr,&junk};if(cap==1)carried.items.push_back(&basic);if(cap==2)carried.items.push_back(&manpack);
int before=carried.enumerations;assert(Query::Capability(&actor)==cap&&carried.enumerations==before+1);
}
HQ::current=nullptr;assert(Query::Capability(&actor)==2);
}
''')
print('PASS: actual HQ/vehicle capability preserves living/inventory/ownership/physical-radio gates; max2 skips128 carried-item checks and one enumeration; changes remain live')
+147
View File
@@ -0,0 +1,147 @@
"""Execute actual collection policy; compare outputs and classification calls.
Native sphere queries and FriendlyActor's engine getters are not benchmarked.
"""
from pathlib import Path
import os
from policy_harness import ROOT, body, run
source = Path(os.environ.get('ERR_MINE_SOURCE', str(
ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_KnownMines.c')))
candidate = body(source, 'protected bool Collect(').replace('protected ', '')
new = 'if (ChimeraCharacter.Cast(actor) && !m_Near.Contains(actor) && FriendlyActor(actor))'
old = 'if (FriendlyActor(actor) && !m_Near.Contains(actor))'
# Retain the exact predecessor expression as a differential reference. Nothing
# else in the actual extracted body is replaced except the function name.
baseline = candidate.replace(new, old).replace('bool Collect(', 'bool Baseline(')
friendly = body(source, 'protected bool FriendlyActor(').replace('protected ', '')
friendly = friendly.replace('{', '{ ++calls;', 1)
for name in ('ChimeraCharacter', 'ERR_Actors', 'CompartmentAccessComponent', 'SCR_FactionManager'):
friendly = friendly.replace(name + '.', name + '::')
friendly = friendly.replace('m_Mode.', 'm_Mode->').replace('faction.', 'faction->')
friendly = friendly.replace('EnemyFaction().', 'EnemyFaction()->')
candidate = candidate.replace('ChimeraCharacter.Cast', 'ChimeraCharacter::Cast')
run(r'''
#include <algorithm>
#include <cassert>
#include <iostream>
#include <vector>
struct Actor {bool friendly=false;bool character=true;};
using IEntity=Actor*;
int casts=0;
struct ChimeraCharacter {static IEntity Cast(IEntity actor) {
++casts;return actor && actor->character ? actor : nullptr;
}};
struct ERR_Actors {
static bool CanFight(IEntity actor) {return actor && actor->friendly;}
static bool Player(IEntity) {return false;}
};
struct CompartmentAccessComponent {static bool GetVehicleIn(IEntity) {return false;}};
struct FactionState {bool IsFactionEnemy(FactionState*) {return false;}};
using Faction=FactionState*;
FactionState faction;
struct SCR_FactionManager {static Faction SGetFaction(IEntity) {return &faction;}};
struct Mode {Faction EnemyFaction() {return &faction;}};
struct Entities:std::vector<IEntity> {
mutable int contains=0;
int Count() const {return size();}
bool Contains(IEntity actor) const {
++contains;
return std::find(begin(),end(),actor)!=end();
}
void Insert(IEntity actor) {push_back(actor);}
};
struct Collector {
Entities m_Near;
int calls=0,castCalls=0;
Mode mode;Mode* m_Mode=&mode;
''' + friendly + '\n' + candidate + '\n' + baseline + r'''
};
bool Step(Collector& before,Collector& after,IEntity actor) {
int stamp=casts;bool b=before.Baseline(actor);before.castCalls+=casts-stamp;
stamp=casts;bool a=after.Collect(actor);after.castCalls+=casts-stamp;
assert(b==a);
assert(before.m_Near==after.m_Near); // identity, order and multiplicity
return a;
}
int main() {
{
Collector b,a;Actor actor{true};
for(int i=0;i<32;i++) assert(Step(b,a,&actor));
assert(a.m_Near.size()==1 && b.calls==32 && a.calls==1);
// Already accepted actors remain in this pass even if later classified false.
actor.friendly=false;assert(Step(b,a,&actor));
assert(b.calls==33 && a.calls==1);
std::cout<<"duplicate_accepted baseline_calls=33 candidate_calls=1 members=1\n";
}
{
Collector b,a;Actor actor{false};
assert(Step(b,a,&actor));assert(Step(b,a,&actor));
assert(a.m_Near.empty() && b.calls==2 && a.calls==2);
actor.friendly=true;assert(Step(b,a,&actor));
assert(Step(b,a,&actor));
assert(a.m_Near.size()==1 && b.calls==4 && a.calls==3);
std::cout<<"rejected_then_friendly baseline_calls=4 candidate_calls=3 members=1\n";
}
{
Collector b,a;
assert(Step(b,a,nullptr));assert(Step(b,a,nullptr));
assert(a.m_Near.empty() && b.calls==2 && a.calls==0);
assert(b.castCalls==2 && a.castCalls==2);
assert(b.m_Near.contains==0 && a.m_Near.contains==0);
std::cout<<"null baseline_calls=2 candidate_calls=0 casts_each=2 contains_each=0 members=0\n";
}
{
Collector b,a;Actor actors[129];
for(int i=0;i<128;i++) {actors[i].friendly=true;assert(Step(b,a,&actors[i]));}
assert(a.m_Near.size()==128 && b.calls==128 && a.calls==128);
actors[128].friendly=true;
assert(!Step(b,a,&actors[128])); // new eligible actor
assert(!Step(b,a,&actors[0])); // accepted duplicate
assert(!Step(b,a,nullptr)); // rejected/null callback
assert(b.calls==128 && a.calls==128);
std::cout<<"capacity baseline_calls=128 candidate_calls=128 blocked_callbacks=3 members=128\n";
}
{
Collector b,a;Actor actors[3]{{true},{false},{true}};
for(IEntity actor:{&actors[2],&actors[0],&actors[2],&actors[1],IEntity(nullptr),&actors[0]})
assert(Step(b,a,actor));
assert(a.m_Near[0]==&actors[2] && a.m_Near[1]==&actors[0]);
assert(b.calls==6 && a.calls==3);
// A fresh tick must reconsider every actor; no cache survives m_Near.Clear().
b.m_Near.clear();a.m_Near.clear();
assert(Step(b,a,&actors[0]));assert(Step(b,a,&actors[2]));
assert(b.calls==8 && a.calls==5);
std::cout<<"mixed_and_fresh_pass baseline_calls=8 candidate_calls=5 members=2\n";
}
{
Collector b,a;Actor accepted{true},prop{true,false};
assert(Step(b,a,&accepted));
int bc=b.castCalls,ac=a.castCalls,bl=b.m_Near.contains,al=a.m_Near.contains;
for(int i=0;i<1000;i++) assert(Step(b,a,&prop));
assert(b.castCalls-bc==1000 && a.castCalls-ac==1000);
assert(b.m_Near.contains==bl && a.m_Near.contains==al);
assert(b.calls==1001 && a.calls==1);
std::cout<<"props callbacks=1000 casts_each=1000 contains_each=0 baseline_classifications=1000 candidate_classifications=0\n";
}
{
Collector b,a;Actor accepted{true};
assert(Step(b,a,&accepted));
assert(b.castCalls==1 && a.castCalls==2); // Explicit first-seen character overhead.
for(int i=0;i<31;i++) assert(Step(b,a,&accepted));
assert(b.castCalls==32 && a.castCalls==33);
assert(b.calls==32 && a.calls==1);
std::cout<<"character_cast_tradeoff callbacks=32 baseline_casts=32 candidate_casts=33 baseline_classifications=32 candidate_classifications=1\n";
}
{
Collector b,a;Actor accepted[127],rejected{false};
for(auto& actor:accepted) {actor.friendly=true;assert(Step(b,a,&actor));}
int bl=b.m_Near.contains,al=a.m_Near.contains;
for(int i=0;i<100;i++) assert(Step(b,a,&rejected));
assert(b.m_Near.contains==bl && a.m_Near.contains-al==100);
assert(a.m_Near.size()==127 && b.calls==227 && a.calls==227);
std::cout<<"rejected_character_tradeoff callbacks=100 accepted_members=127 baseline_contains=0 candidate_contains=100 classifications_each=100\n";
}
}
''')
print('PASS: actual Collect output/order/capacity equivalence and classification operation counts')