Integrate finite infantry resupply with delivered low-ammunition reports

This commit is contained in:
2026-09-20 07:32:24 +02:00
parent 915476f709
commit 782c163c68
44 changed files with 5690 additions and 31 deletions
@@ -113,6 +113,8 @@ class ERR_ConvoyVehicle
bool Released;
bool Registered;
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
int SupplyToken;
bool SupplyHeld;
bool Arrived;
float LegDeadline;
float NextReadinessNote;
@@ -127,7 +129,7 @@ class ERR_ConvoyVehicle
{
if (Released) return;
Released = true;
Combat.Held = false;
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
ClearOrder();
if (mode && mode.Forces())
{
@@ -153,7 +155,7 @@ class ERR_ConvoyVehicle
}
bool Order(ERR_ForceRoute route, float now)
{
if (Released || Arrived || Combat.Held || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
if (Released || Arrived || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
if (Waypoint) return true;
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
if (!Waypoint) return false;
@@ -166,7 +168,7 @@ class ERR_ConvoyVehicle
{
if (Released || Arrived) return;
if (!CrewValid(mode)) { Release(mode); return; }
if (Combat.Held) return;
if (Combat.Held || SupplyHeld) return;
// Native completion measures the seated group, not the vehicle origin.
// Keep the same predicate after native removal of the completed waypoint.
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
@@ -419,7 +421,7 @@ class ERR_ConvoyService
else
{
unit.Combat.Held = false;
if (!unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
}
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
return true;
+29 -6
View File
@@ -17,6 +17,7 @@ class ERR_HQUnit
ref ERR_HQSearchReport PendingSweep;
ref ERR_HQStatusReport PendingStatus;
ref ERR_HQStatusSampler StatusSampler = new ERR_HQStatusSampler();
ref ERR_HQAmmoReporting AmmoReporting = new ERR_HQAmmoReporting();
string ForceSourceId;
float NextStatus;
bool PendingCheckin;
@@ -96,6 +97,7 @@ class ERR_HQ : ERR_CommandService
protected ref ERR_HQBodyObserver m_Bodies = new ERR_HQBodyObserver();
protected ref ERR_HQWatch m_Watch = new ERR_HQWatch();
protected ref ERR_MortarLogistics m_Logistics;
protected ref ERR_InfantryResupply m_Resupply;
protected ref array<ref ERR_HQUnit> m_Units = {};
protected ref array<ref ERR_HQReport> m_Contacts = {};
protected ref ERR_HQMemory m_Memory = new ERR_HQMemory();
@@ -109,7 +111,7 @@ class ERR_HQ : ERR_CommandService
protected float m_NextNetwork;
protected float m_NextAssignment;
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Equipment = new ERR_EnemyEquipment(mode); m_ConvoyPool = new ERR_ConvoyPool(mode, 1); m_Convoys = new ERR_ConvoyService(mode, m_ConvoyPool); m_Mortars = new ERR_MortarSystem(this); m_Logistics = new ERR_MortarLogistics(this, mode); }
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Equipment = new ERR_EnemyEquipment(mode); m_ConvoyPool = new ERR_ConvoyPool(mode, 1); m_Convoys = new ERR_ConvoyService(mode, m_ConvoyPool); m_Mortars = new ERR_MortarSystem(this); m_Logistics = new ERR_MortarLogistics(this, mode); m_Resupply = new ERR_InfantryResupply(this, mode); }
static ERR_HQ Get()
{
ERR_GameMode mode = ERR_GameMode.Get();
@@ -283,12 +285,14 @@ class ERR_HQ : ERR_CommandService
if (ERR_MortarPrediction.Fresh(contact, now)) contacts.Insert(contact.Snapshot());
}
ERR_HQWatch Watch() { return m_Watch; }
ERR_InfantryResupply Resupply() { return m_Resupply; }
ERR_HQMemory Memory() { return m_Memory; }
ERR_HQCoverage Coverage() { return m_Coverage; }
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()) + string.Format("; %1 remembered tracks, %2 swept cells", m_Memory.Count(), m_Coverage.Count()) + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status();
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + string.Format("; %1 remembered tracks, %2 swept cells", m_Memory.Count(), m_Coverage.Count()) + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status() + "; " + m_Resupply.Status();
}
protected void BindPhysical(ERR_HQUnit unit)
@@ -325,6 +329,7 @@ class ERR_HQ : ERR_CommandService
m_Equipment.RegisterGroup(group, unit.Id, !m_Mortars || !m_Mortars.FindGroup(group));
BindPhysical(unit);
unit.NextStatus = GetGame().GetWorld().GetWorldTime() + 10000 + phase * 1000;
unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());
m_Journal.Write("REGISTER", string.Format("group %1", unit.Id));
}
@@ -424,17 +429,22 @@ class ERR_HQ : ERR_CommandService
protected void StatusReport(ERR_HQUnit unit, float now)
{
unit.StatusSampler.ObserveDeaths();
if (unit.Uplink || !unit.Radio || now < unit.NextStatus) return;
if (unit.Uplink || !unit.Radio) return;
bool routine = now >= unit.NextStatus;
if (!routine && !unit.AmmoReporting.ScanDue(now)) return;
unit.AmmoReporting.Scanned(now);
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route) return;
// A local sample reserves an identifier; a healthy early sample creates no frame.
ERR_HQStatusReport report = unit.StatusSampler.Sample(now, m_Journal.Message(), unit.Radio);
if (!report) return;
if (!report || (!routine && !unit.AmmoReporting.Urgent(report, now))) return;
route.MessageId = report.MessageId;
route.Payload = string.Format("status %1: %2 living, %3 fit, %4 confirmed losses; sampled=%5", report.SourceId, report.Count, report.Fit, report.ConfirmedLosses, report.ObservedAt);
string ammunition = "unknown";
if (report.AmmoState && report.AmmoState.Complete) ammunition = string.Format("%1 magazine rounds, %2 chambered", report.AmmoState.SmallArmsRounds, report.AmmoState.SmallArmsChambers);
route.Payload += "; small-arms ammunition=" + ammunition;
unit.PendingStatus = report; unit.Uplink = route; unit.NextStatus = now + 180000;
unit.AmmoReporting.Queued(now);
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
}
// Due routine status and requested check-ins get the next free uplink.
@@ -784,12 +794,23 @@ class ERR_HQ : ERR_CommandService
}
return best;
}
protected void PlanResupply(float now)
{
if (!m_Resupply || m_Resupply.State() != 1 || !m_ConvoyTask || m_ConvoyTask.Stopped
|| !m_ConvoyTask.Ready || m_ConvoyTask.Vehicles.Count() != 6) return;
ERR_HQUnit donor;
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == m_ConvoyTask.Vehicles[1].Group) donor = unit;
if (!donor || ConvoyContact(donor, now)) return;
ERR_HQWatchEntry donorStatus = m_Watch.Find(donor.ForceSourceId);
foreach (ERR_HQUnit recipient : m_Units)
if (m_Resupply.TryPlan(recipient, donor, m_Watch.Find(recipient.ForceSourceId), donorStatus, now)) return;
}
protected void PlanConvoy(float now)
{
if (!m_ConvoyTask || m_ConvoyTask.Stopped || !m_ConvoyTask.Ready) return;
foreach (ERR_ConvoyVehicle vehicle : m_ConvoyTask.Vehicles)
{
if (vehicle.Released || vehicle.Arrived || !vehicle.Registered || !vehicle.Group) continue;
if (vehicle.Released || vehicle.Arrived || vehicle.SupplyToken > 0 || !vehicle.Registered || !vehicle.Group) continue;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != vehicle.Group || !unit.ReservedForSupport || unit.Downlink || !unit.Radio) continue;
@@ -992,7 +1013,8 @@ class ERR_HQ : ERR_CommandService
if (!unit.Group && !unit.Uplink) { ReleaseUnit(unit, now); m_Units.Remove(i); continue; }
QueueReports(unit, now);
}
if (now >= m_NextAssignment) { PlanConvoy(now); Assign(now); m_NextAssignment = now + 5000; }
if (now >= m_NextAssignment) { PlanConvoy(now); PlanResupply(now); Assign(now); m_NextAssignment = now + 5000; }
m_Resupply.Tick(now);
m_Logistics.Tick(now);
m_Mortars.Tick(now);
m_Journal.Flush();
@@ -1000,6 +1022,7 @@ class ERR_HQ : ERR_CommandService
override void Stop()
{
m_Resupply.Stop();
m_Convoys.Stop();
m_Equipment.Stop();
m_Logistics.Stop();
@@ -0,0 +1,44 @@
// Local own-roster sampling schedule only. Urgency requests an ordinary status
// frame; it grants no HQ knowledge and never changes ammunition or assignments.
class ERR_HQAmmoReporting
{
protected float m_NextScan = -1;
protected float m_NextCall = 0;
protected float m_LastAttempt = -1;
void Initialize(int sourceId, float now)
{
if (m_NextScan >= 0 || sourceId <= 0 || !ERR_HQPursuitRules.Finite(now) || now < 0) return;
int phase = sourceId % 6;
m_NextScan = now + phase * 5000;
}
bool ScanDue(float now)
{
return m_NextScan >= 0 && ERR_HQPursuitRules.Finite(now) && now >= m_NextScan;
}
// An unavailable route also consumes this local attempt. Routine/requested
// check-ins retain their independent existing retry and deadline semantics.
void Scanned(float now)
{
if (!ERR_HQPursuitRules.Finite(now) || now < 0 || now < m_LastAttempt) return;
m_LastAttempt = now;
m_NextScan = now + 30000;
}
bool Urgent(ERR_HQStatusReport report, float now)
{
if (!report || !report.AmmoState || !ERR_HQPursuitRules.Finite(now) || now < 0
|| now < m_NextCall || report.ObservedAt != now) return false;
ERR_HQAmmoSnapshot stock = report.AmmoState;
if (!stock.Complete || !stock.Valid(report.Count) || stock.Members <= 0
|| stock.SmallArmsMuzzles <= 0) return false;
// Chambers are usable rounds too. Launchers never substitute for small arms.
return stock.SmallArmsRounds + stock.SmallArmsChambers <= 30 * stock.Members;
}
// Any queued status already reports current stock. Persistent low ammunition
// can ask again after one minute, even when the previous route was interrupted.
void Queued(float now)
{
if (!ERR_HQPursuitRules.Finite(now) || now < 0 || now < m_LastAttempt) return;
m_NextCall = now + 60000;
}
}
@@ -0,0 +1,187 @@
enum ERR_InfantryAmmoTransferState { FAILED = -1, PENDING, SUCCEEDED }
class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback
{
ERR_InfantryAmmoTransfer Transfer;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
protected override void OnAllComplete() { if (Transfer) Transfer.Acknowledge(true); }
protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false); }
}
// One exact physical magazine movement. The caller owns HQ authorization and
// the spent-item reservation; this leaf never selects tasks or retries a move.
class ERR_InfantryAmmoTransfer
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Donor;
protected SCR_AIGroup m_DonorGroup;
protected IEntity m_Recipient;
protected SCR_AIGroup m_RecipientGroup;
protected IEntity m_Magazine;
protected IEntity m_Driver;
protected ERR_ForceRecord m_DonorRecord;
protected ERR_ForceRecord m_RecipientRecord;
protected InventoryStorageManagerComponent m_Source;
protected InventoryStorageManagerComponent m_Destination;
protected BaseInventoryStorageComponent m_Storage;
protected ref ERR_InfantryAmmoCallback m_Callback;
protected bool m_Submitted;
protected bool m_Accepted;
protected int m_Acknowledgement;
protected float m_Started = -1;
protected ERR_InfantryAmmoTransferState m_State = ERR_InfantryAmmoTransferState.PENDING;
protected string m_Reason;
void ERR_InfantryAmmoTransfer(ERR_GameMode mode, Vehicle donorVehicle, SCR_AIGroup donorGroup, IEntity recipientActor, SCR_AIGroup recipientGroup, IEntity exactMagazine)
{
m_Mode = mode; m_Donor = donorVehicle; m_DonorGroup = donorGroup;
m_Recipient = recipientActor; m_RecipientGroup = recipientGroup; m_Magazine = exactMagazine;
}
static bool Compatible(IEntity actor, IEntity exactMagazine)
{
if (!actor || !exactMagazine) return false;
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent));
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
if (!magazine || !manager || !magazine.GetMagazineWell() || magazine.GetAmmoCount() <= 0 || magazine.GetMaxAmmoCount() <= 0 || magazine.GetAmmoCount() > magazine.GetMaxAmmoCount()) return false;
array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);
if (weapons.IsEmpty() || weapons.Count() > 8) return false;
foreach (IEntity rifle : weapons)
{
if (!rifle) continue;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent));
if (!weapon || weapon.GetOwner() != rifle) continue;
EWeaponType kind = weapon.GetWeaponType();
if (kind != EWeaponType.WT_RIFLE && kind != EWeaponType.WT_SNIPERRIFLE && kind != EWeaponType.WT_MACHINEGUN && kind != EWeaponType.WT_HANDGUN) continue;
array<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
if (muzzles.Count() > 4) continue;
foreach (BaseMuzzleComponent muzzle : muzzles)
{
if (!muzzle || !muzzle.GetMagazineWell()) continue;
EMuzzleType muzzleKind = muzzle.GetMuzzleType();
if (muzzleKind != EMuzzleType.MT_BaseMuzzle) continue;
// Same direction as the stock magazine inventory predicate: a magazine's
// derived well must inherit the actual carried small-arms muzzle's well.
if (magazine.GetMagazineWell().IsInherited(muzzle.GetMagazineWell().Type())) return true;
}
}
return false;
}
static bool StoredUnder(IEntity entity, IEntity root)
{
for (int depth = 0; entity && depth < 16; depth++)
{
if (entity == root) return true;
InventoryItemComponent item = InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent));
if (item && item.GetParentSlot())
{
BaseInventoryStorageComponent storage = item.GetParentSlot().GetStorage();
if (!storage) return false;
entity = storage.GetOwner();
}
else entity = entity.GetParent();
}
return false;
}
protected bool ManagedStorage(InventoryStorageManagerComponent inventory, BaseInventoryStorageComponent storage, IEntity owner)
{
if (!inventory || !storage || inventory.GetOwner() != owner || !StoredUnder(storage.GetOwner(), owner)) return false;
array<BaseInventoryStorageComponent> storages = {};
inventory.GetStorages(storages, EStoragePurpose.PURPOSE_DEPOSIT);
return storages.Count() <= 64 && storages.Contains(storage);
}
protected int Ownership(InventoryStorageManagerComponent inventory)
{
if (!inventory || !m_Magazine) return -1;
array<IEntity> items = {}; inventory.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
if (items.Count() > 512) return -1;
if (items.Contains(m_Magazine)) return 1;
return 0;
}
protected bool PhysicalProof()
{
if (!m_Magazine || m_Acknowledgement != 1 || Ownership(m_Source) != 0 || Ownership(m_Destination) != 1 || !ManagedStorage(m_Destination, m_Storage, m_Recipient)) return false;
InventoryItemComponent item = InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent));
return item && item.GetParentSlot() && item.GetParentSlot().GetStorage() == m_Storage;
}
void Acknowledge(bool success)
{
if (m_Acknowledgement != 0) return;
if (success) m_Acknowledgement = 1;
else m_Acknowledgement = -1;
}
// This read-only observation works even after Stop; the caller must retain
// ambiguous accepted operations and cannot interpret failure as cancellation.
bool Unresolved()
{
if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;
return !PhysicalProof();
}
bool Submitted() { return m_Submitted; }
string Reason() { return m_Reason; }
protected ERR_InfantryAmmoTransferState Fail(string reason)
{
if (m_State == ERR_InfantryAmmoTransferState.PENDING) { m_State = ERR_InfantryAmmoTransferState.FAILED; m_Reason = reason; }
return m_State;
}
void Stop() { Fail("HQ authorization stopped; an accepted native move cannot be cancelled"); }
protected bool Context()
{
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || !m_Mode.Forces()) return false;
if (!m_Donor || !ERR_Actors.Alive(m_Donor) || !m_DonorGroup || !m_RecipientGroup || m_DonorGroup == m_RecipientGroup) return false;
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, m_RecipientGroup, m_Recipient) || CompartmentAccessComponent.GetVehicleIn(m_Recipient)) return false;
if (m_DonorGroup.IsSlave() || m_RecipientGroup.IsSlave() || m_DonorGroup.GetPlayerCount(true) > 0 || m_DonorGroup.GetFaction() != m_Mode.EnemyFaction()) return false;
if (SCR_FactionManager.SGetFaction(m_Donor) != m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(m_Recipient) != m_Mode.EnemyFaction()) return false;
ERR_ForceRecord donorRecord = m_Mode.Forces().FindGroup(m_DonorGroup);
ERR_ForceRecord recipientRecord = m_Mode.Forces().FindGroup(m_RecipientGroup);
if (!donorRecord || !recipientRecord || donorRecord.Group != m_DonorGroup || recipientRecord.Group != m_RecipientGroup || !donorRecord.Physical || !recipientRecord.Physical || donorRecord.Eliminated || recipientRecord.Eliminated) return false;
if (m_DonorRecord && (m_DonorRecord != donorRecord || m_RecipientRecord != recipientRecord)) return false;
SCR_AIGroupUtilityComponent utility = m_DonorGroup.GetGroupUtilityComponent();
if (!utility || !utility.IsMilitary() || !utility.m_VehicleMgr || !utility.m_VehicleMgr.FindVehicle(m_Donor)) return false;
if (ERR_ConvoyRules.Captured(m_Donor, m_DonorGroup)) return false;
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Donor.FindComponent(SCR_AIVehicleUsageComponent));
if (!usage || !usage.GetPilotCompartmentSlot()) return false;
IEntity driver = usage.GetPilotCompartmentSlot().GetOccupant();
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, m_DonorGroup, driver) || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(driver), m_Donor)) return false;
if (SCR_FactionManager.SGetFaction(driver) != m_Mode.EnemyFaction() || (m_Driver && driver != m_Driver)) return false;
Physics physics = m_Donor.GetPhysics();
if (!physics) return false;
float speed = physics.GetVelocity().Length();
float angular = physics.GetAngularVelocity().Length();
float distance = vector.Distance(m_Donor.GetOrigin(), m_Recipient.GetOrigin());
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1 && distance >= 0 && distance <= 4)) return false;
if (!Compatible(m_Recipient, m_Magazine)) return false;
m_Driver = driver; m_DonorRecord = donorRecord; m_RecipientRecord = recipientRecord;
return true;
}
ERR_InfantryAmmoTransferState Tick(float now)
{
if (m_State != ERR_InfantryAmmoTransferState.PENDING) return m_State;
if (!ERR_ForceRules.Finite(now) || now < 0) return Fail("invalid transaction clock");
if (m_Started < 0) m_Started = now;
if (now < m_Started || now - m_Started >= 8000) return Fail("native magazine move deadline expired");
if (!Context()) return Fail("original owned groups, vehicle, driver, recipient, proximity or compatible weapon unavailable");
if (!m_Submitted)
{
m_Source = InventoryStorageManagerComponent.Cast(m_Donor.FindComponent(InventoryStorageManagerComponent));
m_Destination = InventoryStorageManagerComponent.Cast(m_Recipient.FindComponent(InventoryStorageManagerComponent));
if (!m_Source || !m_Destination || m_Source == m_Destination || Ownership(m_Source) != 1 || Ownership(m_Destination) != 0) return Fail("exact magazine is not exclusive donor stock");
InventoryItemComponent item = InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent));
if (!item || !item.GetParentSlot() || !ManagedStorage(m_Source, item.GetParentSlot().GetStorage(), m_Donor)) return Fail("exact source magazine storage does not belong to original donor");
m_Storage = m_Destination.FindStorageForItem(m_Magazine, EStoragePurpose.PURPOSE_DEPOSIT);
if (!ManagedStorage(m_Destination, m_Storage, m_Recipient) || !m_Source.CanMoveItemToStorage(m_Magazine, m_Storage, -1)) return Fail("recipient has no native compatible available storage");
m_Callback = new ERR_InfantryAmmoCallback(); m_Callback.Transfer = this;
m_Submitted = true;
m_Accepted = m_Source.TryMoveItemToStorage(m_Magazine, m_Storage, -1, m_Callback);
if (m_Acknowledgement == 1) m_Accepted = true;
if (!m_Accepted) return Fail("native magazine move rejected; exact item will not be retried");
}
if (PhysicalProof())
{
m_State = ERR_InfantryAmmoTransferState.SUCCEEDED;
m_Reason = "native callback and exact destination storage confirm one physical magazine transferred";
return m_State;
}
if (m_Acknowledgement < 0) return Fail("native magazine move failed");
return ERR_InfantryAmmoTransferState.PENDING;
}
}
@@ -0,0 +1,278 @@
// HQ-owned ground rendezvous using delivered positions and existing finite cargo.
class ERR_InfantryResupplyRules
{
static bool Fresh(ERR_HQWatchEntry entry, float now)
{
return entry && !entry.Closed && entry.Count > 0 && entry.Fit > 0
&& ERR_ForceRules.Finite(now) && entry.ObservedAt >= 0 && now >= entry.ObservedAt
&& now - entry.ObservedAt <= 120000 && ERR_ForceRules.FiniteVector(entry.Position);
}
static bool Need(ERR_HQWatchEntry entry, float now)
{
return Fresh(entry, now) && entry.AmmoState && entry.AmmoState.Valid(entry.Count)
&& entry.AmmoState.Complete && entry.AmmoState.Members == entry.Count
&& entry.AmmoState.SmallArmsMuzzles > 0
&& entry.AmmoState.SmallArmsRounds + entry.AmmoState.SmallArmsChambers <= entry.Count * 30;
}
static bool Pair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
{
return Need(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
&& vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
}
}
class ERR_InfantryResupply
{
protected ERR_HQ m_HQ;
protected ERR_GameMode m_Mode;
protected ERR_ConvoyTask m_Convoy;
protected ERR_ConvoyVehicle m_Truck;
protected ERR_HQUnit m_Recipient;
protected ERR_HQUnit m_Donor;
protected ref array<IEntity> m_Stock = {};
protected ref array<IEntity> m_Spent = {};
protected ref array<IEntity> m_Members = {};
protected ref array<IEntity> m_Served = {};
protected ref map<string, int> m_Consumed = new map<string, int>();
protected ref map<string, float> m_NextAttempt = new map<string, float>();
protected ref ERR_HQRoute m_Order;
protected ref ERR_InfantryAmmoTransfer m_Move;
protected IEntity m_Receiving;
protected AIWaypoint m_MoveWaypoint;
protected AIWaypoint m_HoldWaypoint;
protected vector m_Rendezvous;
protected int m_State; // 0 stock startup, 1 idle, 2 truck downlink, 3 stopped status, 4 infantry downlink, 5 transfer
protected int m_Trip;
protected int m_Token;
protected int m_ReportId;
protected int m_Delivered;
protected int m_TripDelivered;
protected float m_Deadline;
protected float m_HeldAt;
protected float m_NextTick;
protected bool m_Stopped;
protected string m_Reason;
void ERR_InfantryResupply(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
int State() { return m_State; }
int Delivered() { return m_Delivered; }
int StockCount() { return m_Stock.Count(); }
protected void Audit(string reason)
{
m_Reason = reason;
m_HQ.Journal().Write("INFANTRY SUPPLY", string.Format("trip=%1 state=%2 moved=%3 spent=%4/%5; %6", m_Trip, m_State, m_Delivered, m_Spent.Count(), m_Stock.Count(), reason), m_ReportId);
}
protected bool Current()
{
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active();
}
protected bool Contains(IEntity owner, IEntity item)
{
if (!owner || !item) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
return items.Count() <= 128 && items.Contains(item);
}
protected bool TruckValid()
{
if (!m_Convoy || m_Convoy.Stopped || !m_Convoy.Ready || !m_Convoy.Receipt || !m_Convoy.Receipt.Consumed
|| m_HQ.ConvoyTask() != m_Convoy || m_Convoy.Vehicles.Count() != 6 || m_Convoy.Vehicles[1] != m_Truck) return false;
if (!m_Truck || m_Truck.Index != 1 || !m_Truck.Registered || m_Truck.Released || m_Truck.Arrived
|| !m_Truck.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(m_Truck.Entity, m_Truck.Group)) return false;
ERR_ForceRecord record = m_Mode.Forces().FindGroup(m_Truck.Group);
return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
&& m_Convoy.Receipt.Records.Count() == 6 && record.Id == m_Convoy.Receipt.Records[1];
}
protected bool Quiet(ERR_HQUnit unit)
{
return unit && unit.Group && unit.Utility && !unit.Group.IsSlave() && unit.Group.GetPlayerCount(true) == 0
&& unit.Group.GetFaction() == m_Mode.EnemyFaction()
&& (!unit.Utility.m_Perception || unit.Utility.m_Perception.m_aTargets.IsEmpty());
}
protected bool Roster()
{
if (!m_Recipient || !Quiet(m_Recipient) || !m_Recipient.StatusSampler.OwnsRoster(m_Recipient.Group)) return false;
int strength = m_Recipient.StatusSampler.InitialStrength();
if (strength < 1 || strength > 32) return false;
array<AIAgent> agents = {}; m_Recipient.Group.GetAgents(agents);
if (agents.IsEmpty() || agents.Count() > 32) return false;
foreach (AIAgent agent : agents)
if (!agent || !m_Members.Contains(agent.GetControlledEntity())) return false;
bool living;
foreach (IEntity actor : m_Members)
{
if (!actor || !ERR_Actors.Alive(actor)) continue;
// Returning to the group cannot undo a sampler's permanent release of an
// original slot after transfer or player control during this trip.
bool attributed = false;
for (int slot = 0; slot < strength; slot++)
{
if (m_Recipient.StatusSampler.InitialMember(slot) != actor) continue;
attributed = m_Recipient.StatusSampler.CanAttribute(slot, actor);
break;
}
if (!attributed) return false;
if (ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Recipient.Group
|| SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction() || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
if (ERR_Actors.CanFight(actor)) living = true;
}
return living;
}
protected void Stock(float now)
{
m_Convoy = m_HQ.ConvoyTask();
if (!m_Convoy || !m_Convoy.Ready || m_Convoy.Vehicles.Count() != 6) return;
m_Truck = m_Convoy.Vehicles[1];
if (!TruckValid()) { Stop(); return; }
foreach (ERR_ConvoyCargoCallback operation : m_Convoy.Cargo)
if (!operation.Finished) { if (now >= m_Convoy.Deadline) Stop(); return; }
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Truck.Entity.FindComponent(InventoryStorageManagerComponent));
if (!inventory) { Stop(); return; }
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
if (items.Count() > 128) { Stop(); return; }
foreach (IEntity item : items)
{
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.Magazine()) continue;
if (m_Stock.Count() >= 48) { Stop(); return; }
m_Stock.Insert(item);
}
m_State = 1; Audit("truck magazine identities captured once after startup; absent/looted stock is never replaced");
}
bool TryPlan(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
{
if (!Current() || m_State != 1 || m_Move || m_Trip >= 128 || !TruckValid()) return false;
if (m_Truck.Combat.Held || m_Truck.SupplyToken > 0 || !recipient || !donor) return false;
if (donor.Group != m_Truck.Group || recipient.HomeGuard || !Quiet(recipient) || !Quiet(donor)) return false;
if (donor.Downlink || donor.PendingOrder || donor.PendingConvoyOrder) return false;
if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;
if (demand.SourceId != recipient.ForceSourceId || donorStatus.SourceId != donor.ForceSourceId) return false;
if (demand.MessageId <= m_Consumed.Get(demand.SourceId) || now < m_NextAttempt.Get(demand.SourceId)) return false;
bool available;
foreach (IEntity magazine : m_Stock) if (magazine && !m_Spent.Contains(magazine) && Contains(m_Truck.Entity, magazine)) { available = true; break; }
if (!available) return false;
m_Members.Clear();
for (int i = 0; i < recipient.StatusSampler.InitialStrength(); i++)
{
IEntity actor = recipient.StatusSampler.InitialMember(i);
if (actor && recipient.StatusSampler.CanAttribute(i, actor)) m_Members.Insert(actor);
}
m_Recipient = recipient; m_Donor = donor;
if (!Roster()) { m_Recipient = null; m_Donor = null; return false; }
// Disjoint bounded token range from the existing mortar courier's trip IDs.
int token = 100000 + m_Trip + 1;
if (!m_HQ.ReserveLogistics(recipient.Group, token)) { m_Recipient = null; m_Donor = null; return false; }
m_Trip++; m_Token = token; m_Truck.SupplyToken = token; m_ReportId = demand.MessageId; m_TripDelivered = 0; m_Served.Clear();
m_Consumed.Set(demand.SourceId, demand.MessageId); m_NextAttempt.Set(demand.SourceId, now + 180000);
m_Deadline = Math.Min(now + 120000, demand.ObservedAt + 180000); m_State = 2;
m_Order = m_HQ.Downlink(donor.Group, "INFANTRY SUPPLY HOLD", "stop existing ammo truck and report position", m_ReportId);
if (!m_Order) Finish("truck radio downlink unavailable");
else Audit("existing patrol reserved; truck hold awaits radio delivery");
return true;
}
protected bool Waypoints()
{
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);
m_Recipient.Group.AddWaypointAt(m_HoldWaypoint, 0); m_Recipient.Group.AddWaypointAt(m_MoveWaypoint, 0);
m_Recipient.Group.ActivateAI(); return true;
}
protected void ClearWaypoints()
{
if (m_MoveWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_MoveWaypoint); ERR_Actors.DeleteSpawned(m_MoveWaypoint); }
if (m_HoldWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_HoldWaypoint); ERR_Actors.DeleteSpawned(m_HoldWaypoint); }
m_MoveWaypoint = null; m_HoldWaypoint = null;
}
protected void Finish(string reason)
{
Audit(reason);
if (m_Move) m_Move.Stop();
ClearWaypoints();
if (m_Recipient) m_HQ.ReleaseLogistics(m_Recipient.Group, m_Token);
if (m_Truck && m_Truck.SupplyToken == m_Token) { m_Truck.SupplyToken = 0; m_Truck.SupplyHeld = false; }
m_Recipient = null; m_Donor = null; m_Order = null; m_Receiving = null; m_State = 1;
}
protected void Transfer(float now)
{
if (m_Move)
{
int result = m_Move.Tick(now);
if (result == 0) return;
if (result < 0) { Finish("native transfer rejected, expired or lost authority; no item retry"); return; }
m_Move = null; m_Delivered++; m_TripDelivered++; m_Served.Insert(m_Receiving); m_Receiving = null;
Audit("exact finite magazine confirmed in original soldier inventory");
if (m_TripDelivered >= 6) { Finish("six-magazine trip allowance reached"); return; }
}
foreach (IEntity actor : m_Members)
{
if (!ERR_Actors.CanFight(actor) || m_Served.Contains(actor) || vector.DistanceXZ(actor.GetOrigin(), m_Truck.Entity.GetOrigin()) > 4) continue;
ERR_HQAmmoSnapshot local = new ERR_HQAmmoSnapshot(); local.Complete = true; local.LauncherComplete = true;
ERR_HQAmmoSampler.Inspect(actor, m_Recipient.Group, local);
if (!local.Complete || local.SmallArmsMuzzles <= 0 || local.SmallArmsRounds + local.SmallArmsChambers > 30) continue;
foreach (IEntity magazine : m_Stock)
{
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;
}
}
}
void Tick(float now)
{
if (!Current() || !ERR_ForceRules.Finite(now) || now < m_NextTick) return;
m_NextTick = now + 1000;
if (m_State == 0) { Stock(now); return; }
if (m_State == 1)
{
if (m_Move && !m_Move.Unresolved()) m_Move = null;
return;
}
if (now >= m_Deadline || !TruckValid() || !Quiet(m_Donor) || !Roster() || m_Truck.Combat.Held
|| m_HQ.LogisticsToken(m_Recipient.Group) != m_Token)
{ Finish("rendezvous expired, contact appeared or original ownership changed"); return; }
if (m_Truck.SupplyToken != m_Token) { Finish("truck hold authority lost"); return; }
if (m_State == 2)
{
int delivered = m_Order.Advance(now, m_HQ.Journal());
if (delivered == 0) return;
if (delivered < 0 || m_Truck.SupplyToken != m_Token) { Finish("truck hold downlink interrupted or reservation lost"); return; }
m_Truck.SupplyHeld = true; m_Truck.ClearOrder(); m_HeldAt = -1;
m_Order = null; m_State = 3; Audit("truck received stop; waiting for delivered stopped-position report"); return;
}
if (m_State == 3)
{
// The delivered hold asks the original crew to report only after physically stopping.
if (m_HeldAt < 0)
{
Physics physics = m_Truck.Entity.GetPhysics();
if (!physics) return;
float speed = physics.GetVelocity().Length();
float angular = physics.GetAngularVelocity().Length();
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1)) return;
m_HeldAt = now; m_Donor.NextStatus = now; return;
}
ERR_HQWatchEntry stopped = m_HQ.Watch().Find(m_Donor.ForceSourceId);
if (!ERR_InfantryResupplyRules.Fresh(stopped, now) || stopped.ObservedAt < m_HeldAt) return;
m_Rendezvous = stopped.Position + "3 0 0";
m_Order = m_HQ.Downlink(m_Recipient.Group, "INFANTRY SUPPLY RENDEZVOUS", string.Format("walk to reported stopped ammo truck at %1", m_Rendezvous), m_ReportId);
if (!m_Order) { Finish("infantry radio downlink unavailable"); return; }
m_State = 4; return;
}
if (m_State == 4)
{
int delivered = m_Order.Advance(now, m_HQ.Journal());
if (delivered == 0) return;
if (delivered < 0 || !Waypoints()) { Finish("infantry order interrupted or native waypoint creation failed"); return; }
m_Order = null; m_State = 5; Audit("infantry received reported rendezvous; native movement and physical transfer required");
}
if (m_State == 5) Transfer(now);
}
string Status() { return string.Format("Infantry supply: state=%1 trips=%2 moved=%3 stock=%4 spent=%5; %6", m_State, m_Trip, m_Delivered, m_Stock.Count(), m_Spent.Count(), m_Reason); }
void Stop()
{
if (m_Stopped) return;
Finish("service stopped; original forces and all remaining cargo retained"); m_Stopped = true;
}
}