Files
jakub 1b1a8b5173 Add mission customization, HQ support and aggression-driven AI
Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
2026-09-23 15:07:14 +02:00

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[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]));}
assert(service.m_Roster.Count()==4096&&logs==1);service.RegisterGroup(&groups[128],Roster(&groups[128]));assert(logs==1);
for(int i=0;i<32;i++)service.m_Roster[i]->Corpse=nullptr;
service.RegisterGroup(&groups[128],Roster(&groups[128]));assert(service.m_Roster.Count()==4096);
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')