"""Actual delivered-only roster, missing-patrol and cumulative confirmed-loss rules.""" from policy_harness import ROOT, body, run from pathlib import Path import os,re P=Path(os.environ.get('ERR_WATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQWatch.c')) U=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c' def cpp(s): for old,new in [('protected ',''),('Math.','Math::'),('ERR_HQPursuitRules.','ERR_HQPursuitRules::'), ('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','ERR_HQReport','ERR_HQDeliveredCasualties','ERR_HQAmmoSnapshot'): s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s) 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,') s=s.replace('entry->AmmoState.', 'entry->AmmoState->').replace('report->AmmoState.', 'report->AmmoState->') return s helper=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQDeliveredCasualties.c').read_text() helper=helper.replace('class ','struct ').replace('protected ','').replace('ref ','').replace('Math.','Math::') helper=helper.replace('array slots','Array* slots').replace('slots.', 'slots->') helper=helper.replace('array','Array').replace('foreach (','for (') helper=helper.replace(' : slots)', ' : *slots)').replace('int m_Strength;', 'int m_Strength=0;') helper=helper.replace('\n}\n','\n};\n') ammo_source=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c')).read_text() ammo=ammo_source[:ammo_source.index('class ERR_HQAmmoSampler')] ammo=ammo.replace('class ','struct ').replace('ERR_HQAmmoSnapshot Snapshot(', 'ERR_HQAmmoSnapshot* Snapshot(') ammo=ammo.replace('ERR_HQAmmoSnapshot copy', 'ERR_HQAmmoSnapshot* copy').replace('copy.', 'copy->') ammo=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',ammo).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(', 'static int Aggression(', 'static int Hunters(')) methods='\n'.join(cpp(body(P,s)) for s in ('int Count()', 'ERR_HQWatchEntry Find(', 'ERR_HQWatchEntry At(', 'int IdentifiedBodies()', 'int Aggression()', 'int HunterTarget()', 'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'bool ArmedSearchBlocked(', '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/core/Scripts/Game/ERR/10_AI/ERR_HQReport.c','ERR_HQReport Snapshot(')).replace('copy.', 'copy->') accept=cpp(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/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 #include #include #include #include #include #include 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;} 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++;} templatestruct Array:std::vector{using std::vector::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();} }; templatestruct Map:std::map{ V Get(K k){return this->count(k)?this->at(k):V{};}void Set(K k,V v){(*this)[k]=v;} void Clear(){this->clear();} }; struct ERR_HQPursuitRules{ '''+common+r''' }; '''+helper+ammo+r''' struct ERR_HQReport{ string RoadStatus="road unknown"; 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}; ERR_HQAmmoSnapshot* AmmoState=nullptr; int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;Array DeathSlots; }; struct ERR_HQWatchEntry{ string SourceId;int MessageId=0,RequestId=0;vector Position; ERR_HQAmmoSnapshot* AmmoState=nullptr; int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;Array IdentifiedSlots; ERR_HQDeliveredCasualties* Casualties=new ERR_HQDeliveredCasualties();float LastBodyDeliveredAt=-1; float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0; bool Investigating=false,Closed=false; }; struct ERR_HQWatchRules{ '''+rules+r''' }; struct Watch{ Arraym_Entries;Mapm_BySource; int m_ConfirmedLosses=0,m_IdentifiedBodies=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){}}; template string Format(string,Args...){return "delivered";} struct HQDelivery{float m_NextAssignment=5000; Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;struct RecoveryAdapter{int calls=0;void ReportBody(ERR_HQReport*){calls++;}} m_Recovery;CoverageAdapter m_Coverage;JournalAdapter m_Journal; Arraym_Contacts; '''+accept+r''' }; int main(){ using R=ERR_HQWatchRules; assert(R::SquadLimit(0,100)==0&&R::SquadLimit(-1,100)==0); assert(R::SquadLimit(2,0)==2&&R::SquadLimit(2,1)==2&&R::SquadLimit(2,2)==3); assert(R::SquadLimit(2,-1)==2&&R::SquadLimit(2,15)==8); assert(R::SquadLimit(100,0)==8&&R::SquadLimit(2,2147483647)==8); Watch ammunition;ERR_HQStatusReport ammoReport;ERR_HQAmmoSnapshot ammoState; ammoState.Complete=true;ammoState.LauncherComplete=true;ammoState.Members=4;ammoState.SmallArmsMuzzles=4; ammoReport.AmmoState=&ammoState; assert(!ammunition.ArmedSearchBlocked("patrol-1",1000)); // local scan is not HQ knowledge assert(ammunition.Ingest(&ammoReport,1000)&&ammunition.ArmedSearchBlocked("patrol-1",1000)); ammoState.SmallArmsRounds=120;assert(ammunition.ArmedSearchBlocked("patrol-1",1001)); // immutable delivered copy assert(ammunition.ArmedSearchBlocked("patrol-1",180000)&&!ammunition.ArmedSearchBlocked("patrol-1",180001)); assert(!ammunition.ArmedSearchBlocked("patrol-1",-1)&&!ammunition.ArmedSearchBlocked("patrol-1",NAN)); ammoReport.MessageId=2;ammoReport.ObservedAt=2000; assert(ammunition.Ingest(&ammoReport,2000)&&!ammunition.ArmedSearchBlocked("patrol-1",2000)); ammoReport.MessageId=3;ammoReport.ObservedAt=3000;ammoState.SmallArmsRounds=0;ammoState.Complete=false; assert(ammunition.Ingest(&ammoReport,3000)&&!ammunition.ArmedSearchBlocked("patrol-1",3000)); // unknown != empty ammoReport.MessageId=4;ammoReport.ObservedAt=4000;ammoState.Complete=true;ammoState.SmallArmsChambers=1; assert(ammunition.Ingest(&ammoReport,4000)&&!ammunition.ArmedSearchBlocked("patrol-1",4000)); ammoReport.MessageId=5;ammoReport.ObservedAt=5000;ammoState.SmallArmsChambers=0; assert(ammunition.Ingest(&ammoReport,5000)&&ammunition.ArmedSearchBlocked("patrol-1",5000)); ERR_HQReport casualty;casualty.MessageId=901;casualty.ObservedAt=5000; assert(ammunition.IngestBody(&casualty,5001)&&!ammunition.ArmedSearchBlocked("patrol-1",5001)); // changed roster requires fresh inventory evidence ammoReport.MessageId=6;ammoReport.ObservedAt=6000;ammoReport.AmmoState=nullptr; assert(ammunition.Ingest(&ammoReport,6000)&&!ammunition.ArmedSearchBlocked("patrol-1",6000)); // missing payload clears stale ammo claim Watch watch;ERR_HQStatusReport report; // Until an actual delivery calls Ingest the local status produces no HQ roster or missing case. assert(watch.Count()==0&&!watch.RequestDue(report.SourceId,999999)); assert(watch.Ingest(&report,5000)&&watch.Count()==1); auto entry=watch.Find(report.SourceId); report.Position={999,40,999}; // immutable intake: changing sender object cannot relocate HQ's snapshot assert(entry->Position.x==100&&entry->LastDeliveredAt==5000); assert(!watch.RequestDue(report.SourceId,359999)); assert(watch.RequestDue(report.SourceId,360000)); assert(!watch.MarkRequested(report.SourceId,2,360000)); assert(watch.MarkRequested(report.SourceId,1,360000)); assert(!watch.MarkRequested(report.SourceId,1,360001)); assert(!watch.InvestigationDue(report.SourceId,389999)); assert(watch.InvestigationDue(report.SourceId,390000)); assert(watch.MarkInvestigating(report.SourceId,1,1,390000)); assert(!watch.MarkInvestigating(report.SourceId,1,1,390000)); assert(watch.ConfirmedLosses()==0&&entry->Position.x==100); // silence proves neither casualties nor target position assert(!watch.FinishInvestigation(report.SourceId,2,1,391000)); assert(watch.FinishInvestigation(report.SourceId,1,1,391000)); assert(!watch.FinishInvestigation(report.SourceId,1,1,391000)); assert(!watch.RequestDue(report.SourceId,570999)&&watch.RequestDue(report.SourceId,571000)); assert(watch.MarkRequested(report.SourceId,1,571000)); assert(watch.MarkInvestigating(report.SourceId,1,2,601000)); assert(!watch.FinishInvestigation(report.SourceId,1,1,601001)); // late completion from first request // A fresh delayed check-in cancels the old missing episode, never refreshes an enemy contact. report.MessageId=2;report.ObservedAt=600000;report.Fit=2; assert(watch.Ingest(&report,602000)); assert(!entry->Investigating&&entry->AskedAt==-1&&entry->Position.x==999); assert(watch.ConfirmedLosses()==0); // two unconscious members still alive 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.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.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.DeathSlots={1}; assert(!watch.Ingest(&report,606000)&&watch.ConfirmedLosses()==2); 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.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.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::quiet_NaN(); for(int field=0;field<13;field++){ ERR_HQStatusReport bad;bad.SourceId="invalid"; if(field==0)bad.SourceId=""; if(field==1)bad.MessageId=0; if(field==2)bad.Position.x=nan; if(field==3)bad.ObservedAt=1001; if(field==4)bad.ObservedAt=-1; if(field==5)bad.ObservedAt=nan; if(field==6)bad.Strength=33; if(field==7)bad.Count=5; if(field==8)bad.Fit=5; if(field==9)bad.Fit=-1; if(field==10)bad.ConfirmedLosses=1; if(field==11)bad.ConfirmedLosses=-1; if(field==12)bad.SourceId=string(97,'a'); assert(!watch.Ingest(&bad,1000)&&watch.Count()==1); } ERR_HQStatusReport late;late.SourceId="late"; 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;item.DeathSlots={0,1,2,3}; for(int i=0;i<256;i++){item.SourceId=std::to_string(i);assert(cap.Ingest(&item,1000));} assert(cap.Count()==256&&cap.ConfirmedLosses()==1024); item.SourceId="overflow";assert(!cap.Ingest(&item,1000)); item.SourceId="overflow2";assert(!cap.Ingest(&item,1000)&&logs==1); item.SourceId="0";item.MessageId=2;item.ObservedAt=2000; assert(cap.Ingest(&item,2000)&&cap.ConfirmedLosses()==1024&&cap.Count()==256); 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)); assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==4); for(int bodies=0;bodies<=300;bodies++){ assert(ERR_HQWatchRules::Aggression(bodies)==std::min(30,bodies/3)); assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==3+(std::min(30,bodies/3)*12+29)/30); } assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==3); merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==3); // 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);assert(intake.m_NextAssignment==5000); 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_NextAssignment==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'): 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 256-record cap')