--- a/tests/test_hq_watch.py +++ b/tests/test_hq_watch.py @@ -48,6 +48,11 @@ accept=accept.replace('ERR_HQMemoryKind.', 'ERR_HQMemoryKind::').replace('vector.DistanceXZ(', 'vector::DistanceXZ(') accept=accept.replace('previous.', 'previous->').replace('vector.Zero', 'vector{}') accept=accept.replace('string.Format(', 'Format(') +# Execute the real existing HQ uplink delivery branch, with the native route result doubled. +uplink=body(Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')), 'if (unit.Uplink)') +uplink=cpp(uplink).replace('unit.', 'unit->') +for field in ('Uplink','PendingSweep','Task'): + uplink=uplink.replace('unit->'+field+'.','unit->'+field+'->') run(r''' #include #include @@ -119,7 +124,40 @@ Arraym_Contacts; '''+accept+r''' }; +struct SweepWire {int BeliefId=0,BeliefRevision=0,WatchMessageId=0,WatchRequestId=0;string WatchSourceId;bool SweepQueued=false;}; +struct RouteWire {int result=0,calls=0;int Advance(float,JournalAdapter&){calls++;return result;}}; +struct StatusUnit {RouteWire* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQReport* PendingReport=nullptr;SweepWire *PendingSweep=nullptr,*Task=nullptr;void* PendingSupplyStatus=nullptr;}; +struct RadioIntake {Watch m_Watch;JournalAdapter m_Journal;struct Logistics {void Receive(void*,float){assert(false);}}m_Logistics; + void AcceptSweep(SweepWire*,float){assert(false);}void Accept(ERR_HQReport*,float){assert(false);} + void Deliver(StatusUnit* unit,float now) { +'''+uplink+r''' +} +}; int main(){ + // Physical mine evidence is detached at sampling, then copied again only on delivered radio. + ERR_HQAmmoSnapshot original;original.Complete=true;original.Members=4; + original.MineComplete=true;original.MineMembers=4;original.ReadyMines=3; + ERR_HQStatusReport mineWire;mineWire.AmmoState=original.Snapshot(); + original.ReadyMines=0;original.MineComplete=false;original.MineMembers=0; + assert(mineWire.AmmoState!=&original&&mineWire.AmmoState->ReadyMines==3&&mineWire.AmmoState->MineComplete&&mineWire.AmmoState->MineMembers==4); + RadioIntake radio;StatusUnit sender;RouteWire route;sender.Uplink=&route;sender.PendingStatus=&mineWire; + radio.Deliver(&sender,1000);assert(!radio.m_Watch.Count()&&sender.PendingStatus==&mineWire); // still travelling + route.result=-1;radio.Deliver(&sender,2000);assert(!radio.m_Watch.Count()&&!sender.PendingStatus&&!sender.Uplink); // interrupted route cannot deliver + sender.Uplink=&route;sender.PendingStatus=&mineWire;route.result=1;radio.Deliver(&sender,3000); + auto mines=radio.m_Watch.Find(mineWire.SourceId);assert(mines&&mines->AmmoState!=mineWire.AmmoState&&mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete&&mines->AmmoState->MineMembers==4); + mineWire.AmmoState->ReadyMines=0;mineWire.AmmoState->MineComplete=false;assert(mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete); + // Relabelled stale observations and old radio arrivals cannot overwrite the latest delivered evidence. + mineWire.MessageId=2;sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,4000); + assert(mines->MessageId==1&&mines->AmmoState->ReadyMines==3&&!sender.PendingStatus); + mineWire.ObservedAt=4000;sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,124001); + assert(mines->MessageId==1&&mines->AmmoState->MineComplete); + sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,5000); + assert(mines->MessageId==2&&!mines->AmmoState->MineComplete&&mines->AmmoState->ReadyMines==0&&mines->AmmoState->MineMembers==4); + mineWire.MessageId=3;mineWire.ObservedAt=5000;mineWire.AmmoState->MineComplete=true;mineWire.AmmoState->MineMembers=3; + assert(!radio.m_Watch.Ingest(&mineWire,6000)&&mines->MessageId==2); // false full-roster claim + mineWire.AmmoState->MineMembers=4;mineWire.AmmoState->ReadyMines=513; + assert(!radio.m_Watch.Ingest(&mineWire,6000)&&mines->MessageId==2); // impossible finite count + mineWire.AmmoState->ReadyMines=0;assert(radio.m_Watch.Ingest(&mineWire,6000)&&mines->AmmoState->MineComplete&&mines->AmmoState->ReadyMines==0); // Mounted payload obeys the same delivered chronology and is deep copied. Watch mountedWatch;ERR_HQStatusReport mountedReport;ERR_HQMountedAmmoSnapshot mountedState; mountedState.Complete=true;mountedState.Weapons=1;mountedState.Muzzles=1;mountedState.Magazines=1; @@ -314,4 +352,4 @@ '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, detached/validated mounted payload replacement and 256-record cap') +print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence, detached/validated mounted payload replacement 256-record cap and detached carried-mine radio delivery/chronology') --- a/tests/test_hq_ammo_reporting.py +++ b/tests/test_hq_ammo_reporting.py @@ -4,8 +4,8 @@ from policy_harness import ROOT,body,run BASE=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT',ROOT)) P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c' -H=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c' -A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c' +H=Path(os.environ.get('ERR_HQ_SOURCE',BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')) +A=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c')) M=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c' def cpp(s): @@ -32,6 +32,7 @@ templatestring Format(T...){return "sample";} struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v)&&v>-1e20&&v<1e20;}}; struct ERR_HQAmmoSnapshot{ + bool MineComplete=false;int MineMembers=0,ReadyMines=0; bool Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0; int LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0; '''+body(A,' bool Valid(')+r''' @@ -64,6 +65,18 @@ u.Uplink=nullptr;u.PendingStatus=nullptr;u.contact=false;} }; int main(){ + // Mine scarcity/completeness alone never consumes the early status slot. + for(int state=0;state<3;state++){ + HQ onlyMines;ERR_HQUnit squad;squad.NextStatus=180000;squad.StatusSampler.stock.MineMembers=4; + squad.StatusSampler.stock.ReadyMines=state==2?2:0;squad.StatusSampler.stock.MineComplete=state!=1; + onlyMines.QueueReports(&squad,0);assert(squad.contact&&!squad.PendingStatus&&onlyMines.m_Journal.writes==0); + onlyMines.Finish(squad);squad.visible=false;squad.NextStatus=1000;onlyMines.QueueReports(&squad,1000); + assert(squad.PendingStatus&&squad.PendingStatus->AmmoState->ReadyMines==(state==2?2:0)&&squad.PendingStatus->AmmoState->MineComplete==(state!=1)&&squad.PendingStatus->AmmoState->MineMembers==4); + assert(squad.Uplink->Payload.find("; carried mines=")!=string::npos); + if(state==1)assert(squad.Uplink->Payload.find("carried mines=unknown/incomplete")!=string::npos); + auto sent=squad.PendingStatus->AmmoState;squad.StatusSampler.stock.ReadyMines=77;squad.StatusSampler.stock.MineMembers=0;squad.StatusSampler.stock.MineComplete=!sent->MineComplete; + assert(sent->ReadyMines==(state==2?2:0)&&sent->MineComplete==(state!=1)&&sent->MineMembers==4); + } // Exact inclusive threshold counts chambers, ignores launcher stock, requires the complete current living roster. ERR_HQAmmoReporting p; p.Initialize(6,0);ERR_HQStatusReport r;ERR_HQAmmoSnapshot a;r.AmmoState=&a; assert(!p.Urgent(&r,0));a.SmallArmsRounds=120;assert(p.Urgent(&r,0)); @@ -138,4 +151,4 @@ assert(status==10&&contacts==90&&firing.StatusSampler.samples<=21); } ''') -print('PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures and requested check-ins') +print('PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures requested check-ins and non-urgent detached carried-mine status')