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());
}
''')