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

177 lines
11 KiB
Python

"""Run actual alarm emitter/native receiver methods against explicit native doubles.
Does not establish engine danger broadcast routing, attenuation, or event subclass
preservation; those remain native qualification requirements.
"""
import re
from policy_harness import ROOT, body, run
base = ROOT / 'addons/core/Scripts/Game/ERR'
source = base / '10_AI/ERR_VehicleAlarmPerception.c'
awareness = base / '10_AI/ERR_HQLocalAwareness.c'
def cpp(s):
s = s.replace('override ', '').replace('static void ', 'void ')
s = s.replace('SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent))', 'group.utility')
for old, new in [('Replication.', 'Replication::'), ('ERR_Actors.', 'ERR_Actors::'),
('ERR_GameMode.', 'Mode::'), ('ERR_HQLocalAwareness.', 'ERR_HQLocalAwareness::'),
('ERR_HQMemoryRules.', 'ERR_HQMemoryRules::'), ('ERR_Phase.', 'ERR_Phase::'),
('AIAgent.Cast', 'Agent::Cast'), ('SCR_AIGroup.Cast', 'Group::Cast'),
('SCR_FactionManager.', 'SCR_FactionManager::'), ('Math.', 'Math::'),
('EAIDangerEventType.', 'EAIDangerEventType::'), ('vector.', 'vector::')]:
s = s.replace(old, new)
s = s.replace('ERR_VehicleAlarmDangerEvent.Cast', 'Event::Cast')
s = s.replace('ERR_VehicleAlarmDangerEvent danger', 'Event* danger')
s = s.replace('new ERR_VehicleAlarmDangerEvent()', 'new Event()')
for name in ('vehicle', 'mode', 'utility', 'agent', 'witness', 'group', 'groupUtility', 'danger', 'm_ERRAwareness', 'originMode'):
s = re.sub(r'\b' + name + r'\.', name + '->', s)
for name in ('GetGame()', 'GetWorld()', 'GetAIWorld()', 'm_ConfigComponent', 'm_Perception'):
s = s.replace(name + '.', name + '->')
s = s.replace('ERR_VehicleAlarmPerception.Receive(utility);', 'perception.Receive(utility);')
return s.replace('super.Update', 'Base::Update')
methods = '\n'.join(cpp(body(source, signature)) for signature in ('static void Emit(', 'static void Receive('))
callback = cpp(body(awareness, 'void ERR_HearAlarm('))
hook = cpp(body(source, 'override void Update('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <vector>
struct vector {float x=0,y=0,z=0;
static float DistanceSq(vector a,vector b){float x=a.x-b.x,y=a.y-b.y,z=a.z-b.z;return x*x+y*y+z*z;}};
struct Actor;struct Mode;struct Group;struct Agent;struct Utility;struct GroupUtility;
using IEntity=Actor*;using ERR_GameMode=Mode*;using SCR_AIGroup=Group*;using AIAgent=Agent*;
using SCR_AIUtilityComponent=Utility*;using SCR_AIGroupUtilityComponent=GroupUtility*;
struct Faction{};
struct Actor {vector position;bool alive=true,player=false,awake=true;Group* group=nullptr;Faction* faction=nullptr;vector GetOrigin(){return position;}};
struct ERR_Phase{static const int PRISON=1;};
struct Mode {inline static Mode* current=nullptr;bool active=true;int phase=2;Faction* enemy=nullptr;
static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}Faction* EnemyFaction(){return enemy;}};
struct EAIDangerEventType {static const int Danger_VehicleHorn=14,Other=0;};
struct Danger {virtual ~Danger()=default;int type=0;Actor* victim=nullptr;vector position;
int GetDangerType(){return type;}void SetDangerType(int value){type=value;}void SetVictim(Actor* value){victim=value;}
vector GetPosition(){return position;}void SetPosition(vector value){position=value;}};
struct Event:Danger {ERR_GameMode Mode=nullptr;float ObservedAt=-1;static Event* Cast(Danger* value){return dynamic_cast<Event*>(value);}};
struct World{float now=100;float GetWorldTime(){return now;}};
struct AIWorld{std::vector<Danger*> broadcast;void RequestBroadcastDangerEvent(Danger* event){broadcast.push_back(event);}};
struct Game{World* world=nullptr;AIWorld* ai=nullptr;World* GetWorld(){return world;}AIWorld* GetAIWorld(){return ai;}};
Game* game=nullptr;Game* GetGame(){return game;}
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_Actors{static bool Alive(Actor* actor){return actor&&actor->alive;}static bool CanFight(Actor* actor){return Alive(actor)&&actor->awake;}
static bool Player(Actor* actor){return actor&&actor->player;}static struct Group* Group(Actor* actor){return actor?actor->group:nullptr;}};
struct SCR_FactionManager{static Faction* SGetFaction(Actor* actor){return actor?actor->faction:nullptr;}};
struct ERR_HQLocalAwareness{
int heard=0;SCR_AIGroup source=nullptr;vector position;float observed=-1,received=-1;
static bool Fresh(float at,float now){return std::isfinite(at)&&std::isfinite(now)&&at>=0&&now>=at&&now-at<=30000;}
void HearAlarm(SCR_AIGroup who,vector where,float at,float now){heard++;source=who;position=where;observed=at;received=now;}
};
struct ERR_HQMemoryRules{static bool PositionValid(vector p){return std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z);}};
struct Perception{ERR_HQLocalAwareness* m_ERRAwareness=nullptr;SCR_AIGroup m_Group=nullptr;bool enabled=true;
bool ERR_AwarenessActive(){return enabled&&m_ERRAwareness;}
''' + callback + r'''
};
struct GroupUtility{Perception* m_Perception=nullptr;};
struct Group{bool slave=false;int players=0;Faction* faction=nullptr;GroupUtility* utility=nullptr;
static Group* Cast(Group* g){return g;}bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}Faction* GetFaction(){return faction;}};
struct Agent{Actor* actor=nullptr;Group* group=nullptr;std::vector<Danger*> events;int reads=0;
static Agent* Cast(Agent* a){return a;}Actor* GetControlledEntity(){return actor;}Group* GetParentGroup(){return group;}
int GetDangerEventsCount(){return events.size();}Danger* GetDangerEvent(int i,int& count){reads++;count=1;return events.at(i);}};
struct Config{bool m_EnableDangerEvents=true;};
struct Utility{Config* m_ConfigComponent=nullptr;Agent* owner=nullptr;Agent* GetOwner(){return owner;}};
struct Math{static int Min(int a,int b){return std::min(a,b);}};
struct PerceptionDriver{
''' + methods + r'''
};
struct Base{int calls=0;void Update(Utility* utility,float){calls++;if(utility&&utility->owner)utility->owner->events.clear();}};
struct Threat:Base{PerceptionDriver perception;
''' + hook + r'''
};
struct Fixture{
World world;AIWorld ai;Game context;Mode mode;Faction enemy,other;Actor vehicle,witness;
Group group;GroupUtility groupUtility;Perception perception;ERR_HQLocalAwareness awareness;Agent agent;Utility utility;Config config;PerceptionDriver driver;
Fixture(){
game=&context;context.world=&world;context.ai=&ai;Mode::current=&mode;mode.enemy=&enemy;Replication::server=true;
group.faction=&enemy;group.utility=&groupUtility;groupUtility.m_Perception=&perception;
perception.m_ERRAwareness=&awareness;perception.m_Group=&group;
witness.group=&group;witness.faction=&enemy;agent.actor=&witness;agent.group=&group;utility.owner=&agent;utility.m_ConfigComponent=&config;
vehicle.position={20,0,0};
}
Event* emit(){driver.Emit(&vehicle);assert(ai.broadcast.size()==1);return Event::Cast(ai.broadcast[0]);}
};
int main(){
{
Fixture f;Event* event=f.emit();assert(event&&event->type==EAIDangerEventType::Danger_VehicleHorn&&event->victim==&f.vehicle&&event->Mode==&f.mode);
assert(event->ObservedAt==100&&event->position.x==20);
// A broadcast alone creates no observer or HQ evidence. Native queue delivery
// is supplied explicitly by this double; no engine-routing claim is made.
f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
f.vehicle.position={900,0,0};f.world.now=200;f.agent.events.push_back(event);Threat threat;threat.Update(&f.utility,100);
assert(f.awareness.heard==1&&f.awareness.source==&f.group&&f.awareness.position.x==20&&f.awareness.observed==100&&f.awareness.received==200);
assert(threat.calls==1&&f.agent.events.empty()); // read before native superclass clears
threat.Update(&f.utility,100);assert(f.awareness.heard==1&&threat.calls==2);
}
for(int variant=0;variant<10;variant++){
Fixture f;
if(variant==0)Replication::server=false;
if(variant==1)game=nullptr;
if(variant==2)f.context.world=nullptr;
if(variant==3)f.context.ai=nullptr;
if(variant==4)f.vehicle.alive=false;
if(variant==5)Mode::current=nullptr;
if(variant==6)f.mode.active=false;
if(variant==7)f.world.now=-1;
if(variant==8)f.world.now=std::numeric_limits<float>::quiet_NaN();
if(variant==9)f.vehicle.position.x=std::numeric_limits<float>::quiet_NaN();
f.driver.Emit(&f.vehicle);assert(f.ai.broadcast.empty());
}
for(int variant=0;variant<23;variant++){
Fixture f;Event* event=f.emit();f.agent.events.push_back(event);Mode otherMode;Group otherGroup;
if(variant==0)Replication::server=false;
if(variant==1)f.utility.m_ConfigComponent=nullptr;
if(variant==2)f.config.m_EnableDangerEvents=false;
if(variant==3)f.context.world=nullptr;
if(variant==4)Mode::current=nullptr;
if(variant==5)f.mode.active=false;
if(variant==6)f.mode.phase=ERR_Phase::PRISON;
if(variant==7)f.utility.owner=nullptr;
if(variant==8)f.witness.alive=false;
if(variant==9)f.witness.awake=false;
if(variant==10)f.witness.player=true;
if(variant==11)f.witness.group=&otherGroup;
if(variant==12)f.group.slave=true;
if(variant==13)f.group.players=1;
if(variant==14)f.group.faction=&f.other;
if(variant==15)f.witness.faction=&f.other;
if(variant==16)f.group.utility=nullptr;
if(variant==17)f.groupUtility.m_Perception=nullptr;
if(variant==18)event->Mode=&otherMode;
if(variant==19)event->type=EAIDangerEventType::Other;
if(variant==20)f.world.now=30101;
if(variant==21)f.world.now=99;
if(variant==22)f.witness.position={70.01f,0,0};
f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
}
{
Fixture f;Event* event=f.emit();Danger ordinary;ordinary.type=EAIDangerEventType::Danger_VehicleHorn;
f.agent.events={nullptr,&ordinary,event};f.perception.enabled=false;f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
f.perception.enabled=true;f.witness.position={70,0,0};f.world.now=30100;f.driver.Receive(&f.utility);
assert(f.awareness.heard==1&&f.awareness.observed==100); // inclusive local range/freshness boundary
}
{
Fixture f;Event* event=f.emit();Danger ordinary;
f.agent.events.assign(32,&ordinary);f.agent.events.push_back(event);f.driver.Receive(&f.utility);
assert(f.agent.reads==32&&f.awareness.heard==0&&f.agent.events.size()==33);
f.agent.events[31]=event;f.driver.Receive(&f.utility);assert(f.agent.reads==64&&f.awareness.heard==1);
}
{
Fixture f;Event* event=f.emit();f.agent.events={event};
event->position.x=std::numeric_limits<float>::quiet_NaN();f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
event->position={20,0,0};f.witness.position.x=std::numeric_limits<float>::quiet_NaN();f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
f.witness.position={0,0,0};f.perception.m_ERRAwareness=nullptr;f.driver.Receive(&f.utility);assert(f.awareness.heard==0);
}
}
''')
print('PASS: production alarm emitter/receiver original time/location, explicit native queue only, 32-entry cap, 50m local witness eligibility, bound perception, native-super preservation; actual engine broadcast/hearing remains unverified.')