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:
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-e35h_ah0/check.cpp:471: int main(): Assertion `f.h.m_MountedIntercept&&f.h.m_MountedIntercept->Armored&&f.h.m_MountedIntercept->VehicleIndex==4&&f.h.m_MountedIntercept->ConfirmedLosses==6' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-bmp-dispatch-1406/tests/test_hq_mounted_dispatch.py", line 27, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<184 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-e35h_ah0/check']' died with <Signals.SIGABRT: 6>.
|
||||
+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 Escort;
|
||||
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.Escort = 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.Escort.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();
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
"""Execute actual mounted HQ dispatch methods; radio/physical service are adversarial doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
||||
names=['protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
||||
for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice']:
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
||||
for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string']:
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute']:
|
||||
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
||||
for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt']:
|
||||
s=s.replace('->'+name+'.','->'+name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
|
||||
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
||||
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
||||
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
||||
s=s.replace('Array<vector> reserved,','Array<vector>& reserved,')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,n)) for n in names if n in P.read_text())
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){} template<class...T>static string Format(const char*fmt,T...v){std::ostringstream out;out<<fmt;((out<<"|"<<v),...);return out.str();}};
|
||||
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
||||
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Group{};struct Entity{};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
||||
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
||||
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
||||
struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
||||
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct ERR_ConvoyInterceptOrder{int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
||||
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
||||
struct ERR_HQRoute{int result=0,calls=0,MessageId=0;string Payload;int Advance(float,Journal*){calls++;return result;}};
|
||||
struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:nullptr);}};
|
||||
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
||||
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
||||
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
||||
struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
||||
int Count(){return tasks.Count();}ERR_ConvoyTask*At(int n){return tasks[n];}
|
||||
bool InterceptCurrent(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*){return current;}
|
||||
bool ReserveIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){reserves++;return reserve;}
|
||||
bool ApplyIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){applies++;return apply;}
|
||||
void ReleaseIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){releases++;if(onRelease)onRelease();}
|
||||
};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
||||
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
||||
struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
||||
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
||||
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
||||
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
||||
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
||||
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
||||
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
||||
struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;vector Destination;float Distance=0;};
|
||||
struct HQ:Base{
|
||||
ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
|
||||
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
||||
bool m_MountedInterceptReceipt=false,m_MountedInterceptPartnerReceipt=false;int m_MountedTokenCount=0;
|
||||
int m_MountedSurveyCursor=0;
|
||||
Array<int>m_MountedAttemptBeliefs,m_MountedAttemptRevisions;Array<ERR_HQUnit*>m_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network;
|
||||
bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
||||
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
||||
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
||||
};
|
||||
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
||||
PairFixture(){unit2=unit;unit2.Group=&group2;unit2.Radio=&radio2;unit2.StatusSampler=&sampler2;unit2.ForceSourceId="partner";entry2=entry;entry2.SourceId="partner";entry2.Position={200,0};watch.second=&entry2;vehicle2=vehicle;vehicle2.Index=3;vehicle2.Group=&group2;vehicle2.Entity=&entity2;task.Vehicles[3]=&vehicle2;h.m_Units.Insert(&unit2);network.routes[&radio2]=&route2;ERR_HQMountedInterceptRules::pairGeometry=true;}
|
||||
void pair(){plan();assert(h.m_MountedIntercept&&h.m_MountedInterceptPartner);assert(h.m_MountedInterceptTask==&task);assert(unit.LogisticsReservation==400001&&unit2.LogisticsReservation==400002);assert(service.pairReserves==1&&service.reserves==0&&service.pairApplies==0);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;assert(f.h.Status()=="base HQ status");f.queued();string queued=f.h.Status();assert(queued.find("base HQ status")!=string::npos&&queued.find("|0|queued|20|100,0")!=string::npos);f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.Status().find("|0|delivered|20|100,0")!=string::npos);f.h.Stop();assert(f.h.Status()=="base HQ status");}
|
||||
{Fixture f;f.queued();assert(f.h.Assigned()==1&&f.h.HuntingAssigned()==1);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);assert(r.Count()==1&&r[0]==f.belief.destination);f.h.PlanMountedIntercept(4000);assert(f.service.reserves==1);f.h.TickMountedIntercept(4000);assert(f.route.calls==1&&f.service.applies==0);f.route.result=1;f.h.TickMountedIntercept(5000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.belief.BeliefRevision++;f.h.TickMountedIntercept(6000);assert(f.h.m_MountedInterceptDelivered&&f.service.releases==0);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&f.unit.LogisticsReservation==0);}
|
||||
// Task-wide logistics and queued sibling downlinks cannot consume a revision or starve a free farther convoy.
|
||||
for(int kind=0;kind<5;kind++){Fixture f,far;far.unit.ForceSourceId=far.entry.SourceId="crew2";far.receipt.SourceId="convoy2";far.entry.Position={-200,0};f.watch.second=&far.entry;f.service.tasks.Insert(&far.task);f.h.m_Units.Insert(&far.unit);ERR_HQUnit sibling; sibling.Group=&f.otherGroup;f.unused.Group=&f.otherGroup;f.h.m_Units.Insert(&sibling);if(kind==0)f.unused.SupplyToken=123;if(kind==1)f.unused.SupplyHeld=true;if(kind==2)sibling.PendingConvoyOrder=true;if(kind==3)sibling.Downlink=true;if(kind==4)sibling.LogisticsReservation=123;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&far.unit&&!f.unit.LogisticsReservation&&far.unit.LogisticsReservation==400001&&f.service.reserves==1);}
|
||||
// A disconnected nearest escort cannot starve the farther reachable original escort.
|
||||
{Fixture f;Group group2;Entity radio2,entity2;ERR_HQUnit u2=f.unit;u2.Group=&group2;u2.Radio=&radio2;u2.ForceSourceId="crew2";ERR_HQWatchEntry entry2=f.entry;entry2.SourceId="crew2";entry2.Position={-200,0};f.watch.second=&entry2;ERR_ConvoyVehicle vehicle2=f.vehicle;vehicle2.Group=&group2;vehicle2.Entity=&entity2;vehicle2.Index=3;f.task.Vehicles[3]=&vehicle2;f.h.m_Units.Insert(&u2);f.network.denied=&f.radio;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&u2&&f.h.m_MountedIntercept->VehicleIndex==3&&f.network.calls>=2&&!f.unit.LogisticsReservation&&u2.LogisticsReservation==400001);}
|
||||
// Invalid world clock must preserve the exact claim for the next finite tick.
|
||||
{Fixture f;f.queued();f.h.TickMountedIntercept(std::nanf(""));assert(f.h.m_MountedIntercept&&f.unit.LogisticsReservation==400001&&f.service.releases==0&&f.route.calls==0);}
|
||||
// No own vehicle/fresh crew evidence means no native road surveys.
|
||||
for(int kind=0;kind<6;kind++){Fixture f;if(kind==0)f.service.tasks.clear();if(kind==1)f.task.Stopped=true;if(kind==2)f.unit.LogisticsReservation=1;if(kind==3)f.entry.Closed=true;if(kind==4)f.sampler.bound=false;if(kind==5)f.combat.Held=true;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&f.service.reserves==0);}
|
||||
// Interrupted pending claims never apply physical orders, and the same delivered revision cannot retry.
|
||||
{Fixture f;f.queued();f.route.result=-1;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.applies==0);f.plan();assert(f.service.reserves==1);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==2&&f.unit.LogisticsReservation==400002);}
|
||||
{Fixture f;f.queued();f.belief.BeliefRevision++;f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.route.calls==0&&f.service.applies==0&&!f.h.m_MountedIntercept);}
|
||||
// Physical validation changes after radio delivery cannot trigger a stale order.
|
||||
for(int kind=0;kind<7;kind++){Fixture f;f.queued();f.route.result=1;if(kind==0)f.ammo.Complete=false;if(kind==1)f.ammo.valid=false;if(kind==2)f.sampler.bound=false;if(kind==3)f.entry.Fit--;if(kind==4)f.entry.LastDeliveredAt=9000;if(kind==5)f.unit.contact=true;if(kind==6)f.service.apply=false;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation);assert(f.service.applies==(kind==6?1:0));}
|
||||
// Conditional release and recursive unregister preserve unrelated reservations.
|
||||
{Fixture f;f.queued();f.unit.LogisticsReservation=123;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept);f.h.ReleaseUnit(&f.unit,4000);};f.h.TickMountedIntercept(4000);assert(f.unit.LogisticsReservation==123&&f.service.releases==1&&f.h.releaseUnits==1);}
|
||||
{Fixture f;f.queued();f.h.Stop();assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1&&f.h.stops==1&&f.service.applies==0);}
|
||||
// Ordinary assigned and pursuit capacities reserve space before any road query or radio action.
|
||||
for(int kind=0;kind<3;kind++){Fixture f;if(kind==0)f.mode.settings.cap=0;if(kind==1)f.h.assigned=3;if(kind==2)f.h.hunting=3;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&ERR_HQMountedInterceptRules::selects==before);}
|
||||
{Fixture f;f.queued();f.h.assigned=3;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&f.service.applies==0);}
|
||||
// Admission is based on detached delivered position, not physical entity telemetry; stock/crew/roles guard it.
|
||||
for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);}
|
||||
{Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);assert(f.h.m_MountedAttemptBeliefs.empty());int after=ERR_HQMountedInterceptRules::selects;f.h.PlanMountedIntercept(8000);assert(ERR_HQMountedInterceptRules::selects-after==1);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.service.reserves==1);}
|
||||
// Cursor normalization handles track expiry/removal without out-of-range access.
|
||||
{Fixture f;ERR_HQBelief second;second.BeliefId=2;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second};f.plan();assert(!f.h.m_MountedIntercept);f.h.m_MountedSurveyCursor=31;f.memory.entries={&f.belief};f.belief.select=true;f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==1);}
|
||||
// Skipped ineligible/already-attempted tracks do not spend the two-road-query budget.
|
||||
{Fixture f;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.eligible=false;f.h.m_MountedAttemptBeliefs.Insert(2);f.h.m_MountedAttemptRevisions.Insert(second.BeliefRevision);f.memory.entries={&f.belief,&second,&third};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==1&&f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3);}
|
||||
// No eligible reports and no reports both produce zero native road queries.
|
||||
{Fixture f;f.belief.eligible=false;int before=ERR_HQMountedInterceptRules::selects;f.plan();f.memory.entries.clear();f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
// Full bounded memory: every pass has at most two surveys, no failed revision is consumed.
|
||||
{Fixture f;ERR_HQBelief beliefs[64];f.memory.entries.clear();for(int i=0;i<64;i++){beliefs[i].BeliefId=i+1;beliefs[i].select=false;f.memory.entries.Insert(&beliefs[i]);}for(int pass=0;pass<32;pass++){int before=ERR_HQMountedInterceptRules::selects;float now=3000+pass*5000;f.entry.ObservedAt=f.entry.LastDeliveredAt=now;f.h.PlanMountedIntercept(now);assert(ERR_HQMountedInterceptRules::selects-before==2&&f.h.m_MountedSurveyCursor==((pass+1)*2)%64&&f.h.m_MountedAttemptBeliefs.empty());}assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
|
||||
// Paired admission counts and reserves both original slots; one receipt never detours either.
|
||||
{PairFixture f;f.pair();assert(f.h.Assigned()==2&&f.h.HuntingAssigned()==2);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);f.h.ReserveUnit(&f.unit2,r,a,b);assert(r.Count()==2&&vector::DistanceXZ(r[0],r[1])>=40);assert(f.h.Status().find("Paired")!=string::npos);
|
||||
f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptReceipt&&!f.h.m_MountedInterceptPartnerReceipt&&!f.h.m_MountedInterceptDelivered&&f.service.pairApplies==0&&f.service.applies==0);f.h.TickMountedIntercept(4500);assert(f.route.calls==1&&f.route2.calls==2&&f.h.m_MountedInterceptRoute==&f.route&&f.h.m_MountedInterceptPartnerRoute==&f.route2);
|
||||
f.route2.result=1;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&f.h.m_MountedInterceptDelivered&&f.h.m_MountedIntercept->ExpiresAt==121000&&f.h.m_MountedInterceptPartner->ExpiresAt==121000);f.h.TickMountedIntercept(6000);assert(f.service.pairApplies==1);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// Either receipt may arrive first; interrupted/expired/revised/changed ownership never creates half a pair.
|
||||
for(int kind=0;kind<9;kind++){PairFixture f;f.pair();f.route2.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptPartnerReceipt&&f.service.pairApplies==0);float now=5000;
|
||||
if(kind==0)f.route.result=-1;
|
||||
if(kind==1)now=121000;
|
||||
if(kind==2)f.belief.BeliefRevision++;
|
||||
if(kind==3)f.unit2.LogisticsReservation=999;
|
||||
if(kind==4)f.unit2.contact=true;
|
||||
if(kind==5)f.entry2.Fit--;
|
||||
if(kind==6)f.sampler2.bound=false;
|
||||
if(kind==7)f.h.assigned=2;
|
||||
if(kind==8)now=34000;
|
||||
if(kind!=0)f.route.result=1;
|
||||
f.h.TickMountedIntercept(now);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairApplies==0&&f.service.applies==0&&!f.unit.LogisticsReservation);assert(f.unit2.LogisticsReservation==(kind==3?999:0));}
|
||||
// Native preparation failure settles BOTH claims. No retry or singleton replacement after commitment.
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.apply=false;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0);}
|
||||
{PairFixture f;f.service.reserve=false;f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.h.m_MountedTokenCount==2);f.service.reserve=true;f.plan();assert(f.service.pairReserves==1);f.belief.BeliefRevision++;f.plan();assert(f.h.m_MountedIntercept->Token==400003&&f.h.m_MountedInterceptPartner->Token==400004);}
|
||||
// Stop/reentrant unregister detach both owned handles before external release; unrelated claims survive.
|
||||
{PairFixture f;f.pair();f.unit2.LogisticsReservation=999;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner);f.h.ReleaseUnit(&f.unit2,5000);};f.h.Stop();f.h.Stop();assert(f.service.releases==2&&!f.unit.LogisticsReservation&&f.unit2.LogisticsReservation==999);}
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.onApply=[&]{f.h.Stop();};f.h.TickMountedIntercept(5000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptDelivered&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);}
|
||||
// Two free slots and two IDs are required for pair. Singleton fallback occurs only before claims.
|
||||
for(int kind=0;kind<5;kind++){PairFixture f;if(kind==0)f.h.assigned=2;if(kind==1)f.h.hunting=2;if(kind==2)f.h.m_MountedTokenCount=255;if(kind==3)f.network.denied=&f.radio2;if(kind==4)ERR_HQMountedInterceptRules::pairGeometry=false;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.reserves==1&&f.service.pairReserves==0);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before<=2);}
|
||||
{PairFixture f;f.h.m_MountedTokenCount=256;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects==before);}
|
||||
// A valid reversed assignment must win over greedy singleton fallback (same bounded road survey).
|
||||
{PairFixture f;f.mode.settings.radius=800;f.belief.destination={0,0};f.entry.Position={0,0};f.entry2.Position={-800,0};int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedInterceptPartner&&f.h.m_MountedInterceptUnit==&f.unit2&&f.h.m_MountedInterceptPartnerUnit==&f.unit);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==1);}
|
||||
// Stop during reservation cannot resurrect a job after external callback returns.
|
||||
{PairFixture f;f.service.onReserve=[&]{f.h.Stop();};f.plan();assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// No mixed-convoy pair; both geometric failures still cost at most two queries, no attempts/tokens.
|
||||
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
||||
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
||||
|
||||
}
|
||||
''')
|
||||
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|
||||
@@ -0,0 +1,287 @@
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c
|
||||
@@ -5,6 +5,7 @@
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Escort;
|
||||
ref ERR_HQRoute Route;
|
||||
+ ref ERR_InterceptVehicleFootprint Footprint;
|
||||
vector Destination;
|
||||
float Distance;
|
||||
}
|
||||
@@ -34,6 +35,7 @@
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
+ if (m_MountedIntercept.Armored) result += "\nOriginal BMP singleton; delivered confirmed losses " + m_MountedIntercept.ConfirmedLosses.ToString();
|
||||
if (m_MountedInterceptPartner) result += "\nPaired mounted interception (two original escorts)";
|
||||
return result + "\n" + string.Format("Mounted interception: escort %1; %2; report M%3; road %4", m_MountedIntercept.VehicleIndex, phase, m_MountedIntercept.ReportId, m_MountedIntercept.Position);
|
||||
}
|
||||
@@ -103,6 +105,7 @@
|
||||
protected bool MountedMemberCurrent(ERR_HQUnit unit, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || !unit || !m_Units.Contains(unit) || unit.Group != order.Group) return false;
|
||||
+ if (order.Armored && (m_Watch.ConfirmedLosses() < 6 || order.ConfirmedLosses < 6)) return false;
|
||||
if (unit.LogisticsReservation != order.Token || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)) return false;
|
||||
return m_Convoys && m_Convoys.InterceptCurrent(m_MountedInterceptTask, order);
|
||||
}
|
||||
@@ -151,6 +154,9 @@
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
if (partner && !MountedMemberReady(m_MountedInterceptPartnerUnit, partner, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original partner"); return; }
|
||||
+ // Radio delivery may synchronously change the current mission knowledge.
|
||||
+ if (order.Armored && (m_Watch.ConfirmedLosses() < 6 || partner))
|
||||
+ { ReleaseMountedIntercept(now, "armored escalation no longer authorized"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
@@ -171,6 +177,42 @@
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
+ }
|
||||
+ protected bool MountedArmoredOriginal(ERR_ConvoyTask task, ERR_ConvoyVehicle vehicle)
|
||||
+ {
|
||||
+ if (!task || task.Vehicles.Count() != 6 || !task.Receipt || !task.Receipt.Consumed
|
||||
+ || task.Receipt.Records.Count() <= 4 || task.Vehicles[4] != vehicle || !vehicle || vehicle.Index != 4
|
||||
+ || !vehicle.Entity || !vehicle.Entity.GetPrefabData() || !m_Mode.Forces()) return false;
|
||||
+ if (vehicle.Entity.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.VehiclePrefab(4)) return false;
|
||||
+ ERR_ForceRecord record = m_Mode.Forces().FindGroup(vehicle.Group);
|
||||
+ return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
|
||||
+ && record.Id == task.Receipt.Records[4] && record.Group == vehicle.Group
|
||||
+ && record.SourceId == task.Receipt.SourceId + "-vehicle-4";
|
||||
+ }
|
||||
+ protected ERR_HQMountedChoice MountedArmoredCandidate(float now)
|
||||
+ {
|
||||
+ // Cumulative delivered/reconciled original losses only, never a physical scan.
|
||||
+ if (m_Watch.ConfirmedLosses() < 6) return null;
|
||||
+ for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
+ {
|
||||
+ ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
+ if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
+ ERR_ConvoyVehicle vehicle = task.Vehicles[4];
|
||||
+ if (!MountedArmoredOriginal(task, vehicle) || vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
+ foreach (ERR_HQUnit unit : m_Units)
|
||||
+ {
|
||||
+ if (unit.Group != vehicle.Group || unit.ForceSourceId != task.Receipt.SourceId + "-vehicle-4" || !MountedLogisticsEligible(unit)
|
||||
+ || ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
+ ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
+ if (!route) continue;
|
||||
+ ref ERR_InterceptVehicleFootprint footprint = ERR_InterceptVehicleFootprint.Capture(vehicle.Entity);
|
||||
+ if (!footprint || !footprint.Valid() || !footprint.Matches(vehicle.Entity)) continue;
|
||||
+ ref ERR_HQMountedChoice choice = new ERR_HQMountedChoice();
|
||||
+ choice.Unit = unit; choice.Task = task; choice.Escort = vehicle; choice.Route = route; choice.Footprint = footprint;
|
||||
+ return choice;
|
||||
+ }
|
||||
+ }
|
||||
+ return null;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
@@ -244,6 +286,8 @@
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = token; order.Group = choice.Unit.Group; order.SourceId = choice.Task.Receipt.SourceId;
|
||||
order.VehicleIndex = choice.Escort.Index; order.Position = choice.Destination;
|
||||
+ if (choice.Escort.Index == 4)
|
||||
+ { order.Armored = true; order.Footprint = choice.Footprint; order.ConfirmedLosses = m_Watch.ConfirmedLosses(); }
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
@@ -251,6 +295,8 @@
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
+ if (choice && choice.Escort.Index == 4 && (partnerChoice || m_Watch.ConfirmedLosses() < 6
|
||||
+ || !choice.Footprint || !choice.Footprint.Valid() || !choice.Footprint.Matches(choice.Escort.Entity))) return;
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
@@ -290,7 +336,10 @@
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
int freeGlobal = globalLimit - Assigned();
|
||||
int freeHunt = ERR_HQPursuitRules.Limit(globalLimit) - HuntingAssigned();
|
||||
- if (freeGlobal < 1 || freeHunt < 1 || !HasMountedInterceptCandidate(now)) return;
|
||||
+ if (freeGlobal < 1 || freeHunt < 1) return;
|
||||
+ ref ERR_HQMountedChoice armored = MountedArmoredCandidate(now);
|
||||
+ bool escortPossible = HasMountedInterceptCandidate(now);
|
||||
+ if (!armored && !escortPossible) return;
|
||||
bool pairPossible = freeGlobal >= 2 && freeHunt >= 2 && m_MountedTokenCount <= 254 && HasMountedPairCandidate(now);
|
||||
array<vector> reserved = {}; int first = -1; int second = -1;
|
||||
foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second);
|
||||
@@ -304,7 +353,25 @@
|
||||
m_MountedSurveyCursor = (b + 1) % count;
|
||||
ERR_HQBelief belief = m_Memory.At(b);
|
||||
if (!ERR_HQMountedInterceptRules.Eligible(belief, now) || MountedInterceptAttempted(belief.BeliefId, belief.BeliefRevision)) continue;
|
||||
- if (pairPossible)
|
||||
+ if (armored && m_Watch.ConfirmedLosses() >= 6)
|
||||
+ {
|
||||
+ vector armoredPosition; queries++;
|
||||
+ if (ERR_HQMountedInterceptRules.SelectArmored(belief, now, reserved, armored.Footprint, armoredPosition))
|
||||
+ {
|
||||
+ ERR_HQWatchEntry armoredStatus = m_Watch.Find(armored.Unit.ForceSourceId);
|
||||
+ if (!armoredStatus || !MountedInterceptTaskAvailable(armored.Task)
|
||||
+ || !MountedArmoredOriginal(armored.Task, armored.Escort)
|
||||
+ || !MountedInterceptStatus(armored.Unit, armored.Escort, now)
|
||||
+ || !armored.Footprint.Matches(armored.Escort.Entity)) continue;
|
||||
+ float armoredDistance = vector.DistanceXZ(armoredStatus.Position, armoredPosition);
|
||||
+ if (ERR_ForceRules.Finite(armoredDistance) && armoredDistance <= m_Mode.Settings().Runtime("responseRadiusMetres"))
|
||||
+ {
|
||||
+ armored.Destination = armoredPosition; armored.Distance = armoredDistance;
|
||||
+ QueueMountedIntercept(armored, null, belief, now); return;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ if (pairPossible && queries < 2)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
@@ -331,7 +398,7 @@
|
||||
}
|
||||
}
|
||||
// Only pre-admission fallback, sharing the SAME two-survey pass budget.
|
||||
- if (queries >= 2) continue;
|
||||
+ if (queries >= 2 || !escortPossible) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ref ERR_HQMountedChoice best;
|
||||
--- a/tests/test_hq_mounted_dispatch.py
|
||||
+++ b/tests/test_hq_mounted_dispatch.py
|
||||
@@ -3,19 +3,21 @@
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
||||
-names=['protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
||||
+names=['protected bool MountedArmoredOriginal(', 'protected ERR_HQMountedChoice MountedArmoredCandidate(', 'protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
||||
- for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice']:
|
||||
+ for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice','ERR_InterceptVehicleFootprint','ERR_ForceRecord']:
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
||||
- for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string']:
|
||||
+ for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string','ERR_InterceptVehicleFootprint','ERR_ConvoyResources','ERR_ForceKind']:
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
- for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute']:
|
||||
+ for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute','footprint','armored','armoredStatus','record']:
|
||||
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
||||
- for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt']:
|
||||
+ for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt','Footprint','Entity']:
|
||||
s=s.replace('->'+name+'.','->'+name+'->')
|
||||
- s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
|
||||
+ s=s.replace('choice->Escort.Entity','choice->Escort->Entity').replace('armored->Unit.ForceSourceId','armored->Unit->ForceSourceId').replace('armored->Escort.Entity','armored->Escort->Entity')
|
||||
+ s=s.replace('m_MountedIntercept->ConfirmedLosses.ToString()', 'std::to_string(m_MountedIntercept->ConfirmedLosses)')
|
||||
+ s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->').replace('Forces().','Forces()->').replace('GetPrefabData().','GetPrefabData()->')
|
||||
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
||||
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
||||
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
||||
@@ -35,22 +37,28 @@
|
||||
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
||||
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
-struct Group{};struct Entity{};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
||||
+struct Group{};struct Prefab{string name="bmp";string GetPrefabName(){return name;}};
|
||||
+struct Entity{bool footprintValid=true,prefabPresent=true;Prefab prefab;Prefab*GetPrefabData(){return prefabPresent?&prefab:nullptr;}};
|
||||
+struct ERR_ConvoyResources{static string VehiclePrefab(int){return "bmp";}};
|
||||
+enum class ERR_ForceKind{CONVOY,GARRISON};
|
||||
+struct ERR_ForceRecord{struct Group*Group=nullptr;bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=14;string SourceId="convoy-vehicle-4";};
|
||||
+struct Ledger{Array<ERR_ForceRecord*>records;ERR_ForceRecord*FindGroup(Group*g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
|
||||
+struct ERR_InterceptVehicleFootprint{Entity*root=nullptr;bool Valid(){return root&&root->footprintValid;}bool Matches(Entity*e){return Valid()&&root==e;}static ERR_InterceptVehicleFootprint*Capture(Entity*e){if(!e||!e->footprintValid)return nullptr;auto*p=new ERR_InterceptVehicleFootprint();p->root=e;return p;}};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
||||
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
||||
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
||||
-struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";};
|
||||
+struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";bool Consumed=true;Array<int>Records={10,11,12,13,14,15};};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
||||
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
-struct ERR_ConvoyInterceptOrder{int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
||||
+struct ERR_ConvoyInterceptOrder{bool Armored=false;int ConfirmedLosses=0;ERR_InterceptVehicleFootprint*Footprint=nullptr;int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
||||
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
||||
-struct ERR_HQRoute{int result=0,calls=0,MessageId=0;string Payload;int Advance(float,Journal*){calls++;return result;}};
|
||||
-struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:nullptr);}};
|
||||
+struct ERR_HQRoute{int result=0,calls=0,MessageId=0;std::function<void()>onAdvance;string Payload;int Advance(float,Journal*){calls++;if(onAdvance)onAdvance();return result;}};
|
||||
+struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr,*third=nullptr;int losses=0;int ConfirmedLosses(){return losses;}int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:(third&&third->SourceId==id?third:nullptr));}};
|
||||
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
||||
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
||||
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
||||
-struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
+struct Mode{Ledger ledger;Ledger*Forces(){return &ledger;}struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
||||
@@ -64,14 +72,15 @@
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
||||
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
||||
-struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
||||
+struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0,armoredSelects=0;inline static bool armoredGeometry=true;inline static std::function<void()>onArmoredSelect;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
||||
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
||||
+ static bool SelectArmored(ERR_HQBelief*b,float,Array<vector>&reserved,ERR_InterceptVehicleFootprint*p,vector&dest){armoredSelects++;dest=b->destination;if(onArmoredSelect)onArmoredSelect();return armoredGeometry&&b->select&&p&&p->Valid()&&!reserved.Contains(dest);}
|
||||
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
||||
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
||||
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
||||
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
||||
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
||||
-struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;vector Destination;float Distance=0;};
|
||||
+struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;ERR_InterceptVehicleFootprint*Footprint=nullptr;vector Destination;float Distance=0;};
|
||||
struct HQ:Base{
|
||||
ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
|
||||
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
||||
@@ -81,8 +90,9 @@
|
||||
bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
-struct Fixture{Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
||||
+struct Fixture{ERR_ForceRecord record;Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
||||
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
||||
+ void armor(){vehicle.Index=4;task.Vehicles[0]=&unused;task.Vehicles[4]=&vehicle;unit.ForceSourceId=entry.SourceId=receipt.SourceId+"-vehicle-4";record.SourceId=unit.ForceSourceId;record.Group=&group;mode.ledger.records.Insert(&record);}
|
||||
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
||||
};
|
||||
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
||||
@@ -159,6 +169,47 @@
|
||||
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
||||
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
||||
|
||||
+ // Ordinary pairs remain preferred below the delivered armor threshold, even with a ready BMP.
|
||||
+ {PairFixture f;f.watch.losses=5;int before=ERR_HQMountedInterceptRules::armoredSelects;f.pair();assert(ERR_HQMountedInterceptRules::armoredSelects==before&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);}
|
||||
+ // Armor is a singleton alternative requiring six delivered losses; no survey at five.
|
||||
+ {Fixture f;f.armor();f.watch.losses=5;
|
||||
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);
|
||||
+ f.watch.losses=6;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->Armored&&f.h.m_MountedIntercept->VehicleIndex==4&&f.h.m_MountedIntercept->ConfirmedLosses==6);
|
||||
+ assert(!f.h.m_MountedInterceptPartner&&f.h.Assigned()==1&&f.h.m_MountedTokenCount==1&&f.service.reserves==1&&f.service.pairReserves==0&&f.service.applies==0);
|
||||
+ assert(f.h.m_MountedIntercept->Footprint->Matches(&f.entity));f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
+ // Current delivered threshold, real radio, exact slot, complete ammo and task locks precede armor geometry.
|
||||
+ for(int kind=0;kind<8;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
+ if(kind==0){f.ammo.Complete=false;}if(kind==1){f.entity.footprintValid=false;}if(kind==2){f.network.denied=&f.radio;}if(kind==3){f.task.Vehicles[4]=&f.unused;}
|
||||
+ if(kind==4){f.unused.SupplyToken=4;}if(kind==5){f.entry.LastDeliveredAt=9000;}if(kind==6){f.entry.Fit--;}if(kind==7){f.vehicle.Released=true;}
|
||||
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);}
|
||||
+ // A changed loss ledger even inside radio delivery cancels without a physical order.
|
||||
+ for(int kind=0;kind<2;kind++){Fixture f;f.armor();f.watch.losses=6;f.queued();f.route.result=1;
|
||||
+ if(kind==0)f.watch.losses=5;else f.route.onAdvance=[&]{f.watch.losses=5;};f.h.TickMountedIntercept(4000);assert(f.service.applies==0&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
+ // Failed BMP survey can fall back to a UAZ pair, but never exceeds the shared two queries.
|
||||
+ {PairFixture f,armor;armor.receipt.SourceId="second-convoy";armor.armor();
|
||||
+ f.mode.ledger.records.Insert(&armor.record);f.watch.third=&armor.entry;f.watch.losses=6;f.service.tasks.Insert(&armor.task);f.h.m_Units.Insert(&armor.unit);
|
||||
+ ERR_HQMountedInterceptRules::armoredGeometry=false;int a=ERR_HQMountedInterceptRules::armoredSelects,p=ERR_HQMountedInterceptRules::pairSelects,q=ERR_HQMountedInterceptRules::selects;
|
||||
+ f.plan();assert(f.h.m_MountedInterceptPartner&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);
|
||||
+ assert(ERR_HQMountedInterceptRules::armoredSelects-a==1&&ERR_HQMountedInterceptRules::pairSelects-p==1&&ERR_HQMountedInterceptRules::selects==q);ERR_HQMountedInterceptRules::armoredGeometry=true;}
|
||||
+ // No UAZ is surveyed on an armor-only failed pass; rotating tracks retain the same two-query cap.
|
||||
+ {Fixture f;f.armor();f.watch.losses=6;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second,&third};
|
||||
+ int a=ERR_HQMountedInterceptRules::armoredSelects,q=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::armoredSelects-a==2&&ERR_HQMountedInterceptRules::selects==q&&!f.h.m_MountedIntercept);
|
||||
+ f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.h.m_MountedTokenCount==1);}
|
||||
+
|
||||
+ // Same global token namespace and conditional cleanup apply to the armored singleton.
|
||||
+ {Fixture f;f.armor();f.watch.losses=6;f.h.m_MountedTokenCount=255;f.plan();assert(f.h.m_MountedIntercept->Token==400256&&!f.h.m_MountedInterceptPartner);f.h.Stop();assert(f.service.releases==1&&!f.unit.LogisticsReservation);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==1);}
|
||||
+ {Fixture f;f.armor();f.watch.losses=6;f.queued();f.unit.LogisticsReservation=991;f.h.Stop();assert(f.service.releases==1&&f.unit.LogisticsReservation==991&&!f.h.m_MountedIntercept);}
|
||||
+
|
||||
+ for(int kind=0;kind<5;kind++){Fixture f,later;f.armor();later.receipt.SourceId="later";later.armor();f.watch.losses=6;
|
||||
+ if(kind==0){f.entity.prefab.name="foreign";}if(kind==1){f.receipt.Consumed=false;}if(kind==2){f.receipt.Records[4]++;}if(kind==3){f.record.SourceId="wrong";}if(kind==4){f.record.Kind=ERR_ForceKind::GARRISON;}
|
||||
+ f.mode.ledger.records.Insert(&later.record);f.watch.second=&later.entry;f.service.tasks.Insert(&later.task);f.h.m_Units.Insert(&later.unit);
|
||||
+ int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&later.unit&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==1&&ERR_HQMountedInterceptRules::armoredSelects-before==1);}
|
||||
+
|
||||
+ // Native geometry may invalidate the previously usable original; nothing is claimed afterward.
|
||||
+ for(int kind=0;kind<6;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
+ ERR_HQMountedInterceptRules::onArmoredSelect=[&]{if(kind==0){f.watch.entry=nullptr;}if(kind==1){f.ammo.Complete=false;}if(kind==2){f.receipt.Consumed=false;}if(kind==3){f.entity.prefab.name="foreign";}if(kind==4){f.entity.footprintValid=false;}if(kind==5){f.unused.SupplyHeld=true;}};
|
||||
+ f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==0);ERR_HQMountedInterceptRules::onArmoredSelect=nullptr;}
|
||||
+
|
||||
}
|
||||
''')
|
||||
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"reviewer": "convoy_supply_pool",
|
||||
"scope": "Independent read-only HQ dispatch and geometry/service interface review; no execution or edits to proposal",
|
||||
"decision": "Approved reviewed final HQ source/test proposal; no concrete blocker",
|
||||
"complete": true,
|
||||
"HQ_source_sha256": "f62f8bb8fde222f3f47d89df1439083deb6f5b14633402a2afb7c6a97803582f",
|
||||
"HQ_test_sha256": "36c1d3dfc69d98b5b25d7c8f86a94a9d9df0745eddfef44cda378533181b7a97",
|
||||
"checks": [
|
||||
"Shared queries counter never exceeds two across armor, pair and singleton; rotating cursor advances examined belief.",
|
||||
"Armored candidate prefilters exact original consumed slot4 record, root prefab, source, fit original roster and complete fresh delivered ammunition before footprint capture.",
|
||||
"Below six Watch-confirmed delivered losses performs no armored road survey; threshold checked again after real downlink Advance receipt and before physical apply.",
|
||||
"BMP geometry failure can use remaining pre-admission UAZ budget; token/revision reservation refusal cannot fallback or spawn a replacement.",
|
||||
"Release detaches all owned job references before native service release; conditional token cleanup preserves unrelated owner.",
|
||||
"Footprint APIs match service. Private bounded original node/envelope capture is immutable; current exact root remeasurement cannot enlarge claim. Existing UAZ geometry/current behavior retained.",
|
||||
"Final native-survey boundary controls revoke Watch entry, ammo completeness, consumed receipt, exact prefab, footprint or sibling availability before any claim/token."
|
||||
],
|
||||
"geometry_draft_hashes": {
|
||||
"ERR_ConvoyPlacement.c": "c4a46189f3aa5b535d3f9c6d366fecc3c246d176029128ed0f131d8945b9836d",
|
||||
"ERR_HQMountedIntercept.c": "92dc6928e7a1ac64723cec8e89d027833045bfe6c114d1560513a7f204b5bf7c",
|
||||
"ERR_InterceptVehicleFootprint.c": "29fe896e551088af98ce85cc18a3319a5d2f90969064bb1cf2c93f38c1f89efa"
|
||||
},
|
||||
"limits": [
|
||||
"Source review does not execute native radio, footprint bounds or physical navigation.",
|
||||
"Offline HQ harness stubs geometry/service/native callbacks; separate service harness already tests actual service logic.",
|
||||
"No proof of actual BMP interception or route/arrival/heading performance.",
|
||||
"Geometry hashes are observed draft cross-review only, not a final geometry freeze or test signoff."
|
||||
],
|
||||
"native_or_compiler_run": false,
|
||||
"patch_sha256": "e73756f0906bb038f9ca96441b6d7d312e4b377e7d0255ccf645da35bce497fa",
|
||||
"author_result_sha256": "668b18711da7378b9dcd31a993e82d0c7392d3bef17a1bcfcb7fde225a93741b",
|
||||
"reviewer_execution": false,
|
||||
"author_execution_evidence": {
|
||||
"baseline": "compiled then runtime SIGABRT",
|
||||
"proposed": "pass",
|
||||
"six_to_five_threshold_mutation": "compiled then runtime SIGABRT",
|
||||
"log_hashes": {
|
||||
"redundant-candidate-guard-mutation.log": "0ef29fa159293cac8ba289a719e5741ed398b432efb94ebbb4dfbd5708a5b5bb",
|
||||
"mutation-test.log": "d3cf0b93143f44980d5ec89a8bd074072c528cabeb3eab6ea18aaf2dcd509a65",
|
||||
"baseline-test.log": "62b2a0009d8c8880128d3e0914263632cb80a769c68d5a3c3932f0b3051e95ae",
|
||||
"proposed-test.log": "0ef29fa159293cac8ba289a719e5741ed398b432efb94ebbb4dfbd5708a5b5bb",
|
||||
"proposal-first.log": "e4cf67c6ec071ea059f573ff9e5ac3e81e82234d81c09ad262a6cb73fa786c3a"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-kyryav09/check.cpp:536: int main(): Assertion `!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-bmp-dispatch-1406/tests/test_hq_mounted_dispatch.py", line 27, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<184 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-kyryav09/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,32 @@
|
||||
/tmp/err-policy-h34veohi/check.cpp: In function ‘int main()’:
|
||||
/tmp/err-policy-h34veohi/check.cpp:519:2: error: this ‘if’ clause does not guard... [-Werror=misleading-indentation]
|
||||
519 | if(kind==0)f.ammo.Complete=false;if(kind==1)f.entity.footprintValid=false;if(kind==2)f.network.denied=&f.radio;if(kind==3)f.task.Vehicles[4]=&f.unused;
|
||||
| ^~
|
||||
/tmp/err-policy-h34veohi/check.cpp:519:35: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
|
||||
519 | if(kind==0)f.ammo.Complete=false;if(kind==1)f.entity.footprintValid=false;if(kind==2)f.network.denied=&f.radio;if(kind==3)f.task.Vehicles[4]=&f.unused;
|
||||
| ^~
|
||||
/tmp/err-policy-h34veohi/check.cpp:520:2: error: this ‘if’ clause does not guard... [-Werror=misleading-indentation]
|
||||
520 | if(kind==4)f.unused.SupplyToken=4;if(kind==5)f.entry.LastDeliveredAt=9000;if(kind==6)f.entry.Fit--;if(kind==7)f.vehicle.Released=true;
|
||||
| ^~
|
||||
/tmp/err-policy-h34veohi/check.cpp:520:36: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
|
||||
520 | if(kind==4)f.unused.SupplyToken=4;if(kind==5)f.entry.LastDeliveredAt=9000;if(kind==6)f.entry.Fit--;if(kind==7)f.vehicle.Released=true;
|
||||
| ^~
|
||||
cc1plus: all warnings being treated as errors
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-bmp-dispatch-1406/tests/test_hq_mounted_dispatch.py", line 27, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<162 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-h34veohi/check.cpp', '-o', '/tmp/err-policy-h34veohi/check']' returned non-zero exit status 1.
|
||||
@@ -0,0 +1 @@
|
||||
actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
// Detached planning choice; the route remains strongly owned before admission.
|
||||
class ERR_HQMountedChoice
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
ERR_ConvoyTask Task;
|
||||
ERR_ConvoyVehicle Escort;
|
||||
ref ERR_HQRoute Route;
|
||||
ref ERR_InterceptVehicleFootprint Footprint;
|
||||
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_MountedIntercept.Armored) result += "\nOriginal BMP singleton; delivered confirmed losses " + m_MountedIntercept.ConfirmedLosses.ToString();
|
||||
if (m_MountedInterceptPartner) result += "\nPaired mounted interception (two original escorts)";
|
||||
return result + "\n" + string.Format("Mounted interception: escort %1; %2; report M%3; road %4", m_MountedIntercept.VehicleIndex, phase, m_MountedIntercept.ReportId, m_MountedIntercept.Position);
|
||||
}
|
||||
|
||||
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 (order.Armored && (m_Watch.ConfirmedLosses() < 6 || order.ConfirmedLosses < 6)) return false;
|
||||
if (unit.LogisticsReservation != order.Token || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)) return false;
|
||||
return m_Convoys && m_Convoys.InterceptCurrent(m_MountedInterceptTask, order);
|
||||
}
|
||||
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; }
|
||||
// Radio delivery may synchronously change the current mission knowledge.
|
||||
if (order.Armored && (m_Watch.ConfirmedLosses() < 6 || partner))
|
||||
{ ReleaseMountedIntercept(now, "armored escalation no longer authorized"); return; }
|
||||
bool applied;
|
||||
if (partner) applied = m_Convoys.ApplyInterceptPair(m_MountedInterceptTask, order, partner, now);
|
||||
else applied = m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now);
|
||||
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 MountedArmoredOriginal(ERR_ConvoyTask task, ERR_ConvoyVehicle vehicle)
|
||||
{
|
||||
if (!task || task.Vehicles.Count() != 6 || !task.Receipt || !task.Receipt.Consumed
|
||||
|| task.Receipt.Records.Count() <= 4 || task.Vehicles[4] != vehicle || !vehicle || vehicle.Index != 4
|
||||
|| !vehicle.Entity || !vehicle.Entity.GetPrefabData() || !m_Mode.Forces()) return false;
|
||||
if (vehicle.Entity.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.VehiclePrefab(4)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(vehicle.Group);
|
||||
return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
|
||||
&& record.Id == task.Receipt.Records[4] && record.Group == vehicle.Group
|
||||
&& record.SourceId == task.Receipt.SourceId + "-vehicle-4";
|
||||
}
|
||||
protected ERR_HQMountedChoice MountedArmoredCandidate(float now)
|
||||
{
|
||||
// Cumulative delivered/reconciled original losses only, never a physical scan.
|
||||
if (m_Watch.ConfirmedLosses() < 6) return null;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
ERR_ConvoyVehicle vehicle = task.Vehicles[4];
|
||||
if (!MountedArmoredOriginal(task, vehicle) || vehicle.Released || vehicle.Arrived || !vehicle.Registered) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || unit.ForceSourceId != task.Receipt.SourceId + "-vehicle-4" || !MountedLogisticsEligible(unit)
|
||||
|| ConvoyContact(unit, now) || !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ref ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) continue;
|
||||
ref ERR_InterceptVehicleFootprint footprint = ERR_InterceptVehicleFootprint.Capture(vehicle.Entity);
|
||||
if (!footprint || !footprint.Valid() || !footprint.Matches(vehicle.Entity)) continue;
|
||||
ref ERR_HQMountedChoice choice = new ERR_HQMountedChoice();
|
||||
choice.Unit = unit; choice.Task = task; choice.Escort = vehicle; choice.Route = route; choice.Footprint = footprint;
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// 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.Escort = 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.Escort.Index; order.Position = choice.Destination;
|
||||
if (choice.Escort.Index == 4)
|
||||
{ order.Armored = true; order.Footprint = choice.Footprint; order.ConfirmedLosses = m_Watch.ConfirmedLosses(); }
|
||||
order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
return order;
|
||||
}
|
||||
protected void QueueMountedIntercept(ERR_HQMountedChoice choice, ERR_HQMountedChoice partnerChoice, ERR_HQBelief belief, float now)
|
||||
{
|
||||
if (choice && choice.Escort.Index == 4 && (partnerChoice || m_Watch.ConfirmedLosses() < 6
|
||||
|| !choice.Footprint || !choice.Footprint.Valid() || !choice.Footprint.Matches(choice.Escort.Entity))) return;
|
||||
int members = 1;
|
||||
if (partnerChoice) members = 2;
|
||||
if (m_MountedTokenCount + members > 256) return;
|
||||
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) return;
|
||||
ref ERR_HQMountedChoice armored = MountedArmoredCandidate(now);
|
||||
bool escortPossible = HasMountedInterceptCandidate(now);
|
||||
if (!armored && !escortPossible) return;
|
||||
bool pairPossible = freeGlobal >= 2 && freeHunt >= 2 && m_MountedTokenCount <= 254 && HasMountedPairCandidate(now);
|
||||
array<vector> reserved = {}; int first = -1; int second = -1;
|
||||
foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second);
|
||||
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 (armored && m_Watch.ConfirmedLosses() >= 6)
|
||||
{
|
||||
vector armoredPosition; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectArmored(belief, now, reserved, armored.Footprint, armoredPosition))
|
||||
{
|
||||
ERR_HQWatchEntry armoredStatus = m_Watch.Find(armored.Unit.ForceSourceId);
|
||||
if (!armoredStatus || !MountedInterceptTaskAvailable(armored.Task)
|
||||
|| !MountedArmoredOriginal(armored.Task, armored.Escort)
|
||||
|| !MountedInterceptStatus(armored.Unit, armored.Escort, now)
|
||||
|| !armored.Footprint.Matches(armored.Escort.Entity)) continue;
|
||||
float armoredDistance = vector.DistanceXZ(armoredStatus.Position, armoredPosition);
|
||||
if (ERR_ForceRules.Finite(armoredDistance) && armoredDistance <= m_Mode.Settings().Runtime("responseRadiusMetres"))
|
||||
{
|
||||
armored.Destination = armoredPosition; armored.Distance = armoredDistance;
|
||||
QueueMountedIntercept(armored, null, belief, now); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pairPossible && queries < 2)
|
||||
{
|
||||
vector positionA; vector positionB; queries++;
|
||||
if (ERR_HQMountedInterceptRules.SelectPair(belief, now, reserved, positionA, positionB))
|
||||
{
|
||||
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 || !escortPossible) 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();
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
"""Execute actual mounted HQ dispatch methods; radio/physical service are adversarial doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
||||
names=['protected bool MountedArmoredOriginal(', 'protected ERR_HQMountedChoice MountedArmoredCandidate(', 'protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
||||
for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice','ERR_InterceptVehicleFootprint','ERR_ForceRecord']:
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
||||
for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string','ERR_InterceptVehicleFootprint','ERR_ConvoyResources','ERR_ForceKind']:
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute','footprint','armored','armoredStatus','record']:
|
||||
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
||||
for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt','Footprint','Entity']:
|
||||
s=s.replace('->'+name+'.','->'+name+'->')
|
||||
s=s.replace('choice->Escort.Entity','choice->Escort->Entity').replace('armored->Unit.ForceSourceId','armored->Unit->ForceSourceId').replace('armored->Escort.Entity','armored->Escort->Entity')
|
||||
s=s.replace('m_MountedIntercept->ConfirmedLosses.ToString()', 'std::to_string(m_MountedIntercept->ConfirmedLosses)')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->').replace('Forces().','Forces()->').replace('GetPrefabData().','GetPrefabData()->')
|
||||
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
||||
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
||||
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
||||
s=s.replace('Array<vector> reserved,','Array<vector>& reserved,')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,n)) for n in names if n in P.read_text())
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){} template<class...T>static string Format(const char*fmt,T...v){std::ostringstream out;out<<fmt;((out<<"|"<<v),...);return out.str();}};
|
||||
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
||||
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Group{};struct Prefab{string name="bmp";string GetPrefabName(){return name;}};
|
||||
struct Entity{bool footprintValid=true,prefabPresent=true;Prefab prefab;Prefab*GetPrefabData(){return prefabPresent?&prefab:nullptr;}};
|
||||
struct ERR_ConvoyResources{static string VehiclePrefab(int){return "bmp";}};
|
||||
enum class ERR_ForceKind{CONVOY,GARRISON};
|
||||
struct ERR_ForceRecord{struct Group*Group=nullptr;bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=14;string SourceId="convoy-vehicle-4";};
|
||||
struct Ledger{Array<ERR_ForceRecord*>records;ERR_ForceRecord*FindGroup(Group*g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
|
||||
struct ERR_InterceptVehicleFootprint{Entity*root=nullptr;bool Valid(){return root&&root->footprintValid;}bool Matches(Entity*e){return Valid()&&root==e;}static ERR_InterceptVehicleFootprint*Capture(Entity*e){if(!e||!e->footprintValid)return nullptr;auto*p=new ERR_InterceptVehicleFootprint();p->root=e;return p;}};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
||||
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
||||
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
||||
struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";bool Consumed=true;Array<int>Records={10,11,12,13,14,15};};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
||||
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct ERR_ConvoyInterceptOrder{bool Armored=false;int ConfirmedLosses=0;ERR_InterceptVehicleFootprint*Footprint=nullptr;int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
||||
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
||||
struct ERR_HQRoute{int result=0,calls=0,MessageId=0;std::function<void()>onAdvance;string Payload;int Advance(float,Journal*){calls++;if(onAdvance)onAdvance();return result;}};
|
||||
struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr,*third=nullptr;int losses=0;int ConfirmedLosses(){return losses;}int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:(third&&third->SourceId==id?third:nullptr));}};
|
||||
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
||||
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
||||
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
||||
struct Mode{Ledger ledger;Ledger*Forces(){return &ledger;}struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
||||
int Count(){return tasks.Count();}ERR_ConvoyTask*At(int n){return tasks[n];}
|
||||
bool InterceptCurrent(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*){return current;}
|
||||
bool ReserveIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){reserves++;return reserve;}
|
||||
bool ApplyIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){applies++;return apply;}
|
||||
void ReleaseIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){releases++;if(onRelease)onRelease();}
|
||||
};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
||||
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
||||
struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0,armoredSelects=0;inline static bool armoredGeometry=true;inline static std::function<void()>onArmoredSelect;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
||||
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
||||
static bool SelectArmored(ERR_HQBelief*b,float,Array<vector>&reserved,ERR_InterceptVehicleFootprint*p,vector&dest){armoredSelects++;dest=b->destination;if(onArmoredSelect)onArmoredSelect();return armoredGeometry&&b->select&&p&&p->Valid()&&!reserved.Contains(dest);}
|
||||
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
||||
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
||||
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
||||
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
||||
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
||||
struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;ERR_InterceptVehicleFootprint*Footprint=nullptr;vector Destination;float Distance=0;};
|
||||
struct HQ:Base{
|
||||
ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
|
||||
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
||||
bool m_MountedInterceptReceipt=false,m_MountedInterceptPartnerReceipt=false;int m_MountedTokenCount=0;
|
||||
int m_MountedSurveyCursor=0;
|
||||
Array<int>m_MountedAttemptBeliefs,m_MountedAttemptRevisions;Array<ERR_HQUnit*>m_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network;
|
||||
bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{ERR_ForceRecord record;Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
||||
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
||||
void armor(){vehicle.Index=4;task.Vehicles[0]=&unused;task.Vehicles[4]=&vehicle;unit.ForceSourceId=entry.SourceId=receipt.SourceId+"-vehicle-4";record.SourceId=unit.ForceSourceId;record.Group=&group;mode.ledger.records.Insert(&record);}
|
||||
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
||||
};
|
||||
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
||||
PairFixture(){unit2=unit;unit2.Group=&group2;unit2.Radio=&radio2;unit2.StatusSampler=&sampler2;unit2.ForceSourceId="partner";entry2=entry;entry2.SourceId="partner";entry2.Position={200,0};watch.second=&entry2;vehicle2=vehicle;vehicle2.Index=3;vehicle2.Group=&group2;vehicle2.Entity=&entity2;task.Vehicles[3]=&vehicle2;h.m_Units.Insert(&unit2);network.routes[&radio2]=&route2;ERR_HQMountedInterceptRules::pairGeometry=true;}
|
||||
void pair(){plan();assert(h.m_MountedIntercept&&h.m_MountedInterceptPartner);assert(h.m_MountedInterceptTask==&task);assert(unit.LogisticsReservation==400001&&unit2.LogisticsReservation==400002);assert(service.pairReserves==1&&service.reserves==0&&service.pairApplies==0);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;assert(f.h.Status()=="base HQ status");f.queued();string queued=f.h.Status();assert(queued.find("base HQ status")!=string::npos&&queued.find("|0|queued|20|100,0")!=string::npos);f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.Status().find("|0|delivered|20|100,0")!=string::npos);f.h.Stop();assert(f.h.Status()=="base HQ status");}
|
||||
{Fixture f;f.queued();assert(f.h.Assigned()==1&&f.h.HuntingAssigned()==1);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);assert(r.Count()==1&&r[0]==f.belief.destination);f.h.PlanMountedIntercept(4000);assert(f.service.reserves==1);f.h.TickMountedIntercept(4000);assert(f.route.calls==1&&f.service.applies==0);f.route.result=1;f.h.TickMountedIntercept(5000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.belief.BeliefRevision++;f.h.TickMountedIntercept(6000);assert(f.h.m_MountedInterceptDelivered&&f.service.releases==0);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&f.unit.LogisticsReservation==0);}
|
||||
// Task-wide logistics and queued sibling downlinks cannot consume a revision or starve a free farther convoy.
|
||||
for(int kind=0;kind<5;kind++){Fixture f,far;far.unit.ForceSourceId=far.entry.SourceId="crew2";far.receipt.SourceId="convoy2";far.entry.Position={-200,0};f.watch.second=&far.entry;f.service.tasks.Insert(&far.task);f.h.m_Units.Insert(&far.unit);ERR_HQUnit sibling; sibling.Group=&f.otherGroup;f.unused.Group=&f.otherGroup;f.h.m_Units.Insert(&sibling);if(kind==0)f.unused.SupplyToken=123;if(kind==1)f.unused.SupplyHeld=true;if(kind==2)sibling.PendingConvoyOrder=true;if(kind==3)sibling.Downlink=true;if(kind==4)sibling.LogisticsReservation=123;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&far.unit&&!f.unit.LogisticsReservation&&far.unit.LogisticsReservation==400001&&f.service.reserves==1);}
|
||||
// A disconnected nearest escort cannot starve the farther reachable original escort.
|
||||
{Fixture f;Group group2;Entity radio2,entity2;ERR_HQUnit u2=f.unit;u2.Group=&group2;u2.Radio=&radio2;u2.ForceSourceId="crew2";ERR_HQWatchEntry entry2=f.entry;entry2.SourceId="crew2";entry2.Position={-200,0};f.watch.second=&entry2;ERR_ConvoyVehicle vehicle2=f.vehicle;vehicle2.Group=&group2;vehicle2.Entity=&entity2;vehicle2.Index=3;f.task.Vehicles[3]=&vehicle2;f.h.m_Units.Insert(&u2);f.network.denied=&f.radio;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&u2&&f.h.m_MountedIntercept->VehicleIndex==3&&f.network.calls>=2&&!f.unit.LogisticsReservation&&u2.LogisticsReservation==400001);}
|
||||
// Invalid world clock must preserve the exact claim for the next finite tick.
|
||||
{Fixture f;f.queued();f.h.TickMountedIntercept(std::nanf(""));assert(f.h.m_MountedIntercept&&f.unit.LogisticsReservation==400001&&f.service.releases==0&&f.route.calls==0);}
|
||||
// No own vehicle/fresh crew evidence means no native road surveys.
|
||||
for(int kind=0;kind<6;kind++){Fixture f;if(kind==0)f.service.tasks.clear();if(kind==1)f.task.Stopped=true;if(kind==2)f.unit.LogisticsReservation=1;if(kind==3)f.entry.Closed=true;if(kind==4)f.sampler.bound=false;if(kind==5)f.combat.Held=true;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&f.service.reserves==0);}
|
||||
// Interrupted pending claims never apply physical orders, and the same delivered revision cannot retry.
|
||||
{Fixture f;f.queued();f.route.result=-1;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.applies==0);f.plan();assert(f.service.reserves==1);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==2&&f.unit.LogisticsReservation==400002);}
|
||||
{Fixture f;f.queued();f.belief.BeliefRevision++;f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.route.calls==0&&f.service.applies==0&&!f.h.m_MountedIntercept);}
|
||||
// Physical validation changes after radio delivery cannot trigger a stale order.
|
||||
for(int kind=0;kind<7;kind++){Fixture f;f.queued();f.route.result=1;if(kind==0)f.ammo.Complete=false;if(kind==1)f.ammo.valid=false;if(kind==2)f.sampler.bound=false;if(kind==3)f.entry.Fit--;if(kind==4)f.entry.LastDeliveredAt=9000;if(kind==5)f.unit.contact=true;if(kind==6)f.service.apply=false;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation);assert(f.service.applies==(kind==6?1:0));}
|
||||
// Conditional release and recursive unregister preserve unrelated reservations.
|
||||
{Fixture f;f.queued();f.unit.LogisticsReservation=123;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept);f.h.ReleaseUnit(&f.unit,4000);};f.h.TickMountedIntercept(4000);assert(f.unit.LogisticsReservation==123&&f.service.releases==1&&f.h.releaseUnits==1);}
|
||||
{Fixture f;f.queued();f.h.Stop();assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1&&f.h.stops==1&&f.service.applies==0);}
|
||||
// Ordinary assigned and pursuit capacities reserve space before any road query or radio action.
|
||||
for(int kind=0;kind<3;kind++){Fixture f;if(kind==0)f.mode.settings.cap=0;if(kind==1)f.h.assigned=3;if(kind==2)f.h.hunting=3;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&ERR_HQMountedInterceptRules::selects==before);}
|
||||
{Fixture f;f.queued();f.h.assigned=3;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&f.service.applies==0);}
|
||||
// Admission is based on detached delivered position, not physical entity telemetry; stock/crew/roles guard it.
|
||||
for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);}
|
||||
{Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);assert(f.h.m_MountedAttemptBeliefs.empty());int after=ERR_HQMountedInterceptRules::selects;f.h.PlanMountedIntercept(8000);assert(ERR_HQMountedInterceptRules::selects-after==1);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.service.reserves==1);}
|
||||
// Cursor normalization handles track expiry/removal without out-of-range access.
|
||||
{Fixture f;ERR_HQBelief second;second.BeliefId=2;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second};f.plan();assert(!f.h.m_MountedIntercept);f.h.m_MountedSurveyCursor=31;f.memory.entries={&f.belief};f.belief.select=true;f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==1);}
|
||||
// Skipped ineligible/already-attempted tracks do not spend the two-road-query budget.
|
||||
{Fixture f;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.eligible=false;f.h.m_MountedAttemptBeliefs.Insert(2);f.h.m_MountedAttemptRevisions.Insert(second.BeliefRevision);f.memory.entries={&f.belief,&second,&third};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==1&&f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3);}
|
||||
// No eligible reports and no reports both produce zero native road queries.
|
||||
{Fixture f;f.belief.eligible=false;int before=ERR_HQMountedInterceptRules::selects;f.plan();f.memory.entries.clear();f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
// Full bounded memory: every pass has at most two surveys, no failed revision is consumed.
|
||||
{Fixture f;ERR_HQBelief beliefs[64];f.memory.entries.clear();for(int i=0;i<64;i++){beliefs[i].BeliefId=i+1;beliefs[i].select=false;f.memory.entries.Insert(&beliefs[i]);}for(int pass=0;pass<32;pass++){int before=ERR_HQMountedInterceptRules::selects;float now=3000+pass*5000;f.entry.ObservedAt=f.entry.LastDeliveredAt=now;f.h.PlanMountedIntercept(now);assert(ERR_HQMountedInterceptRules::selects-before==2&&f.h.m_MountedSurveyCursor==((pass+1)*2)%64&&f.h.m_MountedAttemptBeliefs.empty());}assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
|
||||
// Paired admission counts and reserves both original slots; one receipt never detours either.
|
||||
{PairFixture f;f.pair();assert(f.h.Assigned()==2&&f.h.HuntingAssigned()==2);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);f.h.ReserveUnit(&f.unit2,r,a,b);assert(r.Count()==2&&vector::DistanceXZ(r[0],r[1])>=40);assert(f.h.Status().find("Paired")!=string::npos);
|
||||
f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptReceipt&&!f.h.m_MountedInterceptPartnerReceipt&&!f.h.m_MountedInterceptDelivered&&f.service.pairApplies==0&&f.service.applies==0);f.h.TickMountedIntercept(4500);assert(f.route.calls==1&&f.route2.calls==2&&f.h.m_MountedInterceptRoute==&f.route&&f.h.m_MountedInterceptPartnerRoute==&f.route2);
|
||||
f.route2.result=1;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&f.h.m_MountedInterceptDelivered&&f.h.m_MountedIntercept->ExpiresAt==121000&&f.h.m_MountedInterceptPartner->ExpiresAt==121000);f.h.TickMountedIntercept(6000);assert(f.service.pairApplies==1);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// Either receipt may arrive first; interrupted/expired/revised/changed ownership never creates half a pair.
|
||||
for(int kind=0;kind<9;kind++){PairFixture f;f.pair();f.route2.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptPartnerReceipt&&f.service.pairApplies==0);float now=5000;
|
||||
if(kind==0)f.route.result=-1;
|
||||
if(kind==1)now=121000;
|
||||
if(kind==2)f.belief.BeliefRevision++;
|
||||
if(kind==3)f.unit2.LogisticsReservation=999;
|
||||
if(kind==4)f.unit2.contact=true;
|
||||
if(kind==5)f.entry2.Fit--;
|
||||
if(kind==6)f.sampler2.bound=false;
|
||||
if(kind==7)f.h.assigned=2;
|
||||
if(kind==8)now=34000;
|
||||
if(kind!=0)f.route.result=1;
|
||||
f.h.TickMountedIntercept(now);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairApplies==0&&f.service.applies==0&&!f.unit.LogisticsReservation);assert(f.unit2.LogisticsReservation==(kind==3?999:0));}
|
||||
// Native preparation failure settles BOTH claims. No retry or singleton replacement after commitment.
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.apply=false;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0);}
|
||||
{PairFixture f;f.service.reserve=false;f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.h.m_MountedTokenCount==2);f.service.reserve=true;f.plan();assert(f.service.pairReserves==1);f.belief.BeliefRevision++;f.plan();assert(f.h.m_MountedIntercept->Token==400003&&f.h.m_MountedInterceptPartner->Token==400004);}
|
||||
// Stop/reentrant unregister detach both owned handles before external release; unrelated claims survive.
|
||||
{PairFixture f;f.pair();f.unit2.LogisticsReservation=999;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner);f.h.ReleaseUnit(&f.unit2,5000);};f.h.Stop();f.h.Stop();assert(f.service.releases==2&&!f.unit.LogisticsReservation&&f.unit2.LogisticsReservation==999);}
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.onApply=[&]{f.h.Stop();};f.h.TickMountedIntercept(5000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptDelivered&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);}
|
||||
// Two free slots and two IDs are required for pair. Singleton fallback occurs only before claims.
|
||||
for(int kind=0;kind<5;kind++){PairFixture f;if(kind==0)f.h.assigned=2;if(kind==1)f.h.hunting=2;if(kind==2)f.h.m_MountedTokenCount=255;if(kind==3)f.network.denied=&f.radio2;if(kind==4)ERR_HQMountedInterceptRules::pairGeometry=false;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.reserves==1&&f.service.pairReserves==0);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before<=2);}
|
||||
{PairFixture f;f.h.m_MountedTokenCount=256;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects==before);}
|
||||
// A valid reversed assignment must win over greedy singleton fallback (same bounded road survey).
|
||||
{PairFixture f;f.mode.settings.radius=800;f.belief.destination={0,0};f.entry.Position={0,0};f.entry2.Position={-800,0};int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedInterceptPartner&&f.h.m_MountedInterceptUnit==&f.unit2&&f.h.m_MountedInterceptPartnerUnit==&f.unit);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==1);}
|
||||
// Stop during reservation cannot resurrect a job after external callback returns.
|
||||
{PairFixture f;f.service.onReserve=[&]{f.h.Stop();};f.plan();assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// No mixed-convoy pair; both geometric failures still cost at most two queries, no attempts/tokens.
|
||||
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
||||
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
||||
|
||||
// Ordinary pairs remain preferred below the delivered armor threshold, even with a ready BMP.
|
||||
{PairFixture f;f.watch.losses=5;int before=ERR_HQMountedInterceptRules::armoredSelects;f.pair();assert(ERR_HQMountedInterceptRules::armoredSelects==before&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);}
|
||||
// Armor is a singleton alternative requiring six delivered losses; no survey at five.
|
||||
{Fixture f;f.armor();f.watch.losses=5;
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);
|
||||
f.watch.losses=6;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->Armored&&f.h.m_MountedIntercept->VehicleIndex==4&&f.h.m_MountedIntercept->ConfirmedLosses==6);
|
||||
assert(!f.h.m_MountedInterceptPartner&&f.h.Assigned()==1&&f.h.m_MountedTokenCount==1&&f.service.reserves==1&&f.service.pairReserves==0&&f.service.applies==0);
|
||||
assert(f.h.m_MountedIntercept->Footprint->Matches(&f.entity));f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
// Current delivered threshold, real radio, exact slot, complete ammo and task locks precede armor geometry.
|
||||
for(int kind=0;kind<8;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
if(kind==0){f.ammo.Complete=false;}if(kind==1){f.entity.footprintValid=false;}if(kind==2){f.network.denied=&f.radio;}if(kind==3){f.task.Vehicles[4]=&f.unused;}
|
||||
if(kind==4){f.unused.SupplyToken=4;}if(kind==5){f.entry.LastDeliveredAt=9000;}if(kind==6){f.entry.Fit--;}if(kind==7){f.vehicle.Released=true;}
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);}
|
||||
// A changed loss ledger even inside radio delivery cancels without a physical order.
|
||||
for(int kind=0;kind<2;kind++){Fixture f;f.armor();f.watch.losses=6;f.queued();f.route.result=1;
|
||||
if(kind==0)f.watch.losses=5;else f.route.onAdvance=[&]{f.watch.losses=5;};f.h.TickMountedIntercept(4000);assert(f.service.applies==0&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
// Failed BMP survey can fall back to a UAZ pair, but never exceeds the shared two queries.
|
||||
{PairFixture f,armor;armor.receipt.SourceId="second-convoy";armor.armor();
|
||||
f.mode.ledger.records.Insert(&armor.record);f.watch.third=&armor.entry;f.watch.losses=6;f.service.tasks.Insert(&armor.task);f.h.m_Units.Insert(&armor.unit);
|
||||
ERR_HQMountedInterceptRules::armoredGeometry=false;int a=ERR_HQMountedInterceptRules::armoredSelects,p=ERR_HQMountedInterceptRules::pairSelects,q=ERR_HQMountedInterceptRules::selects;
|
||||
f.plan();assert(f.h.m_MountedInterceptPartner&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);
|
||||
assert(ERR_HQMountedInterceptRules::armoredSelects-a==1&&ERR_HQMountedInterceptRules::pairSelects-p==1&&ERR_HQMountedInterceptRules::selects==q);ERR_HQMountedInterceptRules::armoredGeometry=true;}
|
||||
// No UAZ is surveyed on an armor-only failed pass; rotating tracks retain the same two-query cap.
|
||||
{Fixture f;f.armor();f.watch.losses=6;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second,&third};
|
||||
int a=ERR_HQMountedInterceptRules::armoredSelects,q=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::armoredSelects-a==2&&ERR_HQMountedInterceptRules::selects==q&&!f.h.m_MountedIntercept);
|
||||
f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.h.m_MountedTokenCount==1);}
|
||||
|
||||
// Same global token namespace and conditional cleanup apply to the armored singleton.
|
||||
{Fixture f;f.armor();f.watch.losses=6;f.h.m_MountedTokenCount=255;f.plan();assert(f.h.m_MountedIntercept->Token==400256&&!f.h.m_MountedInterceptPartner);f.h.Stop();assert(f.service.releases==1&&!f.unit.LogisticsReservation);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==1);}
|
||||
{Fixture f;f.armor();f.watch.losses=6;f.queued();f.unit.LogisticsReservation=991;f.h.Stop();assert(f.service.releases==1&&f.unit.LogisticsReservation==991&&!f.h.m_MountedIntercept);}
|
||||
|
||||
for(int kind=0;kind<5;kind++){Fixture f,later;f.armor();later.receipt.SourceId="later";later.armor();f.watch.losses=6;
|
||||
if(kind==0){f.entity.prefab.name="foreign";}if(kind==1){f.receipt.Consumed=false;}if(kind==2){f.receipt.Records[4]++;}if(kind==3){f.record.SourceId="wrong";}if(kind==4){f.record.Kind=ERR_ForceKind::GARRISON;}
|
||||
f.mode.ledger.records.Insert(&later.record);f.watch.second=&later.entry;f.service.tasks.Insert(&later.task);f.h.m_Units.Insert(&later.unit);
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&later.unit&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==1&&ERR_HQMountedInterceptRules::armoredSelects-before==1);}
|
||||
|
||||
// Native geometry may invalidate the previously usable original; nothing is claimed afterward.
|
||||
for(int kind=0;kind<6;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
ERR_HQMountedInterceptRules::onArmoredSelect=[&]{if(kind==0){f.watch.entry=nullptr;}if(kind==1){f.ammo.Complete=false;}if(kind==2){f.receipt.Consumed=false;}if(kind==3){f.entity.prefab.name="foreign";}if(kind==4){f.entity.footprintValid=false;}if(kind==5){f.unused.SupplyHeld=true;}};
|
||||
f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==0);ERR_HQMountedInterceptRules::onArmoredSelect=nullptr;}
|
||||
|
||||
}
|
||||
''')
|
||||
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|
||||
+1
@@ -0,0 +1 @@
|
||||
actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"complete": true,
|
||||
"source_manifest": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c": {
|
||||
"before": "cee3ae301ac2f211c290450b644a7942c9786a2a40d06f16e2a47efa96fd15b6",
|
||||
"proposed": "f62f8bb8fde222f3f47d89df1439083deb6f5b14633402a2afb7c6a97803582f",
|
||||
"index": null
|
||||
},
|
||||
"tests/test_hq_mounted_dispatch.py": {
|
||||
"before": "3eb793719b21d508ffc294f5a1bf5c5153f5717a1408a8f779f0a4c1ddefe2a6",
|
||||
"proposed": "36c1d3dfc69d98b5b25d7c8f86a94a9d9df0745eddfef44cda378533181b7a97",
|
||||
"index": null
|
||||
}
|
||||
},
|
||||
"patch_sha256": "e73756f0906bb038f9ca96441b6d7d312e4b377e7d0255ccf645da35bce497fa",
|
||||
"tests": {
|
||||
"baseline": {
|
||||
"compiled": true,
|
||||
"result": "runtime SIGABRT at first six-loss BMP positive assertion",
|
||||
"log": "baseline-test.log"
|
||||
},
|
||||
"proposed": {
|
||||
"result": "passed all original and added actual-method assertions",
|
||||
"log": "proposed-test.log"
|
||||
},
|
||||
"mutation": {
|
||||
"change": "Policy threshold lowered from six to five at all authoritative dispatcher checks",
|
||||
"compiled": true,
|
||||
"result": "runtime SIGABRT at five-loss no-survey/no-claim assertion",
|
||||
"log": "mutation-test.log"
|
||||
},
|
||||
"earlier_redundant_guard_control": "Changing candidate helper alone to five is still rejected by planner six-loss guard; preserved redundant-candidate-guard-mutation.log."
|
||||
},
|
||||
"contract": [
|
||||
"Exact consumed original slot4 receipt, force record, source, group and BMP prefab checked before footprint capture/survey.",
|
||||
"At least six delivered Watch losses before survey, queue and after ordinary radio Advance; no direct physical loss scan.",
|
||||
"One existing BMP singleton alternative, no BMP pair; same token allocator, claim count, fixed expiry and whole-task exclusion.",
|
||||
"One hull footprint is selected deterministically in fleet order and retained; its survey cannot be reused for another hull.",
|
||||
"Armor and unchanged UAZ fallbacks share two queries per pass and rotating belief cursor; no fallback after committed revision/token.",
|
||||
"After native survey revalidate watch entry, complete fresh ammo, task/original identity and footprint.",
|
||||
"Existing original singleton/pair tests retained; invalid earlier BMP falls through to valid later original; max token and conditional Stop cleanup tested."
|
||||
],
|
||||
"limits": [
|
||||
"Geometry/footprint/native service and cumulative Watch knowledge are explicit boundary doubles; production dispatcher methods execute unchanged after syntax translation. Their independent actual-method suites are separately owned.",
|
||||
"No claim physical BMP arrival, exact road navigability, native API compilation, multiplayer or FPS.",
|
||||
"No live successor/original changes or native processes; focused policy compiler work now finished."
|
||||
],
|
||||
"logs_sha256": {
|
||||
"redundant-candidate-guard-mutation.log": "0ef29fa159293cac8ba289a719e5741ed398b432efb94ebbb4dfbd5708a5b5bb",
|
||||
"mutation-test.log": "d3cf0b93143f44980d5ec89a8bd074072c528cabeb3eab6ea18aaf2dcd509a65",
|
||||
"baseline-test.log": "62b2a0009d8c8880128d3e0914263632cb80a769c68d5a3c3932f0b3051e95ae",
|
||||
"proposed-test.log": "0ef29fa159293cac8ba289a719e5741ed398b432efb94ebbb4dfbd5708a5b5bb",
|
||||
"proposal-first.log": "e4cf67c6ec071ea059f573ff9e5ac3e81e82234d81c09ad262a6cb73fa786c3a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c": {
|
||||
"before": "cee3ae301ac2f211c290450b644a7942c9786a2a40d06f16e2a47efa96fd15b6",
|
||||
"proposed": "f62f8bb8fde222f3f47d89df1439083deb6f5b14633402a2afb7c6a97803582f",
|
||||
"index": null
|
||||
},
|
||||
"tests/test_hq_mounted_dispatch.py": {
|
||||
"before": "3eb793719b21d508ffc294f5a1bf5c5153f5717a1408a8f779f0a4c1ddefe2a6",
|
||||
"proposed": "36c1d3dfc69d98b5b25d7c8f86a94a9d9df0745eddfef44cda378533181b7a97",
|
||||
"index": null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user