Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
173 lines
12 KiB
Python
173 lines
12 KiB
Python
"""Execute production spotting math/delivery/adoption with native API doubles.
|
|
This is not evidence of native explosion callbacks or live impact accuracy.
|
|
"""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSpotting.c'
|
|
r=p.parent
|
|
|
|
def cpp(path, signature):
|
|
s=body(path,signature).replace('protected ','')
|
|
# Logging/formatting are inert at the native boundary, not delivery logic.
|
|
while 'string.Format(' in s:
|
|
start=s.index('string.Format('); end=start+len('string.Format('); level=1
|
|
while level:
|
|
level+=(s[end]=='(')-(s[end]==')');end+=1
|
|
s=s[:start]+'"log"'+s[end:]
|
|
for typ in ['ERR_MortarSpotReport','ERR_MortarBattery','ERR_HQReport']:
|
|
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
|
s=s.replace('ERR_MortarSpotReport* best;','ERR_MortarSpotReport* best=nullptr;')
|
|
s=s.replace('bool searched;', 'bool searched=false;')
|
|
for cls in ['ERR_ForceRules','ERR_MortarSpotRules','ERR_MortarRefreshRules','ERR_MortarPrediction','ERR_MortarSalvo','vector','Math']:
|
|
s=s.replace(cls+'.',cls+'::')
|
|
for field in ['spot','battery','report','best','m_HQ','update','shot','Observation','Gun','AimSolution','Refresh','Report','Battery','Downlink']:
|
|
s=s.replace(field+'.',field+'->')
|
|
s=s.replace('foreach (ERR_MortarSpotReport* spot : m_Reports)', 'for (ERR_MortarSpotReport* spot : m_Reports.values)')
|
|
s=s.replace('null','nullptr').replace('nullptrptr','nullptr')
|
|
return s
|
|
|
|
rules='\n'.join(cpp(p,s) for s in ['static bool Fresh(', 'static vector Bias(', 'static vector CorrectedOffset('])
|
|
newer=cpp(r/'ERR_MortarRefresh.c','static bool Newer(').replace('current.', 'current->').replace('incoming.','incoming->')
|
|
methods='\n'.join(cpp(p,s) for s in ['protected bool Usable(', 'protected ERR_MortarSpotReport Latest(', 'vector Offset(', 'void PrepareAim(', 'void ReceiveHQ(', 'void Tick('])
|
|
run(r'''#include <cassert>
|
|
#include <cmath>
|
|
#include <vector>
|
|
#include <limits>
|
|
struct vector {
|
|
float v[3]{};float& operator[](int i){return v[i];}
|
|
vector operator+(vector b) const{return {v[0]+b.v[0],v[1]+b.v[1],v[2]+b.v[2]};}
|
|
vector operator-(vector b) const{return {v[0]-b.v[0],v[1]-b.v[1],v[2]-b.v[2]};}
|
|
vector operator*(float b) const{return {v[0]*b,v[1]*b,v[2]*b};}
|
|
float Length() const{return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}
|
|
vector Normalized() const {float l=Length();return l?(*this)*(1/l):vector{};}
|
|
static float Distance(vector a,vector b){return (a-b).Length();}
|
|
};
|
|
struct Math {static float Round(float v){return std::round(v);}};
|
|
struct ERR_ForceRules {static bool Finite(float v){return std::isfinite(v);}};
|
|
struct ERR_MortarSpotRules {
|
|
'''+rules+r'''};
|
|
struct Entity {vector position;vector GetOrigin(){return position;}};
|
|
struct ERR_HQReport {Entity* Target=nullptr;int MessageId=1;float ObservedAt=0;vector Position;ERR_HQReport* Snapshot(){return this;}};
|
|
struct ERR_MortarPrediction {static bool Fresh(ERR_HQReport* r,float t){return t>=r->ObservedAt&&t-r->ObservedAt<=60000;}};
|
|
struct ERR_MortarRefreshRules {
|
|
'''+newer+r'''};
|
|
struct Aim {vector SalvoOffset;};
|
|
struct RefreshState {ERR_HQReport* Report=nullptr;};
|
|
struct ERR_MortarBattery {
|
|
Entity* Gun=nullptr;void* Group=this;ERR_HQReport* Report=nullptr;RefreshState* Refresh=nullptr;Aim* AimSolution=nullptr;
|
|
bool operational=true,CommandOpen=true,Released=false,RefreshRejected=false,Rejected=false;
|
|
int SiteId=1,OrderId=1,OrderShots=1;bool Operational(){return operational;}
|
|
void Audit(const char*,const char*){}
|
|
};
|
|
struct ERR_HQRoute {int result=0;int Advance(float,void*){return result;}};
|
|
struct ERR_MortarSpotReport {
|
|
ERR_MortarBattery* Battery=nullptr;Entity *Gun=nullptr,*Target=nullptr;
|
|
ERR_HQReport* Observation=nullptr;ERR_HQRoute* Downlink=nullptr;
|
|
vector GunOrigin,Bias;bool Delivered=false;float FiredAt=0,ImpactAt=-1;int GroupsChecked=0;
|
|
};
|
|
template<class T> struct Items {
|
|
std::vector<T> values;int Count(){return values.size();}void Insert(T t){values.push_back(t);}
|
|
void Remove(int i){values.erase(values.begin()+i);}T operator[](int i){return values[i];}
|
|
};
|
|
struct HQ {ERR_HQRoute* route=nullptr;int surveys=0;
|
|
ERR_HQRoute* Downlink(void*,const char*,const char*,int){return route;}
|
|
void* Journal(){return nullptr;}bool ReportMortarSplash(ERR_MortarSpotReport*,float){surveys++;return false;}
|
|
};
|
|
struct ERR_MortarSalvo {static vector Offset(int,int,int){return {80,0,0};}};
|
|
struct Spotting {bool m_Stopped=false;HQ* m_HQ=nullptr;Items<ERR_MortarSpotReport*> m_Reports,m_Flying;
|
|
'''+methods+r'''};
|
|
struct Fixture {
|
|
Entity gun,target;ERR_HQReport original,observed;ERR_MortarBattery battery;ERR_MortarSpotReport spot;HQ hq;ERR_HQRoute route;Spotting service;
|
|
Fixture(){original.Target=⌖observed=original;observed.MessageId=2;observed.ObservedAt=2000;
|
|
battery.Gun=&gun;battery.Report=&original;spot.Battery=&battery;spot.Gun=&gun;spot.Target=⌖spot.Observation=&observed;
|
|
spot.Bias={15,0,0};spot.FiredAt=1000;service.m_HQ=&hq;hq.route=&route;}
|
|
void receive(){service.ReceiveHQ(&spot,3000);}
|
|
void deliver(){route.result=1;service.Tick(4000);}
|
|
vector offset(){return service.Offset(&battery,battery.Report,4000);}
|
|
};
|
|
int main(){
|
|
assert(ERR_MortarSpotRules::Fresh(0,45000)&&!ERR_MortarSpotRules::Fresh(0,45001));
|
|
assert(!ERR_MortarSpotRules::Fresh(1,0)&&!ERR_MortarSpotRules::Fresh(-1,0));
|
|
assert(!ERR_MortarSpotRules::Fresh(0,std::numeric_limits<float>::quiet_NaN()));
|
|
vector aim{1000,40,1000},landing{985,10,1000};
|
|
vector correction=ERR_MortarSpotRules::Bias(aim,landing);
|
|
assert(correction[0]==15&&correction[1]==0&&correction[2]==0);
|
|
// After applying +15m, another identical -15m ballistic error stays +15m.
|
|
assert(ERR_MortarSpotRules::Bias(aim+correction,landing+correction)[0]==15);
|
|
assert(ERR_MortarSpotRules::Bias(aim,aim).Length()==0); // deliberate spread is not ballistic error
|
|
assert(std::fabs(ERR_MortarSpotRules::Bias({1000,0,1000},{}).Length()-60)<.001);
|
|
Fixture f;assert(f.offset()[0]==80);f.receive();assert(f.offset()[0]==80);
|
|
f.service.Tick(3500);assert(!f.spot.Delivered&&f.offset()[0]==80); // radio still in flight
|
|
f.deliver();assert(f.spot.Delivered&&f.offset()[0]==23); // 8m spread plus 15m calibration
|
|
f.battery.Released=true;f.service.PrepareAim(&f.battery,4000);assert(f.battery.Report==&f.original);
|
|
f.battery.Released=false;Aim commanded;commanded.SalvoOffset={42,0,1};f.battery.AimSolution=&commanded;
|
|
assert(f.offset()[0]==42);f.service.PrepareAim(&f.battery,4000);assert(f.battery.Report==&f.original);
|
|
f.battery.AimSolution=nullptr;f.service.PrepareAim(&f.battery,4000);assert(f.battery.Report==&f.observed&&f.observed.ObservedAt==2000);
|
|
ERR_MortarSpotReport older=f.spot;older.FiredAt=500;older.Bias={-40,0,0};f.service.m_Reports.Insert(&older);
|
|
assert(f.offset()[0]==23); // older airborne round arrives after newer correction
|
|
f.gun.position={3,0,0};assert(f.offset()[0]==80);f.gun.position={};
|
|
Entity another;f.battery.Report->Target=&another;assert(f.offset()[0]==80);f.battery.Report->Target=&f.target;
|
|
assert(f.service.Offset(&f.battery,f.battery.Report,47001)[0]==80); // age not renewed by delivery
|
|
Fixture lost;lost.receive();lost.route.result=-1;lost.service.Tick(4000);assert(lost.service.m_Reports.Count()==0&&!lost.spot.Delivered&&lost.offset()[0]==80);
|
|
Fixture disconnected;disconnected.hq.route=nullptr;disconnected.receive();assert(disconnected.service.m_Reports.Count()==0);
|
|
Fixture dead;dead.receive();dead.battery.operational=false;dead.deliver();assert(!dead.spot.Delivered&&dead.service.m_Reports.Count()==0);
|
|
Fixture stale;stale.service.ReceiveHQ(&stale.spot,47001);assert(stale.service.m_Reports.Count()==0);
|
|
Fixture bounded;for(int i=0;i<33;i++)bounded.receive();assert(bounded.service.m_Reports.Count()==32);
|
|
Fixture scan;ERR_MortarSpotReport second=scan.spot;scan.spot.ImpactAt=second.ImpactAt=3000;
|
|
scan.service.m_Flying.Insert(&scan.spot);scan.service.m_Flying.Insert(&second);scan.service.Tick(4000);assert(scan.hq.surveys==1);
|
|
scan.service.Tick(9000);assert(scan.service.m_Flying.Count()==0); // splash not observable indefinitely
|
|
}
|
|
''')
|
|
# Wiring and native-boundary guards, distinct from the executed state tests.
|
|
source=p.read_text()
|
|
firing=(r/'ERR_MortarFiring.c').read_text()
|
|
assert firing.index('system.TrackProjectile(')<firing.index('battery.ObserveProjectile(')
|
|
assert 'spot.ShellEntity != source' in body(p,'protected void Explosion(')
|
|
assert 'spot.Splash = matrix[0]' in body(p,'protected void Explosion(')
|
|
assert 'ERR_HQObservation.Capture(target, now)' in source and 'TraceMove(sight, null)' in source
|
|
assert 'GetTargetsList(targets, ETargetCategory.ENEMY)' in source
|
|
assert 'Target.GetOrigin()' not in source
|
|
assert 'SCR_ExplosionAmmoEffect.GetOnExplosionAmmoEffect().Remove(Explosion)' in source
|
|
assert 'system.ShotOffset(battery, report, now)' in firing
|
|
assert 'AcceptMortarSpot(unit.PendingMortarSpot, now)' in (r/'ERR_HQ.c').read_text()
|
|
print('PASS: observed mortar corrections, radio delivery, bounded expiry, aim preservation and out-of-order impacts; native flight untested')
|
|
|
|
# Execute the actual HQ correction intake. The existing memory regression owns
|
|
# velocity estimation; this boundary supplies its resulting fire-support sample.
|
|
intake=cpp(r/'ERR_HQ.c','protected void AcceptMortarSpot(')
|
|
intake=intake.replace('observation.', 'observation->').replace('contact.', 'contact->')
|
|
intake=intake.replace('foreach (ERR_HQReport* contact : m_Contacts)', 'for (ERR_HQReport* contact : m_Contacts)')
|
|
run(r'''#include <cassert>
|
|
#include <vector>
|
|
struct vector {float x=0,y=0,z=0;};
|
|
struct ERR_HQReport {void* Target=nullptr;float ObservedAt=0;vector Position,Velocity;bool HasVelocity=false;ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
|
|
struct ERR_MortarSpotReport {ERR_HQReport* Observation=nullptr;};
|
|
struct ERR_MortarSpotRules {static bool Fresh(float t,float now){return t>=0&&now>=t&&now-t<=45000;}};
|
|
struct ERR_MortarPrediction {static bool Fresh(ERR_HQReport* r,float now){return r->Target&&now>=r->ObservedAt&&now-r->ObservedAt<=60000;}};
|
|
struct Mortars {int calls=0;ERR_HQReport* delivered=nullptr;void ReceiveSpot(ERR_MortarSpotReport* spot,float){calls++;delivered=spot->Observation;}};
|
|
struct HQ {std::vector<ERR_HQReport*> m_Contacts;Mortars m_Mortars;int accepts=0;
|
|
// Represents Accept's output, not a substitute motion-estimation algorithm.
|
|
void Accept(ERR_HQReport*,float){accepts++;}
|
|
'''+intake+r'''};
|
|
int main(){
|
|
int target=1,other=2;ERR_HQReport sighting;sighting.Target=⌖sighting.ObservedAt=1000;sighting.Position={100,0,100};
|
|
ERR_HQReport enriched=sighting;enriched.HasVelocity=true;enriched.Velocity={3,0,-2};
|
|
ERR_MortarSpotReport spot;spot.Observation=&sighting;HQ hq;hq.m_Contacts.push_back(&enriched);
|
|
hq.AcceptMortarSpot(&spot,2000);assert(hq.accepts==1&&hq.m_Mortars.calls==1);
|
|
assert(spot.Observation!=&sighting&&spot.Observation!=&enriched&&spot.Observation->HasVelocity);
|
|
assert(spot.Observation->Velocity.x==3&&spot.Observation->Velocity.z==-2&&spot.Observation->ObservedAt==1000);
|
|
assert(!sighting.HasVelocity); // the captured sighting was not magically given native live speed
|
|
// A more recent delivered sighting carries its OWN position, time and velocity.
|
|
enriched.ObservedAt=1500;enriched.Position={102,0,99};enriched.Velocity={-1,0,4};spot.Observation=&sighting;
|
|
hq.AcceptMortarSpot(&spot,2000);assert(spot.Observation->ObservedAt==1500&&spot.Observation->Position.x==102&&spot.Observation->Velocity.z==4);
|
|
// Insufficient/conflicting history must not preserve an older movement estimate.
|
|
enriched.HasVelocity=false;enriched.Velocity={};spot.Observation=&sighting;
|
|
hq.AcceptMortarSpot(&spot,2000);assert(!spot.Observation->HasVelocity);
|
|
HQ rejected;rejected.m_Contacts.push_back(&enriched);enriched.Target=&other;spot.Observation=&sighting;
|
|
rejected.AcceptMortarSpot(&spot,2000);assert(rejected.m_Mortars.calls==0);
|
|
enriched.Target=⌖enriched.ObservedAt=999;rejected.AcceptMortarSpot(&spot,2000);assert(rejected.m_Mortars.calls==0);
|
|
int before=rejected.accepts;rejected.AcceptMortarSpot(&spot,46001);assert(rejected.accepts==before);
|
|
}
|
|
''')
|
|
print('PASS: splash reports retain HQ-estimated movement with matching position/time; stale, mismatched or unsupported estimates rejected')
|