Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
169 lines
12 KiB
Python
169 lines
12 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/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
|
|
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','ERR_HQStatusSampler'):
|
|
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','sampler'):
|
|
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
|
code=code.replace('candidate->Roster.', 'candidate->Roster->')
|
|
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>
|
|
#include <map>
|
|
using string=std::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++;}
|
|
// Deterministic native roster adapter. Actual sampler bind/release methods have their own executable suite.
|
|
struct ERR_HQStatusSampler {
|
|
Group* group;Array<IEntity> initial;Array<int> released;string source="formation-1";
|
|
ERR_HQStatusSampler(Group* g):group(g){for(auto agent:g->agents)initial.Insert(agent->actor);}
|
|
bool OwnsRoster(Group* g){return group==g;}string InitialSourceId(){return source;}
|
|
int InitialStrength(){return initial.Count();}IEntity InitialMember(int slot){return initial[slot];}
|
|
bool CanAttribute(int slot,IEntity actor){return actor&&initial[slot]==actor&&!released.Contains(slot);}
|
|
};
|
|
ERR_HQStatusSampler* Roster(Group* group){return new ERR_HQStatusSampler(group);}
|
|
struct ERR_HQReport {string CasualtySourceId;int CasualtySlot=-1,CasualtyStrength=0;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};
|
|
auto* immutable=Roster(&group); // first roster binds [observer,corpse]
|
|
group.agents={&b,&a}; // current native order must not determine attribution
|
|
ERR_HQBodyObserver service;service.RegisterGroup(&group,immutable);service.RegisterGroup(&group,immutable);
|
|
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);
|
|
assert(report->CasualtySourceId=="formation-1"&&report->CasualtySlot==1&&report->CasualtyStrength==2);
|
|
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);
|
|
// A previously known transfer/player conversion excludes the immutable slot permanently.
|
|
immutable->released.Insert(1);assert(!service.Observe(&group,45000,report));
|
|
immutable->released.Clear(); // test adapter restores fixture before delivery suppression
|
|
service.ConfirmDelivered(&corpse);assert(!service.Observe(&group,60000,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,Roster(&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,Roster(&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],Roster(&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],Roster(&contenders[0]));service.RegisterGroup(&contenders[1],Roster(&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],Roster(&groups[g]));}
|
|
assert(service.m_Roster.Count()==128&&logs==1);service.RegisterGroup(&groups[4],Roster(&groups[4]));assert(logs==1);
|
|
for(int i=0;i<32;i++)service.m_Roster[i]->Corpse=nullptr;
|
|
service.RegisterGroup(&groups[4],Roster(&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')
|