Integrate radio-delivered convoy cover and conservative squad ammunition reports

This commit is contained in:
2026-09-20 04:23:11 +02:00
parent 9b6596eb3e
commit 9205e2129a
88 changed files with 13934 additions and 28 deletions
+92 -3
View File
@@ -28,6 +28,7 @@ class ERR_HQUnit
bool ReservedForSupport;
int LogisticsReservation;
ref ERR_MortarSupplyStatus PendingSupplyStatus;
ref ERR_ConvoyCombatOrder PendingConvoyOrder;
bool Eligible(ERR_GameMode mode)
{
@@ -47,6 +48,7 @@ class ERR_HQUnit
void CancelPending()
{
PendingOrder = null;
PendingConvoyOrder = null;
PendingCheckin = false;
PendingTask = null;
Downlink = null;
@@ -429,6 +431,9 @@ class ERR_HQ : ERR_CommandService
if (!report) return;
route.MessageId = report.MessageId;
route.Payload = string.Format("status %1: %2 living, %3 fit, %4 confirmed losses; sampled=%5", report.SourceId, report.Count, report.Fit, report.ConfirmedLosses, report.ObservedAt);
string ammunition = "unknown";
if (report.AmmoState && report.AmmoState.Complete) ammunition = string.Format("%1 magazine rounds, %2 chambered", report.AmmoState.SmallArmsRounds, report.AmmoState.SmallArmsChambers);
route.Payload += "; small-arms ammunition=" + ammunition;
unit.PendingStatus = report; unit.Uplink = route; unit.NextStatus = now + 180000;
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
}
@@ -514,6 +519,7 @@ class ERR_HQ : ERR_CommandService
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.ForceSourceId == entry.SourceId || !unit.Radio || !unit.PlanEligible || unit.PlanFighting || unit.Assignment || unit.PendingOrder || unit.Downlink) continue;
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
if (unit.HomeGuard && HomeAvailable(unit) <= 1) continue;
if (!ERR_HQPursuitRules.BearingAllowed(ERR_HQPursuitRules.Bearing(unit.PlanPosition, entry.Position), first, second)) continue;
float candidate = vector.DistanceXZ(unit.PlanPosition, entry.Position);
@@ -610,6 +616,7 @@ class ERR_HQ : ERR_CommandService
else unit.CancelPending();
return;
}
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) { LostOrder(unit, now); return; }
if (task.Investigation())
{
if (!WatchTaskValid(task)) { LostOrder(unit, now); return; }
@@ -718,7 +725,8 @@ class ERR_HQ : ERR_CommandService
{
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Assignment || !unit.Task || unit.Task.Investigation() || unit.PendingOrder || !unit.Radio || unit.PlanFighting || !unit.PlanEligible) continue;
if (!unit.Assignment || !unit.Task || unit.Task.Investigation() || unit.PendingOrder || unit.Downlink || !unit.Radio || unit.PlanFighting || !unit.PlanEligible) continue;
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
ERR_HQBelief belief = m_Memory.GetById(unit.Task.BeliefId);
if (!belief || belief.BeliefRevision == unit.Task.BeliefRevision || now - belief.ObservedAt > 45000) continue;
// Nearby updates refine memory without churning native movement orders.
@@ -753,6 +761,85 @@ class ERR_HQ : ERR_CommandService
if (hunt) hunt.NextHunt = Math.Max(hunt.NextHunt, task.ExpiresAt + 180000);
}
}
// Convoy planning reads delivered own-position status and delivered sightings.
// A silent convoy keeps its last order; no target or vehicle position is queried.
protected bool ConvoyPositionKnown(ERR_HQUnit unit, float now)
{
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
return entry && !entry.Closed && entry.Count > 0 && entry.ObservedAt >= 0
&& ERR_HQPursuitRules.Finite(now) && now >= entry.ObservedAt
&& now - entry.ObservedAt <= 180000;
}
protected ERR_HQReport ConvoyContact(ERR_HQUnit unit, float now)
{
if (!ConvoyPositionKnown(unit, now)) return null;
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
ERR_HQReport best;
foreach (ERR_HQReport report : m_Contacts)
{
if (report.ObservedAt < 0 || now < report.ObservedAt || now - report.ObservedAt >= 60000
|| vector.DistanceXZ(entry.Position, report.Position) > 400) continue;
if (!best || report.ObservedAt > best.ObservedAt
|| (report.ObservedAt == best.ObservedAt && report.MessageId > best.MessageId)) best = report;
}
return best;
}
protected void PlanConvoy(float now)
{
if (!m_ConvoyTask || m_ConvoyTask.Stopped || !m_ConvoyTask.Ready) return;
foreach (ERR_ConvoyVehicle vehicle : m_ConvoyTask.Vehicles)
{
if (vehicle.Released || vehicle.Arrived || !vehicle.Registered || !vehicle.Group) continue;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != vehicle.Group || !unit.ReservedForSupport || unit.Downlink || !unit.Radio) continue;
if (!ConvoyPositionKnown(unit, now)) break;
ERR_HQReport contact = ConvoyContact(unit, now);
if (contact)
{
if (vehicle.Combat.Held && contact.MessageId == vehicle.Combat.ReportId) break;
if (contact.ObservedAt < vehicle.Combat.ObservedAt) break;
}
else if (!vehicle.Combat.Held || now - vehicle.Combat.ObservedAt < 60000) break;
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
if (!route) break;
ERR_ConvoyCombatOrder order = new ERR_ConvoyCombatOrder();
order.CommandId = m_Journal.Message(); order.SourceId = m_ConvoyTask.Receipt.SourceId;
order.VehicleIndex = vehicle.Index; order.Group = unit.Group;
order.Hold = contact != null; order.IssuedAt = now; order.ExpiresAt = now + 30000;
order.ReportId = vehicle.Combat.ReportId; order.ObservedAt = vehicle.Combat.ObservedAt;
string action = "resume authored route";
if (contact)
{
order.ReportId = contact.MessageId; order.ObservedAt = contact.ObservedAt;
action = "suspend travel for mounted cover";
}
route.MessageId = order.CommandId;
route.Payload = string.Format("convoy vehicle %1: %2; delivered sighting M%3", vehicle.Index, action, order.ReportId);
unit.PendingConvoyOrder = order; unit.Downlink = route;
m_Journal.Write("CONVOY ORDER QUEUED", route.Payload, route.MessageId);
break;
}
}
}
protected void DeliverConvoyOrder(ERR_HQUnit unit, float now)
{
ERR_ConvoyCombatOrder order = unit.PendingConvoyOrder;
bool accepted;
if (order && order.Group == unit.Group && ConvoyPositionKnown(unit, now))
{
ERR_HQReport contact = ConvoyContact(unit, now);
// A newer nearby sighting still justifies HOLD: frequent reports must not
// starve a slower radio downlink. New threat evidence always invalidates RESUME.
bool current = !order.Hold && !contact;
if (order.Hold && contact) current = contact.ObservedAt >= order.ObservedAt;
if (current) accepted = m_Convoys.ApplyCombatOrder(m_ConvoyTask, order, now);
}
if (accepted) m_Journal.Write("CONVOY ORDER DELIVERED", "original crew applied radio travel command", order.CommandId);
else if (order) m_Journal.Write("CONVOY ORDER REJECTED", "stale evidence or unavailable original crew; current order retained", order.CommandId);
unit.CancelPending();
}
protected void Assign(float now)
{
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Get("supportSquadLimit"));
@@ -832,6 +919,7 @@ class ERR_HQ : ERR_CommandService
int bestBearing = -1;
foreach (ERR_HQUnit unit : m_Units)
{
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
bool busy = unit.Assignment || unit.PendingOrder || unit.Downlink || unit.PlanFighting;
if (!unit.Radio || !ERR_HQPursuitRules.CanCommit(unit.Group != null, unit.PlanEligible, busy, unit.HomeGuard, HomeAvailable(unit), assigned, limit)) continue;
vector position = unit.PlanPosition;
@@ -895,7 +983,8 @@ class ERR_HQ : ERR_CommandService
int delivered = unit.Downlink.Advance(now, m_Journal);
if (delivered == 1)
{
if (unit.PendingCheckin) { unit.NextStatus = now; unit.CancelPending(); }
if (unit.PendingConvoyOrder) DeliverConvoyOrder(unit, now);
else if (unit.PendingCheckin) { unit.NextStatus = now; unit.CancelPending(); }
else DeliverOrder(unit, now);
}
else if (delivered < 0) LostOrder(unit, now);
@@ -903,7 +992,7 @@ class ERR_HQ : ERR_CommandService
if (!unit.Group && !unit.Uplink) { ReleaseUnit(unit, now); m_Units.Remove(i); continue; }
QueueReports(unit, now);
}
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
if (now >= m_NextAssignment) { PlanConvoy(now); Assign(now); m_NextAssignment = now + 5000; }
m_Logistics.Tick(now);
m_Mortars.Tick(now);
m_Journal.Flush();