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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#ifdef WORKBENCH
|
||||
// Arranged report/physical local handoff fixture, NOT a native perception proof.
|
||||
// Existing source radios and one recipient's radios are deliberately removed;
|
||||
// no equipment or force is spawned to repair that disposable test-world setup.
|
||||
// Read-only readiness: a terminal allocator may still own an accepted native
|
||||
// insertion. Admit only terminal allocators with no submitted operation.
|
||||
modded class ERR_EnemyRadio
|
||||
{
|
||||
bool ERR_LocalFixtureSettled()
|
||||
{
|
||||
return m_Stopped && !m_Submitted;
|
||||
}
|
||||
}
|
||||
modded class ERR_EnemyEquipment
|
||||
{
|
||||
bool ERR_LocalFixtureRadiosSettled(SCR_AIGroup group)
|
||||
{
|
||||
if (!ERR_NativeLocalHandoffProbe.Enabled() || !group) return false;
|
||||
foreach (ERR_EnemyEquipmentGroup state : m_Groups)
|
||||
if (state.Group == group) return !state.Radio || state.Radio.ERR_LocalFixtureSettled();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
bool ERR_LocalFixtureRadiosSettled(ERR_HQUnit unit)
|
||||
{
|
||||
return unit && m_Equipment && m_Equipment.ERR_LocalFixtureRadiosSettled(unit.Group);
|
||||
}
|
||||
void ERR_LocalFixtureUnits(out array<ERR_HQUnit> units)
|
||||
{
|
||||
units.Clear();
|
||||
if (!ERR_NativeLocalHandoffProbe.Enabled()) return;
|
||||
foreach (ERR_HQUnit unit : m_Units) units.Insert(unit);
|
||||
}
|
||||
ERR_HQRoute ERR_LocalFixtureQueue(ERR_HQUnit unit, ERR_HQReport arranged, IEntity witness, SCR_AIGroup receiver, out string reason)
|
||||
{
|
||||
reason = "fixture owner unavailable";
|
||||
if (!ERR_NativeLocalHandoffProbe.Enabled() || !unit || !m_Units.Contains(unit) || !arranged) return null;
|
||||
reason = "source has an existing pending uplink";
|
||||
if (unit.Uplink || unit.PendingReport || unit.PendingStatus || unit.PendingSweep || unit.PendingSupplyStatus) return null;
|
||||
// Rebuild the real topology after deliberate physical radio removal.
|
||||
IndexRadios();
|
||||
ERR_HQRoute route = ObservationRoute(unit);
|
||||
reason = "no eligible original local route";
|
||||
if (!route) return null;
|
||||
if (route.Downlink || route.LocalSource != unit.Group || !route.LocalWitness || route.Nodes.IsEmpty())
|
||||
{ reason = string.Format("route is not local; source radio=%1 capability=%2", unit.Radio, ERR_HQRadio.Capability(unit.Radio)); return null; }
|
||||
if (route.LocalWitness != witness || route.Nodes[0].Group != receiver)
|
||||
{ reason = string.Format("different local pair chosen: witness=%1 receiver=%2", route.LocalWitness, route.Nodes[0].UnitId); return null; }
|
||||
arranged.Source = unit.Group;
|
||||
unit.PendingReport = arranged.Snapshot(); unit.Uplink = route;
|
||||
route.MessageId = arranged.MessageId;
|
||||
route.Payload = "FIXTURE arranged immutable sighting; real local handoff and radio required";
|
||||
m_Journal.Write("REPORT", route.Payload, route.MessageId);
|
||||
return route;
|
||||
}
|
||||
}
|
||||
class ERR_NativeLocalHandoffProbe
|
||||
{
|
||||
static const int TOKEN = 220001;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_HQUnit m_Source;
|
||||
protected ERR_HQUnit m_Recipient;
|
||||
protected IEntity m_Witness;
|
||||
protected IEntity m_Radio;
|
||||
protected vector m_OriginalPosition;
|
||||
protected ref ERR_HQRoute m_Route;
|
||||
protected ref ERR_HQReport m_Payload;
|
||||
protected int m_DeliveredId;
|
||||
protected int m_State;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
protected float m_NextQueueDiagnostic;
|
||||
protected bool m_Positioned;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Failed;
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-local-handoff";
|
||||
}
|
||||
void ERR_NativeLocalHandoffProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: local handoff " + detail); return true; }
|
||||
m_Failed = true; Print("ERR SMOKE FAIL: local handoff " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool Original(ERR_HQUnit unit, IEntity actor)
|
||||
{
|
||||
if (!unit || !actor || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)) return false;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, unit.Group, actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
|
||||
int count = unit.StatusSampler.InitialStrength();
|
||||
if (count < 1 || count > 32) return false;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (unit.StatusSampler.InitialMember(i) == actor) return unit.StatusSampler.CanAttribute(i, actor);
|
||||
return false;
|
||||
}
|
||||
protected int RemoveRadios(ERR_HQUnit unit, IEntity actor)
|
||||
{
|
||||
if (!Original(unit, actor)) return -1;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return -1;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
if (items.Count() > 128) return -1;
|
||||
int removed;
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(item); removed++;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
protected bool Select()
|
||||
{
|
||||
array<ERR_HQUnit> units = {}; m_HQ.ERR_LocalFixtureUnits(units);
|
||||
foreach (ERR_HQUnit recipient : units)
|
||||
{
|
||||
if (!m_HQ.ERR_LocalFixtureRadiosSettled(recipient)) continue;
|
||||
if (!recipient || !recipient.Radio || !Original(recipient, recipient.Radio) || ERR_HQRadio.Capability(recipient.Radio) != 2) continue;
|
||||
if (!m_HQ.ReserveLogistics(recipient.Group, TOKEN)) continue;
|
||||
m_Recipient = recipient; m_Radio = recipient.Radio;
|
||||
foreach (ERR_HQUnit source : units)
|
||||
{
|
||||
if (!m_HQ.ERR_LocalFixtureRadiosSettled(source)) continue;
|
||||
if (!source || source == recipient || !source.StatusSampler || !m_HQ.ReserveLogistics(source.Group, TOKEN)) continue;
|
||||
int count = source.StatusSampler.InitialStrength();
|
||||
if (count >= 1 && count <= 32)
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
IEntity actor = source.StatusSampler.InitialMember(i);
|
||||
if (Original(source, actor)) { m_Source = source; m_Witness = actor; break; }
|
||||
}
|
||||
if (m_Source) return true;
|
||||
m_HQ.ReleaseLogistics(source.Group, TOKEN);
|
||||
}
|
||||
m_HQ.ReleaseLogistics(recipient.Group, TOKEN); m_Recipient = null; m_Radio = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool Arrange()
|
||||
{
|
||||
if (!Original(m_Source, m_Witness) || !Original(m_Recipient, m_Radio)) return false;
|
||||
int removed;
|
||||
int count = m_Source.StatusSampler.InitialStrength();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
IEntity actor = m_Source.StatusSampler.InitialMember(i);
|
||||
int result = RemoveRadios(m_Source, actor);
|
||||
if (result < 0) return false;
|
||||
removed += result;
|
||||
}
|
||||
vector point = ERR_Actors.Ground(m_Radio.GetOrigin() + "4 0 0");
|
||||
TraceBox volume = new TraceBox(); volume.Start = point + "0 1 0"; volume.End = volume.Start + "0 0.01 0";
|
||||
volume.Mins = "-0.3 -0.9 -0.3"; volume.Maxs = "0.3 0.9 0.3"; volume.Flags = TraceFlags.ENTS;
|
||||
array<IEntity> exclude = {m_Witness}; volume.ExcludeArray = exclude;
|
||||
float fraction = GetGame().GetWorld().TraceMove(volume, null);
|
||||
if (!(fraction >= 1 && fraction <= 1)) return false;
|
||||
m_OriginalPosition = m_Witness.GetOrigin(); m_Witness.SetOrigin(point); m_Positioned = true;
|
||||
PrintFormat("ERR LOCAL HANDOFF ARRANGEMENT: source=%1 recipient=%2 original witness=%3 caller=%4 source radios removed=%5; arranged snapshot, no perception assertion", m_Source.Id, m_Recipient.Id, m_Witness, m_Radio, removed);
|
||||
return ERR_HQRadio.Capability(m_Witness) == 0;
|
||||
}
|
||||
protected void QueueWait(string reason, float now)
|
||||
{
|
||||
if (now < m_NextQueueDiagnostic) return;
|
||||
m_NextQueueDiagnostic = now + 10000;
|
||||
PrintFormat("ERR LOCAL HANDOFF WAIT: state=%1 reason=%2 sourceRadio=%3 witnessCapability=%4 distance=%5", m_State, reason, m_Source.Radio, ERR_HQRadio.Capability(m_Witness), vector.Distance(m_Witness.GetOrigin(), m_Radio.GetOrigin()));
|
||||
}
|
||||
protected bool Queue(float now)
|
||||
{
|
||||
if (m_Recipient.Uplink) { QueueWait("recipient has an existing uplink", now); return false; }
|
||||
ERR_HQReport arranged = new ERR_HQReport();
|
||||
arranged.MessageId = m_HQ.Journal().Message(); arranged.Target = m_Mode;
|
||||
arranged.Position = m_Mode.PrisonPosition() + "300 0 300";
|
||||
if (m_DeliveredId > 0) arranged.Position += "10 0 0";
|
||||
arranged.ObservedAt = now; arranged.SeenEscapists = 1; arranged.TravelMode = "on foot";
|
||||
string reason;
|
||||
m_Route = m_HQ.ERR_LocalFixtureQueue(m_Source, arranged, m_Witness, m_Recipient.Group, reason);
|
||||
if (!m_Route) { QueueWait(reason, now); return false; }
|
||||
m_Payload = m_Source.PendingReport.Snapshot();
|
||||
// Mutating the caller's arranged object must not mutate the queued snapshot.
|
||||
arranged.Position += "100 0 0"; arranged.ObservedAt += 1000;
|
||||
return true;
|
||||
}
|
||||
protected bool SameSnapshot(ERR_HQReport report)
|
||||
{
|
||||
return report && m_Payload && report.MessageId == m_Payload.MessageId && report.Source == m_Source.Group
|
||||
&& report.Target == m_Mode && report.Position == m_Payload.Position && report.ObservedAt == m_Payload.ObservedAt;
|
||||
}
|
||||
int Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (!Enabled() || m_Stopped || m_Failed || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return -1;
|
||||
m_HQ = hq; if (!hq) return -1;
|
||||
if (m_Deadline == 0) m_Deadline = now + 120000;
|
||||
if (now >= m_Deadline) { Check(false, "bounded fixture deadline"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_State == 0)
|
||||
{
|
||||
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
||||
if (!Select()) return 0;
|
||||
if (!Check(Arrange(), "two original owned squads retained; source radios removed and witness positioned with clear volume")) return -1;
|
||||
// Native aiming/trace poses may lag SetOrigin until the next world tick.
|
||||
m_Wait = now + 1500; m_State = 1; return 0;
|
||||
}
|
||||
if (!Original(m_Source, m_Witness) || !Original(m_Recipient, m_Radio)
|
||||
|| hq.LogisticsToken(m_Source.Group) != TOKEN || hq.LogisticsToken(m_Recipient.Group) != TOKEN)
|
||||
{ Check(false, "original non-player source/recipient or fixture reservation changed"); return -1; }
|
||||
if (m_State == 1 || m_State == 3)
|
||||
{
|
||||
if (!Queue(now)) return 0;
|
||||
if (!Check(m_Route.LocalSource == m_Source.Group && m_Route.LocalWitness == m_Witness && m_Route.Nodes[0].Group == m_Recipient.Group
|
||||
&& m_Route.Nodes[0].Carrier != m_Witness && SameSnapshot(m_Source.PendingReport), "arranged source snapshot uses distinct original nearby caller without becoming its sighting")) return -1;
|
||||
if (!Check(m_Route.Advance(now, hq.Journal()) == 0 && !m_Route.Originated && !m_Route.Sending,
|
||||
"local handoff is pending and has not started a radio transmission")) return -1;
|
||||
ERR_HQBelief before = hq.Memory().Get(m_Mode);
|
||||
if (!Check((!before && m_DeliveredId == 0) || (before && before.MessageId == m_DeliveredId), "pending local handoff adds no HQ belief")) return -1;
|
||||
if (m_State == 3)
|
||||
{
|
||||
IEntity caller = m_Route.Nodes[0].Carrier;
|
||||
if (!Check(RemoveRadios(m_Recipient, caller) > 0, "original receiving caller radio removed during pending local handoff")) return -1;
|
||||
m_State = 4; return 0;
|
||||
}
|
||||
m_State = 2; return 0;
|
||||
}
|
||||
ERR_HQBelief belief = hq.Memory().Get(m_Mode);
|
||||
if (m_State == 2)
|
||||
{
|
||||
if (!Check(!m_Route.Failed, "positive arranged route retains real local/radio eligibility")) return -1;
|
||||
if (!belief)
|
||||
{
|
||||
if (!Check(SameSnapshot(m_Source.PendingReport), "queued snapshot remains unchanged before actual HQ delivery")) return -1;
|
||||
return 0;
|
||||
}
|
||||
if (!Check(m_Route.Originated && !m_Route.Sending && m_Route.Cursor == m_Route.Nodes.Count()
|
||||
&& SameSnapshot(belief.Report),
|
||||
"actual local plus radio completion delivers the unchanged original source snapshot to HQ")) return -1;
|
||||
m_DeliveredId = m_Payload.MessageId; m_State = 3; return 0;
|
||||
}
|
||||
if (m_State == 4)
|
||||
{
|
||||
if (!Check(belief && belief.MessageId == m_DeliveredId, "interrupted second snapshot never replaces delivered HQ knowledge")) return -1;
|
||||
if (!m_Route.Failed) return 0;
|
||||
if (!Check(!m_Route.Originated && !m_Route.Sending, "loss of actual local recipient radio interrupts before originating handoff completes")) return -1;
|
||||
Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
bool restore;
|
||||
if (m_HQ && m_Source && m_HQ.LogisticsToken(m_Source.Group) == TOKEN)
|
||||
{
|
||||
restore = m_Positioned && Original(m_Source, m_Witness);
|
||||
if (m_Source.Uplink == m_Route && m_Source.PendingReport && m_Payload && m_Source.PendingReport.MessageId == m_Payload.MessageId)
|
||||
{ m_Source.Uplink = null; m_Source.PendingReport = null; }
|
||||
m_HQ.ReleaseLogistics(m_Source.Group, TOKEN);
|
||||
}
|
||||
if (m_HQ && m_Recipient) m_HQ.ReleaseLogistics(m_Recipient.Group, TOKEN);
|
||||
if (restore) m_Witness.SetOrigin(m_OriginalPosition);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -17,6 +17,10 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestRadioNetwork;
|
||||
protected bool m_ERRTestHQResponse;
|
||||
protected bool m_ERRTestHQMemory;
|
||||
protected bool m_ERRTestLocalHandoff;
|
||||
protected ref ERR_NativeLocalHandoffProbe m_ERRLocalHandoff;
|
||||
protected ERR_PlayerLife m_ERRLocalHandoffLife;
|
||||
protected SCR_PlayerController m_ERRLocalHandoffController;
|
||||
protected bool m_ERRTestHQBody;
|
||||
protected ref ERR_NativeHQBodyProbe m_ERRHQBodyProbe;
|
||||
protected bool m_ERRTestInfantryTransfer;
|
||||
@@ -63,6 +67,7 @@ modded class ERR_GameMode
|
||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestHQMemory = outcome == "hq-memory";
|
||||
m_ERRTestLocalHandoff = outcome == "hq-local-handoff";
|
||||
m_ERRTestHQBody = outcome == "hq-body";
|
||||
m_ERRTestInfantryTransfer = outcome == "infantry-transfer";
|
||||
m_ERRTestConvoy = outcome == "convoy";
|
||||
@@ -79,7 +84,9 @@ modded class ERR_GameMode
|
||||
m_ERRTestMortarTransfer = outcome == "mortar-transfer";
|
||||
m_ERRTestPatrolHQ = outcome == "patrol-hq";
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy || m_ERRTestInfantryTransfer;
|
||||
if (m_ERRTestLocalHandoff) m_ERRTestPlayerDeath = true;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
if (m_ERRTestLocalHandoff) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
|
||||
if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;
|
||||
if (ERR_NativeConvoyCoverProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000;
|
||||
if (m_ERRTestHQMemory) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 360000;
|
||||
@@ -517,6 +524,18 @@ modded class ERR_GameMode
|
||||
m_ERRSmokeStep = 110;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestLocalHandoff)
|
||||
{
|
||||
m_ERRLocalHandoffController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_ERRLocalHandoffLife = Players().CurrentActor(m_ERRSmokePlayer);
|
||||
if (!ERR_Check(m_ERRLocalHandoffController && m_ERRLocalHandoffLife && m_ERRLocalHandoffController.GetControlledEntity() == m_ERRSmokePlayer, "local handoff retains original canonical prisoner")) return;
|
||||
DamageManagerComponent protection = DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent));
|
||||
if (!ERR_Check(protection != null, "local handoff original prisoner damage component exists")) return;
|
||||
protection.EnableDamageHandling(false);
|
||||
m_ERRLocalHandoff = new ERR_NativeLocalHandoffProbe(this);
|
||||
OnStop().Insert(m_ERRLocalHandoff.Stop);
|
||||
m_ERRSmokeStep = 109; return;
|
||||
}
|
||||
if (m_ERRTestInfantryTransfer)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
@@ -718,6 +737,21 @@ modded class ERR_GameMode
|
||||
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
||||
m_ERRSmokeWait = now + 4000;
|
||||
}
|
||||
else if (m_ERRSmokeStep == 109)
|
||||
{
|
||||
int result = m_ERRLocalHandoff.Tick(ERR_HQ.Cast(Command()), now);
|
||||
if (result == 0) return;
|
||||
m_ERRLocalHandoff.Stop();
|
||||
if (!ERR_Check(result == 1, "arranged immutable report, actual local handoff/radio delivery and interrupted recipient radio")) return;
|
||||
if (!ERR_Check(m_ERRSmokePlayer && m_ERRLocalHandoffLife && m_ERRLocalHandoffController && m_ERRLocalHandoffController == GetGame().GetPlayerController(), "local handoff final original controller and body retained")) return;
|
||||
if (!ERR_Check(Players().CurrentActor(m_ERRSmokePlayer) == m_ERRLocalHandoffLife && m_ERRLocalHandoffController.GetControlledEntity() == m_ERRSmokePlayer, "local handoff final prisoner identity unchanged")) return;
|
||||
m_ERRSmokeStep = 9;
|
||||
SCR_CharacterDamageManagerComponent damage;
|
||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!ERR_Check(damage != null, "local handoff final original prisoner damage component retained")) return;
|
||||
damage.EnableDamageHandling(true); damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
||||
m_ERRSmokeWait = now + 4000;
|
||||
}
|
||||
else if (m_ERRSmokeStep == 108)
|
||||
{
|
||||
int result = m_ERRInfantryTransfer.Tick(ERR_HQ.Cast(Command()), now);
|
||||
|
||||
Reference in New Issue
Block a user