Reduce redundant AI equipment, radio and observation checks
continuous-integration/drone/push Build is passing
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:
+5
-1
@@ -1,6 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Latest published Workshop version: **0.0.149**
|
Latest published Workshop version: **0.0.150**
|
||||||
|
|
||||||
|
## 2026-11-09 — AI processing
|
||||||
|
|
||||||
|
- Reduced redundant AI equipment, radio, corpse-observation and mine-avoidance checks.
|
||||||
|
|
||||||
## 2026-11-07 — Clearer setting choices
|
## 2026-11-07 — Clearer setting choices
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ modded class SCR_AICombatComponent
|
|||||||
void ERR_RefreshEnemySkill()
|
void ERR_RefreshEnemySkill()
|
||||||
{
|
{
|
||||||
if (!m_ERRSkillMode || !ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())) return;
|
if (!m_ERRSkillMode || !ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())) return;
|
||||||
EAISkill skill = ERR_EnemyGunnerSkill.Profile(m_ERRRequestedSkill, ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner()));
|
bool originalGunner;
|
||||||
|
// A maximum floor already determines the profile; preserve the lower-floor path.
|
||||||
|
if (ERR_EnemyEquipmentRules.Floor(m_ERRRequestedSkill) < EAISkill.CYLON)
|
||||||
|
originalGunner = ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner());
|
||||||
|
EAISkill skill = ERR_EnemyGunnerSkill.Profile(m_ERRRequestedSkill, originalGunner);
|
||||||
if (GetAISkill() != skill) super.SetAISkill(skill);
|
if (GetAISkill() != skill) super.SetAISkill(skill);
|
||||||
}
|
}
|
||||||
void ERR_ReleaseEnemySkill(ERR_GameMode mode)
|
void ERR_ReleaseEnemySkill(ERR_GameMode mode)
|
||||||
@@ -50,7 +54,10 @@ modded class SCR_AICombatComponent
|
|||||||
if (m_ERRSkillMode && ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner()))
|
if (m_ERRSkillMode && ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner()))
|
||||||
{
|
{
|
||||||
m_ERRRequestedSkill = skill;
|
m_ERRRequestedSkill = skill;
|
||||||
skill = ERR_EnemyGunnerSkill.Profile(skill, ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner()));
|
bool originalGunner;
|
||||||
|
if (ERR_EnemyEquipmentRules.Floor(skill) < EAISkill.CYLON)
|
||||||
|
originalGunner = ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner());
|
||||||
|
skill = ERR_EnemyGunnerSkill.Profile(skill, originalGunner);
|
||||||
}
|
}
|
||||||
super.SetAISkill(skill);
|
super.SetAISkill(skill);
|
||||||
}
|
}
|
||||||
@@ -184,7 +191,8 @@ class ERR_EnemyEquipment
|
|||||||
if (!ERR_EnemyEquipmentRules.Owned(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
|
if (!ERR_EnemyEquipmentRules.Owned(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
|
||||||
// Incapacitation suspends work on the existing entry; it must not permanently
|
// Incapacitation suspends work on the existing entry; it must not permanently
|
||||||
// remove the skill binding or recreate completed equipment grants on recovery.
|
// remove the skill binding or recreate completed equipment grants on recovery.
|
||||||
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) continue;
|
// Owned was checked immediately above; only the consciousness gate remains.
|
||||||
|
if (!ERR_Actors.CanFight(entry.Actor)) continue;
|
||||||
// Combat may arrive after the group's initial membership notification.
|
// Combat may arrive after the group's initial membership notification.
|
||||||
if (!entry.Combat && entry.SkillChecks < 5)
|
if (!entry.Combat && entry.SkillChecks < 5)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ class ERR_HQBodyObserver
|
|||||||
vector observerPosition = observer.GetOrigin();
|
vector observerPosition = observer.GetOrigin();
|
||||||
vector delta = bodyPosition - observerPosition;
|
vector delta = bodyPosition - observerPosition;
|
||||||
float distance = delta.Length();
|
float distance = delta.Length();
|
||||||
|
// No facing can admit an invalid or distant body; avoid native transform work.
|
||||||
|
if (!ERR_HQBodyRules.InView(distance, 1)) return false;
|
||||||
delta[1] = 0;
|
delta[1] = 0;
|
||||||
vector transform[4]; observer.GetWorldTransform(transform);
|
vector transform[4]; observer.GetWorldTransform(transform);
|
||||||
vector forward = transform[2]; forward[1] = 0;
|
vector forward = transform[2]; forward[1] = 0;
|
||||||
@@ -162,7 +164,8 @@ class ERR_HQBodyObserver
|
|||||||
report = null;
|
report = null;
|
||||||
if (!Replication.IsServer() || !ERR_HQMemoryRules.Finite(now) || now < m_Now || now < 0) return false;
|
if (!Replication.IsServer() || !ERR_HQMemoryRules.Finite(now) || now < m_Now || now < 0) return false;
|
||||||
TickBudget(now);
|
TickBudget(now);
|
||||||
if (!OwnedPhysical(group) || group.GetPlayerCount(true) > 0 || m_Bodies.IsEmpty()) return false;
|
// Refresh death candidates first, then skip ownership lookups if none exist.
|
||||||
|
if (m_Bodies.IsEmpty() || !OwnedPhysical(group) || group.GetPlayerCount(true) > 0) return false;
|
||||||
ERR_HQBodyScan state;
|
ERR_HQBodyScan state;
|
||||||
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) state = scan;
|
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) state = scan;
|
||||||
if (!state) return false;
|
if (!state) return false;
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ class ERR_HQRadio
|
|||||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));
|
||||||
if (!inventory)
|
if (!inventory)
|
||||||
return 0;
|
return 0;
|
||||||
array<IEntity> items = {};
|
|
||||||
inventory.GetItems(items);
|
|
||||||
int result;
|
int result;
|
||||||
ERR_HQ hq = ERR_HQ.Get();
|
ERR_HQ hq = ERR_HQ.Get();
|
||||||
if (hq && hq.Hunters()) result = hq.Hunters().VehicleRadioCapability(character);
|
if (hq && hq.Hunters()) result = hq.Hunters().VehicleRadioCapability(character);
|
||||||
|
// A live physical vehicle manpack already supplies the maximum capability.
|
||||||
|
if (result == 2) return 2;
|
||||||
|
array<IEntity> items = {};
|
||||||
|
inventory.GetItems(items);
|
||||||
foreach (IEntity item : items)
|
foreach (IEntity item : items)
|
||||||
{
|
{
|
||||||
if (!item || !item.FindComponent(BaseRadioComponent))
|
if (!item || !item.FindComponent(BaseRadioComponent))
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ class ERR_KnownMines
|
|||||||
protected bool Collect(IEntity actor)
|
protected bool Collect(IEntity actor)
|
||||||
{
|
{
|
||||||
if (m_Near.Count() >= 128) return false;
|
if (m_Near.Count() >= 128) return false;
|
||||||
if (FriendlyActor(actor) && !m_Near.Contains(actor)) m_Near.Insert(actor);
|
// Reject props cheaply; overlapping queries need not reclassify accepted soldiers.
|
||||||
|
if (ChimeraCharacter.Cast(actor) && !m_Near.Contains(actor) && FriendlyActor(actor)) m_Near.Insert(actor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
protected ERR_KnownMine Blocking(vector start, vector goal, float radius)
|
protected ERR_KnownMine Blocking(vector start, vector goal, float radius)
|
||||||
|
|||||||
@@ -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'):
|
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
|
||||||
coordinator=coordinator.replace(obj+'.',obj+'->')
|
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')
|
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('string.Format', 'Format')
|
||||||
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
||||||
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
|
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;}};
|
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){}}
|
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 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 AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
|
||||||
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
|
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{
|
struct ERR_EnemyOptics{
|
||||||
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
|
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
|
||||||
ERR_EnemyOptics(IEntity* a,SCR_AIGroup* g):actor(a),group(g){}
|
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_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.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);
|
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);
|
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.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.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);
|
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);
|
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);
|
assert(capturedOptics->checks==1&&capturedOptics->stops==1&&capture.m_Entries[0]->Released);
|
||||||
capturedBody.eligible=true;capture.Tick(9000);assert(capturedOptics->checks==1);
|
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')
|
print('PASS: actual equipment coordinator registration dedupe, startup delay, five member scans, finite component retries, unregister/stop terminal behavior and 256-group bound')
|
||||||
|
|||||||
@@ -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.
|
# Declare helper before rules, then define its real method bodies after rules.
|
||||||
helper=helper[helper.index(' static ERR_ConvoyTask TaskForGroup'):helper.rfind('}')]
|
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')
|
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'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
enum class EAISkill{ROOKIE=20,REGULAR=50,EXPERT=80,CYLON=100};
|
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 ERR_ForceKind{CONVOY,GUARD};
|
||||||
enum class ECompartmentType{TURRET,CARGO};
|
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();}};
|
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;
|
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.
|
// 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);
|
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.ResetAISkill();assert(combat.skill==EAISkill::CYLON);
|
||||||
combat.SetAISkill(static_cast<EAISkill>(90));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);
|
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);
|
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.SetAISkill(static_cast<EAISkill>(90));assert(combat.skill==EAISkill::CYLON);
|
||||||
combat.ERR_ReleaseEnemySkill(&mode);assert(combat.skill==EAISkill::REGULAR);
|
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')
|
||||||
|
|||||||
@@ -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'):
|
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
|
||||||
coordinator=coordinator.replace(obj+'.',obj+'->')
|
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')
|
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('string.Format', 'Format')
|
||||||
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
||||||
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
|
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 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_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_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;
|
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;}
|
void GetItems(Array<IEntity>&out){out=items;}
|
||||||
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
|
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
from policy_harness import ROOT, run
|
from policy_harness import ROOT, run
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re, os
|
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')))
|
path=Path(os.environ.get('ERR_BODY_SOURCE', str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQBodyObserver.c')))
|
||||||
s=path.read_text()
|
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
|
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=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('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 ')
|
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 <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <iostream>
|
||||||
using string=std::string;
|
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;}
|
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};}
|
||||||
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);}
|
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;}};
|
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;
|
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;
|
||||||
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
|
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);}
|
void Remove(int n){this->erase(this->begin()+n);}
|
||||||
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||||
struct ECharacterLifeState{static constexpr int DEAD=1;};
|
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;
|
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]){v[2]=forward;}
|
vector GetOrigin(){reads++;return position;}void GetWorldTransform(vector v[4]){transformReads++;v[2]=forward;}
|
||||||
Entity* GetCharacterController(){return character?this:nullptr;}
|
Entity* GetCharacterController(){return character?this:nullptr;}
|
||||||
int GetLifeState(){return dead?1:0;}};
|
int GetLifeState(){return dead?1:0;}};
|
||||||
using IEntity=Entity*;Entity* CastCharacter(Entity* p){return p&&p->character?p:nullptr;}
|
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;}
|
int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}
|
||||||
void GetAgents(Array<AIAgent>& out){out=agents;}};using SCR_AIGroup=Group*;
|
void GetAgents(Array<AIAgent>& out){out=agents;}};using SCR_AIGroup=Group*;
|
||||||
struct ForceRecord {bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
|
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;
|
struct GameMode {Ledger ledger;static GameMode* current;
|
||||||
static GameMode* Get(){return current;}int EnemyFaction(){return 1;}Ledger* Forces(){return &ledger;}};
|
static GameMode* Get(){return current;}int EnemyFaction(){return 1;}Ledger* Forces(){return &ledger;}};
|
||||||
GameMode* GameMode::current=nullptr;
|
GameMode* GameMode::current=nullptr;
|
||||||
@@ -156,6 +160,39 @@ int main(){
|
|||||||
service.m_Groups[0]->LastAsked=0;service.m_Groups[1]->NextScan=1;
|
service.m_Groups[0]->LastAsked=0;service.m_Groups[1]->NextScan=1;
|
||||||
assert(!service.Observe(&contenders[1],1000,report));
|
assert(!service.Observe(&contenders[1],1000,report));
|
||||||
service.Observe(&contenders[1],6000,report);assert(service.m_Groups[1]->NextScan==21000);
|
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.
|
// 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];
|
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]));}
|
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]));}
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -21,6 +21,8 @@ def check():
|
|||||||
subprocess.run([sys.executable,'-m','unittest','discover','-s',str(ROOT/'tests'),'-p','test_discord_changelog_ci.py'],check=True)
|
subprocess.run([sys.executable,'-m','unittest','discover','-s',str(ROOT/'tests'),'-p','test_discord_changelog_ci.py'],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_round_statistics.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_round_statistics.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_ai_driver_horn.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_ai_driver_horn.py')],check=True)
|
||||||
|
subprocess.run([sys.executable,str(ROOT/'tests/test_hq_radio_capability.py')],check=True)
|
||||||
|
subprocess.run([sys.executable,str(ROOT/'tests/test_known_mine_collection.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_credentials.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_credentials.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_server_name.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_server_name.py')],check=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user