151 lines
11 KiB
Python
151 lines
11 KiB
Python
"""Execute actual bounded local body observer with a deterministic native API adapter."""
|
|
from policy_harness import ROOT, run
|
|
from pathlib import Path
|
|
import re, os
|
|
path=Path(os.environ.get('ERR_BODY_SOURCE', str(ROOT/'addons/ai/Scripts/Game/ERR/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
|
|
assert 'GetPlayerManager' not in s and 'DeleteEntity' not in s and 'GetTargets' not in s
|
|
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','')
|
|
for kind in ('ERR_HQBodyCandidate','ERR_HQBodyScan','ERR_HQReport'):
|
|
code=re.sub(r'\b'+kind+r' (?=\w)',kind+'* ',code)
|
|
code=code.replace('array<'+kind+'>','Array<'+kind+'*>')
|
|
code=code.replace('array<AIAgent>','Array<AIAgent>')
|
|
code=code.replace('out vector ','vector& ').replace('out ERR_HQReport* ','ERR_HQReport*& ')
|
|
code=code.replace('ERR_GameMode mode','GameMode* mode').replace('ERR_GameMode.Get()','GameMode::Get()')
|
|
code=code.replace('ChimeraCharacter character = ChimeraCharacter.Cast(actor);','Entity* character = CastCharacter(actor);')
|
|
code=code.replace('TraceParam trace','TraceParam* trace')
|
|
code=code.replace('foreach (','for (')
|
|
for name in ('Replication','ERR_HQMemoryRules','ERR_HQBodyRules','ERR_Actors','CompartmentAccessComponent','TraceFlags','ECharacterLifeState','vector'):
|
|
code=code.replace(name+'.',name+'::')
|
|
code=code.replace('null','nullptr')
|
|
for name in ('mode','group','force','character','scan','created','agent','actor','entry','candidate','observer','body','trace','state','report','selected'):
|
|
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
|
code=code.replace('scan->Group.GetPlayerCount', 'scan->Group->GetPlayerCount')
|
|
code=code.replace('mode->Forces().','mode->Forces()->').replace('character->GetCharacterController().','character->GetCharacterController()->')
|
|
code=code.replace('GetGame().GetWorld().','GetGame()->GetWorld()->')
|
|
code=re.sub(r'(m_(?:Bodies|Roster|Groups)\[[^\]]+\])\.',r'\1->',code)
|
|
code=code.replace('"0 1.5 0"','vector{0,1.5,0}').replace('"0 0.3 0"','vector{0,.3,0}')
|
|
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'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <string>
|
|
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};}
|
|
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();}
|
|
void Clear(){this->clear();}void Insert(T v){this->push_back(v);}
|
|
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;}
|
|
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;}
|
|
struct Agent {Entity* actor=nullptr;Entity* GetControlledEntity(){return actor;}};using AIAgent=Agent*;
|
|
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 GameMode {Ledger ledger;static GameMode* current;
|
|
static GameMode* Get(){return current;}int EnemyFaction(){return 1;}Ledger* Forces(){return &ledger;}};
|
|
GameMode* GameMode::current=nullptr;
|
|
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
|
|
struct ERR_HQMemoryRules {static bool Finite(float v){return std::isfinite(v);}};
|
|
struct ERR_Actors {static bool CanFight(Entity* a){return a&&!a->dead&&a->awake;}};
|
|
struct CompartmentAccessComponent {static bool GetVehicleIn(Entity* a){return a->inVehicle;}};
|
|
struct TraceFlags {static constexpr int ENTS=1,WORLD=2;};
|
|
struct TraceParam {vector Start,End;IEntity Exclude=nullptr,TraceEnt=nullptr;int Flags=0;};
|
|
struct World {int traces=0;float fraction=1;IEntity hit=nullptr;
|
|
float TraceMove(TraceParam* trace,void*){traces++;trace->TraceEnt=hit;return fraction;}};
|
|
struct Game {World world;World* GetWorld(){return &world;}};Game game;Game* GetGame(){return &game;}
|
|
int logs=0;void Print(std::string){logs++;}
|
|
struct ERR_HQReport {IEntity Target=nullptr;Group* Source=nullptr;vector Position,Velocity;
|
|
float ObservedAt=-1;std::string TravelMode;int MessageId=0;bool Distress=false,HasVelocity=false;};
|
|
'''+code+r'''
|
|
int main(){
|
|
assert(ERR_HQBodyRules::InView(12,-1));assert(!ERR_HQBodyRules::InView(12.01,-1));
|
|
assert(ERR_HQBodyRules::InView(60,.5));assert(!ERR_HQBodyRules::InView(60.01,1));
|
|
assert(!ERR_HQBodyRules::InView(-1,1));assert(!ERR_HQBodyRules::InView(NAN,1));
|
|
assert(!ERR_HQBodyRules::Clear(.5,false));assert(ERR_HQBodyRules::Clear(.5,true));
|
|
assert(!ERR_HQBodyRules::Clear(NAN,true));
|
|
GameMode mode;GameMode::current=&mode;
|
|
Entity observer,corpse;corpse.position={0,0,20};corpse.dead=true;
|
|
Agent a,b;a.actor=&observer;b.actor=&corpse;Group group;group.agents={&a,&b};
|
|
ERR_HQBodyObserver service;service.RegisterGroup(&group);service.RegisterGroup(&group);
|
|
assert(service.m_Roster.Count()==2&&service.m_Groups.Count()==1);
|
|
service.TickBudget(0);assert(service.m_Bodies.Count()==1&&corpse.reads==0&&game.world.traces==0);
|
|
ERR_HQReport* report=nullptr;game.world.fraction=.2;
|
|
assert(!service.Observe(&group,0,report)&&!report&&game.world.traces==1);
|
|
// Fifteen-second group cadence and five-second global budget are independent.
|
|
assert(!service.Observe(&group,5000,report)&&game.world.traces==1);
|
|
game.world.fraction=1;assert(service.Observe(&group,15000,report));
|
|
assert(report->Target==&corpse&&report->Source==&group&&report->TravelMode=="body");
|
|
assert(report->Position.z==20&&report->ObservedAt==15000&&!report->MessageId);
|
|
assert(!report->HasVelocity&&!report->Distress);
|
|
corpse.position.z=30;assert(report->Position.z==20); // immutable output snapshot
|
|
assert(!service.Observe(&group,15001,report));
|
|
// Lost radio delivery receives no acknowledgement: fresh visibility permits retry.
|
|
assert(service.Observe(&group,30000,report)&&report->Position.z==30);
|
|
service.ConfirmDelivered(&corpse);assert(!service.Observe(&group,45000,report));
|
|
assert(service.m_Roster.Count()==2&&service.m_Bodies.IsEmpty());
|
|
service.Stop();assert(service.m_Roster.IsEmpty()&&service.m_Groups.IsEmpty()&&service.m_Traces==0);
|
|
// Unconscious/incapacitated is not a corpse; own physical awake observers only.
|
|
corpse.dead=false;corpse.awake=false;service.RegisterGroup(&group);service.TickBudget(0);
|
|
assert(service.m_Bodies.IsEmpty());corpse.dead=true;observer.awake=false;
|
|
assert(!service.Observe(&group,15000,report));observer.awake=true;observer.inVehicle=true;
|
|
assert(!service.Observe(&group,30000,report));observer.inVehicle=false;
|
|
mode.ledger.force.Physical=false;assert(!service.Observe(&group,45000,report));
|
|
mode.ledger.force.Physical=true;group.players=1;assert(!service.Observe(&group,45000,report));
|
|
group.players=0;group.faction=2;assert(!service.Observe(&group,45000,report));group.faction=1;
|
|
Replication::server=false;assert(!service.Observe(&group,45000,report));Replication::server=true;
|
|
// Twelve native LOS calls maximum across multiple groups in the same interval.
|
|
service.Stop();Entity dead[24];Agent da[24];Group many;many.agents={&a};
|
|
for(int i=0;i<24;i++){dead[i].dead=true;dead[i].position={0,0,float(i+15)};da[i].actor=&dead[i];many.agents.Insert(&da[i]);}
|
|
service.RegisterGroup(&many);game.world.fraction=.2;game.world.traces=0;
|
|
assert(!service.Observe(&many,0,report));assert(game.world.traces==12&&service.m_Traces==0);
|
|
service.TickBudget(4999);assert(service.m_Traces==0);service.TickBudget(5000);assert(service.m_Traces==12);
|
|
assert(service.m_Bodies.Count()==16);
|
|
service.TickBudget(4000);assert(service.m_Traces==12&&service.m_Now==5000);
|
|
// Candidate window rotates past the first16 undiscovered dead identities.
|
|
bool later=false;for(auto* candidate:service.m_Bodies)if(candidate->Corpse==&dead[23])later=true;
|
|
assert(later);
|
|
// Closed LOS can hit the corpse collider itself; close rear visibility remains trace-gated.
|
|
Entity nearBody;nearBody.dead=true;nearBody.position={0,0,-10};vector point;
|
|
game.world.hit=&nearBody;assert(service.Visible(&observer,&nearBody,point)&&point.z==-10);
|
|
nearBody.position.z=-13;int before=game.world.traces;assert(!service.Visible(&observer,&nearBody,point));
|
|
assert(game.world.traces==before);
|
|
// Oldest due group wins even when the first three callers exhaust every trace budget.
|
|
service.Stop();Group contenders[4];for(int i=0;i<4;i++){contenders[i].agents=many.agents;service.RegisterGroup(&contenders[i]);}
|
|
game.world.hit=nullptr;game.world.fraction=.2;
|
|
for(int t=0;t<=15000;t+=1000)for(int g=0;g<4;g++)service.Observe(&contenders[g],t,report);
|
|
assert(service.m_Groups[3]->NextScan==30000); // fourth group actually consumed the t15s turn
|
|
assert(service.m_Groups[0]->NextScan==15000); // first did not leapfrog never-scanned groups
|
|
// A caller that becomes inactive can delay others at most one budget window.
|
|
service.Stop();service.RegisterGroup(&contenders[0]);service.RegisterGroup(&contenders[1]);
|
|
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);
|
|
// Registry cap is finite, logs once, and deleted identities free future slots.
|
|
service.Stop();logs=0;Entity actors[160];Agent agents[160];Group groups[5];
|
|
for(int g=0;g<5;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]);}
|
|
assert(service.m_Roster.Count()==128&&logs==1);service.RegisterGroup(&groups[4]);assert(logs==1);
|
|
for(int i=0;i<32;i++)service.m_Roster[i]->Corpse=nullptr;
|
|
service.RegisterGroup(&groups[4]);assert(service.m_Roster.Count()==128);
|
|
service.Stop();assert(service.m_Roster.IsEmpty());
|
|
}
|
|
''')
|
|
print('PASS: actual body observer enforces owned physical roster, local death-only candidates, native trace gating, budget/cadence/caps, immutable body-only snapshots, radio retries and confirmed suppression')
|