Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
155 lines
11 KiB
Python
155 lines
11 KiB
Python
"""Actual HQ report methods, with deterministic radio/perception boundary doubles.
|
|
Native radio and perception remain explicit boundary doubles.
|
|
"""
|
|
from pathlib import Path
|
|
import os,re
|
|
from policy_harness import ROOT,body,run
|
|
P=Path(os.environ.get('ERR_HQ_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')))
|
|
original=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c').read_text()
|
|
legacy='Observe(unit, now);\n ObserveBody(unit, now);\n StatusReport(unit, now);\n Sweep(unit, now);'
|
|
assert legacy in original or 'QueueReports(unit, now);' in original
|
|
|
|
def cpp(s):
|
|
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
|
|
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
|
|
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
|
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
|
|
s=s.replace('ERR_HQReport* report;','ERR_HQReport* report = nullptr;')
|
|
for v in ('unit','route','report','seen','task','stock'):
|
|
s=s.replace(v+'.',v+'->')
|
|
for v in ('Utility','ReportSelection','StatusSampler','Group','Task'):
|
|
s=s.replace('->'+v+'.','->'+v+'->')
|
|
s=s.replace('m_Mode.Settings().Runtime(', 'm_Mode->Settings()->Runtime(')
|
|
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::')
|
|
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
|
|
return s.replace('AmmoState.', 'AmmoState->').replace('SecuritySite.IsEmpty()', 'SecuritySite.empty()')
|
|
policy_path=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT', ROOT))/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c'
|
|
policy=cpp(policy_path.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
|
|
methods='\n'.join(cpp(body(P,s)) for s in ('protected void Observe(', 'protected void ObserveBody(', 'protected void StatusReport(', 'protected void Sweep(', 'protected void QueueReports('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <vector>
|
|
#include <string>
|
|
using string=std::string;
|
|
template<class... T>string Format(T... args){return "diagnostic";}
|
|
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
|
template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}};
|
|
struct Group{vector position;vector GetOrigin(){return position;}};
|
|
struct Settings{int interval=5;int Runtime(string){return interval;}};
|
|
struct Mode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
|
struct ERR_HQVisualContact{void* Entity=this;vector Position;float ObservedAt=0;};
|
|
struct Utility{bool visible=true;};
|
|
struct ERR_HQObservation{static void Gather(Utility* u,float now,Array<ERR_HQVisualContact*>& a){
|
|
static ERR_HQVisualContact contact;if(u->visible){contact.ObservedAt=now;a.push_back(&contact);}}
|
|
template<class T>static void Describe(T*,Array<ERR_HQVisualContact*>&,float){}
|
|
};
|
|
struct Selection{int attempts=0;ERR_HQVisualContact* Select(Array<ERR_HQVisualContact*>& a){return a.front();}void Queued(void*){attempts++;}};
|
|
struct ERR_HQReport{int MessageId=0;void* Target=nullptr;Group* Source=nullptr;vector Position;float ObservedAt=0;
|
|
string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
|
|
struct ERR_HQPursuitRules{static bool Finite(float value){return std::isfinite(value);}};
|
|
struct ERR_HQAmmoSnapshot{bool Complete=false;int SmallArmsRounds=0,SmallArmsChambers=0,SmallArmsMuzzles=0,Members=0;bool Valid(int){return false;}};
|
|
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
|
|
'''+policy+r'''
|
|
struct Sampler{int observations=0,samples=0;bool valid=true;void ObserveDeaths(){observations++;}
|
|
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;auto r=new ERR_HQStatusReport;r->MessageId=id;r->ObservedAt=now;return r;}};
|
|
struct ERR_HQSearchReport{int SourceId=0,MessageId=0,WatchMessageId=0,WatchRequestId=0,BeliefId=0,BeliefRevision=0;
|
|
string WatchSourceId;vector Position;float ObservedAt=0;bool SweepCompleted=false,ContactFound=false;};
|
|
struct ERR_HQTask{std::string SecuritySite;bool RoadIntercept=false,SweepQueued=false;float ArrivedAt=-1;vector Destination;
|
|
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
|
|
struct ERR_HQRoute{Group* LocalSource=nullptr;int MessageId=0;string Payload;};
|
|
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
|
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
|
|
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
|
|
struct History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
|
|
struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
|
|
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
|
|
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
|
|
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
|
|
ERR_HQAmmoReporting AmmoReporting;
|
|
float NextReport=0,NextStatus=0;bool fighting=false,eligible=true;
|
|
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
|
|
bool Eligible(Mode*){return eligible;}
|
|
};
|
|
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
|
|
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
|
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
|
|
bool Fighting(ERR_HQUnit* u){return u->fighting;}
|
|
'''+methods+r'''
|
|
void Legacy(ERR_HQUnit* unit,float now){
|
|
'''+legacy+r'''
|
|
}
|
|
};
|
|
struct Stats{int sight=0,status=0,sweep=0;};
|
|
void Finish(ERR_HQUnit& u,Stats& s,bool failed=false){
|
|
assert(u.Uplink);if(u.PendingStatus){if(!failed)s.status++;}
|
|
else if(u.PendingSweep){if(!failed)s.sweep++;else u.Task->SweepQueued=false;}
|
|
else {assert(u.PendingReport);if(!failed)s.sight++;}
|
|
u.Uplink=nullptr;u.PendingStatus=nullptr;u.PendingSweep=nullptr;u.PendingReport=nullptr;
|
|
}
|
|
Stats Simulate(bool legacy){HQ h;ERR_HQUnit u;Stats counts;float due=-1;
|
|
// Model three native two-second legs; real transport is intentionally outside this harness.
|
|
for(int now=0;now<=600000;now+=1000){
|
|
if(u.Uplink&&now>=due)Finish(u,counts);
|
|
bool busy=u.Uplink;if(legacy)h.Legacy(&u,now);else h.QueueReports(&u,now);
|
|
if(!busy&&u.Uplink)due=now+6000;
|
|
}
|
|
assert(u.sampler.observations==601);return counts;
|
|
}
|
|
int main(){
|
|
auto before=Simulate(true);assert(before.sight==100&&before.status==0);
|
|
auto after=Simulate(false);assert(after.status>=4&&after.sight>=95);
|
|
HQ h;ERR_HQUnit u;Stats counts;
|
|
// Queueing creates no delivered roster knowledge; exact sampled time stays on the pending report.
|
|
h.QueueReports(&u,0);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==0&&counts.status==0);
|
|
auto original=u.Uplink;h.QueueReports(&u,1000);assert(u.Uplink==original&&u.sampler.observations==2);
|
|
Finish(u,counts);assert(counts.status==1);h.QueueReports(&u,6000);assert(u.PendingReport);
|
|
// HQ check-in may be delivered while sighting uplink is busy; next free slot must carry status.
|
|
u.NextStatus=7000;h.QueueReports(&u,7000);assert(u.PendingReport&&!u.PendingStatus);
|
|
Finish(u,counts);h.QueueReports(&u,12000);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==12000);
|
|
// Quiet-but-visible contact must suppress negative evidence, even though Fighting is false.
|
|
Finish(u,counts);ERR_HQTask task;task.ArrivedAt=0;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;
|
|
h.QueueReports(&u,30000);assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1&&!u.fighting);
|
|
// Start dwell only once native perception sees no actual contact.
|
|
Finish(u,counts);u.ownUtility.visible=false;h.QueueReports(&u,36000);
|
|
assert(task.ArrivedAt==36000&&!u.Uplink);
|
|
// While a status frame is busy, newly visible contact still resets the dwell clock.
|
|
u.NextStatus=37000;h.QueueReports(&u,37000);assert(u.PendingStatus);
|
|
u.ownUtility.visible=true;h.QueueReports(&u,38000);
|
|
assert(task.ArrivedAt==-1&&u.PendingStatus&&!u.PendingReport);
|
|
// Clearing vision begins a fresh 30s dwell even while the existing uplink remains occupied.
|
|
u.ownUtility.visible=false;h.QueueReports(&u,39000);assert(task.ArrivedAt==39000);
|
|
Finish(u,counts);h.QueueReports(&u,43000);assert(!u.Uplink);
|
|
h.QueueReports(&u,68000);assert(!u.PendingSweep);
|
|
h.QueueReports(&u,69000);assert(u.PendingSweep&&u.PendingSweep->ObservedAt==69000&&task.SweepQueued);
|
|
// Delivered search is never queued a second time.
|
|
Finish(u,counts);h.QueueReports(&u,75000);assert(!u.Uplink&&counts.sweep==1);
|
|
// A failed negative retry remains subordinate to current positive observation.
|
|
task.SweepQueued=false;u.ownUtility.visible=true;h.QueueReports(&u,80000);
|
|
assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1);
|
|
// Quiet vision is insufficient while Fighting: combat and movement each reset uninterrupted dwell.
|
|
Finish(u,counts);u.ownUtility.visible=false;u.fighting=true;task.ArrivedAt=0;
|
|
h.QueueReports(&u,90000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
|
|
u.fighting=false;h.QueueReports(&u,91000);assert(task.ArrivedAt==91000);
|
|
u.ownGroup.position.x=100;h.QueueReports(&u,92000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
|
|
u.ownGroup.position.x=0;h.QueueReports(&u,93000);assert(task.ArrivedAt==93000);
|
|
h.QueueReports(&u,122000);assert(!u.PendingSweep);h.QueueReports(&u,123000);assert(u.PendingSweep);
|
|
// Route loss or an unavailable status sampler must not fabricate a report or block useful contacts.
|
|
HQ lost;ERR_HQUnit v;lost.m_Network.available=false;lost.QueueReports(&v,0);
|
|
assert(!v.Uplink&&v.NextStatus==0&&v.selection.attempts==0);
|
|
lost.m_Network.available=true;v.sampler.valid=false;lost.QueueReports(&v,6000);assert(v.PendingReport&&!v.PendingStatus);
|
|
// An unavailable radio cannot manufacture a frame or a delivery.
|
|
HQ quiet;ERR_HQUnit q;q.Radio=nullptr;quiet.QueueReports(&q,48000);assert(!q.Uplink);
|
|
// Body evidence still gets its physical radio frame ahead of a completed negative sweep.
|
|
ERR_HQTask qt;q.Radio=&q;q.NextStatus=999999;q.Task=&qt;q.Assignment=&qt;q.Waypoint=&qt;
|
|
qt.ArrivedAt=0;q.ownUtility.visible=false;quiet.m_Bodies.visible=true;
|
|
quiet.QueueReports(&q,50000);assert(q.PendingReport&&!q.PendingSweep);
|
|
|
|
// A staffed road hold must never manufacture negative search evidence from idle dwell.
|
|
HQ road;ERR_HQUnit held;ERR_HQTask hold;hold.RoadIntercept=true;hold.ArrivedAt=0;
|
|
held.Task=&hold;held.Assignment=&hold;held.Waypoint=&hold;held.ownUtility.visible=false;
|
|
road.Sweep(&held,60000);assert(!held.PendingSweep&&!held.Uplink&&!hold.SweepQueued);
|
|
}
|
|
''')
|
|
print('PASS: actual report methods reproduce old starvation; status-first scheduler delivers check-ins and requires 30s without combat or actual visible contacts before negative reports')
|