Harden base progression and cleanup; validate finite physical resupply

This commit is contained in:
2026-09-19 10:39:39 +02:00
parent 40a474934c
commit 7c53b4bb81
377 changed files with 51882 additions and 92 deletions
+58 -6
View File
@@ -12,10 +12,12 @@ class ERR_HQUnit
ref ERR_HQReport Assignment;
AIWaypoint Waypoint;
bool ReservedForSupport;
int LogisticsReservation;
ref ERR_MortarSupplyStatus PendingSupplyStatus;
bool Eligible(ERR_GameMode mode)
{
if (ReservedForSupport || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
if (ReservedForSupport || LogisticsReservation > 0 || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
array<AIAgent> agents = {};
Group.GetAgents(agents);
bool survivor;
@@ -46,6 +48,7 @@ class ERR_HQ : ERR_CommandService
{
protected ERR_GameMode m_Mode;
protected ref ERR_MortarSystem m_Mortars;
protected ref ERR_MortarLogistics m_Logistics;
protected ref array<ref ERR_HQUnit> m_Units = {};
protected ref array<ref ERR_HQReport> m_Contacts = {};
protected ref ERR_HQNetwork m_Network = new ERR_HQNetwork();
@@ -54,7 +57,7 @@ class ERR_HQ : ERR_CommandService
protected float m_NextNetwork;
protected float m_NextAssignment;
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); }
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); m_Logistics = new ERR_MortarLogistics(this, mode); }
static ERR_HQ Get()
{
ERR_GameMode mode = ERR_GameMode.Get();
@@ -72,7 +75,8 @@ class ERR_HQ : ERR_CommandService
if (hq) hq.Journal().Write(kind, message, messageId);
}
ERR_MortarSystem Mortars() { return m_Mortars; }
bool PrepareSupport() { return m_Mortars.Prepare(); }
bool PrepareSupport() { return m_Mortars.Prepare() && m_Logistics.Prepare(); }
ERR_MortarLogistics Logistics() { return m_Logistics; }
void ReserveGroup(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units)
@@ -82,6 +86,48 @@ class ERR_HQ : ERR_CommandService
unit.ReservedForSupport = true;
}
}
// Temporary reservations never alter the permanent support reservation.
int LogisticsToken(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.LogisticsReservation;
return 0;
}
bool ReserveLogistics(SCR_AIGroup group, int token)
{
if (!Replication.IsServer() || token <= 0 || !MissionActive()) return false;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group || !unit.Eligible(m_Mode) || !unit.Radio || unit.Assignment || unit.PendingOrder || unit.Downlink || unit.Waypoint) continue;
if (unit.Utility.m_Perception && !unit.Utility.m_Perception.m_aTargets.IsEmpty()) continue;
unit.LogisticsReservation = token;
return true;
}
return false;
}
void ReleaseLogistics(SCR_AIGroup group, int token)
{
foreach (ERR_HQUnit unit : m_Units)
if (unit.Group == group && unit.LogisticsReservation == token) unit.LogisticsReservation = 0;
}
bool QueueSupplyStatus(ERR_MortarBattery battery, float now)
{
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;
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route) return false;
ERR_MortarSupplyStatus report = new ERR_MortarSupplyStatus();
report.Group = battery.Group; report.Gun = battery.Gun; report.Position = battery.Gun.GetOrigin();
report.Shells = battery.CountShells(); report.ObservedAt = now; report.MessageId = m_Journal.Message();
route.MessageId = report.MessageId;
route.Payload = string.Format("mortar supply status; usable HE=%1; position=%2; sampled=%3", report.Shells, report.Position, report.ObservedAt);
unit.PendingSupplyStatus = report; unit.Uplink = route;
m_Journal.Write("SUPPLY REPORT", route.Payload, route.MessageId);
return true;
}
return false;
}
void PhysicalForces(out array<IEntity> entities)
{
entities.Clear();
@@ -131,7 +177,7 @@ class ERR_HQ : ERR_CommandService
ERR_HQJournal Journal() { return m_Journal; }
string Status()
{
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + "; " + m_Mortars.Status();
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + "; " + m_Mortars.Status() + "; " + m_Logistics.Status();
}
override void RegisterGroup(SCR_AIGroup group)
@@ -304,8 +350,12 @@ class ERR_HQ : ERR_CommandService
if (unit.Uplink)
{
int delivered = unit.Uplink.Advance(now, m_Journal);
if (delivered == 1) Accept(unit.PendingReport, now);
if (delivered != 0) { unit.Uplink = null; unit.PendingReport = null; }
if (delivered == 1)
{
if (unit.PendingSupplyStatus) m_Logistics.Receive(unit.PendingSupplyStatus, now);
else Accept(unit.PendingReport, now);
}
if (delivered != 0) { unit.Uplink = null; unit.PendingReport = null; unit.PendingSupplyStatus = null; }
}
if (unit.Downlink)
{
@@ -317,12 +367,14 @@ class ERR_HQ : ERR_CommandService
Observe(unit, now);
}
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
m_Logistics.Tick(now);
m_Mortars.Tick(now);
m_Journal.Flush();
}
override void Stop()
{
m_Logistics.Stop();
m_Mortars.Stop();
foreach (ERR_HQUnit unit : m_Units) unit.Release();
m_Units.Clear();
@@ -0,0 +1,68 @@
class ERR_MortarCourierCallback : ScriptedInventoryOperationCallback
{
ERR_MortarCourierTransfer Transfer;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
}
// One exact native move belonging to one courier trip. Never retries ambiguous work.
class ERR_MortarCourierTransfer
{
protected ERR_MortarLogistics m_Service;
protected IEntity m_Shell;
protected IEntity m_From;
protected IEntity m_To;
protected InventoryStorageManagerComponent m_Source;
protected InventoryStorageManagerComponent m_Destination;
protected BaseInventoryStorageComponent m_Storage;
protected ref ERR_MortarCourierCallback m_Callback;
protected int m_Trip;
protected int m_Acknowledgement;
protected float m_Deadline;
protected bool m_Accepted;
protected bool m_Invalidated;
protected bool m_Resolved;
void ERR_MortarCourierTransfer(ERR_MortarLogistics service, int trip, IEntity shell, IEntity from, IEntity to, BaseInventoryStorageComponent storage, float now)
{
m_Service = service; m_Trip = trip; m_Shell = shell; m_From = from; m_To = to; m_Storage = storage; m_Deadline = now + 8000;
m_Source = InventoryStorageManagerComponent.Cast(from.FindComponent(InventoryStorageManagerComponent));
m_Destination = InventoryStorageManagerComponent.Cast(to.FindComponent(InventoryStorageManagerComponent));
}
static bool Proven(bool complete, bool source, bool destination, bool exact) { return complete && !source && destination && exact; }
protected bool Contains(InventoryStorageManagerComponent inventory)
{
if (!inventory || !m_Shell) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(m_Shell);
}
void Acknowledge(bool success)
{
if (m_Acknowledgement != 0) return;
if (success) m_Acknowledgement = 1;
else m_Acknowledgement = -1;
}
bool Begin(float now)
{
if (m_Callback || !Replication.IsServer() || !m_Service.TransferAllowed(m_Trip, m_Shell, m_From, m_To, now) || !Contains(m_Source) || Contains(m_Destination)) return false;
m_Callback = new ERR_MortarCourierCallback(); m_Callback.Transfer = this;
m_Accepted = m_Source.TryMoveItemToStorage(m_Shell, m_Storage, -1, m_Callback);
if (!m_Accepted) { Acknowledge(false); m_Resolved = true; }
return m_Accepted;
}
void Invalidate() { m_Invalidated = true; }
bool Unresolved() { return m_Accepted && !m_Resolved; }
IEntity Shell() { return m_Shell; }
int Poll(float now)
{
// A deleted exact entity cannot complete later; close as a permanent loss.
if (!m_Shell) { m_Resolved = true; m_Invalidated = true; return -1; }
bool exact;
InventoryItemComponent item;
if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));
if (item && item.GetParentSlot()) exact = item.GetParentSlot().GetStorage() == m_Storage;
bool proven = Proven(m_Acknowledgement == 1, Contains(m_Source), Contains(m_Destination), exact);
if (proven || m_Acknowledgement < 0) m_Resolved = true;
if (m_Invalidated || now >= m_Deadline || !m_Service.TransferAllowed(m_Trip, m_Shell, m_From, m_To, now)) { m_Invalidated = true; return -1; }
if (proven) return 1;
if (m_Acknowledgement < 0 || !m_Accepted) return -1;
return 0;
}
}
@@ -0,0 +1,347 @@
class ERR_MortarSupplyStatus
{
SCR_AIGroup Group;
IEntity Gun;
vector Position;
float ObservedAt;
int MessageId;
int Shells;
}
class ERR_MortarLogisticsRules
{
static bool Fresh(float observedAt, float now) { return observedAt >= 0 && now >= observedAt && now - observedAt <= 180000; }
static bool Need(int shells) { return shells >= 0 && shells <= 6; }
static bool NewRequest(int message, int consumed, bool fresh) { return fresh && message > 0 && message > consumed; }
static bool Near(float distance) { return distance >= 0 && distance <= 12; }
static bool ActiveTrip(bool server, bool stopped, int trip, int current, bool fresh, bool ownershipValid) { return server && !stopped && trip > 0 && trip == current && fresh && ownershipValid; }
}
// One finite western-cache courier, owned and ticked exclusively by HQ.
class ERR_MortarLogistics
{
protected ERR_HQ m_HQ;
protected ERR_GameMode m_Mode;
protected ERR_SiteRuntime m_Site;
protected IEntity m_Cache;
protected SCR_AIGroup m_Group;
protected ERR_MortarBattery m_Battery;
protected IEntity m_OriginalGun;
protected SCR_AIGroup m_OriginalBatteryGroup;
protected ref array<IEntity> m_BatteryMembers = {};
protected ref array<IEntity> m_OriginalMembers = {};
protected ref array<IEntity> m_Stock = {};
protected ref array<IEntity> m_Cargo = {};
protected ref array<IEntity> m_AttemptedPickup = {};
protected ref array<IEntity> m_AttemptedUnload = {};
protected ref array<ref ERR_MortarInitialItem> m_Initial = {};
protected ref ERR_MortarSupplyStatus m_Report;
protected ref ERR_HQRoute m_Order;
protected ref ERR_MortarCourierTransfer m_Move;
protected IEntity m_Courier;
protected AIWaypoint m_Waypoint;
protected AIWaypoint m_HoldWaypoint;
protected float m_InitialDeadline;
protected float m_NextReport;
protected float m_LastDeliveryAt = -1;
protected int m_ConsumedMessage;
protected int m_Trip;
protected int m_State; // 0 initial, 1 idle, 2 downlink, 3 pickup, 4 travel, 5 unload
protected int m_Delivered;
protected int m_CompletedTrips;
protected bool m_Started;
protected bool m_Stopped;
protected string m_Reason;
static const int INITIAL_SHELLS = 12;
static const int TRIP_SHELLS = 6;
void ERR_MortarLogistics(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
int State() { return m_State; }
int Trip() { return m_Trip; }
int Delivered() { return m_Delivered; }
int CompletedTrips() { return m_CompletedTrips; }
IEntity Cache() { return m_Cache; }
SCR_AIGroup Group() { return m_Group; }
IEntity Courier() { return m_Courier; }
ERR_MortarBattery Battery() { return m_Battery; }
ERR_MortarSupplyStatus Report() { return m_Report; }
bool Stopped() { return m_Stopped; }
void InitialStock(out array<IEntity> items) { items.Copy(m_Stock); }
protected void Audit(string kind, string reason)
{
int message; if (m_Report) message = m_Report.MessageId;
m_Reason = reason;
m_HQ.Journal().Write("COURIER " + kind, string.Format("trip=%1 state=%2 delivered=%3; %4", m_Trip, m_State, m_Delivered, reason), message);
}
protected InventoryStorageManagerComponent Inventory(IEntity entity)
{
if (!entity) return null;
return InventoryStorageManagerComponent.Cast(entity.FindComponent(InventoryStorageManagerComponent));
}
protected bool Contains(IEntity owner, IEntity shell)
{
InventoryStorageManagerComponent inventory = Inventory(owner);
if (!inventory || !shell) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(shell);
}
protected void Shells(IEntity owner, out array<IEntity> shells)
{
shells.Clear(); InventoryStorageManagerComponent inventory = Inventory(owner); if (!inventory) return;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) shells.Insert(item);
}
bool Prepare()
{
if (!Replication.IsServer() || m_Started || m_Stopped) return false;
m_Started = true;
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(m_Mode.FindComponent(ERR_ScenarioComponent));
if (!scenario || !scenario.Sites()) return false;
m_Site = scenario.Sites().Find("everon-supply-2");
if (!m_Site || !m_Site.Group || !m_Site.PatrolCycle || !scenario.Sites().Owns(m_Site.Container)) return false;
m_Cache = m_Site.Container; m_Group = m_Site.Group;
array<ref ERR_MortarBattery> batteries = {}; m_HQ.Mortars().Batteries(batteries);
if (batteries.Count() != 1) return false;
m_Battery = batteries[0]; m_OriginalGun = m_Battery.Gun; m_OriginalBatteryGroup = m_Battery.Group;
InventoryStorageManagerComponent inventory = Inventory(m_Cache);
if (!inventory || !inventory.CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT)) return false;
array<IEntity> existing = {}; Shells(m_Cache, existing); if (!existing.IsEmpty()) return false;
array<AIAgent> agents = {}; m_Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
m_OriginalMembers.Insert(agent.GetControlledEntity());
}
if (m_OriginalMembers.Count() != m_Site.InitialGuardCount) return false;
m_InitialDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
// SiteService owns this existing cache and its contents through rollback/world teardown.
// This bounded startup allocation is never reopened, including partial failure/theft.
for (int i = 0; i < INITIAL_SHELLS; i++)
{
IEntity shell = ERR_Actors.Spawn(ERR_MortarBattery.SHELL, m_Cache.GetOrigin());
if (!shell) return false;
// Keep the exact created identity; counts cannot prove an external item was
// not substituted while an asynchronous insertion was pending.
m_Mode.Own(shell); m_Stock.Insert(shell);
ERR_MortarInitialItem operation = new ERR_MortarInitialItem(); m_Initial.Insert(operation);
if (!inventory.TryInsertItem(shell, EStoragePurpose.PURPOSE_DEPOSIT, operation)) { operation.Rejected(); return false; }
}
return true;
}
protected bool OriginalGroup()
{
if (!m_Site || m_Site.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
array<AIAgent> agents = {}; m_Group.GetAgents(agents); bool living;
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity();
if (!m_OriginalMembers.Contains(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Group || SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction()) return false;
if (ERR_Actors.CanFight(actor)) living = true;
}
return living;
}
protected bool BindBatteryMembers()
{
if (!m_Battery || !m_Battery.StartupActivated || m_Battery.Group != m_OriginalBatteryGroup || m_Battery.Gun != m_OriginalGun) return false;
if (!m_BatteryMembers.IsEmpty()) return true;
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
if (agent && agent.GetControlledEntity() && !m_BatteryMembers.Contains(agent.GetControlledEntity())) m_BatteryMembers.Insert(agent.GetControlledEntity());
return !m_BatteryMembers.IsEmpty();
}
bool TargetAvailable()
{
if (!m_Battery || m_Battery.Gun != m_OriginalGun || m_Battery.Group != m_OriginalBatteryGroup || !m_OriginalGun || !m_Battery.StartupActivated || m_Battery.Stopped() || !m_Battery.Operational() || m_HQ.Mortars().FindGun(m_OriginalGun) != m_Battery || m_BatteryMembers.IsEmpty()) return false;
if (SCR_FactionManager.SGetFaction(m_OriginalGun) != m_Mode.EnemyFaction()) return false;
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_OriginalGun.FindComponent(SCR_AIVehicleUsageComponent));
if (!usage || !usage.GetTurretCompartmentSlot()) return false;
IEntity occupant = usage.GetTurretCompartmentSlot().GetOccupant();
if (!occupant) return true;
return m_BatteryMembers.Contains(occupant) && ERR_Actors.CanFight(occupant) && !ERR_Actors.Player(occupant) && ERR_Actors.Group(occupant) == m_OriginalBatteryGroup && SCR_FactionManager.SGetFaction(occupant) == m_Mode.EnemyFaction();
}
bool CanSample(ERR_MortarBattery battery) { return battery == m_Battery && TargetAvailable(); }
protected bool Bearer(IEntity actor)
{
return actor && m_OriginalMembers.Contains(actor) && ERR_Actors.Group(actor) == m_Group && ERR_Actors.CanFight(actor) && !ERR_Actors.Player(actor) && !CompartmentAccessComponent.GetVehicleIn(actor) && SCR_FactionManager.SGetFaction(actor) == m_Mode.EnemyFaction() && ERR_HQRadio.Capability(actor) != 2;
}
bool AcceptsStatus(ERR_MortarSupplyStatus report, float now)
{
return !m_Stopped && m_State == 1 && report && m_Battery && report.Group == m_Battery.Group && report.Gun == m_Battery.Gun && ERR_MortarLogisticsRules.Need(report.Shells) && ERR_MortarLogisticsRules.NewRequest(report.MessageId, m_ConsumedMessage, ERR_MortarLogisticsRules.Fresh(report.ObservedAt, now));
}
void Receive(ERR_MortarSupplyStatus report, float now)
{
if (!AcceptsStatus(report, now)) return;
m_Report = report;
Audit("RECEIVED", "immutable low-ammunition status delivered by native physical radio route");
}
protected bool Authorized(float now)
{
if (!m_Mode || !m_Mode.Active() || !m_Report || !m_Battery) return false;
return ERR_MortarLogisticsRules.ActiveTrip(Replication.IsServer(), m_Stopped, m_Trip, m_HQ.LogisticsToken(m_Group), ERR_MortarLogisticsRules.Fresh(m_Report.ObservedAt, now), OriginalGroup()) && TargetAvailable() && m_Report.Gun == m_Battery.Gun && m_Report.Group == m_Battery.Group && vector.Distance(m_Report.Position, m_Battery.Gun.GetOrigin()) <= 5;
}
bool TransferAllowed(int trip, IEntity shell, IEntity from, IEntity to, float now)
{
if (trip != m_Trip || !Authorized(now) || !shell || !m_Stock.Contains(shell) || !Bearer(m_Courier)) return false;
if (m_State == 3) return from == m_Cache && to == m_Courier && ERR_MortarLogisticsRules.Near(vector.Distance(from.GetOrigin(), to.GetOrigin()));
if (m_State != 5 || from != m_Courier || !m_Cargo.Contains(shell) || !ERR_Actors.CanFight(to) || ERR_Actors.Player(to) || to == m_Battery.RadioOperator || !m_BatteryMembers.Contains(to) || ERR_Actors.Group(to) != m_Battery.Group || SCR_FactionManager.SGetFaction(to) != m_Mode.EnemyFaction()) return false;
return !CompartmentAccessComponent.GetVehicleIn(to) && ERR_MortarLogisticsRules.Near(vector.Distance(from.GetOrigin(), to.GetOrigin()));
}
protected void ClearWaypoint()
{
if (m_Waypoint) { if (m_Group) m_Group.RemoveWaypoint(m_Waypoint); ERR_Actors.DeleteSpawned(m_Waypoint); }
m_Waypoint = null;
if (m_HoldWaypoint) { if (m_Group) m_Group.RemoveWaypoint(m_HoldWaypoint); ERR_Actors.DeleteSpawned(m_HoldWaypoint); }
m_HoldWaypoint = null;
}
protected bool MoveTo(vector position)
{
ClearWaypoint();
m_Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), ERR_Actors.Ground(position)));
if (!m_Waypoint) return false;
// A completed Move alone would let the original patrol resume during the
// asynchronous item operation. This owned, non-completing native Defend
// keeps that queue below us until release; its original entries are untouched.
m_HoldWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), ERR_Actors.Ground(position)));
if (!m_HoldWaypoint) { ClearWaypoint(); return false; }
m_HoldWaypoint.SetCompletionRadius(8);
m_Group.AddWaypointAt(m_HoldWaypoint, 0); m_Group.AddWaypointAt(m_Waypoint, 0); m_Group.ActivateAI(); return true;
}
protected void Finish(string reason)
{
Audit("END", reason);
if (m_Move) m_Move.Invalidate();
ClearWaypoint(); m_HQ.ReleaseLogistics(m_Group, m_Trip);
m_Order = null; m_Report = null; m_Courier = null; m_Cargo.Clear(); m_State = 1;
// Native requests cannot be canceled. A pending move remains held until its
// physical outcome resolves; callbacks cannot authorize a later trip.
m_NextReport = GetGame().GetWorld().GetWorldTime() + 60000;
}
protected bool BeginMove(IEntity shell, IEntity from, IEntity to, float now)
{
InventoryStorageManagerComponent destination = Inventory(to);
BaseInventoryStorageComponent storage; if (destination) storage = destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT);
if (!storage) return false;
m_Move = new ERR_MortarCourierTransfer(this, m_Trip, shell, from, to, storage, now);
return m_Move.Begin(now);
}
protected bool PollMove(float now)
{
if (!m_Move) return true;
int result = m_Move.Poll(now);
if (result == 0) return false;
if (result < 0) { Finish("native item operation rejected, expired or lost authority; physical items retained"); return false; }
IEntity shell = m_Move.Shell(); m_Move = null;
if (m_State == 3) { m_Cargo.Insert(shell); Audit("PICKUP", "exact depot shell confirmed in original bearer inventory"); }
else if (m_State == 5) { m_Cargo.RemoveItem(shell); m_Delivered++; m_LastDeliveryAt = now; Audit("DELIVERED", "exact original shell confirmed in living mortar crew inventory"); }
return true;
}
protected void Pickup(float now)
{
if (!m_Courier)
{
foreach (IEntity candidate : m_OriginalMembers)
if (Bearer(candidate) && Inventory(candidate) && Inventory(candidate).CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT) && ERR_MortarLogisticsRules.Near(vector.Distance(candidate.GetOrigin(), m_Cache.GetOrigin()))) { m_Courier = candidate; break; }
if (!m_Courier) return;
}
if (!Bearer(m_Courier)) { Finish("original bearer lost or captured"); return; }
if (!PollMove(now)) return;
if (m_Cargo.Count() < TRIP_SHELLS)
{
foreach (IEntity shell : m_Stock)
{
if (!shell || !Contains(m_Cache, shell) || m_AttemptedPickup.Contains(shell)) continue;
InventoryStorageManagerComponent destination = Inventory(m_Courier);
if (!destination || !destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT)) break;
m_AttemptedPickup.Insert(shell);
if (!BeginMove(shell, m_Cache, m_Courier, now)) Finish("native pickup rejected; no second request for that shell");
return;
}
}
if (m_Cargo.IsEmpty()) { Finish("finite cache empty or bearer native storage full"); return; }
m_State = 4;
if (!MoveTo(m_Report.Position)) { Finish("native delivery waypoint unavailable"); return; }
Audit("OUTBOUND", "original patrol walking with verified finite cargo to reported battery position");
}
protected void Unload(float now)
{
if (!PollMove(now)) return;
if (m_Cargo.IsEmpty()) { m_CompletedTrips++; Finish("finite delivery complete; original patrol restored"); return; }
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (IEntity shell : m_Cargo)
{
if (!shell || !Contains(m_Courier, shell)) { Finish("carried shell lost or stolen; no replacement"); return; }
if (m_AttemptedUnload.Contains(shell)) { Finish("previous unload unresolved or failed; no retry"); return; }
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity recipient = agent.GetControlledEntity();
if (!TransferAllowed(m_Trip, shell, m_Courier, recipient, now)) continue;
InventoryStorageManagerComponent destination = Inventory(recipient);
if (!destination || !destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
m_AttemptedUnload.Insert(shell);
if (!BeginMove(shell, m_Courier, recipient, now)) Finish("native recipient move rejected; cargo remains physical");
return;
}
}
}
void Tick(float now)
{
if (m_Stopped || !m_Started || !Replication.IsServer() || !m_Mode.Active()) return;
if (m_State == 0)
{
bool failed; foreach (ERR_MortarInitialItem operation : m_Initial) { if (!operation.Finished) { if (now < m_InitialDeadline) return; failed = true; } else if (!operation.Succeeded) failed = true; }
if (!OriginalGroup()) failed = true;
array<IEntity> shells = {}; Shells(m_Cache, shells);
if (shells.Count() != INITIAL_SHELLS) { if (now < m_InitialDeadline && !failed) return; failed = true; }
foreach (IEntity originalShell : m_Stock) if (!originalShell || !shells.Contains(originalShell)) failed = true;
if (m_Stock.Count() != INITIAL_SHELLS) failed = true;
if (failed) { Audit("STARTUP FAILED", "one-time native stock allocation incomplete; no refill"); Stop(); return; }
m_State = 1; Audit("READY", "12 distinct original HE shells physically confirmed in existing western cache");
}
if (m_State == 1)
{
if (m_Move) { m_Move.Poll(now); if (m_Move.Unresolved()) return; m_Move = null; }
if (!OriginalGroup() || !BindBatteryMembers() || !TargetAvailable()) return;
bool available; foreach (IEntity stocked : m_Stock) if (Contains(m_Cache, stocked) && !m_AttemptedPickup.Contains(stocked)) available = true;
if (!available) return;
if (now >= m_NextReport && ERR_MortarLogisticsRules.Need(m_Battery.CountShells())) { if (m_HQ.QueueSupplyStatus(m_Battery, now)) m_NextReport = now + 60000; }
if (!m_Report || !ERR_MortarLogisticsRules.NewRequest(m_Report.MessageId, m_ConsumedMessage, ERR_MortarLogisticsRules.Fresh(m_Report.ObservedAt, now))) return;
int nextTrip = m_Trip + 1;
if (!m_HQ.ReserveLogistics(m_Group, nextTrip)) return;
m_Trip = nextTrip; m_ConsumedMessage = m_Report.MessageId;
m_Order = m_HQ.Downlink(m_Group, "MORTAR SUPPLY", "collect finite western-cache HE; walk to reported battery", m_Report.MessageId);
m_State = 2;
if (!m_Order) { Finish("no native courier downlink"); return; }
Audit("QUEUED", "original patrol reserved pending physical radio command delivery");
}
if (!Authorized(now)) { Finish("supply report expired or original ownership/gun/group changed"); return; }
if (m_Courier && !Bearer(m_Courier)) { Finish("original bearer unavailable; surviving cargo retained"); return; }
if (m_State == 2)
{
int delivered = m_Order.Advance(now, m_HQ.Journal());
if (delivered == 0) return;
if (delivered < 0) { Finish("courier command interrupted"); return; }
m_Order = null; m_State = 3;
if (!MoveTo(m_Cache.GetOrigin())) { Finish("native pickup waypoint unavailable"); return; }
Audit("DISPATCH", "same original patrol receives delivered order and walks to finite cache");
}
if (m_State == 3) { Pickup(now); return; }
if (m_State == 4)
{
if (!ERR_MortarLogisticsRules.Near(vector.Distance(m_Courier.GetOrigin(), m_Report.Position))) return;
m_State = 5; Audit("ARRIVED", "physical bearer reached reported gun area; exact recipient checks still required");
}
if (m_State == 5) Unload(now);
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Move) m_Move.Invalidate();
ClearWaypoint(); m_HQ.ReleaseLogistics(m_Group, m_Trip); m_Order = null; m_Report = null;
}
#ifdef WORKBENCH
ERR_HQRoute ProbeOrder() { return m_Order; }
int ProbeSourceMessage() { return m_ConsumedMessage; }
float ProbeLastDelivery() { return m_LastDeliveryAt; }
#endif
string Status() { return string.Format("courier state=%1 finite initial=%2 delivered=%3 stopped=%4 (%5)", m_State, m_Stock.Count(), m_Delivered, m_Stopped, m_Reason); }
}
@@ -60,6 +60,11 @@ class ERR_MortarSystem
return true;
}
static ResourceName MortarGroup() { return "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"; }
void Batteries(out array<ref ERR_MortarBattery> batteries)
{
batteries.Clear();
foreach (ERR_MortarBattery battery : m_Batteries) batteries.Insert(battery);
}
ERR_MortarBattery FindGun(IEntity gun)
{
foreach (ERR_MortarBattery battery : m_Batteries)
@@ -0,0 +1,213 @@
#ifdef WORKBENCH
class ERR_NativeCourierMoveCallback : ScriptedInventoryOperationCallback
{
int Result;
protected override void OnComplete() { if (Result == 0) Result = 1; }
protected override void OnFailed() { if (Result == 0) Result = -1; }
}
modded class ERR_HQ
{
override bool QueueSupplyStatus(ERR_MortarBattery battery, float now)
{
bool queued = super.QueueSupplyStatus(battery, now);
ERR_NativeMortarCourierProbe probe = ERR_NativeMortarCourierProbe.Current();
if (queued && probe)
foreach (ERR_HQUnit unit : m_Units)
if (unit.Group == battery.Group && unit.PendingSupplyStatus) probe.Uplink(unit.Uplink, unit.PendingSupplyStatus);
return queued;
}
}
class ERR_NativeMortarCourierProbe
{
protected static ERR_NativeMortarCourierProbe s_Current;
protected ERR_GameMode m_Mode;
protected ERR_MortarLogistics m_Service;
protected ERR_MortarBattery m_Battery;
protected ref array<IEntity> m_Original = {};
protected ref array<IEntity> m_Crew = {};
protected ref array<IEntity> m_Depot = {};
protected ref array<IEntity> m_Forces = {};
protected ref array<IEntity> m_Boxes = {};
protected ref array<BaseInventoryStorageComponent> m_Stores = {};
protected ref ERR_NativeCourierMoveCallback m_Callback;
protected IEntity m_Moving;
protected IEntity m_Source;
protected BaseInventoryStorageComponent m_Destination;
protected ref ERR_HQRoute m_Uplink;
protected ref ERR_HQRoute m_Downlink;
protected ref ERR_MortarSupplyStatus m_Status;
protected float m_ObservedAt;
protected int m_Message;
protected int m_Index;
protected int m_Stage;
protected float m_Deadline;
protected float m_MoveDeadline;
protected bool m_Complete;
protected bool m_Stopped;
protected vector m_Departure;
protected IEntity m_Bearer;
protected AIWaypointCycle m_OriginalPatrolCycle;
protected ref array<AIWaypoint> m_OriginalPatrolMoves = {};
static const int STORAGE_CANDIDATES = 64;
static const int STORAGE_COUNT = 5;
// The compact 8x8 grid keeps 3m between centers; blocked points are skipped.
static vector StorageOffset(int index)
{
if (index < 0 || index >= STORAGE_CANDIDATES) return vector.Zero;
int column = Math.Mod(index, 8);
int row = index / 8;
return Vector(20 + column * 3, 0, 15 + row * 3);
}
static const ResourceName BOX = "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et";
static ERR_NativeMortarCourierProbe Current() { return s_Current; }
static bool Enabled()
{
string smoke, outcome;
return Replication.IsServer() && System.GetCLIParam("errSmoke", smoke) && smoke == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mortar-courier";
}
void ERR_NativeMortarCourierProbe(ERR_GameMode mode) { m_Mode = mode; if (Enabled()) s_Current = this; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: mortar courier " + detail); return true; }
Print("ERR SMOKE FAIL: mortar courier " + detail, LogLevel.ERROR); Stop(); return false;
}
protected bool Has(IEntity owner, IEntity shell)
{
if (!owner || !shell) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(shell);
}
void Uplink(ERR_HQRoute route, ERR_MortarSupplyStatus report)
{
if (m_Stopped || !report || !m_Battery || report.Group != m_Battery.Group || m_Status) return;
m_Uplink = route; m_Status = report; m_ObservedAt = report.ObservedAt; m_Message = report.MessageId;
}
protected bool OriginalStored()
{
foreach (IEntity shell : m_Original)
{
if (!shell) return false;
int owners;
foreach (BaseInventoryStorageComponent storage : m_Stores) if (storage && storage.Contains(shell)) owners++;
foreach (IEntity actor : m_Crew) if (Has(actor, shell)) owners++;
if (owners != 1) return false;
}
return true;
}
protected bool PreservedForces(ERR_HQ hq)
{
array<IEntity> forces = {}; hq.PhysicalForces(forces);
if (forces.Count() != m_Forces.Count()) return false;
foreach (IEntity original : m_Forces) if (!original || !forces.Contains(original)) return false;
return true;
}
int Tick(float now)
{
if (m_Complete) return 1;
if (m_Stopped || !Enabled()) return -1;
if (m_Deadline == 0) m_Deadline = now + 360000;
if (now > m_Deadline) { Check(false, "bounded native courier deadline"); return -1; }
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return 0;
m_Service = hq.Logistics(); if (!m_Service) return 0;
m_Battery = m_Service.Battery();
if (m_Service.Stopped()) { Check(false, "production finite stock setup remains available"); return -1; }
if (!m_Battery || !m_Battery.StartupActivated || m_Service.State() == 0) return 0;
// No fire-controller hold, synthetic contact or routing bypass: this fixture
// requires the actual local prisoner to remain inside the gun's minimum range.
PlayerController controller = GetGame().GetPlayerController();
IEntity player; if (controller) player = controller.GetControlledEntity();
if (!player || !m_Mode.Players().CurrentActor(player) || vector.DistanceXZ(player.GetOrigin(), m_Battery.Gun.GetOrigin()) >= 300) { Check(false, "actual local prisoner remains below artillery minimum engagement range"); return -1; }
if (m_Stage == 0)
{
m_Service.InitialStock(m_Depot); hq.PhysicalForces(m_Forces);
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(m_Mode.FindComponent(ERR_ScenarioComponent));
ERR_SiteRuntime site = scenario.Sites().Find("everon-supply-2");
if (!Check(site && site.Group == m_Service.Group() && site.PatrolCycle, "same original western cache patrol")) return -1;
m_OriginalPatrolCycle = site.PatrolCycle; m_OriginalPatrolMoves.Copy(site.PatrolWaypoints);
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity(); m_Crew.Insert(actor);
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items) if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL && !m_Original.Contains(item)) m_Original.Insert(item);
}
if (!Check(m_Original.Count() == 27 && m_Depot.Count() == 12 && m_Battery.TotalShots == 0, "27 original crew rounds and 12 distinct finite depot rounds")) return -1;
foreach (IEntity depotShell : m_Depot) if (!Check(!m_Original.Contains(depotShell) && Has(m_Service.Cache(), depotShell), "depot round is a distinct original cache item")) return -1;
int tested, lowGround, obstructed;
for (int i = 0; i < STORAGE_CANDIDATES && m_Stores.Count() < STORAGE_COUNT; i++)
{
tested++;
vector position = ERR_Actors.Ground(m_Battery.Gun.GetOrigin() + StorageOffset(i));
if (position[1] <= 1) { lowGround++; continue; }
if (!SCR_WorldTools.TraceCilinderUtil(position + "0 1 0", 1.3, 2, TraceFlags.ENTS | TraceFlags.WORLD)) { obstructed++; continue; }
IEntity box = ERR_Actors.Spawn(BOX, position);
if (!Check(box != null, "fixture-only empty box created; no new ammunition")) return -1;
m_Mode.Own(box); m_Boxes.Insert(box);
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(box.FindComponent(BaseInventoryStorageComponent));
if (!Check(storage != null, "fixture box has real native storage")) return -1;
m_Stores.Insert(storage);
}
Print(string.Format("ERR COURIER STORAGE: tested=%1/64 ready=%2/5 lowGround=%3 obstructed=%4", tested, m_Stores.Count(), lowGround, obstructed));
if (!Check(m_Boxes.Count() == STORAGE_COUNT && m_Stores.Count() == STORAGE_COUNT, "five owned empty native stores ready before moving any original shell")) return -1;
Print("ERR COURIER FIXTURE: original 27 shells will be physically sequestered in five fixture boxes, not fired/destroyed; no report insertion, teleport, AI hold or replacement ammunition");
m_Stage = 1;
}
if (m_Stage == 1)
{
if (m_Callback)
{
if (m_Callback.Result < 0 || now >= m_MoveDeadline || !m_Moving) { Check(false, "native depletion move neither failed nor timed out/item lost"); return -1; }
InventoryItemComponent item = InventoryItemComponent.Cast(m_Moving.FindComponent(InventoryItemComponent));
if (m_Callback.Result == 0 || !item || !item.GetParentSlot() || item.GetParentSlot().GetStorage() != m_Destination || Has(m_Source, m_Moving) || !m_Destination.Contains(m_Moving)) return 0;
m_Callback = null; m_Moving = null; m_Source = null; m_Destination = null; m_Index++;
}
if (m_Index < m_Original.Count())
{
m_Moving = m_Original[m_Index];
foreach (IEntity owner : m_Crew) if (Has(owner, m_Moving)) m_Source = owner;
if (!Check(m_Source != null, "original shell still belongs to original crew before depletion")) return -1;
foreach (BaseInventoryStorageComponent candidate : m_Stores) if (candidate.PerformVolumeValidation(m_Moving)) { m_Destination = candidate; break; }
if (!Check(m_Destination != null, "five native boxes have capacity for original shells")) return -1;
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(m_Source.FindComponent(InventoryStorageManagerComponent));
m_Callback = new ERR_NativeCourierMoveCallback(); m_MoveDeadline = now + 8000;
if (!Check(source.TryMoveItemToStorage(m_Moving, m_Destination, -1, m_Callback), "native depletion move accepted, awaiting exact ownership proof")) return -1;
return 0;
}
if (!Check(OriginalStored() && m_Battery.CountShells() == 0, "all 27 original shells remain physical while real battery inventory is exhausted")) return -1;
m_Stage = 2;
}
if (!OriginalStored() || !PreservedForces(hq)) { Check(false, "all original shells and all original physical forces preserved"); return -1; }
if (!m_Downlink && m_Service.ProbeOrder()) m_Downlink = m_Service.ProbeOrder();
if (!m_Bearer && m_Service.Courier()) { m_Bearer = m_Service.Courier(); m_Departure = m_Bearer.GetOrigin(); }
if (m_Service.Delivered() < 1 || m_Service.CompletedTrips() < 1 || m_Service.State() != 1) return 0;
AIWaypoint current = m_Service.Group().GetCurrentWaypoint();
if (!current || (current != m_OriginalPatrolCycle && !m_OriginalPatrolMoves.Contains(current))) return 0;
if (!Check(m_Service.CompletedTrips() == 1 && hq.LogisticsToken(m_Service.Group()) == 0, "complete physical trip released HQ reservation and restored original native patrol")) return -1;
if (!Check(m_Status && m_Service.ProbeSourceMessage() == m_Message && m_Status.MessageId == m_Message && m_Status.ObservedAt == m_ObservedAt && ERR_MortarLogisticsRules.Fresh(m_ObservedAt, m_Service.ProbeLastDelivery()), "same immutable originally delivered supply status authorizes delivery")) return -1;
if (!Check(m_Uplink && !m_Uplink.Failed && !m_Uplink.Sending && m_Uplink.Cursor >= m_Uplink.Nodes.Count(), "actual low-ammo radio uplink reached HQ")) return -1;
if (!Check(m_Downlink && !m_Downlink.Failed && !m_Downlink.Sending && m_Downlink.Cursor == m_Downlink.Nodes.Count() - 1, "actual courier downlink completed before item delivery")) return -1;
if (!Check(m_Bearer && ERR_Actors.Group(m_Bearer) == m_Service.Group() && vector.DistanceXZ(m_Departure, m_Bearer.GetOrigin()) > 100, "same original bearer physically walked more than 100m")) return -1;
bool found; foreach (IEntity deliveredShell : m_Depot) foreach (IEntity recipient : m_Crew) if (Has(recipient, deliveredShell)) found = true;
if (!Check(found && m_Battery.CountShells() > 0 && m_Battery.TotalShots == 0, "previously empty battery now owns an exact original depot shell; no synthetic rounds or shots")) return -1;
foreach (IEntity retained : m_Depot)
{
int owners;
if (Has(m_Service.Cache(), retained)) owners++;
if (Has(m_Bearer, retained)) owners++;
foreach (IEntity crew : m_Crew) if (Has(crew, retained)) owners++;
if (!Check(retained && owners == 1, "each of 12 initial depot shells has exactly one physical owner")) return -1;
}
m_Complete = true; Stop(); return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true; if (s_Current == this) s_Current = null;
// Boxes with borrowed originals remain owned by normal mission cleanup.
// No manual restoration/deletion can race the production/native moves.
}
}
#endif