Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
+123
-4
@@ -2,13 +2,26 @@
|
||||
|
||||
No fake world polling: identity stubs deliberately have no position/health API.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, run
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c'
|
||||
source = path.read_text()
|
||||
for forbidden in ('GetOrigin(', 'GetControlledEntity(', 'GetPlayerManager(', 'GetDamageManager(', 'GetWorld('):
|
||||
assert forbidden not in source
|
||||
assert max(map(len, source.splitlines())) <= 120
|
||||
# Native Enforce weak parameters do not retain a block-local sanitized report.
|
||||
# Guard the owner scope that crashed on real radio delivery. The raw-pointer C++
|
||||
# adapter below checks values/caller isolation, not native reference counting.
|
||||
ingest = body(path, 'bool Ingest(ERR_HQReport report, float now)')
|
||||
ingest = re.sub(r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"', '', ingest, flags=re.S)
|
||||
owner = ingest.index('ref ERR_HQReport alarm;')
|
||||
branch = ingest.index('if (kind == ERR_HQMemoryKind.ALARM)')
|
||||
allocate = ingest.index('alarm = new ERR_HQReport();')
|
||||
append = ingest.index('track.Append(report, now);')
|
||||
assert owner < branch < allocate < ingest.index('report = alarm;') < append
|
||||
for position, depth in ((owner, 1), (allocate, 2), (append, 1)):
|
||||
assert ingest[:position].count('{') - ingest[:position].count('}') == depth
|
||||
code = source
|
||||
for kind in ('ERR_HQReport', 'ERR_HQMemoryTrack'):
|
||||
code = code.replace('ref array<ref ' + kind + '>', 'Array<' + kind + '*>')
|
||||
@@ -21,12 +34,24 @@ code = re.sub(r'^}', '};', code, flags=re.M)
|
||||
code = re.sub(r'\bvector\b', 'Vec', code)
|
||||
for cls in ('ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math', 'Vec'):
|
||||
code = code.replace(cls + '.', cls + '::')
|
||||
for name in ('report', 'last', 'belief', 'previous', 'track'):
|
||||
for name in ('report', 'last', 'belief', 'previous', 'track', 'alarm'):
|
||||
code = re.sub(r'\b' + name + r'\.', name + '->', code)
|
||||
code = re.sub(r'm_Tracks\[([^\]]+)\]\.', r'm_Tracks[\1]->', code)
|
||||
code = code.replace('Latest().', 'Latest()->').replace('null', 'nullptr')
|
||||
code = re.sub(r'^(\s+(?:IEntity|Vec|bool|float|int|string|ERR_HQReport\*|ERR_HQMemoryTrack\*) \w+);$',
|
||||
r'\1{};', code, flags=re.M)
|
||||
# Execute the production delivery boundary against this actual memory service.
|
||||
# World/radio/body adapters below only isolate unrelated side effects.
|
||||
accept = body(Path(os.environ.get('ERR_HQ_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')),
|
||||
'protected void Accept(')
|
||||
accept = accept.replace('protected ', '').replace('null', 'nullptr')
|
||||
accept = re.sub(r'\b(ERR_HQReport|ERR_HQBelief) (\w+)', r'\1* \2', accept)
|
||||
for name in ('report', 'previous', 'contact', 'belief'):
|
||||
accept = re.sub(r'\b' + name + r'\.', name + '->', accept)
|
||||
for cls in ('ERR_MissionRules', 'ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math'):
|
||||
accept = accept.replace(cls + '.', cls + '::')
|
||||
accept = accept.replace('vector.DistanceXZ(', 'Vec::DistanceXZ(').replace('vector.Zero', 'Vec{}')
|
||||
accept = accept.replace('string.Format(', 'Format(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -55,6 +80,7 @@ template<class T>struct Array:std::vector<T>{
|
||||
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
|
||||
};
|
||||
namespace Math {
|
||||
template<class A,class B>auto Min(A a,B b){using T=std::common_type_t<A,B>;return std::min(T(a),T(b));}
|
||||
template<class A,class B>auto Max(A a,B b){using T=std::common_type_t<A,B>;return std::max(T(a),T(b));}
|
||||
template<class A,class B,class C>auto Clamp(A a,B b,C c){using T=std::common_type_t<A,B,C>;return std::clamp(T(a),T(b),T(c));}
|
||||
float AbsFloat(float v){return std::abs(v);}float Pow(float a,float b){return std::pow(a,b);}
|
||||
@@ -62,7 +88,9 @@ namespace Math {
|
||||
}
|
||||
struct ERR_HQReport {
|
||||
int MessageId=0;IEntity Target=nullptr,Source=nullptr;Vec Position,Velocity;
|
||||
float ObservedAt=-1;bool Distress=false,HasVelocity=false;string TravelMode="unknown";
|
||||
float ObservedAt=-1;bool Distress=false,HasVelocity=false,Engaged=false;string TravelMode="unknown";
|
||||
int SeenEscapists=-1,CasualtySlot=-1,CasualtyStrength=0;float DetailsObservedAt=-1;
|
||||
string SeenWeapons="unknown",RoadStatus="road unknown",CasualtySourceId;
|
||||
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}
|
||||
};
|
||||
''' + code + r'''
|
||||
@@ -70,7 +98,47 @@ ERR_HQReport Report(int id,IEntity target,float when,Vec pos={0,0,0},string mode
|
||||
ERR_HQReport r;r.MessageId=id;r.Target=target;r.Source=target;r.ObservedAt=when;r.Position=pos;r.TravelMode=mode;return r;
|
||||
}
|
||||
bool near(float a,float b,float tolerance=.001){return std::abs(a-b)<tolerance;}
|
||||
struct ERR_MissionRules{static bool ContactFresh(float t,float now){return ERR_HQMemoryRules::DeliveryFresh(t,now);}};
|
||||
struct WatchAdapter{bool IngestBody(ERR_HQReport*,float){return false;}int IdentifiedBodies(){return 0;}
|
||||
int Aggression(){return 0;}int HunterTarget(){return 0;}};
|
||||
struct BodiesAdapter{void ConfirmDelivered(IEntity){}};
|
||||
struct RecoveryAdapter{void ReportBody(ERR_HQReport*){}};
|
||||
struct CoverageAdapter{void Forget(Vec,float){}};
|
||||
struct JournalAdapter{void Write(string,string,int){}};
|
||||
template<class... Args>string Format(string,Args...){return "delivered";}
|
||||
struct HQDelivery{struct LocalCoverage{void Forget(Vec,float){}}m_LocalCoverage;
|
||||
ERR_HQMemory m_Memory;WatchAdapter m_Watch;BodiesAdapter m_Bodies;RecoveryAdapter m_Recovery;
|
||||
CoverageAdapter m_Coverage;JournalAdapter m_Journal;float m_NextAssignment=5000;
|
||||
Array<ERR_HQReport*> m_Contacts;
|
||||
''' + accept + r'''
|
||||
};
|
||||
int main(){
|
||||
// Accepted fire-support contacts use the actual memory history, not a second
|
||||
// looser pair estimate. Native transport is tested separately.
|
||||
Identity contactActor{44};
|
||||
for(int scenario=0;scenario<6;scenario++){
|
||||
HQDelivery h;auto first=Report(1,&contactActor,1000,{0,0,0});h.Accept(&first,1000);
|
||||
auto next=Report(2,&contactActor,11000,{300,0,0});
|
||||
if(scenario==1){next.TravelMode="vehicle";next.Position={60,0,0};}
|
||||
if(scenario==2){next.ObservedAt=2000;next.Position={10,0,0};}
|
||||
if(scenario>=3){next.Position={60,12,0};}
|
||||
if(scenario==4){auto close=Report(3,&contactActor,10000,{0,0,0});h.Accept(&close,10000);}
|
||||
next.HasVelocity=true;next.Velocity={99,22,33}; // input must stay detached and untouched
|
||||
h.Accept(&next,next.ObservedAt);
|
||||
assert(h.m_Contacts.Count()==1&&h.m_Contacts[0]!=&next);
|
||||
auto stored=h.m_Contacts[0];auto belief=h.m_Memory.Get(&contactActor);
|
||||
assert(stored->HasVelocity==belief->HasVelocity);
|
||||
if(scenario<3||scenario==4)assert(!stored->HasVelocity&&near(stored->Velocity.Length(),0));
|
||||
else assert(stored->HasVelocity&&near(stored->Velocity[0],6)&&near(stored->Velocity[1],0));
|
||||
assert(next.HasVelocity&&near(next.Velocity[0],99)&&near(next.Velocity[1],22));
|
||||
auto equal=Report(10,&contactActor,next.ObservedAt,{900,0,0});h.Accept(&equal,next.ObservedAt+1);
|
||||
auto older=Report(11,&contactActor,500,{900,0,0});h.Accept(&older,next.ObservedAt+2);
|
||||
h.Accept(&next,next.ObservedAt+3); // duplicate identity and observation do not refresh contact
|
||||
assert(h.m_Contacts[0]==stored&&stored->ObservedAt==next.ObservedAt);
|
||||
if(scenario==5){auto close=Report(4,&contactActor,12000,{66,12,0});h.Accept(&close,12000);
|
||||
assert(h.m_Contacts[0]->HasVelocity&&near(h.m_Contacts[0]->Velocity[0],6));}
|
||||
}
|
||||
|
||||
using Rules=ERR_HQMemoryRules;
|
||||
float nan=std::numeric_limits<float>::quiet_NaN(),inf=std::numeric_limits<float>::infinity();
|
||||
assert(near(Rules::Confidence(0),1)&&near(Rules::Confidence(600),.5)&&near(Rules::Confidence(1200),.25));
|
||||
@@ -152,6 +220,32 @@ int main(){
|
||||
auto rapidA=Report(1,&a,1000);assert(memory.Ingest(&rapidA,1000));
|
||||
auto rapidB=Report(2,&a,1500);assert(memory.Ingest(&rapidB,1500));
|
||||
assert(!memory.Get(&a)->HasVelocity); // insufficient time baseline still refuses motion
|
||||
// A near-time report may not hide a contradiction by skipping to an older convenient pair.
|
||||
memory.Clear();
|
||||
auto origin=Report(1,&a,0,{0,10,0});assert(memory.Ingest(&origin,0));
|
||||
auto away=Report(2,&a,10000,{100,10,0});assert(memory.Ingest(&away,10000));
|
||||
auto returned=Report(3,&a,11000,{0,10,0});assert(memory.Ingest(&returned,12000));
|
||||
auto contradicted=memory.Get(&a);
|
||||
assert(!contradicted->HasVelocity&&near(contradicted->Speed,0)&&near(contradicted->Velocity.Length(),0));
|
||||
assert(near(contradicted->Position[0],0)&&near(contradicted->PredictedPosition[0],0));
|
||||
assert(contradicted->ObservedAt==11000&&contradicted->LastDeliveredAt==12000);
|
||||
assert(contradicted->SampleCount==3&&contradicted->BeliefRevision==3&&contradicted->EvaluatedAt==12000);
|
||||
assert(near(contradicted->Confidence,Rules::Confidence(1))&&near(contradicted->Radius,26.2));
|
||||
// Equal-time contradiction is refused at intake; it cannot replace the newest position or reset its age.
|
||||
auto sameTime=Report(4,&a,11000,{100,10,0});assert(!memory.Ingest(&sameTime,13000));
|
||||
assert(memory.Get(&a)->ObservedAt==11000&&memory.Get(&a)->LastDeliveredAt==12000);
|
||||
assert(memory.Get(&a)->SampleCount==3&&!memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Position[0],0));
|
||||
// A second identity retains independent credible motion despite the contradiction above.
|
||||
auto otherStart=Report(5,&b,12000,{200,0,0});assert(memory.Ingest(&otherStart,13000));
|
||||
auto otherMove=Report(6,&b,14000,{210,0,0});assert(memory.Ingest(&otherMove,14000));
|
||||
assert(memory.Get(&b)->HasVelocity&&near(memory.Get(&b)->Speed,5)&&!memory.Get(&a)->HasVelocity);
|
||||
// The existing two-second speed envelope tolerates tightly spaced samples up to its limit.
|
||||
for(float separation : {23.99f,24.f,24.01f}){
|
||||
memory.Clear();auto start=Report(1,&a,0,{0,0,0});assert(memory.Ingest(&start,0));
|
||||
auto nearTime=Report(2,&a,10000,{separation,0,0});assert(memory.Ingest(&nearTime,10000));
|
||||
auto last=Report(3,&a,11000,{0,0,0});assert(memory.Ingest(&last,11000));
|
||||
assert(memory.Get(&a)->HasVelocity==(separation<=24));
|
||||
}
|
||||
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
|
||||
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
|
||||
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
|
||||
@@ -171,6 +265,31 @@ int main(){
|
||||
auto visual=Report(3,&a,3000,{400,0,210});assert(memory.Ingest(&visual,3000));
|
||||
assert(memory.Count()==2&&memory.Get(&a)->Actionable()&&near(memory.Get(&a)->Confidence,1));
|
||||
memory.Tick(63000);assert(near(memory.GetById(soundId)->Confidence,.2));
|
||||
// Alarm identity is distinct from gunfire and sightings even for the same source.
|
||||
memory.Clear();auto alarm=Report(1,&a,1000,{75,20,225},"heard vehicle alarm");alarm.Source=&friendly;
|
||||
alarm.HasVelocity=true;alarm.Velocity={40,0,0};alarm.Distress=true;alarm.Engaged=true;
|
||||
alarm.SeenEscapists=12;alarm.SeenWeapons="rifle";alarm.RoadStatus="road";alarm.DetailsObservedAt=1000;
|
||||
alarm.CasualtySourceId="forged";alarm.CasualtySlot=1;alarm.CasualtyStrength=4;
|
||||
assert(memory.Ingest(&alarm,1000));auto heard=memory.At(0);int alarmId=heard->BeliefId;
|
||||
assert(heard->Kind==ERR_HQMemoryKind::ALARM&&!heard->Target&&!heard->HasVelocity&&!heard->Actionable());
|
||||
assert(near(heard->Confidence,.25)&&near(heard->Radius,150)&&near(heard->PredictedPosition[0],75));
|
||||
assert(!heard->Report->Target&&!heard->Report->HasVelocity&&near(heard->Report->Velocity.Length(),0));
|
||||
assert(!heard->Report->Distress&&!heard->Report->Engaged&&heard->Report->SeenEscapists==-1);
|
||||
assert(heard->Report->SeenWeapons=="unknown"&&heard->Report->RoadStatus=="road unknown");
|
||||
assert(heard->Report->DetailsObservedAt==-1&&heard->Report->CasualtySourceId.empty());
|
||||
assert(heard->Report->CasualtySlot==-1&&heard->Report->CasualtyStrength==0);
|
||||
assert(alarm.Target==&a&&alarm.HasVelocity&&alarm.SeenEscapists==12); // caller was not mutated
|
||||
assert(heard->Report->Source==&friendly&&heard->Report->ObservedAt==1000&&heard->Report->MessageId==1);
|
||||
heard->Report->Position={999,0,999};assert(near(memory.GetById(alarmId)->Position[0],75));
|
||||
auto distinctSound=Report(2,nullptr,1000,{75,20,225},"heard gunfire");distinctSound.Source=&friendly;
|
||||
assert(memory.Ingest(&distinctSound,1000)&&memory.Count()==2&&!memory.Get(&a));
|
||||
alarm.MessageId=3;alarm.ObservedAt=4000;alarm.Position={375,20,225};assert(memory.Ingest(&alarm,4000));
|
||||
assert(memory.Count()==2&&memory.GetById(alarmId)->BeliefRevision==2&&!memory.GetById(alarmId)->HasVelocity);
|
||||
assert(!memory.NegativeSearch(alarmId,1,4000)&&memory.NegativeSearch(alarmId,2,4000));
|
||||
assert(!memory.NegativeSearch(alarmId,2,4000)&&near(memory.GetById(alarmId)->Confidence,.1625));
|
||||
memory.Tick(49000);assert(near(memory.GetById(alarmId)->Confidence,.08125));
|
||||
assert(near(memory.GetById(alarmId)->Radius,240));
|
||||
alarm.MessageId=4;alarm.ObservedAt=49000;alarm.Source=nullptr;assert(!memory.Ingest(&alarm,49000));
|
||||
// The global replay table is bounded even when valid observation IDs arrive rapidly.
|
||||
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
|
||||
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
|
||||
|
||||
Reference in New Issue
Block a user