Deliver local sighting handoffs and preserve unsubmitted resupply attempts
This commit is contained in:
@@ -8,6 +8,7 @@ class ERR_HQUnit
|
||||
ref ERR_HQRoute Uplink;
|
||||
ref ERR_HQReport PendingReport;
|
||||
ref ERR_HQReportSelection ReportSelection = new ERR_HQReportSelection();
|
||||
ref ERR_HQObservationHistory SentObservations = new ERR_HQObservationHistory();
|
||||
ref ERR_HQRoute Downlink;
|
||||
ref ERR_HQReport PendingOrder;
|
||||
ref ERR_HQReport Assignment;
|
||||
@@ -248,7 +249,7 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!Replication.IsServer() || !MissionActive() || !battery || !battery.StartupActivated || !battery.Operational() || !m_Logistics.CanSample(battery) || !ERR_MortarLogisticsRules.Need(battery.CountShells())) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != battery.Group || unit.Uplink || !unit.Radio || unit.Radio != battery.RadioOperator) continue;
|
||||
if (unit.Group != battery.Group || unit.Uplink || !unit.Radio || unit.Radio != battery.RadioOperator || OriginOccupied(unit.Radio, unit)) continue;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!route) return false;
|
||||
ERR_MortarSupplyStatus report = new ERR_MortarSupplyStatus();
|
||||
@@ -410,16 +411,45 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Network.Build();
|
||||
}
|
||||
|
||||
protected bool OriginOccupied(IEntity caller, ERR_HQUnit owner)
|
||||
{
|
||||
if (!caller) return false;
|
||||
foreach (ERR_HQUnit other : m_Units)
|
||||
{
|
||||
if (other == owner || !other.Uplink || other.Uplink.Failed || other.Uplink.Originated || other.Uplink.Nodes.IsEmpty()) continue;
|
||||
if (other.Uplink.Nodes[0].Carrier == caller) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected ERR_HQRoute ObservationRoute(ERR_HQUnit unit)
|
||||
{
|
||||
ERR_HQRoute own;
|
||||
if (unit.Radio && !OriginOccupied(unit.Radio, unit)) own = m_Network.Route(unit.Radio);
|
||||
if (own && !own.Nodes.IsEmpty() && own.Nodes[0].Valid(own.Nodes.Count() == 1)) return own;
|
||||
array<IEntity> busy = {};
|
||||
foreach (ERR_HQUnit other : m_Units)
|
||||
{
|
||||
if (!other.Uplink || other.Uplink.Failed) continue;
|
||||
if (other.Radio && !busy.Contains(other.Radio)) busy.Insert(other.Radio);
|
||||
if (!other.Uplink.Originated && !other.Uplink.Nodes.IsEmpty())
|
||||
{
|
||||
IEntity caller = other.Uplink.Nodes[0].Carrier;
|
||||
if (caller && !busy.Contains(caller)) busy.Insert(caller);
|
||||
}
|
||||
}
|
||||
return m_Network.LocalRoute(unit.Group, unit.StatusSampler, busy);
|
||||
}
|
||||
|
||||
protected void Observe(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (!unit.Utility || !unit.Radio || unit.Uplink || now < unit.NextReport) return;
|
||||
if (!unit.Utility || unit.Uplink || now < unit.NextReport) return;
|
||||
unit.NextReport = now + m_Mode.Settings().Get("reportIntervalSeconds") * 1000;
|
||||
array<ref ERR_HQVisualContact> samples = {};
|
||||
ERR_HQObservation.Gather(unit.Utility, now, samples);
|
||||
if (samples.IsEmpty()) return;
|
||||
ERR_HQVisualContact seen = unit.ReportSelection.Select(samples);
|
||||
if (!seen) return;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!seen || !unit.SentObservations.CanQueue(seen.Entity, seen.ObservedAt, now)) return;
|
||||
ERR_HQRoute route = ObservationRoute(unit);
|
||||
if (!route) return;
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.MessageId = m_Journal.Message();
|
||||
@@ -433,6 +463,8 @@ class ERR_HQ : ERR_CommandService
|
||||
unit.PendingReport = report;
|
||||
unit.Uplink = route;
|
||||
unit.ReportSelection.Queued(seen.Entity);
|
||||
unit.SentObservations.Queued(seen.Entity, seen.ObservedAt, now);
|
||||
if (route.LocalSource) m_Journal.Write("LOCAL HANDOFF", "original squad sighting queued for nearby physical radio caller; three-second origin hold required", report.MessageId);
|
||||
m_Journal.Write("REPORT", route.Payload, report.MessageId);
|
||||
}
|
||||
|
||||
@@ -474,6 +506,7 @@ class ERR_HQ : ERR_CommandService
|
||||
// Positive observations precede a negative search report.
|
||||
protected void QueueReports(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (OriginOccupied(unit.Radio, unit)) { if (unit.Task) unit.Task.ArrivedAt = -1; return; }
|
||||
StatusReport(unit, now);
|
||||
Observe(unit, now);
|
||||
ObserveBody(unit, now);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Local sharing of an original squad sighting; only the ordinary radio route
|
||||
// can deliver its immutable report to HQ. No observation or force is invented.
|
||||
class ERR_HQLocalHandoff
|
||||
{
|
||||
static bool Valid(SCR_AIGroup source, ERR_HQStatusSampler roster, IEntity witness, ERR_HQRadioNode receiver)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!roster || !roster.OwnsRoster(source) || !receiver || receiver.Group == source || !receiver.Valid()) return false;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, source, witness) || !ERR_EnemyEquipmentRules.Eligible(mode, receiver.Group, receiver.Carrier)) return false;
|
||||
int count = roster.InitialStrength();
|
||||
if (count < 1 || count > 32) return false;
|
||||
bool original;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (roster.InitialMember(i) == witness && roster.CanAttribute(i, witness)) original = true;
|
||||
if (!original) return false;
|
||||
float distance = vector.Distance(witness.GetOrigin(), receiver.Carrier.GetOrigin());
|
||||
return distance >= 0 && distance <= 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Only a genuinely newer original sighting may be attempted. Old watermarks
|
||||
// can expire because the observation capture itself admits at most 500ms age.
|
||||
class ERR_HQObservationHistory
|
||||
{
|
||||
protected ref array<IEntity> m_Targets = {};
|
||||
protected ref array<float> m_Times = {};
|
||||
bool CanQueue(IEntity target, float observedAt, float now)
|
||||
{
|
||||
if (!target || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observedAt) || observedAt < 0 || now < observedAt || now - observedAt > 500) return false;
|
||||
for (int i = m_Targets.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
if (!m_Targets[i] || now - m_Times[i] > 120000) { m_Targets.Remove(i); m_Times.Remove(i); }
|
||||
}
|
||||
int index = m_Targets.Find(target);
|
||||
if (index >= 0) return observedAt > m_Times[index];
|
||||
return m_Targets.Count() < 64;
|
||||
}
|
||||
void Queued(IEntity target, float observedAt, float now)
|
||||
{
|
||||
if (!CanQueue(target, observedAt, now)) return;
|
||||
int index = m_Targets.Find(target);
|
||||
if (index >= 0) m_Times[index] = observedAt;
|
||||
else { m_Targets.Insert(target); m_Times.Insert(observedAt); }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ class ERR_HQRadioNode
|
||||
class ERR_HQRoute
|
||||
{
|
||||
ref array<ref ERR_HQRadioNode> Nodes = {};
|
||||
SCR_AIGroup LocalSource;
|
||||
IEntity LocalWitness;
|
||||
ref ERR_HQStatusSampler LocalRoster;
|
||||
bool Downlink;
|
||||
bool Originated;
|
||||
float OriginDue = -1;
|
||||
@@ -68,6 +71,8 @@ class ERR_HQRoute
|
||||
{
|
||||
if (Nodes.IsEmpty() || !Nodes[0].Valid(Nodes.Count() == 1))
|
||||
return Fail(journal, "originating call lost its living owned radio carrier");
|
||||
if (LocalRoster && !ERR_HQLocalHandoff.Valid(LocalSource, LocalRoster, LocalWitness, Nodes[0]))
|
||||
return Fail(journal, "local sighting handoff lost original squad member, recipient or 100m reach");
|
||||
if (OriginDue < 0)
|
||||
{
|
||||
OriginDue = now + 3000;
|
||||
@@ -151,6 +156,7 @@ class ERR_HQNetwork
|
||||
protected ref map<IEntity, int> m_Indices = new map<IEntity, int>();
|
||||
protected ref map<string, ref array<int>> m_Cells = new map<string, ref array<int>>();
|
||||
bool Overflow;
|
||||
protected int m_LocalChecks;
|
||||
|
||||
void Clear()
|
||||
{
|
||||
@@ -160,6 +166,7 @@ class ERR_HQNetwork
|
||||
m_Indices.Clear();
|
||||
m_Cells.Clear();
|
||||
Overflow = false;
|
||||
m_LocalChecks = 0;
|
||||
}
|
||||
|
||||
protected string Cell(int x, int z)
|
||||
@@ -243,6 +250,30 @@ class ERR_HQNetwork
|
||||
}
|
||||
}
|
||||
|
||||
// At most 4096 local pairs across all reports per network rebuild. Indexes
|
||||
// provide candidate routes only; current original actors/distance are checked.
|
||||
ERR_HQRoute LocalRoute(SCR_AIGroup source, ERR_HQStatusSampler roster, array<IEntity> busy)
|
||||
{
|
||||
if (Overflow || !source || !roster || !roster.OwnsRoster(source) || !busy || busy.Count() > 256) return null;
|
||||
int count = roster.InitialStrength();
|
||||
if (count < 1 || count > 32) return null;
|
||||
foreach (ERR_HQRadioNode node : m_Nodes)
|
||||
{
|
||||
if (!node || node.Group == source || busy.Contains(node.Carrier)) continue;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (++m_LocalChecks > 4096) return null;
|
||||
IEntity witness = roster.InitialMember(i);
|
||||
if (!ERR_HQLocalHandoff.Valid(source, roster, witness, node)) continue;
|
||||
ERR_HQRoute route = Route(node.Carrier);
|
||||
if (!route) continue;
|
||||
route.LocalSource = source; route.LocalWitness = witness; route.LocalRoster = roster;
|
||||
return route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ERR_HQRoute Route(IEntity caller, bool downlink = false)
|
||||
{
|
||||
int index;
|
||||
|
||||
@@ -174,7 +174,9 @@ class ERR_InfantryResupply
|
||||
m_MoveWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
m_HoldWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
if (!m_MoveWaypoint || !m_HoldWaypoint) return false;
|
||||
m_MoveWaypoint.SetCompletionRadius(2); m_HoldWaypoint.SetCompletionRadius(3);
|
||||
// The reported rendezvous is 3 m from the donor report. A 2 m move
|
||||
// radius allowed arrival 5 m away, beyond the transfer's physical reach.
|
||||
m_MoveWaypoint.SetCompletionRadius(0.5); m_HoldWaypoint.SetCompletionRadius(0.5);
|
||||
m_Recipient.Group.AddWaypointAt(m_HoldWaypoint, 0); m_Recipient.Group.AddWaypointAt(m_MoveWaypoint, 0);
|
||||
m_Recipient.Group.ActivateAI(); return true;
|
||||
}
|
||||
@@ -215,7 +217,13 @@ class ERR_InfantryResupply
|
||||
if (!magazine || m_Spent.Contains(magazine) || !Contains(m_Truck.Entity, magazine) || !ERR_InfantryAmmoTransfer.Compatible(actor, magazine)) continue;
|
||||
m_Spent.Insert(magazine); m_Receiving = actor;
|
||||
m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, actor, m_Recipient.Group, magazine);
|
||||
m_Move.Tick(now); return;
|
||||
m_Move.Tick(now);
|
||||
if (m_Move.Submitted()) return;
|
||||
// No native operation exists yet: approach geometry, stopped state or
|
||||
// capacity may improve while the same bounded rendezvous remains valid.
|
||||
// Keep every submitted identity spent, including a synchronous rejection.
|
||||
m_Spent.RemoveItem(magazine); m_Move = null; m_Receiving = null;
|
||||
break; // At most one unsubmitted preflight per member on this slow tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user