--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c @@ -7,6 +7,7 @@ protected ERR_ConvoyTask m_MountedInterceptTask; protected ref ERR_HQRoute m_MountedInterceptRoute; protected bool m_MountedInterceptDelivered; + protected int m_MountedSurveyCursor; protected ref array m_MountedAttemptBeliefs = {}; protected ref array m_MountedAttemptRevisions = {}; @@ -140,9 +141,16 @@ array reserved = {}; int first = -1; int second = -1; foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second); // At most two bounded native road queries per normal five-second HQ pass. + // Continue the next pass after the last surveyed track, so unsuitable roads + // for older reports cannot indefinitely hide later delivered tracks. + int count = m_Memory.Count(); + if (count == 0) return; + int start = m_MountedSurveyCursor % count; int queries; - for (int b = 0; b < m_Memory.Count() && queries < 2; b++) + for (int scanned = 0; scanned < count && queries < 2; scanned++) { + int b = (start + scanned) % count; + m_MountedSurveyCursor = (b + 1) % count; ERR_HQBelief belief = m_Memory.At(b); if (!ERR_HQMountedInterceptRules.Eligible(belief, now) || MountedInterceptAttempted(belief.BeliefId, belief.BeliefRevision)) continue; vector destination; queries++; --- a/tests/test_hq_mounted_dispatch.py +++ b/tests/test_hq_mounted_dispatch.py @@ -69,6 +69,7 @@ virtual void ReserveUnit(ERR_HQUnit*,Array&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}}; struct HQ:Base{ ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true; + int m_MountedSurveyCursor=0; Arraym_MountedAttemptBeliefs,m_MountedAttemptRevisions;Arraym_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network; bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;} '''+methods+r''' @@ -103,7 +104,16 @@ for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);} {Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);} {Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);} - {Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);} + {Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);assert(f.h.m_MountedAttemptBeliefs.empty());int after=ERR_HQMountedInterceptRules::selects;f.h.PlanMountedIntercept(8000);assert(ERR_HQMountedInterceptRules::selects-after==1);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.service.reserves==1);} + // Cursor normalization handles track expiry/removal without out-of-range access. + {Fixture f;ERR_HQBelief second;second.BeliefId=2;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second};f.plan();assert(!f.h.m_MountedIntercept);f.h.m_MountedSurveyCursor=31;f.memory.entries={&f.belief};f.belief.select=true;f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==1);} + // Skipped ineligible/already-attempted tracks do not spend the two-road-query budget. + {Fixture f;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.eligible=false;f.h.m_MountedAttemptBeliefs.Insert(2);f.h.m_MountedAttemptRevisions.Insert(second.BeliefRevision);f.memory.entries={&f.belief,&second,&third};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==1&&f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3);} + // No eligible reports and no reports both produce zero native road queries. + {Fixture f;f.belief.eligible=false;int before=ERR_HQMountedInterceptRules::selects;f.plan();f.memory.entries.clear();f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&!f.h.m_MountedIntercept&&f.service.reserves==0);} + // Full bounded memory: every pass has at most two surveys, no failed revision is consumed. + {Fixture f;ERR_HQBelief beliefs[64];f.memory.entries.clear();for(int i=0;i<64;i++){beliefs[i].BeliefId=i+1;beliefs[i].select=false;f.memory.entries.Insert(&beliefs[i]);}for(int pass=0;pass<32;pass++){int before=ERR_HQMountedInterceptRules::selects;float now=3000+pass*5000;f.entry.ObservedAt=f.entry.LastDeliveredAt=now;f.h.PlanMountedIntercept(now);assert(ERR_HQMountedInterceptRules::selects-before==2&&f.h.m_MountedSurveyCursor==((pass+1)*2)%64&&f.h.m_MountedAttemptBeliefs.empty());}assert(!f.h.m_MountedIntercept&&f.service.reserves==0);} + } ''') print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')