Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
+353
@@ -0,0 +1,353 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Vehicle;
|
||||
ref ERR_HQRoute Route;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
|
||||
// One or two original escorts may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedInterceptPartner;
|
||||
protected ERR_HQUnit m_MountedInterceptPartnerUnit;
|
||||
protected ref ERR_HQRoute m_MountedInterceptPartnerRoute;
|
||||
protected bool m_MountedInterceptReceipt;
|
||||
protected bool m_MountedInterceptPartnerReceipt;
|
||||
protected int m_MountedTokenCount;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
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);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
if (m_MountedInterceptPartner && unit == m_MountedInterceptPartnerUnit) reserved.Insert(m_MountedInterceptPartner.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_HQUnit partnerUnit = m_MountedInterceptPartnerUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach BOTH first: physical release may synchronously unregister a group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptPartner = null; m_MountedInterceptPartnerUnit = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptPartnerRoute = null;
|
||||
m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false; m_MountedInterceptPartnerReceipt = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (partner && m_Convoys) m_Convoys.ReleaseIntercept(task, partner, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
if (partner && partnerUnit && partnerUnit.LogisticsReservation == partner.Token) partnerUnit.LogisticsReservation = partner.Token;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && (unit == m_MountedInterceptUnit || unit == m_MountedInterceptPartnerUnit)) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) 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);
|
||||
}
|
||||
protected bool MountedMemberReady(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!MountedMemberCurrent(unit, order)) return false;
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
return MountedInterceptStatus(unit, vehicle, now) && !ConvoyContact(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !MountedMemberCurrent(m_MountedInterceptUnit, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (partner && !MountedMemberCurrent(m_MountedInterceptPartnerUnit, partner))
|
||||
{ ReleaseMountedIntercept(now, "partner ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "partner command expired"); return; }
|
||||
if (!m_MountedInterceptReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "missing radio route"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order) return;
|
||||
if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
if (delivered > 0) { m_MountedInterceptReceipt = true; }
|
||||
}
|
||||
if (partner && !m_MountedInterceptPartnerReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptPartnerRoute) { ReleaseMountedIntercept(now, "missing partner route"); return; }
|
||||
int partnerDelivered = m_MountedInterceptPartnerRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (partnerDelivered < 0) { ReleaseMountedIntercept(now, "partner radio interrupted"); return; }
|
||||
if (partnerDelivered > 0) { m_MountedInterceptPartnerReceipt = true; }
|
||||
}
|
||||
if (!m_MountedInterceptReceipt || (partner && !m_MountedInterceptPartnerReceipt)) return;
|
||||
if (!MountedMemberReady(m_MountedInterceptUnit, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!applied) { ReleaseMountedIntercept(now, "physical command refused"); return; }
|
||||
m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escorts ordered to remembered road positions", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedChoice(ERR_ConvoyTask task, vector destination, ERR_HQUnit excluded, float now, ERR_ConvoyVehicle required = null)
|
||||
{
|
||||
if (!MountedInterceptTaskAvailable(task)) return null;
|
||||
ref ERR_HQMountedChoice best;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (required && vehicle != required) continue;
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit == excluded || unit.Group != vehicle.Group || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || (best && distance >= best.Distance)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
best = new ERR_HQMountedChoice();
|
||||
best.Unit = unit; best.Task = task; best.Vehicle = vehicle; best.Route = route;
|
||||
best.Destination = destination; best.Distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
protected bool HasMountedPairCandidate(float now)
|
||||
{
|
||||
// Cheap original/status gate; no geometry or claims before both roles exist.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_HQUnit firstUnit;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
if (firstUnit && firstUnit != unit && firstUnit.Group != unit.Group) return true;
|
||||
firstUnit = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_ConvoyInterceptOrder MountedOrder(ERR_HQMountedChoice choice, ERR_HQBelief belief, float now, int token)
|
||||
{
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Vehicle.Index; order.Position = choice.Destination;
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
ref ERR_ConvoyInterceptOrder order = MountedOrder(choice, belief, now, 400001 + m_MountedTokenCount);
|
||||
ref ERR_ConvoyInterceptOrder partner;
|
||||
if (partnerChoice) partner = MountedOrder(partnerChoice, belief, now, 400002 + m_MountedTokenCount);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) return;
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now)) return;
|
||||
// IDs and this revision remain spent after refusal; no post-claim fallback.
|
||||
m_MountedTokenCount += members;
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
choice.Unit.LogisticsReservation = order.Token;
|
||||
if (partnerChoice) partnerChoice.Unit.LogisticsReservation = partner.Token;
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = choice.Unit; m_MountedInterceptTask = choice.Task;
|
||||
m_MountedInterceptRoute = choice.Route; m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false;
|
||||
m_MountedInterceptPartner = partner; m_MountedInterceptPartnerReceipt = false;
|
||||
if (partnerChoice) { m_MountedInterceptPartnerUnit = partnerChoice.Unit; m_MountedInterceptPartnerRoute = partnerChoice.Route; }
|
||||
bool admitted;
|
||||
if (partner) admitted = m_Convoys.ReserveInterceptPair(choice.Task, order, partner, now);
|
||||
else admitted = m_Convoys.ReserveIntercept(choice.Task, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!admitted) { ReleaseMountedIntercept(now, "reservation refused"); return; }
|
||||
choice.Route.MessageId = m_Journal.Message();
|
||||
choice.Route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, order.Position, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", choice.Route.Payload, choice.Route.MessageId);
|
||||
if (partnerChoice)
|
||||
{
|
||||
partnerChoice.Route.MessageId = m_Journal.Message();
|
||||
partnerChoice.Route.Payload = string.Format("original partner %1 to road %2; report M%3", partner.VehicleIndex, partner.Position, partner.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", partnerChoice.Route.Payload, partnerChoice.Route.MessageId);
|
||||
}
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept
|
||||
|| m_MountedAttemptBeliefs.Count() >= 256 || m_MountedTokenCount >= 256) return;
|
||||
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;
|
||||
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);
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
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;
|
||||
if (pairPossible)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
ref ERR_HQMountedChoice bestA; ref ERR_HQMountedChoice bestB;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
// Enumerate the bounded original escorts: a greedy nearest first crew
|
||||
// can be the only crew within range of the other road position.
|
||||
foreach (ERR_ConvoyVehicle firstVehicle : task.Vehicles)
|
||||
{
|
||||
if (!firstVehicle || (firstVehicle.Index != 0 && firstVehicle.Index != 3 && firstVehicle.Index != 5)) continue;
|
||||
ref ERR_HQMountedChoice candidateA = MountedChoice(task, positionA, null, now, firstVehicle);
|
||||
if (!candidateA) continue;
|
||||
ref ERR_HQMountedChoice candidateB = MountedChoice(task, positionB, candidateA.Unit, now);
|
||||
if (!candidateB || candidateA.Unit.Group == candidateB.Unit.Group || candidateA.Route == candidateB.Route) continue;
|
||||
if (bestA && candidateA.Distance + candidateB.Distance >= bestA.Distance + bestB.Distance) continue;
|
||||
bestA = candidateA; bestB = candidateB;
|
||||
}
|
||||
}
|
||||
if (bestA) { QueueMountedIntercept(bestA, bestB, belief, now); return; }
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
if (queries >= 2) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ref ERR_HQMountedChoice candidate = MountedChoice(m_Convoys.At(t), destination, null, now);
|
||||
if (candidate && (!best || candidate.Distance < best.Distance)) best = candidate;
|
||||
}
|
||||
if (best) { QueueMountedIntercept(best, null, belief, now); return; }
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Vehicle;
|
||||
ref ERR_HQRoute Route;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
|
||||
// One or two original escorts may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedInterceptPartner;
|
||||
protected ERR_HQUnit m_MountedInterceptPartnerUnit;
|
||||
protected ref ERR_HQRoute m_MountedInterceptPartnerRoute;
|
||||
protected bool m_MountedInterceptReceipt;
|
||||
protected bool m_MountedInterceptPartnerReceipt;
|
||||
protected int m_MountedTokenCount;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
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);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
// mutation: partner not counted
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
// mutation: partner not counted
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
if (m_MountedInterceptPartner && unit == m_MountedInterceptPartnerUnit) reserved.Insert(m_MountedInterceptPartner.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_HQUnit partnerUnit = m_MountedInterceptPartnerUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach BOTH first: physical release may synchronously unregister a group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptPartner = null; m_MountedInterceptPartnerUnit = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptPartnerRoute = null;
|
||||
m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false; m_MountedInterceptPartnerReceipt = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (partner && m_Convoys) m_Convoys.ReleaseIntercept(task, partner, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
if (partner && partnerUnit && partnerUnit.LogisticsReservation == partner.Token) partnerUnit.LogisticsReservation = 0;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && (unit == m_MountedInterceptUnit || unit == m_MountedInterceptPartnerUnit)) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) 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);
|
||||
}
|
||||
protected bool MountedMemberReady(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!MountedMemberCurrent(unit, order)) return false;
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
return MountedInterceptStatus(unit, vehicle, now) && !ConvoyContact(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !MountedMemberCurrent(m_MountedInterceptUnit, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (partner && !MountedMemberCurrent(m_MountedInterceptPartnerUnit, partner))
|
||||
{ ReleaseMountedIntercept(now, "partner ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "partner command expired"); return; }
|
||||
if (!m_MountedInterceptReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "missing radio route"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order) return;
|
||||
if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
if (delivered > 0) { m_MountedInterceptReceipt = true; }
|
||||
}
|
||||
if (partner && !m_MountedInterceptPartnerReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptPartnerRoute) { ReleaseMountedIntercept(now, "missing partner route"); return; }
|
||||
int partnerDelivered = m_MountedInterceptPartnerRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (partnerDelivered < 0) { ReleaseMountedIntercept(now, "partner radio interrupted"); return; }
|
||||
if (partnerDelivered > 0) { m_MountedInterceptPartnerReceipt = true; }
|
||||
}
|
||||
if (!m_MountedInterceptReceipt || (partner && !m_MountedInterceptPartnerReceipt)) return;
|
||||
if (!MountedMemberReady(m_MountedInterceptUnit, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!applied) { ReleaseMountedIntercept(now, "physical command refused"); return; }
|
||||
m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escorts ordered to remembered road positions", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedChoice(ERR_ConvoyTask task, vector destination, ERR_HQUnit excluded, float now, ERR_ConvoyVehicle required = null)
|
||||
{
|
||||
if (!MountedInterceptTaskAvailable(task)) return null;
|
||||
ref ERR_HQMountedChoice best;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (required && vehicle != required) continue;
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit == excluded || unit.Group != vehicle.Group || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || (best && distance >= best.Distance)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
best = new ERR_HQMountedChoice();
|
||||
best.Unit = unit; best.Task = task; best.Vehicle = vehicle; best.Route = route;
|
||||
best.Destination = destination; best.Distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
protected bool HasMountedPairCandidate(float now)
|
||||
{
|
||||
// Cheap original/status gate; no geometry or claims before both roles exist.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_HQUnit firstUnit;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
if (firstUnit && firstUnit != unit && firstUnit.Group != unit.Group) return true;
|
||||
firstUnit = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_ConvoyInterceptOrder MountedOrder(ERR_HQMountedChoice choice, ERR_HQBelief belief, float now, int token)
|
||||
{
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Vehicle.Index; order.Position = choice.Destination;
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
ref ERR_ConvoyInterceptOrder order = MountedOrder(choice, belief, now, 400001 + m_MountedTokenCount);
|
||||
ref ERR_ConvoyInterceptOrder partner;
|
||||
if (partnerChoice) partner = MountedOrder(partnerChoice, belief, now, 400002 + m_MountedTokenCount);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) return;
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now)) return;
|
||||
// IDs and this revision remain spent after refusal; no post-claim fallback.
|
||||
m_MountedTokenCount += members;
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
choice.Unit.LogisticsReservation = order.Token;
|
||||
if (partnerChoice) partnerChoice.Unit.LogisticsReservation = partner.Token;
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = choice.Unit; m_MountedInterceptTask = choice.Task;
|
||||
m_MountedInterceptRoute = choice.Route; m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false;
|
||||
m_MountedInterceptPartner = partner; m_MountedInterceptPartnerReceipt = false;
|
||||
if (partnerChoice) { m_MountedInterceptPartnerUnit = partnerChoice.Unit; m_MountedInterceptPartnerRoute = partnerChoice.Route; }
|
||||
bool admitted;
|
||||
if (partner) admitted = m_Convoys.ReserveInterceptPair(choice.Task, order, partner, now);
|
||||
else admitted = m_Convoys.ReserveIntercept(choice.Task, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!admitted) { ReleaseMountedIntercept(now, "reservation refused"); return; }
|
||||
choice.Route.MessageId = m_Journal.Message();
|
||||
choice.Route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, order.Position, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", choice.Route.Payload, choice.Route.MessageId);
|
||||
if (partnerChoice)
|
||||
{
|
||||
partnerChoice.Route.MessageId = m_Journal.Message();
|
||||
partnerChoice.Route.Payload = string.Format("original partner %1 to road %2; report M%3", partner.VehicleIndex, partner.Position, partner.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", partnerChoice.Route.Payload, partnerChoice.Route.MessageId);
|
||||
}
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept
|
||||
|| m_MountedAttemptBeliefs.Count() >= 256 || m_MountedTokenCount >= 256) return;
|
||||
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;
|
||||
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);
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
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;
|
||||
if (pairPossible)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
ref ERR_HQMountedChoice bestA; ref ERR_HQMountedChoice bestB;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
// Enumerate the bounded original escorts: a greedy nearest first crew
|
||||
// can be the only crew within range of the other road position.
|
||||
foreach (ERR_ConvoyVehicle firstVehicle : task.Vehicles)
|
||||
{
|
||||
if (!firstVehicle || (firstVehicle.Index != 0 && firstVehicle.Index != 3 && firstVehicle.Index != 5)) continue;
|
||||
ref ERR_HQMountedChoice candidateA = MountedChoice(task, positionA, null, now, firstVehicle);
|
||||
if (!candidateA) continue;
|
||||
ref ERR_HQMountedChoice candidateB = MountedChoice(task, positionB, candidateA.Unit, now);
|
||||
if (!candidateB || candidateA.Unit.Group == candidateB.Unit.Group || candidateA.Route == candidateB.Route) continue;
|
||||
if (bestA && candidateA.Distance + candidateB.Distance >= bestA.Distance + bestB.Distance) continue;
|
||||
bestA = candidateA; bestB = candidateB;
|
||||
}
|
||||
}
|
||||
if (bestA) { QueueMountedIntercept(bestA, bestB, belief, now); return; }
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
if (queries >= 2) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ref ERR_HQMountedChoice candidate = MountedChoice(m_Convoys.At(t), destination, null, now);
|
||||
if (candidate && (!best || candidate.Distance < best.Distance)) best = candidate;
|
||||
}
|
||||
if (best) { QueueMountedIntercept(best, null, belief, now); return; }
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Vehicle;
|
||||
ref ERR_HQRoute Route;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
|
||||
// One or two original escorts may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedInterceptPartner;
|
||||
protected ERR_HQUnit m_MountedInterceptPartnerUnit;
|
||||
protected ref ERR_HQRoute m_MountedInterceptPartnerRoute;
|
||||
protected bool m_MountedInterceptReceipt;
|
||||
protected bool m_MountedInterceptPartnerReceipt;
|
||||
protected int m_MountedTokenCount;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
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);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
if (m_MountedInterceptPartner && unit == m_MountedInterceptPartnerUnit) reserved.Insert(m_MountedInterceptPartner.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_HQUnit partnerUnit = m_MountedInterceptPartnerUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach BOTH first: physical release may synchronously unregister a group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptPartner = null; m_MountedInterceptPartnerUnit = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptPartnerRoute = null;
|
||||
m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false; m_MountedInterceptPartnerReceipt = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (partner && m_Convoys) m_Convoys.ReleaseIntercept(task, partner, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
if (partner && partnerUnit && partnerUnit.LogisticsReservation == partner.Token) partnerUnit.LogisticsReservation = 0;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && (unit == m_MountedInterceptUnit || unit == m_MountedInterceptPartnerUnit)) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) 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);
|
||||
}
|
||||
protected bool MountedMemberReady(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!MountedMemberCurrent(unit, order)) return false;
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
return MountedInterceptStatus(unit, vehicle, now) && !ConvoyContact(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !MountedMemberCurrent(m_MountedInterceptUnit, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (partner && !MountedMemberCurrent(m_MountedInterceptPartnerUnit, partner))
|
||||
{ ReleaseMountedIntercept(now, "partner ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "partner command expired"); return; }
|
||||
if (!m_MountedInterceptReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "missing radio route"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order) return;
|
||||
if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
if (delivered > 0) { m_MountedInterceptReceipt = true; }
|
||||
}
|
||||
if (partner && !m_MountedInterceptPartnerReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptPartnerRoute) { ReleaseMountedIntercept(now, "missing partner route"); return; }
|
||||
int partnerDelivered = m_MountedInterceptPartnerRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (partnerDelivered < 0) { ReleaseMountedIntercept(now, "partner radio interrupted"); return; }
|
||||
if (partnerDelivered > 0) { m_MountedInterceptPartnerReceipt = true; }
|
||||
}
|
||||
if (!m_MountedInterceptReceipt) return;
|
||||
if (!MountedMemberReady(m_MountedInterceptUnit, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!applied) { ReleaseMountedIntercept(now, "physical command refused"); return; }
|
||||
m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escorts ordered to remembered road positions", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedChoice(ERR_ConvoyTask task, vector destination, ERR_HQUnit excluded, float now, ERR_ConvoyVehicle required = null)
|
||||
{
|
||||
if (!MountedInterceptTaskAvailable(task)) return null;
|
||||
ref ERR_HQMountedChoice best;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (required && vehicle != required) continue;
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit == excluded || unit.Group != vehicle.Group || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || (best && distance >= best.Distance)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
best = new ERR_HQMountedChoice();
|
||||
best.Unit = unit; best.Task = task; best.Vehicle = vehicle; best.Route = route;
|
||||
best.Destination = destination; best.Distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
protected bool HasMountedPairCandidate(float now)
|
||||
{
|
||||
// Cheap original/status gate; no geometry or claims before both roles exist.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_HQUnit firstUnit;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
if (firstUnit && firstUnit != unit && firstUnit.Group != unit.Group) return true;
|
||||
firstUnit = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_ConvoyInterceptOrder MountedOrder(ERR_HQMountedChoice choice, ERR_HQBelief belief, float now, int token)
|
||||
{
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Vehicle.Index; order.Position = choice.Destination;
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
ref ERR_ConvoyInterceptOrder order = MountedOrder(choice, belief, now, 400001 + m_MountedTokenCount);
|
||||
ref ERR_ConvoyInterceptOrder partner;
|
||||
if (partnerChoice) partner = MountedOrder(partnerChoice, belief, now, 400002 + m_MountedTokenCount);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) return;
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now)) return;
|
||||
// IDs and this revision remain spent after refusal; no post-claim fallback.
|
||||
m_MountedTokenCount += members;
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
choice.Unit.LogisticsReservation = order.Token;
|
||||
if (partnerChoice) partnerChoice.Unit.LogisticsReservation = partner.Token;
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = choice.Unit; m_MountedInterceptTask = choice.Task;
|
||||
m_MountedInterceptRoute = choice.Route; m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false;
|
||||
m_MountedInterceptPartner = partner; m_MountedInterceptPartnerReceipt = false;
|
||||
if (partnerChoice) { m_MountedInterceptPartnerUnit = partnerChoice.Unit; m_MountedInterceptPartnerRoute = partnerChoice.Route; }
|
||||
bool admitted;
|
||||
if (partner) admitted = m_Convoys.ReserveInterceptPair(choice.Task, order, partner, now);
|
||||
else admitted = m_Convoys.ReserveIntercept(choice.Task, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!admitted) { ReleaseMountedIntercept(now, "reservation refused"); return; }
|
||||
choice.Route.MessageId = m_Journal.Message();
|
||||
choice.Route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, order.Position, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", choice.Route.Payload, choice.Route.MessageId);
|
||||
if (partnerChoice)
|
||||
{
|
||||
partnerChoice.Route.MessageId = m_Journal.Message();
|
||||
partnerChoice.Route.Payload = string.Format("original partner %1 to road %2; report M%3", partner.VehicleIndex, partner.Position, partner.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", partnerChoice.Route.Payload, partnerChoice.Route.MessageId);
|
||||
}
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept
|
||||
|| m_MountedAttemptBeliefs.Count() >= 256 || m_MountedTokenCount >= 256) return;
|
||||
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;
|
||||
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);
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
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;
|
||||
if (pairPossible)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
ref ERR_HQMountedChoice bestA; ref ERR_HQMountedChoice bestB;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
// Enumerate the bounded original escorts: a greedy nearest first crew
|
||||
// can be the only crew within range of the other road position.
|
||||
foreach (ERR_ConvoyVehicle firstVehicle : task.Vehicles)
|
||||
{
|
||||
if (!firstVehicle || (firstVehicle.Index != 0 && firstVehicle.Index != 3 && firstVehicle.Index != 5)) continue;
|
||||
ref ERR_HQMountedChoice candidateA = MountedChoice(task, positionA, null, now, firstVehicle);
|
||||
if (!candidateA) continue;
|
||||
ref ERR_HQMountedChoice candidateB = MountedChoice(task, positionB, candidateA.Unit, now);
|
||||
if (!candidateB || candidateA.Unit.Group == candidateB.Unit.Group || candidateA.Route == candidateB.Route) continue;
|
||||
if (bestA && candidateA.Distance + candidateB.Distance >= bestA.Distance + bestB.Distance) continue;
|
||||
bestA = candidateA; bestB = candidateB;
|
||||
}
|
||||
}
|
||||
if (bestA) { QueueMountedIntercept(bestA, bestB, belief, now); return; }
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
if (queries >= 2) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ref ERR_HQMountedChoice candidate = MountedChoice(m_Convoys.At(t), destination, null, now);
|
||||
if (candidate && (!best || candidate.Distance < best.Distance)) best = candidate;
|
||||
}
|
||||
if (best) { QueueMountedIntercept(best, null, belief, now); return; }
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Vehicle;
|
||||
ref ERR_HQRoute Route;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
|
||||
// One or two original escorts may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedInterceptPartner;
|
||||
protected ERR_HQUnit m_MountedInterceptPartnerUnit;
|
||||
protected ref ERR_HQRoute m_MountedInterceptPartnerRoute;
|
||||
protected bool m_MountedInterceptReceipt;
|
||||
protected bool m_MountedInterceptPartnerReceipt;
|
||||
protected int m_MountedTokenCount;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
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);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
if (m_MountedInterceptPartner && unit == m_MountedInterceptPartnerUnit) reserved.Insert(m_MountedInterceptPartner.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_HQUnit partnerUnit = m_MountedInterceptPartnerUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach BOTH first: physical release may synchronously unregister a group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptPartner = null; m_MountedInterceptPartnerUnit = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptPartnerRoute = null;
|
||||
m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false; m_MountedInterceptPartnerReceipt = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (partner && m_Convoys) m_Convoys.ReleaseIntercept(task, partner, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
if (partner && partnerUnit && partnerUnit.LogisticsReservation == partner.Token) partnerUnit.LogisticsReservation = 0;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && (unit == m_MountedInterceptUnit || unit == m_MountedInterceptPartnerUnit)) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) 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);
|
||||
}
|
||||
protected bool MountedMemberReady(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!MountedMemberCurrent(unit, order)) return false;
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
return MountedInterceptStatus(unit, vehicle, now) && !ConvoyContact(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !MountedMemberCurrent(m_MountedInterceptUnit, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (partner && !MountedMemberCurrent(m_MountedInterceptPartnerUnit, partner))
|
||||
{ ReleaseMountedIntercept(now, "partner ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "partner command expired"); return; }
|
||||
if (!m_MountedInterceptReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "missing radio route"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order) return;
|
||||
if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
if (delivered > 0) { m_MountedInterceptReceipt = true; }
|
||||
}
|
||||
if (partner && !m_MountedInterceptPartnerReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptPartnerRoute) { ReleaseMountedIntercept(now, "missing partner route"); return; }
|
||||
int partnerDelivered = m_MountedInterceptPartnerRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (partnerDelivered < 0) { ReleaseMountedIntercept(now, "partner radio interrupted"); return; }
|
||||
if (partnerDelivered > 0) { m_MountedInterceptPartnerReceipt = true; }
|
||||
}
|
||||
if (!m_MountedInterceptReceipt || (partner && !m_MountedInterceptPartnerReceipt)) return;
|
||||
if (!MountedMemberReady(m_MountedInterceptUnit, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!applied) { ReleaseMountedIntercept(now, "physical command refused"); return; }
|
||||
m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escorts ordered to remembered road positions", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedChoice(ERR_ConvoyTask task, vector destination, ERR_HQUnit excluded, float now, ERR_ConvoyVehicle required = null)
|
||||
{
|
||||
if (!MountedInterceptTaskAvailable(task)) return null;
|
||||
ref ERR_HQMountedChoice best;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (required && vehicle != required) continue;
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit == excluded || unit.Group != vehicle.Group || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || (best && distance >= best.Distance)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
best = new ERR_HQMountedChoice();
|
||||
best.Unit = unit; best.Task = task; best.Vehicle = vehicle; best.Route = route;
|
||||
best.Destination = destination; best.Distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
protected bool HasMountedPairCandidate(float now)
|
||||
{
|
||||
// Cheap original/status gate; no geometry or claims before both roles exist.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_HQUnit firstUnit;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
if (firstUnit && firstUnit != unit && firstUnit.Group != unit.Group) return true;
|
||||
firstUnit = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_ConvoyInterceptOrder MountedOrder(ERR_HQMountedChoice choice, ERR_HQBelief belief, float now, int token)
|
||||
{
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Vehicle.Index; order.Position = choice.Destination;
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
ref ERR_ConvoyInterceptOrder order = MountedOrder(choice, belief, now, 400001 + m_MountedTokenCount);
|
||||
ref ERR_ConvoyInterceptOrder partner;
|
||||
if (partnerChoice) partner = MountedOrder(partnerChoice, belief, now, 400002 + m_MountedTokenCount);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) return;
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now)) return;
|
||||
// IDs and this revision remain spent after refusal; no post-claim fallback.
|
||||
m_MountedTokenCount += members;
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
choice.Unit.LogisticsReservation = order.Token;
|
||||
if (partnerChoice) partnerChoice.Unit.LogisticsReservation = partner.Token;
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = choice.Unit; m_MountedInterceptTask = choice.Task;
|
||||
m_MountedInterceptRoute = choice.Route; m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false;
|
||||
m_MountedInterceptPartner = partner; m_MountedInterceptPartnerReceipt = false;
|
||||
if (partnerChoice) { m_MountedInterceptPartnerUnit = partnerChoice.Unit; m_MountedInterceptPartnerRoute = partnerChoice.Route; }
|
||||
bool admitted;
|
||||
if (partner) admitted = m_Convoys.ReserveInterceptPair(choice.Task, order, partner, now);
|
||||
else admitted = m_Convoys.ReserveIntercept(choice.Task, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!admitted) { ReleaseMountedIntercept(now, "reservation refused"); return; }
|
||||
choice.Route.MessageId = m_Journal.Message();
|
||||
choice.Route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, order.Position, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", choice.Route.Payload, choice.Route.MessageId);
|
||||
if (partnerChoice)
|
||||
{
|
||||
partnerChoice.Route.MessageId = m_Journal.Message();
|
||||
partnerChoice.Route.Payload = string.Format("original partner %1 to road %2; report M%3", partner.VehicleIndex, partner.Position, partner.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", partnerChoice.Route.Payload, partnerChoice.Route.MessageId);
|
||||
}
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept
|
||||
|| m_MountedAttemptBeliefs.Count() >= 256 || m_MountedTokenCount >= 256) return;
|
||||
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;
|
||||
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);
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
for (int scanned = 0; scanned < count && queries < 3; 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;
|
||||
if (pairPossible)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
ref ERR_HQMountedChoice bestA; ref ERR_HQMountedChoice bestB;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
// Enumerate the bounded original escorts: a greedy nearest first crew
|
||||
// can be the only crew within range of the other road position.
|
||||
foreach (ERR_ConvoyVehicle firstVehicle : task.Vehicles)
|
||||
{
|
||||
if (!firstVehicle || (firstVehicle.Index != 0 && firstVehicle.Index != 3 && firstVehicle.Index != 5)) continue;
|
||||
ref ERR_HQMountedChoice candidateA = MountedChoice(task, positionA, null, now, firstVehicle);
|
||||
if (!candidateA) continue;
|
||||
ref ERR_HQMountedChoice candidateB = MountedChoice(task, positionB, candidateA.Unit, now);
|
||||
if (!candidateB || candidateA.Unit.Group == candidateB.Unit.Group || candidateA.Route == candidateB.Route) continue;
|
||||
if (bestA && candidateA.Distance + candidateB.Distance >= bestA.Distance + bestB.Distance) continue;
|
||||
bestA = candidateA; bestB = candidateB;
|
||||
}
|
||||
}
|
||||
if (bestA) { QueueMountedIntercept(bestA, bestB, belief, now); return; }
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
if (queries >= 2) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ref ERR_HQMountedChoice candidate = MountedChoice(m_Convoys.At(t), destination, null, now);
|
||||
if (candidate && (!best || candidate.Distance < best.Distance)) best = candidate;
|
||||
}
|
||||
if (best) { QueueMountedIntercept(best, null, belief, now); return; }
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Vehicle;
|
||||
ref ERR_HQRoute Route;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
|
||||
// One or two original escorts may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedInterceptPartner;
|
||||
protected ERR_HQUnit m_MountedInterceptPartnerUnit;
|
||||
protected ref ERR_HQRoute m_MountedInterceptPartnerRoute;
|
||||
protected bool m_MountedInterceptReceipt;
|
||||
protected bool m_MountedInterceptPartnerReceipt;
|
||||
protected int m_MountedTokenCount;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
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);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
if (m_MountedInterceptPartner) count++;
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
if (m_MountedInterceptPartner && unit == m_MountedInterceptPartnerUnit) reserved.Insert(m_MountedInterceptPartner.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_HQUnit partnerUnit = m_MountedInterceptPartnerUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach BOTH first: physical release may synchronously unregister a group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptPartner = null; m_MountedInterceptPartnerUnit = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptPartnerRoute = null;
|
||||
m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false; m_MountedInterceptPartnerReceipt = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (partner && m_Convoys) m_Convoys.ReleaseIntercept(task, partner, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
if (partner && partnerUnit && partnerUnit.LogisticsReservation == partner.Token) partnerUnit.LogisticsReservation = 0;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && (unit == m_MountedInterceptUnit || unit == m_MountedInterceptPartnerUnit)) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) 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);
|
||||
}
|
||||
protected bool MountedMemberReady(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!MountedMemberCurrent(unit, order)) return false;
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
return MountedInterceptStatus(unit, vehicle, now) && !ConvoyContact(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ref ERR_ConvoyInterceptOrder partner = m_MountedInterceptPartner;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt || !MissionActive() || m_Stopping
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !MountedMemberCurrent(m_MountedInterceptUnit, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (partner && !MountedMemberCurrent(m_MountedInterceptPartnerUnit, partner))
|
||||
{ ReleaseMountedIntercept(now, "partner ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now))
|
||||
{ ReleaseMountedIntercept(now, "partner command expired"); return; }
|
||||
if (!m_MountedInterceptReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptRoute) { ReleaseMountedIntercept(now, "missing radio route"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order) return;
|
||||
if (delivered < 0) { ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
if (delivered > 0) { m_MountedInterceptReceipt = true; }
|
||||
}
|
||||
if (partner && !m_MountedInterceptPartnerReceipt)
|
||||
{
|
||||
if (!m_MountedInterceptPartnerRoute) { ReleaseMountedIntercept(now, "missing partner route"); return; }
|
||||
int partnerDelivered = m_MountedInterceptPartnerRoute.Advance(now, m_Journal);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (partnerDelivered < 0) { ReleaseMountedIntercept(now, "partner radio interrupted"); return; }
|
||||
if (partnerDelivered > 0) { m_MountedInterceptPartnerReceipt = true; }
|
||||
}
|
||||
if (!m_MountedInterceptReceipt || (partner && !m_MountedInterceptPartnerReceipt)) return;
|
||||
if (!MountedMemberReady(m_MountedInterceptUnit, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!applied) { ReleaseMountedIntercept(now, "physical command refused"); return; }
|
||||
m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escorts ordered to remembered road positions", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedChoice(ERR_ConvoyTask task, vector destination, ERR_HQUnit excluded, float now, ERR_ConvoyVehicle required = null)
|
||||
{
|
||||
if (!MountedInterceptTaskAvailable(task)) return null;
|
||||
ref ERR_HQMountedChoice best;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (required && vehicle != required) continue;
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit == excluded || unit.Group != vehicle.Group || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || (best && distance >= best.Distance)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
best = new ERR_HQMountedChoice();
|
||||
best.Unit = unit; best.Task = task; best.Vehicle = vehicle; best.Route = route;
|
||||
best.Destination = destination; best.Distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
protected bool HasMountedPairCandidate(float now)
|
||||
{
|
||||
// Cheap original/status gate; no geometry or claims before both roles exist.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_HQUnit firstUnit;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
if (firstUnit && firstUnit != unit && firstUnit.Group != unit.Group) return true;
|
||||
firstUnit = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_ConvoyInterceptOrder MountedOrder(ERR_HQMountedChoice choice, ERR_HQBelief belief, float now, int token)
|
||||
{
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Vehicle.Index; order.Position = choice.Destination;
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
ref ERR_ConvoyInterceptOrder order = MountedOrder(choice, belief, now, 400001 + m_MountedTokenCount);
|
||||
ref ERR_ConvoyInterceptOrder partner;
|
||||
if (partnerChoice) partner = MountedOrder(partnerChoice, belief, now, 400001 + m_MountedTokenCount);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) return;
|
||||
if (partner && !ERR_HQMountedInterceptRules.Current(partner, belief, now)) return;
|
||||
// IDs and this revision remain spent after refusal; no post-claim fallback.
|
||||
m_MountedTokenCount += members;
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
choice.Unit.LogisticsReservation = order.Token;
|
||||
if (partnerChoice) partnerChoice.Unit.LogisticsReservation = partner.Token;
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = choice.Unit; m_MountedInterceptTask = choice.Task;
|
||||
m_MountedInterceptRoute = choice.Route; m_MountedInterceptDelivered = false; m_MountedInterceptReceipt = false;
|
||||
m_MountedInterceptPartner = partner; m_MountedInterceptPartnerReceipt = false;
|
||||
if (partnerChoice) { m_MountedInterceptPartnerUnit = partnerChoice.Unit; m_MountedInterceptPartnerRoute = partnerChoice.Route; }
|
||||
bool admitted;
|
||||
if (partner) admitted = m_Convoys.ReserveInterceptPair(choice.Task, order, partner, now);
|
||||
else admitted = m_Convoys.ReserveIntercept(choice.Task, order, now);
|
||||
if (m_MountedIntercept != order || m_MountedInterceptPartner != partner) return;
|
||||
if (!admitted) { ReleaseMountedIntercept(now, "reservation refused"); return; }
|
||||
choice.Route.MessageId = m_Journal.Message();
|
||||
choice.Route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, order.Position, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", choice.Route.Payload, choice.Route.MessageId);
|
||||
if (partnerChoice)
|
||||
{
|
||||
partnerChoice.Route.MessageId = m_Journal.Message();
|
||||
partnerChoice.Route.Payload = string.Format("original partner %1 to road %2; report M%3", partner.VehicleIndex, partner.Position, partner.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", partnerChoice.Route.Payload, partnerChoice.Route.MessageId);
|
||||
}
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept
|
||||
|| m_MountedAttemptBeliefs.Count() >= 256 || m_MountedTokenCount >= 256) return;
|
||||
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;
|
||||
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);
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
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;
|
||||
if (pairPossible)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
ref ERR_HQMountedChoice bestA; ref ERR_HQMountedChoice bestB;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
// Enumerate the bounded original escorts: a greedy nearest first crew
|
||||
// can be the only crew within range of the other road position.
|
||||
foreach (ERR_ConvoyVehicle firstVehicle : task.Vehicles)
|
||||
{
|
||||
if (!firstVehicle || (firstVehicle.Index != 0 && firstVehicle.Index != 3 && firstVehicle.Index != 5)) continue;
|
||||
ref ERR_HQMountedChoice candidateA = MountedChoice(task, positionA, null, now, firstVehicle);
|
||||
if (!candidateA) continue;
|
||||
ref ERR_HQMountedChoice candidateB = MountedChoice(task, positionB, candidateA.Unit, now);
|
||||
if (!candidateB || candidateA.Unit.Group == candidateB.Unit.Group || candidateA.Route == candidateB.Route) continue;
|
||||
if (bestA && candidateA.Distance + candidateB.Distance >= bestA.Distance + bestB.Distance) continue;
|
||||
bestA = candidateA; bestB = candidateB;
|
||||
}
|
||||
}
|
||||
if (bestA) { QueueMountedIntercept(bestA, bestB, belief, now); return; }
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
if (queries >= 2) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ref ERR_HQMountedChoice candidate = MountedChoice(m_Convoys.At(t), destination, null, now);
|
||||
if (candidate && (!best || candidate.Distance < best.Distance)) best = candidate;
|
||||
}
|
||||
if (best) { QueueMountedIntercept(best, null, belief, now); return; }
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user