Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
288 lines
30 KiB
Diff
288 lines
30 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c
|
|
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c
|
|
@@ -5,6 +5,7 @@
|
|
ERR_ConvoyTask Task;
|
|
ERR_ConvoyVehicle Escort;
|
|
ref ERR_HQRoute Route;
|
|
+ ref ERR_InterceptVehicleFootprint Footprint;
|
|
vector Destination;
|
|
float Distance;
|
|
}
|
|
@@ -34,6 +35,7 @@
|
|
if (!m_MountedIntercept) return result;
|
|
string phase = "queued";
|
|
if (m_MountedInterceptDelivered) phase = "delivered";
|
|
+ if (m_MountedIntercept.Armored) result += "\nOriginal BMP singleton; delivered confirmed losses " + m_MountedIntercept.ConfirmedLosses.ToString();
|
|
if (m_MountedInterceptPartner) result += "\nPaired mounted interception (two original escorts)";
|
|
return result + "\n" + string.Format("Mounted interception: escort %1; %2; report M%3; road %4", m_MountedIntercept.VehicleIndex, phase, m_MountedIntercept.ReportId, m_MountedIntercept.Position);
|
|
}
|
|
@@ -103,6 +105,7 @@
|
|
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
|
{
|
|
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) return false;
|
|
+ if (order.Armored && (m_Watch.ConfirmedLosses() < 6 || order.ConfirmedLosses < 6)) return false;
|
|
if (unit.LogisticsReservation != order.Token || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)) return false;
|
|
return m_Convoys && m_Convoys.InterceptCurrent(m_MountedInterceptTask, order);
|
|
}
|
|
@@ -151,6 +154,9 @@
|
|
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
|
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
|
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
|
+ // Radio delivery may synchronously change the current mission knowledge.
|
|
+ if (order.Armored && (m_Watch.ConfirmedLosses() < 6 || partner))
|
|
+ { ReleaseMountedIntercept(now, "armored escalation no longer authorized"); return; }
|
|
bool applied;
|
|
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
|
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
|
@@ -171,6 +177,42 @@
|
|
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
|
}
|
|
return true;
|
|
+ }
|
|
+ protected bool MountedArmoredOriginal(ERR_ConvoyTask task, ERR_ConvoyVehicle vehicle)
|
|
+ {
|
|
+ if (!task || task.Vehicles.Count() != 6 || !task.Receipt || !task.Receipt.Consumed
|
|
+ || task.Receipt.Records.Count() <= 4 || task.Vehicles[4] != vehicle || !vehicle || vehicle.Index != 4
|
|
+ || !vehicle.Entity || !vehicle.Entity.GetPrefabData() || !m_Mode.Forces()) return false;
|
|
+ if (vehicle.Entity.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.VehiclePrefab(4)) return false;
|
|
+ ERR_ForceRecord record = m_Mode.Forces().FindGroup(vehicle.Group);
|
|
+ return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
|
|
+ && record.Id == task.Receipt.Records[4] && record.Group == vehicle.Group
|
|
+ && record.SourceId == task.Receipt.SourceId + "-vehicle-4";
|
|
+ }
|
|
+ protected ERR_HQMountedChoice MountedArmoredCandidate(float now)
|
|
+ {
|
|
+ // Cumulative delivered/reconciled original losses only, never a physical scan.
|
|
+ if (m_Watch.ConfirmedLosses() < 6) return null;
|
|
+ for (int t = 0; t < m_Convoys.Count(); t++)
|
|
+ {
|
|
+ ERR_ConvoyTask task = m_Convoys.At(t);
|
|
+ if (!MountedInterceptTaskAvailable(task)) continue;
|
|
+ ERR_ConvoyVehicle vehicle = task.Vehicles[4];
|
|
+ if (!MountedArmoredOriginal(task, vehicle) || vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
|
+ foreach (ERR_HQUnit unit : m_Units)
|
|
+ {
|
|
+ if (unit.Group != vehicle.Group || unit.ForceSourceId != task.Receipt.SourceId + "-vehicle-4" || !MountedLogisticsEligible(unit)
|
|
+ || ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
|
+ ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
|
+ if (!route) continue;
|
|
+ ref ERR_InterceptVehicleFootprint footprint = ERR_InterceptVehicleFootprint.Capture(vehicle.Entity);
|
|
+ if (!footprint || !footprint.Valid() || !footprint.Matches(vehicle.Entity)) continue;
|
|
+ ref ERR_HQMountedChoice choice = new ERR_HQMountedChoice();
|
|
+ choice.Unit = unit; choice.Task = task; choice.Escort = vehicle; choice.Route = route; choice.Footprint = footprint;
|
|
+ return choice;
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
}
|
|
protected bool HasMountedInterceptCandidate(float now)
|
|
{
|
|
@@ -244,6 +286,8 @@
|
|
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
|
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
|
order.VehicleIndex = choice.Escort.Index; order.Position = choice.Destination;
|
|
+ if (choice.Escort.Index == 4)
|
|
+ { order.Armored = true; order.Footprint = choice.Footprint; order.ConfirmedLosses = m_Watch.ConfirmedLosses(); }
|
|
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
|
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
|
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
|
@@ -251,6 +295,8 @@
|
|
}
|
|
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
|
{
|
|
+ if (choice && choice.Escort.Index == 4 && (partnerChoice || m_Watch.ConfirmedLosses() < 6
|
|
+ || !choice.Footprint || !choice.Footprint.Valid() || !choice.Footprint.Matches(choice.Escort.Entity))) return;
|
|
int members = 1;
|
|
if (partnerChoice) members = 2;
|
|
if (m_MountedTokenCount + members > 256) return;
|
|
@@ -290,7 +336,10 @@
|
|
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
|
int freeGlobal = globalLimit - Assigned();
|
|
int freeHunt = ERR_HQPursuitRules.Limit(globalLimit) - HuntingAssigned();
|
|
- if (freeGlobal < 1 || freeHunt < 1 || !HasMountedInterceptCandidate(now)) return;
|
|
+ if (freeGlobal < 1 || freeHunt < 1) return;
|
|
+ ref ERR_HQMountedChoice armored = MountedArmoredCandidate(now);
|
|
+ bool escortPossible = HasMountedInterceptCandidate(now);
|
|
+ if (!armored && !escortPossible) return;
|
|
bool pairPossible = freeGlobal >= 2 && freeHunt >= 2 && m_MountedTokenCount <= 254 && HasMountedPairCandidate(now);
|
|
array<vector> reserved = {}; int first = -1; int second = -1;
|
|
foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second);
|
|
@@ -304,7 +353,25 @@
|
|
m_MountedSurveyCursor = (b + 1) % count;
|
|
ERR_HQBelief belief = m_Memory.At(b);
|
|
if (!ERR_HQMountedInterceptRules.Eligible(belief, now) || MountedInterceptAttempted(belief.BeliefId, belief.BeliefRevision)) continue;
|
|
- if (pairPossible)
|
|
+ if (armored && m_Watch.ConfirmedLosses() >= 6)
|
|
+ {
|
|
+ vector armoredPosition; queries++;
|
|
+ if (ERR_HQMountedInterceptRules.SelectArmored(belief, now, reserved, armored.Footprint, armoredPosition))
|
|
+ {
|
|
+ ERR_HQWatchEntry armoredStatus = m_Watch.Find(armored.Unit.ForceSourceId);
|
|
+ if (!armoredStatus || !MountedInterceptTaskAvailable(armored.Task)
|
|
+ || !MountedArmoredOriginal(armored.Task, armored.Escort)
|
|
+ || !MountedInterceptStatus(armored.Unit, armored.Escort, now)
|
|
+ || !armored.Footprint.Matches(armored.Escort.Entity)) continue;
|
|
+ float armoredDistance = vector.DistanceXZ(armoredStatus.Position, armoredPosition);
|
|
+ if (ERR_ForceRules.Finite(armoredDistance) && armoredDistance <= m_Mode.Settings().Runtime("responseRadiusMetres"))
|
|
+ {
|
|
+ armored.Destination = armoredPosition; armored.Distance = armoredDistance;
|
|
+ QueueMountedIntercept(armored, null, belief, now); return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (pairPossible && queries < 2)
|
|
{
|
|
vector positionA; vector positionB; queries++;
|
|
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
|
@@ -331,7 +398,7 @@
|
|
}
|
|
}
|
|
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
|
- if (queries >= 2) continue;
|
|
+ if (queries >= 2 || !escortPossible) continue;
|
|
vector destination; queries++;
|
|
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
|
ref ERR_HQMountedChoice best;
|
|
--- a/tests/test_hq_mounted_dispatch.py
|
|
+++ b/tests/test_hq_mounted_dispatch.py
|
|
@@ -3,19 +3,21 @@
|
|
from pathlib import Path
|
|
import os,re
|
|
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
|
-names=['protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
|
+names=['protected bool MountedArmoredOriginal(', 'protected ERR_HQMountedChoice MountedArmoredCandidate(', 'protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
|
def cpp(s):
|
|
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
|
- for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice']:
|
|
+ for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice','ERR_InterceptVehicleFootprint','ERR_ForceRecord']:
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
|
- for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string']:
|
|
+ for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string','ERR_InterceptVehicleFootprint','ERR_ConvoyResources','ERR_ForceKind']:
|
|
s=s.replace(typ+'.',typ+'::')
|
|
- for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute']:
|
|
+ for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute','footprint','armored','armoredStatus','record']:
|
|
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
|
- for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt']:
|
|
+ for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt','Footprint','Entity']:
|
|
s=s.replace('->'+name+'.','->'+name+'->')
|
|
- s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
|
|
+ s=s.replace('choice->Escort.Entity','choice->Escort->Entity').replace('armored->Unit.ForceSourceId','armored->Unit->ForceSourceId').replace('armored->Escort.Entity','armored->Escort->Entity')
|
|
+ s=s.replace('m_MountedIntercept->ConfirmedLosses.ToString()', 'std::to_string(m_MountedIntercept->ConfirmedLosses)')
|
|
+ s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->').replace('Forces().','Forces()->').replace('GetPrefabData().','GetPrefabData()->')
|
|
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
|
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
|
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
|
@@ -35,22 +37,28 @@
|
|
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);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
|
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
|
-struct Group{};struct Entity{};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
|
+struct Group{};struct Prefab{string name="bmp";string GetPrefabName(){return name;}};
|
|
+struct Entity{bool footprintValid=true,prefabPresent=true;Prefab prefab;Prefab*GetPrefabData(){return prefabPresent?&prefab:nullptr;}};
|
|
+struct ERR_ConvoyResources{static string VehiclePrefab(int){return "bmp";}};
|
|
+enum class ERR_ForceKind{CONVOY,GARRISON};
|
|
+struct ERR_ForceRecord{struct Group*Group=nullptr;bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=14;string SourceId="convoy-vehicle-4";};
|
|
+struct Ledger{Array<ERR_ForceRecord*>records;ERR_ForceRecord*FindGroup(Group*g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
|
|
+struct ERR_InterceptVehicleFootprint{Entity*root=nullptr;bool Valid(){return root&&root->footprintValid;}bool Matches(Entity*e){return Valid()&&root==e;}static ERR_InterceptVehicleFootprint*Capture(Entity*e){if(!e||!e->footprintValid)return nullptr;auto*p=new ERR_InterceptVehicleFootprint();p->root=e;return p;}};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
|
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
|
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
|
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
|
-struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";};
|
|
+struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";bool Consumed=true;Array<int>Records={10,11,12,13,14,15};};
|
|
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
|
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
|
-struct ERR_ConvoyInterceptOrder{int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
|
+struct ERR_ConvoyInterceptOrder{bool Armored=false;int ConfirmedLosses=0;ERR_InterceptVehicleFootprint*Footprint=nullptr;int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
|
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
|
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
|
-struct ERR_HQRoute{int result=0,calls=0,MessageId=0;string Payload;int Advance(float,Journal*){calls++;return result;}};
|
|
-struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:nullptr);}};
|
|
+struct ERR_HQRoute{int result=0,calls=0,MessageId=0;std::function<void()>onAdvance;string Payload;int Advance(float,Journal*){calls++;if(onAdvance)onAdvance();return result;}};
|
|
+struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr,*third=nullptr;int losses=0;int ConfirmedLosses(){return losses;}int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:(third&&third->SourceId==id?third:nullptr));}};
|
|
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
|
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
|
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
|
-struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
|
|
+struct Mode{Ledger ledger;Ledger*Forces(){return &ledger;}struct Settings settings;struct Settings*Settings(){return &settings;}};
|
|
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
|
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
|
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
|
@@ -64,14 +72,15 @@
|
|
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
|
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
|
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
|
-struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
|
+struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0,armoredSelects=0;inline static bool armoredGeometry=true;inline static std::function<void()>onArmoredSelect;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
|
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
|
+ static bool SelectArmored(ERR_HQBelief*b,float,Array<vector>&reserved,ERR_InterceptVehicleFootprint*p,vector&dest){armoredSelects++;dest=b->destination;if(onArmoredSelect)onArmoredSelect();return armoredGeometry&&b->select&&p&&p->Valid()&&!reserved.Contains(dest);}
|
|
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
|
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
|
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
|
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
|
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
|
-struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;vector Destination;float Distance=0;};
|
|
+struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;ERR_InterceptVehicleFootprint*Footprint=nullptr;vector Destination;float Distance=0;};
|
|
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;
|
|
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
|
@@ -81,8 +90,9 @@
|
|
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'''
|
|
};
|
|
-struct Fixture{Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
|
+struct Fixture{ERR_ForceRecord record;Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
|
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
|
+ void armor(){vehicle.Index=4;task.Vehicles[0]=&unused;task.Vehicles[4]=&vehicle;unit.ForceSourceId=entry.SourceId=receipt.SourceId+"-vehicle-4";record.SourceId=unit.ForceSourceId;record.Group=&group;mode.ledger.records.Insert(&record);}
|
|
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
|
};
|
|
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
|
@@ -159,6 +169,47 @@
|
|
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
|
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
|
|
|
+ // Ordinary pairs remain preferred below the delivered armor threshold, even with a ready BMP.
|
|
+ {PairFixture f;f.watch.losses=5;int before=ERR_HQMountedInterceptRules::armoredSelects;f.pair();assert(ERR_HQMountedInterceptRules::armoredSelects==before&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);}
|
|
+ // Armor is a singleton alternative requiring six delivered losses; no survey at five.
|
|
+ {Fixture f;f.armor();f.watch.losses=5;
|
|
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);
|
|
+ f.watch.losses=6;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->Armored&&f.h.m_MountedIntercept->VehicleIndex==4&&f.h.m_MountedIntercept->ConfirmedLosses==6);
|
|
+ assert(!f.h.m_MountedInterceptPartner&&f.h.Assigned()==1&&f.h.m_MountedTokenCount==1&&f.service.reserves==1&&f.service.pairReserves==0&&f.service.applies==0);
|
|
+ assert(f.h.m_MountedIntercept->Footprint->Matches(&f.entity));f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
|
+ // Current delivered threshold, real radio, exact slot, complete ammo and task locks precede armor geometry.
|
|
+ for(int kind=0;kind<8;kind++){Fixture f;f.armor();f.watch.losses=6;
|
|
+ if(kind==0){f.ammo.Complete=false;}if(kind==1){f.entity.footprintValid=false;}if(kind==2){f.network.denied=&f.radio;}if(kind==3){f.task.Vehicles[4]=&f.unused;}
|
|
+ if(kind==4){f.unused.SupplyToken=4;}if(kind==5){f.entry.LastDeliveredAt=9000;}if(kind==6){f.entry.Fit--;}if(kind==7){f.vehicle.Released=true;}
|
|
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);}
|
|
+ // A changed loss ledger even inside radio delivery cancels without a physical order.
|
|
+ for(int kind=0;kind<2;kind++){Fixture f;f.armor();f.watch.losses=6;f.queued();f.route.result=1;
|
|
+ if(kind==0)f.watch.losses=5;else f.route.onAdvance=[&]{f.watch.losses=5;};f.h.TickMountedIntercept(4000);assert(f.service.applies==0&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
|
+ // Failed BMP survey can fall back to a UAZ pair, but never exceeds the shared two queries.
|
|
+ {PairFixture f,armor;armor.receipt.SourceId="second-convoy";armor.armor();
|
|
+ f.mode.ledger.records.Insert(&armor.record);f.watch.third=&armor.entry;f.watch.losses=6;f.service.tasks.Insert(&armor.task);f.h.m_Units.Insert(&armor.unit);
|
|
+ ERR_HQMountedInterceptRules::armoredGeometry=false;int a=ERR_HQMountedInterceptRules::armoredSelects,p=ERR_HQMountedInterceptRules::pairSelects,q=ERR_HQMountedInterceptRules::selects;
|
|
+ f.plan();assert(f.h.m_MountedInterceptPartner&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);
|
|
+ assert(ERR_HQMountedInterceptRules::armoredSelects-a==1&&ERR_HQMountedInterceptRules::pairSelects-p==1&&ERR_HQMountedInterceptRules::selects==q);ERR_HQMountedInterceptRules::armoredGeometry=true;}
|
|
+ // No UAZ is surveyed on an armor-only failed pass; rotating tracks retain the same two-query cap.
|
|
+ {Fixture f;f.armor();f.watch.losses=6;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second,&third};
|
|
+ int a=ERR_HQMountedInterceptRules::armoredSelects,q=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::armoredSelects-a==2&&ERR_HQMountedInterceptRules::selects==q&&!f.h.m_MountedIntercept);
|
|
+ f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.h.m_MountedTokenCount==1);}
|
|
+
|
|
+ // Same global token namespace and conditional cleanup apply to the armored singleton.
|
|
+ {Fixture f;f.armor();f.watch.losses=6;f.h.m_MountedTokenCount=255;f.plan();assert(f.h.m_MountedIntercept->Token==400256&&!f.h.m_MountedInterceptPartner);f.h.Stop();assert(f.service.releases==1&&!f.unit.LogisticsReservation);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==1);}
|
|
+ {Fixture f;f.armor();f.watch.losses=6;f.queued();f.unit.LogisticsReservation=991;f.h.Stop();assert(f.service.releases==1&&f.unit.LogisticsReservation==991&&!f.h.m_MountedIntercept);}
|
|
+
|
|
+ for(int kind=0;kind<5;kind++){Fixture f,later;f.armor();later.receipt.SourceId="later";later.armor();f.watch.losses=6;
|
|
+ if(kind==0){f.entity.prefab.name="foreign";}if(kind==1){f.receipt.Consumed=false;}if(kind==2){f.receipt.Records[4]++;}if(kind==3){f.record.SourceId="wrong";}if(kind==4){f.record.Kind=ERR_ForceKind::GARRISON;}
|
|
+ f.mode.ledger.records.Insert(&later.record);f.watch.second=&later.entry;f.service.tasks.Insert(&later.task);f.h.m_Units.Insert(&later.unit);
|
|
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&later.unit&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==1&&ERR_HQMountedInterceptRules::armoredSelects-before==1);}
|
|
+
|
|
+ // Native geometry may invalidate the previously usable original; nothing is claimed afterward.
|
|
+ for(int kind=0;kind<6;kind++){Fixture f;f.armor();f.watch.losses=6;
|
|
+ ERR_HQMountedInterceptRules::onArmoredSelect=[&]{if(kind==0){f.watch.entry=nullptr;}if(kind==1){f.ammo.Complete=false;}if(kind==2){f.receipt.Consumed=false;}if(kind==3){f.entity.prefab.name="foreign";}if(kind==4){f.entity.footprintValid=false;}if(kind==5){f.unused.SupplyHeld=true;}};
|
|
+ f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==0);ERR_HQMountedInterceptRules::onArmoredSelect=nullptr;}
|
|
+
|
|
}
|
|
''')
|
|
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|