158 lines
8.4 KiB
Python
158 lines
8.4 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/ai/Scripts/Game/ERR/ERR_HQWatch.c'))
|
|
U=ROOT/'addons/ai/Scripts/Game/ERR/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'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
for var in ('entry','report'):s=s.replace(var+'.',var+'->')
|
|
return s
|
|
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',
|
|
'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('))
|
|
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;}};
|
|
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>{
|
|
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'''
|
|
};
|
|
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;
|
|
};
|
|
struct ERR_HQWatchEntry{
|
|
string SourceId;int MessageId=0,RequestId=0;vector Position;
|
|
int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
|
|
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'''
|
|
};
|
|
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 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;
|
|
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;
|
|
assert(watch.Ingest(&report,605000)&&watch.ConfirmedLosses()==2&&watch.SquadLimit(2)==3);
|
|
report.MessageId=6;report.ObservedAt=604000;report.ConfirmedLosses=1;
|
|
assert(!watch.Ingest(&report,606000)&&watch.ConfirmedLosses()==2);
|
|
report.ConfirmedLosses=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;
|
|
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;
|
|
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;
|
|
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"));
|
|
}
|
|
''')
|
|
source=P.read_text()
|
|
for forbidden in ('IEntity','SCR_AIGroup','GetGame(','GetOrigin(','GetPlayers(','GetAgents(',
|
|
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory','ERR_HQReport '):
|
|
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')
|