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.
283 lines
19 KiB
Python
283 lines
19 KiB
Python
"""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<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')
|
|
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('))
|
|
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 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 <cassert>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <map>
|
|
#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;}
|
|
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>{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>{
|
|
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 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<int> 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;
|
|
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{
|
|
Array<ERR_HQWatchEntry*>m_Entries;Map<string,ERR_HQWatchEntry*>m_BySource;
|
|
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);
|
|
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<float>::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<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));
|
|
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()==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'):
|
|
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')
|