Deliver local sighting handoffs and preserve unsubmitted resupply attempts

This commit is contained in:
2026-09-20 10:18:18 +02:00
parent d8ec0de765
commit 81c0f060fd
48 changed files with 6994 additions and 30 deletions
+37 -4
View File
@@ -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);