Reconcile radio-delivered body and status casualties by original roster identity

This commit is contained in:
2026-09-20 03:09:16 +02:00
parent a2aab17171
commit 9b6596eb3e
44 changed files with 6042 additions and 86 deletions
+30 -12
View File
@@ -7,7 +7,7 @@ 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'):
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>')
@@ -19,8 +19,9 @@ 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'):
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()->')
@@ -36,6 +37,8 @@ run(r'''
#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};}
@@ -72,7 +75,16 @@ 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;
// 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(){
@@ -84,7 +96,9 @@ int main(){
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);
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;
@@ -95,15 +109,19 @@ int main(){
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);
service.ConfirmDelivered(&corpse);assert(!service.Observe(&group,45000,report));
// 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);service.TickBudget(0);
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;
@@ -114,7 +132,7 @@ int main(){
// 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;
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);
@@ -128,22 +146,22 @@ int main(){
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]);}
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]);service.RegisterGroup(&contenders[1]);
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]);}
assert(service.m_Roster.Count()==128&&logs==1);service.RegisterGroup(&groups[4]);assert(logs==1);
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]);assert(service.m_Roster.Count()==128);
service.RegisterGroup(&groups[4],Roster(&groups[4]));assert(service.m_Roster.Count()==128);
service.Stop();assert(service.m_Roster.IsEmpty());
}
''')
+54
View File
@@ -0,0 +1,54 @@
"""Actual Enforce union methods: delivered body/status evidence is never summed."""
from pathlib import Path
import re
from policy_harness import ROOT, run
s=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQDeliveredCasualties.c').read_text()
assert all(token not in s for token in ('IEntity','GetOrigin','GetWorld','GetPlayer','IsDestroyed','GetLifeState'))
code=s.replace('class ','struct ').replace('protected ','').replace('ref ','').replace('Math.','Math::')
code=code.replace('array<int> slots','Array<int>* slots').replace('slots.', 'slots->')
code=code.replace('array<int>','Array<int>').replace('foreach (','for (')
code=code.replace(' : slots)', ' : *slots)').replace('int m_Strength;', 'int m_Strength=0;')
code=code.replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
struct Math {static int Min(int a,int b){return std::min(a,b);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;
int Count(){return this->size();}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}
void Insert(T n){this->push_back(n);}void Clear(){this->clear();}};
'''+code+r'''
int main(){
ERR_HQDeliveredCasualties ledger;assert(!ledger.Bind(0)&&!ledger.Bind(33));
assert(!ledger.AcceptBody(0));assert(ledger.Bind(6));assert(!ledger.Bind(7));
Array<int> first{0,2};assert(ledger.AcceptStatus(&first,2,4));assert(ledger.Count()==2);
// Same body already confirmed by delivered status never adds a casualty.
assert(ledger.AcceptBody(0)&&ledger.AcceptBody(0)&&ledger.Count()==2);
// Different discovered body adds precisely one; identical rediscovery does not.
assert(ledger.AcceptBody(4)&&ledger.AcceptBody(4)&&ledger.Count()==3);
assert(ledger.LivingUpperBound(4)==3);
// Caller has not seen the outside discovery; valid unchanged status must still arrive.
assert(ledger.AcceptStatus(&first,2,4)&&ledger.Count()==3&&ledger.StatusCount()==2);
Array<int> later{0,2,4};assert(ledger.AcceptStatus(&later,3,3)&&ledger.Count()==3);
Array<int> disjoint{0,2,3,4};assert(ledger.AcceptStatus(&disjoint,4,2)&&ledger.Count()==4);
Array<int> malformed{0,0,2};assert(!ledger.AcceptStatus(&malformed,3,3)&&ledger.Count()==4);
Array<int> outOfRange{0,2,3,4,6};assert(!ledger.AcceptStatus(&outOfRange,5,1)&&ledger.Count()==4);
Array<int> missing{0,2,4};assert(!ledger.AcceptStatus(&missing,3,3));
assert(!ledger.AcceptStatus(&disjoint,3,2)&&!ledger.AcceptStatus(&disjoint,4,3));
assert(!ledger.AcceptStatus(nullptr,0,6)&&!ledger.AcceptBody(-1)&&!ledger.AcceptBody(6));
assert(ledger.AcceptBody(1)&&ledger.AcceptBody(5)&&ledger.Closed()&&ledger.Count()==6);
assert(ledger.LivingUpperBound(-1)==0&&ledger.LivingUpperBound(6)==0);
assert(ledger.Bind(6)&&ledger.Count()==6); // duplicate binding never resets delivered evidence
// Complete wipe can be established from delivered visible bodies even without any check-in.
ERR_HQDeliveredCasualties wiped;assert(wiped.Bind(32));
assert(wiped.LivingUpperBound(-1)==-1&&!wiped.Closed());
for(int slot=31;slot>=0;slot--)for(int replay=0;replay<4;replay++)assert(wiped.AcceptBody(slot));
assert(wiped.Count()==32&&wiped.StatusCount()==0&&wiped.Closed());
Array<int> unknown{};assert(wiped.AcceptStatus(&unknown,0,32));
assert(wiped.Count()==32&&wiped.LivingUpperBound(32)==0); // delayed status cannot reopen known dead roster
// Same numerical slot belongs to separate independent formations, never a global casualty ID.
ERR_HQDeliveredCasualties other;assert(other.Bind(2)&&other.AcceptBody(0)&&other.Count()==1);
assert(ledger.Count()==6&&wiped.Count()==32);
}
''')
print('PASS: actual delivered slot-union bodies preserve overlap/disjoint evidence, replay idempotence, status subset integrity, full-wipe closure and formation-local bounds')
+45 -2
View File
@@ -17,7 +17,8 @@ def cpp(s):
s=s.replace('foreach (AIAgent* agent : m_Agents)','for (AIAgent* agent : m_Agents)')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('bool Bound()', 'protected bool Eligible(', 'bool Bind(',
'bool ObserveDeaths(', 'ERR_HQStatusReport Sample(', 'void Clear('))
'string InitialSourceId(', 'int InitialStrength(', 'bool OwnsRoster(', 'IEntity InitialMember(',
'bool CanAttribute(', 'bool ObserveDeaths(', 'ERR_HQStatusReport Sample(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
run(r'''
#include <cassert>
@@ -57,7 +58,7 @@ struct SCR_FactionManager{static Faction* SGetFaction(IEntity* a){return a?a->fa
struct ERR_HQRadio{static int Capability(IEntity* a){return ERR_Actors::CanFight(a)&&a->radio?1:0;}};
struct ERR_HQStatusReport{
string SourceId;int MessageId=0;vector Position;int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
float ObservedAt=-1;
float ObservedAt=-1;Array<int> DeathSlots;
};
struct ERR_HQPursuitRules{
'''+common+r'''
@@ -82,6 +83,11 @@ int main(){
group.agents[3]=&agents[2];assert(!sample.Bind(&group,"site",4,&enemy));group.agents[3]=&agents[3];
assert(sample.Bind(&group,"site",4,&enemy)&&sample.Bound());
assert(sample.Bind(&group,"site",4,&enemy)&&!sample.Bind(&group,"other-source",4,&enemy));
assert(sample.InitialSourceId()=="site"&&sample.InitialStrength()==4&&sample.OwnsRoster(&group));
assert(sample.InitialMember(1)==&members[1]&&!sample.InitialMember(-1)&&!sample.InitialMember(4));
std::reverse(group.agents.begin(),group.agents.end());
assert(sample.InitialMember(1)==&members[1]&&sample.CanAttribute(1,&members[1]));
assert(!sample.CanAttribute(1,&members[2]));std::reverse(group.agents.begin(),group.agents.end());
auto first=sample.Sample(1000,1,&members[0]);assert(first&&first->Count==4&&first->Fit==4&&first->Strength==4);
assert(first->Position.x==100&&first->SourceId=="site");
group.position.x=200;assert(first->Position.x==100); // detached snapshot
@@ -91,14 +97,22 @@ int main(){
members[1].damage.dead=true;members[1].group=nullptr;group.agents.erase(group.agents.begin()+1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1); // counted once
assert(sample.CanAttribute(1,&members[1]));
sample.m_Group=nullptr; // native all-dead group handle invalidates while the retained corpse still exists
assert(sample.CanAttribute(1,&members[1])); // candidate-held sampler preserves original slot without live group
sample.m_Group=&group;
sample.m_Members[1]=nullptr; // simulate native weak-reference invalidation after corpse cleanup
auto losses=sample.Sample(3000,3,&members[0]);assert(losses&&losses->Count==3&&losses->ConfirmedLosses==1);
assert(losses->DeathSlots.Count()==1&&losses->DeathSlots[0]==1);
assert(first->DeathSlots.Count()==0); // earlier report array is detached
// Deleted unknown actor and missing damage component cannot be manufactured into deaths.
sample.m_Members[2]=nullptr;members[3].hasDamage=false;
auto absent=sample.Sample(4000,4,&members[0]);
assert(absent&&absent->Count==1&&absent->ConfirmedLosses==1);
members[3].hasDamage=true;members[3].player=true;assert(sample.ObserveDeaths());
assert(!sample.CanAttribute(3,&members[3]));
members[3].player=false;members[3].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
assert(!sample.CanAttribute(3,&members[3])); // released slot cannot be rediscovered as our casualty
// Never adopt an externally attached replacement as one of the authored original members.
IEntity replacement;replacement.group=&group;replacement.faction=&enemy;AIAgent replacementAgent;replacementAgent.actor=&replacement;
group.agents.Insert(&replacementAgent);assert(!sample.Sample(5000,5,&replacement));
@@ -114,6 +128,35 @@ int main(){
// No sender survives: local evidence may update, but a radio report cannot be fabricated.
members[0].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==2);
assert(!sample.Sample(7000,7,&members[0])&&!sample.Sample(7000,7,nullptr));
// Transfer and death between slow ticks: a retained foreign group proves the
// old initial slot was released, even without an earlier living observation.
SCR_AIGroup source,destination;source.faction=&enemy;destination.faction=&enemy;
IEntity moved,stayed,recipient;AIAgent movedAgent,stayedAgent,recipientAgent;
moved.faction=&enemy;moved.group=&source;movedAgent.actor=&moved;
stayed.faction=&enemy;stayed.group=&source;stayedAgent.actor=&stayed;
recipient.faction=&enemy;recipient.group=&destination;recipientAgent.actor=&recipient;
source.agents.Insert(&stayedAgent);source.agents.Insert(&movedAgent);destination.agents.Insert(&recipientAgent);
// Independent source instances exercise body and status gates without one
// gate's release masking a bug in the other gate.
Sampler priorStatus,priorBody,later;
assert(priorStatus.Bind(&source,"before-transfer",2,&enemy));
assert(priorBody.Bind(&source,"before-transfer",2,&enemy));
moved.group=&destination;source.agents.pop_back();destination.agents.Insert(&movedAgent);
assert(later.Bind(&destination,"after-transfer",2,&enemy));moved.damage.dead=true;
assert(!priorBody.CanAttribute(1,&moved)&&priorBody.m_Released[1]);
assert(priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0&&priorStatus.m_Released[1]);
assert(later.ObserveDeaths()&&later.m_Deaths==1);
auto oldFormation=priorStatus.Sample(8000,8,&stayed);
auto newFormation=later.Sample(8000,9,&recipient);
assert(oldFormation&&oldFormation->Count==1&&oldFormation->ConfirmedLosses==0&&oldFormation->DeathSlots.Count()==0);
assert(newFormation&&newFormation->Count==1&&newFormation->ConfirmedLosses==1&&newFormation->DeathSlots[0]==1);
// Returning to the original group cannot reactivate a released slot.
moved.group=&source;assert(!priorBody.CanAttribute(1,&moved));
assert(priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0);
// Normal native corpse detachment still permits its destination attribution.
moved.group=nullptr;assert(later.CanAttribute(1,&moved));
assert(!priorBody.CanAttribute(1,&moved)&&priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0);
sample.Clear();assert(!sample.Bound()&&sample.m_Members.Count()==0&&sample.m_Deaths==0);
}
''')
+112 -15
View File
@@ -9,16 +9,33 @@ def cpp(s):
('ERR_HQWatchRules.','ERR_HQWatchRules::'),('null','nullptr'),
('report.SourceId.IsEmpty()','report.SourceId.empty()'),('report.SourceId.Length()','report.SourceId.size()')]:
s=s.replace(old,new)
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry'):
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry','ERR_HQReport','ERR_HQDeliveredCasualties'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('entry','report'):s=s.replace(var+'.',var+'->')
for var in ('entry','report','validation'):s=s.replace(var+'.',var+'->')
s=s.replace('entry->Casualties.', 'entry->Casualties->')
s=s.replace('report->CasualtySourceId.IsEmpty()', 'report->CasualtySourceId.empty()')
s=s.replace('report->CasualtySourceId.Length()', 'report->CasualtySourceId.size()')
s=s.replace('report->DeathSlots,', '&report->DeathSlots,')
return s
helper=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQDeliveredCasualties.c').read_text()
helper=helper.replace('class ','struct ').replace('protected ','').replace('ref ','').replace('Math.','Math::')
helper=helper.replace('array<int> slots','Array<int>* slots').replace('slots.', 'slots->')
helper=helper.replace('array<int>','Array<int>').replace('foreach (','for (')
helper=helper.replace(' : slots)', ' : *slots)').replace('int m_Strength;', 'int m_Strength=0;')
helper=helper.replace('\n}\n','\n};\n')
rules='\n'.join(cpp(body(P,s)) for s in ('static bool Valid(', 'static bool RequestDue(',
'static bool InvestigationDue(', 'static int SquadLimit('))
methods='\n'.join(cpp(body(P,s)) for s in ('int Count()', 'ERR_HQWatchEntry Find(', 'ERR_HQWatchEntry At(',
'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'bool Ingest(', 'bool RequestDue(string',
'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'protected ERR_HQWatchEntry Create(',
'protected void ApplyLosses(', 'bool Ingest(', 'bool IngestBody(', 'bool RequestDue(string',
'bool MarkRequested(', 'bool InvestigationDue(string', 'bool MarkInvestigating(', 'bool FinishInvestigation(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
snapshot=cpp(body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQReport.c','ERR_HQReport Snapshot(')).replace('copy.', 'copy->')
accept=cpp(body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c','protected void Accept('))
accept=accept.replace('ERR_MissionRules.', 'ERR_MissionRules::').replace('ERR_HQMemoryRules.', 'ERR_HQMemoryRules::')
accept=accept.replace('ERR_HQMemoryKind.', 'ERR_HQMemoryKind::').replace('vector.DistanceXZ(', 'vector::DistanceXZ(')
accept=accept.replace('previous.', 'previous->')
accept=accept.replace('string.Format(', 'Format(')
run(r'''
#include <cassert>
#include <cmath>
@@ -28,11 +45,16 @@ run(r'''
#include <vector>
#include <string>
using string=std::string;
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}
vector operator-(vector v){return {x-v.x,y-v.y,z-v.z};}vector operator/(float f){return {x/f,y/f,z/f};}
float Length(){return std::sqrt(x*x+y*y+z*z);}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct Math{static int Clamp(int n,int lo,int hi){return std::clamp(n,lo,hi);}
static int Min(int a,int b){return std::min(a,b);}static float AbsFloat(float n){return std::abs(n);}};
int logs=0;void Print(const char*){logs++;}
template<class T>struct Array:std::vector<T>{
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;
bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}
void Remove(int n){this->erase(this->begin()+n);}
int Count(){return this->size();}void Insert(T n){this->push_back(n);}void Clear(){this->clear();}
};
template<class K,class V>struct Map:std::map<K,V>{
@@ -42,13 +64,22 @@ template<class K,class V>struct Map:std::map<K,V>{
struct ERR_HQPursuitRules{
'''+common+r'''
};
'''+helper+r'''
struct ERR_HQReport{
string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
vector Position{900,30,900},Velocity;float ObservedAt=1000,DetailsObservedAt=-1;
void* Target=nullptr;void* Source=nullptr;bool HasVelocity=false,Engaged=false;
int SeenEscapists=-1;string SeenWeapons="unknown";
'''+snapshot+r'''
};
struct ERR_HQStatusReport{
string SourceId="patrol-1";int MessageId=1;vector Position{100,20,300};
int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;
int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;Array<int> DeathSlots;
};
struct ERR_HQWatchEntry{
string SourceId;int MessageId=0,RequestId=0;vector Position;
int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;
ERR_HQDeliveredCasualties* Casualties=new ERR_HQDeliveredCasualties();float LastBodyDeliveredAt=-1;
float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0;
bool Investigating=false,Closed=false;
};
@@ -60,6 +91,19 @@ struct Watch{
int m_ConfirmedLosses=0;bool m_CapLogged=false;
'''+methods+r'''
};
struct ERR_MissionRules{static bool ContactFresh(float t,float now){return std::isfinite(t)&&std::isfinite(now)&&t>=0&&now>=t&&now-t<=120000;}};
struct ERR_HQMemoryKind{static constexpr int SIGHTING=0,DISTRESS=1,BODY=2;};
struct ERR_HQMemoryRules{static int Kind(ERR_HQReport* r){return r->Distress?1:r->TravelMode=="body"?2:0;}};
struct MemoryAdapter{int attempts=0;bool Ingest(ERR_HQReport* r,float){attempts++;return r->Target!=nullptr;}int Count(){return 0;}};
struct BodyAdapter{int confirmations=0;void ConfirmDelivered(void*){confirmations++;}};
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
struct JournalAdapter{void Write(string,string,int){}};
string Format(string,int,int){return "delivered";}
struct HQDelivery{
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
Array<ERR_HQReport*>m_Contacts;
'''+accept+r'''
};
int main(){
using R=ERR_HQWatchRules;
assert(R::SquadLimit(0,100)==0&&R::SquadLimit(-1,100)==0);
@@ -98,23 +142,23 @@ int main(){
assert(!watch.FinishInvestigation(report.SourceId,1,1,603000));
assert(!watch.Ingest(&report,603000));
report.MessageId=3;assert(!watch.Ingest(&report,603000)); // same observation relabelled cannot renew status
report.ObservedAt=601000;report.Count=3;report.Fit=2;report.ConfirmedLosses=1;
report.ObservedAt=601000;report.Count=3;report.Fit=2;report.ConfirmedLosses=1;report.DeathSlots={1};
assert(watch.Ingest(&report,603000)&&watch.ConfirmedLosses()==1);
report.MessageId=4;report.ObservedAt=602000;
assert(watch.Ingest(&report,604000)&&watch.ConfirmedLosses()==1); // cumulative, not additive
report.MessageId=5;report.ObservedAt=603000;report.Count=2;report.ConfirmedLosses=2;
report.MessageId=5;report.ObservedAt=603000;report.Count=2;report.ConfirmedLosses=2;report.DeathSlots={1,2};
assert(watch.Ingest(&report,605000)&&watch.ConfirmedLosses()==2&&watch.SquadLimit(2)==3);
report.MessageId=6;report.ObservedAt=604000;report.ConfirmedLosses=1;
report.MessageId=6;report.ObservedAt=604000;report.ConfirmedLosses=1;report.DeathSlots={1};
assert(!watch.Ingest(&report,606000)&&watch.ConfirmedLosses()==2);
report.ConfirmedLosses=2;report.Strength=5;
report.ConfirmedLosses=2;report.DeathSlots={1,2};report.Strength=5;
assert(!watch.Ingest(&report,606000));report.Strength=4;
// Missing members cannot be counted as dead. A delivered explicit whole-formation loss can close it.
report.Count=0;report.Fit=0;
assert(watch.Ingest(&report,606000)&&!entry->Closed&&watch.ConfirmedLosses()==2);
report.MessageId=7;report.ObservedAt=605000;report.ConfirmedLosses=4;
report.MessageId=7;report.ObservedAt=605000;report.ConfirmedLosses=4;report.DeathSlots={0,1,2,3};
assert(watch.Ingest(&report,607000)&&entry->Closed&&watch.ConfirmedLosses()==4);
assert(!watch.RequestDue(report.SourceId,2000000));
report.MessageId=8;report.ObservedAt=606000;report.ConfirmedLosses=0;report.Count=4;report.Fit=4;
report.MessageId=8;report.ObservedAt=606000;report.ConfirmedLosses=0;report.DeathSlots={};report.Count=4;report.Fit=4;
assert(!watch.Ingest(&report,608000)); // source cannot resurrect or erase counted deaths
// Validation precedes allocation/watermarks; malformed deliveries cannot poison future genuine reports.
float nan=std::numeric_limits<float>::quiet_NaN();
@@ -139,7 +183,7 @@ int main(){
assert(!watch.Ingest(nullptr,1000)&&!watch.Ingest(&late,nan));
assert(!watch.Ingest(&late,120001)&&watch.Ingest(&late,120000));
assert(!watch.RequestDue("late",nan));
Watch cap;ERR_HQStatusReport item;item.Count=0;item.Fit=0;item.ConfirmedLosses=4;
Watch cap;ERR_HQStatusReport item;item.Count=0;item.Fit=0;item.ConfirmedLosses=4;item.DeathSlots={0,1,2,3};
for(int i=0;i<128;i++){item.SourceId=std::to_string(i);assert(cap.Ingest(&item,1000));}
assert(cap.Count()==128&&cap.ConfirmedLosses()==512);
item.SourceId="overflow";assert(!cap.Ingest(&item,1000));
@@ -147,11 +191,64 @@ int main(){
item.SourceId="0";item.MessageId=2;item.ObservedAt=2000;
assert(cap.Ingest(&item,2000)&&cap.ConfirmedLosses()==512&&cap.Count()==128);
cap.Clear();assert(cap.Count()==0&&cap.ConfirmedLosses()==0&&!cap.Find("0"));
// Delivered bodies merge exact original roster slots, never aggregate totals or body positions.
Watch merged;ERR_HQStatusReport status;status.Count=3;status.Fit=2;status.ConfirmedLosses=1;status.DeathSlots={0};
assert(merged.Ingest(&status,1000));auto known=merged.Find("patrol-1");
ERR_HQReport body;assert(merged.IngestBody(&body,2000)&&merged.ConfirmedLosses()==1); // same casualty
body.CasualtySlot=2;assert(merged.IngestBody(&body,2001)&&merged.ConfirmedLosses()==2);
assert(known->MessageId==1&&known->ObservedAt==0&&known->LastDeliveredAt==1000&&known->Position.x==100);
assert(known->Count==2&&known->Fit==2&&known->AskedAt==-1&&!known->Closed);
assert(known->LastBodyDeliveredAt==2001&&known->LastDeliveredAt==1000); // independent delivery chronology
status.MessageId=2;status.ObservedAt=3000;status.Position.x=200;
assert(merged.Ingest(&status,3000)); // caller still only knows slot0; body id99 cannot block status id2
assert(known->ConfirmedLosses==2&&known->Count==2&&known->Position.x==200);
body.CasualtySlot=1;body.ObservedAt=4000;assert(merged.IngestBody(&body,4000));
body.CasualtySlot=3;assert(merged.IngestBody(&body,4001));
assert(known->Closed&&known->Count==0&&known->Fit==0&&merged.ConfirmedLosses()==4);
assert(known->MessageId==2&&known->ObservedAt==3000&&known->Position.x==200);
assert(merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
status.MessageId=3;status.ObservedAt=5000;assert(merged.Ingest(&status,5000)&&known->Closed);
assert(!merged.RequestDue("patrol-1",999999));
// Exercise the actual HQ delivered-intake method and actual Report.Snapshot, including corpse deletion.
HQDelivery intake;ERR_HQReport local;local.CasualtySourceId="deleted-during-uplink";
local.CasualtyStrength=1;local.CasualtySlot=0;local.Target=&local;local.MessageId=501;
auto wire=local.Snapshot();local.CasualtySlot=9;local.Position.x=1;
assert(wire->CasualtySlot==0&&wire->Position.x==900&&wire->CasualtySourceId=="deleted-during-uplink");
assert(intake.m_Watch.Count()==0); // preparing/queuing local evidence changes nothing
wire->Target=nullptr; // simulate native weak-reference invalidation after captured LOS, before radio completes
intake.Accept(wire,2000);
auto delivered=intake.m_Watch.Find("deleted-during-uplink");
assert(delivered&&delivered->Closed&&delivered->Count==0&&intake.m_Watch.ConfirmedLosses()==1);
assert(delivered->MessageId==0&&delivered->ObservedAt==-1); // no invented check-in or corpse position
assert(intake.m_Memory.attempts==1&&intake.m_Contacts.Count()==0&&intake.m_Coverage.forgotten==0);
intake.Accept(wire,2001);assert(intake.m_Watch.ConfirmedLosses()==1); // delivered duplicates cannot add losses
wire->CasualtySourceId="stale";intake.Accept(wire,121001);assert(!intake.m_Watch.Find("stale"));
wire->ObservedAt=2000;wire->TravelMode="walking";wire->Target=&local;
intake.Accept(wire,2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
wire->TravelMode="body";wire->Distress=true;intake.Accept(wire,2001);
assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1); // distress cannot claim bodily identity
Watch bodyOnly;body.CasualtySourceId="no-survivor";body.CasualtySlot=0;body.ObservedAt=1000;
assert(bodyOnly.IngestBody(&body,1000));auto wiped=bodyOnly.Find("no-survivor");
assert(wiped->Count==-1&&wiped->Fit==-1&&wiped->MessageId==0&&wiped->ObservedAt==-1);
assert(!bodyOnly.RequestDue("no-survivor",999999));
for(int slot=1;slot<4;slot++){body.CasualtySlot=slot;assert(bodyOnly.IngestBody(&body,1000));}
assert(wiped->Closed&&wiped->Count==0&&wiped->Fit==0&&bodyOnly.ConfirmedLosses()==4);
body.CasualtySourceId="invalid-body";body.CasualtySlot=4;
assert(!bodyOnly.IngestBody(&body,1000)&&bodyOnly.Count()==1);
body.CasualtySlot=0;body.MessageId=0;assert(!bodyOnly.IngestBody(&body,1000));body.MessageId=100;
body.TravelMode="walking";assert(!bodyOnly.IngestBody(&body,1000));body.TravelMode="body";
body.Distress=true;assert(!bodyOnly.IngestBody(&body,1000));body.Distress=false;
assert(!bodyOnly.IngestBody(&body,999)&&!bodyOnly.IngestBody(&body,121001));
assert(!bodyOnly.IngestBody(&body,nan)&&bodyOnly.Count()==1);
// Bad slot payloads remain atomic and cannot consume identity slots.
ERR_HQStatusReport corrupt;corrupt.SourceId="badslots";corrupt.Count=2;corrupt.Fit=2;
corrupt.ConfirmedLosses=2;corrupt.DeathSlots={0,0};assert(!bodyOnly.Ingest(&corrupt,1000));
corrupt.DeathSlots={0,4};assert(!bodyOnly.Ingest(&corrupt,1000)&&bodyOnly.Count()==1);
}
''')
source=P.read_text()
for forbidden in ('IEntity','SCR_AIGroup','GetGame(','GetOrigin(','GetPlayers(','GetAgents(',
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory','ERR_HQReport '):
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory'):
assert forbidden not in source,forbidden
assert all(len(line)<120 for line in source.splitlines())
print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence and 128-record cap')
+2 -1
View File
@@ -79,8 +79,9 @@ struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Roll
struct Clock{float GetWorldTime(){return 10;}Clock* GetWorld(){return this;}}clock_;Clock* GetGame(){return &clock_;}
struct Sampler{bool Bound(){return bindings>0;}int bindings=0,strength=0;SCR_AIGroup group=nullptr;string source;void Bind(SCR_AIGroup g,string id,int n,int faction){assert(faction==1);bindings++;group=g;source=id;strength=n;}};
struct GroupService{int registrations=0;Set<SCR_AIGroup> seen;void RegisterGroup(SCR_AIGroup g){if(!seen.Contains(g)){seen.Insert(g);registrations++;}}};
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Unit{string HomeId,ForceSourceId;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;GroupService m_Equipment,m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;
struct HQ{Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;
'''+hqbind+hqregister+r'''
};
Intel* intel_;HQ* hq_;
+68
View File
@@ -0,0 +1,68 @@
"""Narrow Workbench body-fixture contracts; native LOS/radio proof requires an engine run."""
from pathlib import Path
import os, re
from policy_harness import ROOT, body, run
stage=os.environ.get('ERR_NATIVE_BODY_STAGE')
p=Path(stage)/'ERR_NativeHQBodyProbe.c' if stage else ROOT/'addons/chat/Scripts/Game/ERR/ERR_NativeHQBodyProbe.c'
s=p.read_text()
smoke=(Path(stage)/'ERR_NativeSmoke.c' if stage else ROOT/'addons/chat/Scripts/Game/ERR/ERR_NativeSmoke.c').read_text()
runner=(Path(stage)/'headless_workbench.py' if stage else ROOT/'tools/headless_workbench.py').read_text()
assert s.startswith('#ifdef WORKBENCH') and s.rstrip().endswith('#endif')
for forbidden in ('Spawn(', 'SpawnEntity', 'TrySpawn', 'new ERR_HQReport', 'new ERR_HQStatusReport',
'.Ingest(', 'Accept(', '.Advance(', 'SetHealth(', 'DeleteEntity', 'm_Contacts.Insert'):
assert forbidden not in s, forbidden
# One clearly disclosed scene mutation; no repeated movement to keep LOS artificially true.
assert s.count('.SetOrigin(')==1 and s.count('.Kill(')==1
assert 'actor == victim.Radio' in s and 'victim == observer' in s
prepare=body(p,'ERR_NativeHQBodySetup ERR_BodyFixturePrepare(')
assert 'InitialMember(slot)' in prepare and 'CanAttribute(slot, actor)' in prepare
assert 'victim.NextStatus = now + 180000' in prepare
pending=body(p,'ERR_HQUnit ERR_BodyFixturePending(')
assert 'ObserveBody(fixture.ObserverUnit, now)' in pending
assert 'PendingReport.Target == fixture.Actor' in pending and 'PendingReport.TravelMode == "body"' in pending
assert 'PendingReport =' not in pending and 'Uplink =' not in pending
assert 'reporter.PendingReport.Snapshot()' in s and 'm_Route = reporter.Uplink' in s
for proof in ('m_Route.MessageId == m_Report.MessageId','m_Report.Source == reporter.Group',
'm_Report.CasualtySlot == m_Setup.InitialSlot','m_Route.Cursor < m_Route.Nodes.Count()',
'status.MessageId == m_Setup.StatusMessageId','status.Position == m_Setup.StatusPosition',
'status.ConfirmedLosses == 1','m_Status.DeathSlots[0] == m_Setup.InitialSlot',
'm_StatusRoute.Cursor < m_StatusRoute.Nodes.Count()', 'm_StableUntil = now + 20000',
'belief.Kind == ERR_HQMemoryKind.BODY', '!hq.ERR_BodyFixtureContact(m_Setup.Actor)'):
assert proof in s,proof
assert 'now + 150000' in s and 'void Stop()' in s and 'NextStatus = m_Setup.SavedNextStatus' in s
assert 'm_ERRTestHQBody = outcome == "hq-body"' in smoke and 'm_ERRSmokeStep = 107' in smoke
branch=smoke.split('else if (m_ERRSmokeStep == 107)',1)[1].split('else if (m_ERRSmokeStep == 106)',1)[0]
assert 'm_ERRHQBodyProbe.Tick(ERR_HQ.Cast(Command()), now)' in branch
assert 'm_ERRHQBodyProbe.Stop()' in branch and 'm_ERRSmokeStep = 9' in branch
assert "'hq-body'" in runner and "a.project != 'chat' or a.rounds != 1" in runner
print('PASS: native body fixture uses existing immutable actors, real production LOS/queued uplink, pre-delivery isolation, actual status overlap, bounded cleanup and no fabricated reports/spawns')
# Execute the actual bounded lookup: the public Get(actor) API is sighting-only.
assert 'Memory().Get(' not in p.read_text()
lookup=body(p,'protected ERR_HQBelief BodyBelief(')
lookup=lookup.replace('protected ', '').replace('ERR_HQMemoryKind.', 'ERR_HQMemoryKind::')
lookup=lookup.replace('ERR_HQBelief ', 'ERR_HQBelief* ').replace('ERR_HQ hq','ERR_HQ* hq')
lookup=lookup.replace('hq.Memory().', 'hq->Memory()->').replace('belief.', 'belief->').replace('null','nullptr')
run(r'''
#include <cassert>
#include <vector>
using IEntity=void*;
struct ERR_HQMemoryKind{static constexpr int SIGHTING=0,BODY=2;};
struct ERR_HQBelief{int Kind;IEntity Target;int MessageId;};
struct Memory{std::vector<ERR_HQBelief*> entries;int Count(){return entries.size();}
ERR_HQBelief* At(int i){return entries.at(i);}};
struct ERR_HQ{struct Memory memory;struct Memory* Memory(){return &memory;}};
struct Probe{
'''+lookup+r'''
};
int main(){
ERR_HQ hq;Probe probe;int actor=0,other=0;
ERR_HQBelief sighting{0,&actor,101},differentBody{2,&other,101},oldBody{2,&actor,100},exact{2,&actor,101};
hq.memory.entries={nullptr,&sighting,&differentBody,&oldBody};
assert(!probe.BodyBelief(&hq,&actor,101));
hq.memory.entries.push_back(&exact);
assert(probe.BodyBelief(&hq,&actor,101)==&exact);
assert(!probe.BodyBelief(&hq,&actor,102));
}
''')
print('PASS: actual native fixture lookup selects exact BODY kind/identity/message from bounded evaluations, never sighting-only Get')