Files
EscapistsReintroduced/docs/evidence/recovery-launcher-2026-10-02/launcher/fix.patch
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

708 lines
57 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -1296,6 +1296,10 @@
// finite truck and delivered radio entries only after that scan fails.
foreach (ERR_HQUnit mounted : m_Units)
if (!ConvoyContact(mounted, now) && supply.TryPlanMounted(mounted, donor, m_Watch.Find(mounted.ForceSourceId), donorStatus, now)) return true;
+ // Supported depleted launcher magazines follow rifle/mounted demand and
+ // share the same finite stock, radio replay and physical rendezvous service.
+ foreach (ERR_HQUnit launcherRecipient : m_Units)
+ if (supply.TryPlanLauncher(launcherRecipient, donor, m_Watch.Find(launcherRecipient.ForceSourceId), donorStatus, now)) return true;
// Optional finite mines are last: only a delivered known one-mine formation
// may top up an existing carrier; no new issue or engineer-task reset.
foreach (ERR_HQUnit mineRecipient : m_Units)
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c
@@ -12,6 +12,7 @@
int SmallArmsRounds;
int SmallArmsMagazines;
int SmallArmsChambers;
+ int LauncherMuzzles;
int LauncherRounds;
int LauncherMagazines;
int LauncherChambers;
@@ -25,7 +26,7 @@
copy.Complete = Complete; copy.LauncherComplete = LauncherComplete; copy.Members = Members;
copy.SmallArmsMuzzles = SmallArmsMuzzles; copy.SmallArmsRounds = SmallArmsRounds;
copy.SmallArmsMagazines = SmallArmsMagazines; copy.SmallArmsChambers = SmallArmsChambers;
- copy.LauncherRounds = LauncherRounds; copy.LauncherMagazines = LauncherMagazines;
+ copy.LauncherMuzzles = LauncherMuzzles; copy.LauncherRounds = LauncherRounds; copy.LauncherMagazines = LauncherMagazines;
copy.LauncherChambers = LauncherChambers;
copy.MineComplete = MineComplete; copy.MineMembers = MineMembers; copy.ReadyMines = ReadyMines;
return copy;
@@ -39,6 +40,7 @@
if (SmallArmsRounds < 0 || SmallArmsRounds > 4096000) return false;
if (SmallArmsMagazines < 0 || SmallArmsMagazines > 4096) return false;
if (SmallArmsChambers < 0 || SmallArmsChambers > 4096) return false;
+ if (LauncherMuzzles < 0 || LauncherMuzzles > Members * 32) return false;
if (LauncherRounds < 0 || LauncherRounds > 4096000) return false;
if (LauncherMagazines < 0 || LauncherMagazines > 4096) return false;
return LauncherChambers >= 0 && LauncherChambers <= 4096;
@@ -137,7 +139,15 @@
int category = Category(weapon, muzzle);
if (category == 0) { result.Complete = false; result.LauncherComplete = false; continue; }
if (category == 1) { small.Insert(muzzle); result.SmallArmsMuzzles++; }
- else launchers.Insert(muzzle);
+ else
+ {
+ launchers.Insert(muzzle);
+ SCR_MuzzleInMagComponent reloadable = SCR_MuzzleInMagComponent.Cast(muzzle);
+ bool supported = muzzle.GetMagazineWell() && !muzzle.IsDisposable();
+ if (reloadable && !reloadable.CanBeReloaded()) supported = false;
+ if (supported) result.LauncherMuzzles++;
+ else result.LauncherComplete = false;
+ }
if (!muzzle.GetMagazineWell())
{
if (category == 1) result.Complete = false;
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c
@@ -26,8 +26,11 @@
protected InventoryStorageManagerComponent m_Source;
protected InventoryStorageManagerComponent m_Destination;
protected BaseInventoryStorageComponent m_Storage;
+ protected InventoryStorageSlot m_OriginalSlot;
+ protected BaseInventoryStorageComponent m_OriginalStorage;
protected ref ERR_InfantryAmmoCallback m_Callback;
protected bool m_RecycleEmpty;
+ protected bool m_LauncherMagazine;
protected IEntity m_ReturnMagazine;
protected BaseInventoryStorageComponent m_ReturnStorage;
protected ref ERR_InfantryAmmoCallback m_ReturnCallback;
@@ -42,18 +45,19 @@
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, bool recycleEmpty = false)
- {
- m_RecycleEmpty = recycleEmpty;
+ void ERR_InfantryAmmoTransfer(ERR_GameMode mode, Vehicle donorVehicle, SCR_AIGroup donorGroup, IEntity recipientActor, SCR_AIGroup recipientGroup, IEntity exactMagazine, bool recycleEmpty = false, bool launcherMagazine = false)
+ {
+ m_RecycleEmpty = recycleEmpty; m_LauncherMagazine = launcherMagazine;
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)
+ static bool Compatible(IEntity actor, IEntity exactMagazine, bool launcherMagazine = false)
{
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;
+ if (launcherMagazine && magazine.IsUsed()) return false;
array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);
if (weapons.IsEmpty() || weapons.Count() > 8) return false;
foreach (IEntity rifle : weapons)
@@ -62,14 +66,38 @@
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;
+ bool smallWeapon = kind == EWeaponType.WT_RIFLE || kind == EWeaponType.WT_SNIPERRIFLE || kind == EWeaponType.WT_MACHINEGUN || kind == EWeaponType.WT_HANDGUN;
+ if (!launcherMagazine && !smallWeapon) continue;
+ if (launcherMagazine && !smallWeapon && kind != EWeaponType.WT_ROCKETLAUNCHER && kind != EWeaponType.WT_GRENADELAUNCHER) continue;
+ if (launcherMagazine && !StoredUnder(rifle, actor)) 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;
+ if (!launcherMagazine && muzzleKind != EMuzzleType.MT_BaseMuzzle) continue;
+ if (launcherMagazine)
+ {
+ if (muzzleKind != EMuzzleType.MT_RPGMuzzle && muzzleKind != EMuzzleType.MT_UGLMuzzle
+ && kind != EWeaponType.WT_ROCKETLAUNCHER && kind != EWeaponType.WT_GRENADELAUNCHER) continue;
+ // An expended single-round item may no longer exist. The real reloadable
+ // muzzle/well proves capability; existing loaded items need exact custody.
+ if (muzzle.IsDisposable()) continue;
+ SCR_MuzzleInMagComponent reloadable = SCR_MuzzleInMagComponent.Cast(muzzle);
+ if (reloadable && !reloadable.CanBeReloaded()) continue;
+ BaseMagazineComponent loaded = muzzle.GetMagazine();
+ if (loaded)
+ {
+ if (!loaded.GetOwner() || !loaded.GetMagazineWell() || loaded.GetMaxAmmoCount() <= 0) continue;
+ InventoryItemComponent loadedItem = InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent));
+ if (!loadedItem || !loadedItem.GetParentSlot()) continue;
+ InventoryStorageSlot loadedSlot = loadedItem.GetParentSlot();
+ if (loadedSlot.GetParentContainer() != muzzle || loadedSlot.GetAttachedEntity() != loaded.GetOwner()) continue;
+ if (loaded.GetMagazineWell().Type() != muzzle.GetMagazineWell().Type()) continue;
+ }
+ if (magazine.GetMagazineWell().Type() != muzzle.GetMagazineWell().Type()) 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;
@@ -119,6 +147,20 @@
{
return Proof(m_Magazine, m_Source, m_Destination, m_Storage, m_Recipient, m_Acknowledgement);
}
+ // Terminal negative callback is settled only with exact unchanged source
+ // custody. Native false without a callback still retains all uncertainty.
+ protected bool RefusedAtSource()
+ {
+ if (!m_Submitted || m_Acknowledgement != -1 || !m_Magazine || !m_Donor || !m_Recipient) return false;
+ if (!m_OriginalSlot || !m_OriginalStorage) return false;
+ if (InventoryStorageManagerComponent.Cast(m_Donor.FindComponent(InventoryStorageManagerComponent)) != m_Source) return false;
+ if (InventoryStorageManagerComponent.Cast(m_Recipient.FindComponent(InventoryStorageManagerComponent)) != m_Destination) return false;
+ if (Ownership(m_Source) != 1 || Ownership(m_Destination) != 0) return false;
+ if (!ManagedStorage(m_Source, m_OriginalStorage, m_Donor)) return false;
+ InventoryItemComponent item = InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent));
+ if (!item || item.GetParentSlot() != m_OriginalSlot) return false;
+ return m_OriginalSlot.GetStorage() == m_OriginalStorage && m_OriginalSlot.GetAttachedEntity() == m_Magazine;
+ }
protected bool ReturnProof()
{
return Proof(m_ReturnMagazine, m_Destination, m_Source, m_ReturnStorage, m_Donor, m_ReturnAcknowledgement);
@@ -142,7 +184,8 @@
// magazine never proves delivery or grants authority for a later full one.
bool Unresolved()
{
- if (m_ReturnAccepted && !m_ReturnDone && m_ReturnMagazine && m_ReturnAcknowledgement >= 0 && !ReturnProof()) return true;
+ if ((m_ReturnAccepted || (m_LauncherMagazine && m_ReturnSubmitted)) && !m_ReturnDone && m_ReturnMagazine && m_ReturnAcknowledgement >= 0 && !ReturnProof()) return true;
+ if (m_LauncherMagazine && m_Submitted) return m_Magazine && !PhysicalProof() && !RefusedAtSource();
if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;
return !PhysicalProof();
}
@@ -182,7 +225,7 @@
float distance = vector.Distance(m_Donor.GetOrigin(), m_Recipient.GetOrigin());
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1 && distance >= 0 && distance <= 15)) return false;
if (distance > 4 && !m_Donor.FindComponent(VehicleHelicopterSimulation)) return false;
- if (!Compatible(m_Recipient, m_Magazine)) return false;
+ if (!Compatible(m_Recipient, m_Magazine, m_LauncherMagazine)) return false;
m_Driver = driver; m_DonorRecord = donorRecord; m_RecipientRecord = recipientRecord;
return true;
}
@@ -245,12 +288,20 @@
if (!m_ReturnDone && ReturnEmpty()) return ERR_InfantryAmmoTransferState.PENDING;
return Fail("recipient has no native compatible available storage after at most one empty return");
}
+ if (m_LauncherMagazine)
+ {
+ m_OriginalSlot = item.GetParentSlot(); m_OriginalStorage = m_OriginalSlot.GetStorage();
+ if (m_OriginalSlot.GetAttachedEntity() != m_Magazine) return Fail("launcher source slot lacks reciprocal item proof");
+ }
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");
}
+ // Native callbacks may synchronously cancel this leaf or revoke ownership.
+ if (m_LauncherMagazine && m_State != ERR_InfantryAmmoTransferState.PENDING) return m_State;
+ if (m_LauncherMagazine && !Context()) return Fail("launcher custody authority changed during native submission");
if (PhysicalProof())
{
m_State = ERR_InfantryAmmoTransferState.SUCCEEDED;
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
@@ -13,6 +13,19 @@
&& entry.AmmoState.Complete && entry.AmmoState.Members == entry.Count
&& entry.AmmoState.SmallArmsMuzzles > 0
&& entry.AmmoState.SmallArmsRounds + entry.AmmoState.SmallArmsChambers <= entry.Count * 30;
+ }
+ // Only positively sampled reloadable magazine-well muzzles establish launcher demand.
+ static bool LauncherNeed(ERR_HQWatchEntry entry, float now)
+ {
+ return Fresh(entry, now) && entry.AmmoState && entry.AmmoState.Valid(entry.Count)
+ && entry.AmmoState.LauncherComplete && entry.AmmoState.Members == entry.Count
+ && entry.AmmoState.LauncherMuzzles > 0
+ && entry.AmmoState.LauncherRounds == 0 && entry.AmmoState.LauncherChambers == 0;
+ }
+ static bool LauncherPair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
+ {
+ return LauncherNeed(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
+ && vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
}
static bool MinePair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
{
@@ -43,6 +56,7 @@
protected ref ERR_ConvoyMineStock m_MineBatch;
protected bool m_MineStockCaptured;
protected bool m_MineTrip;
+ protected bool m_LauncherTrip;
protected int m_MineRecipientSlot = -1;
protected bool m_EmptyMineReplacement;
protected int m_MinesDelivered;
@@ -58,7 +72,7 @@
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, 7 unresolved mine custody
+ protected int m_State; // 0 stock startup, 1 idle, 2 truck downlink, 3 stopped status, 4 infantry downlink, 5 transfer, 7 unresolved item custody
protected int m_Trip;
protected int m_Token;
protected int m_ReportId;
@@ -92,7 +106,11 @@
protected void PollTransfers(float now)
{
if (m_Mounted) m_Mounted.Tick(now);
- if (m_State <= 1 && m_Move && !m_Move.Unresolved()) m_Move = null;
+ if (m_Move && !m_Move.Unresolved() && (m_State <= 1 || (m_State == 7 && m_LauncherTrip)))
+ {
+ m_Move = null;
+ if (m_State == 7) Finish("launcher custody settled; original claims released without retry or refund");
+ }
if (m_MineMove && !m_MineMove.Unresolved() && (m_State <= 1 || m_State == 7))
{
m_MineMove = null;
@@ -135,6 +153,7 @@
protected void Audit(string reason)
{
if (m_MineTrip) reason = "mine top-up: " + reason;
+ if (m_LauncherTrip) reason = "depleted launcher magazine: " + reason;
m_Reason = reason;
m_HQ.Journal().Write("INFANTRY SUPPLY", string.Format("convoy-slot=%1 trip=%2 state=%3 moved=%4 spent=%5/%6; %7", m_ConvoySlot, m_Trip, m_State, m_Delivered, m_Spent.Count(), m_Stock.Count(), reason), m_ReportId);
}
@@ -230,10 +249,10 @@
}
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, bool mineTrip = false)
+ bool TryPlan(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now, bool mineTrip = false, bool launcherTrip = false)
{
if (!Current() || m_State != 1 || m_Move || m_MineMove || (m_Mounted && m_Mounted.Busy()) || m_Trip >= 128 || !TruckValid()) return false;
- if (m_Truck.Combat.Held || m_Truck.SupplyToken > 0 || !recipient || !donor) return false;
+ if ((mineTrip && launcherTrip) || 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 (mineTrip)
@@ -253,6 +272,10 @@
if (!designated) return false;
}
}
+ else if (launcherTrip)
+ {
+ if (!ERR_InfantryResupplyRules.LauncherPair(demand, donorStatus, now)) return false;
+ }
else 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;
@@ -263,6 +286,7 @@
foreach (IEntity mine : m_MineStock)
if (ERR_InfantryMineTransfer.ReadyMine(mine) && !m_Spent.Contains(mine) && Contains(m_Truck.Entity, mine)) { available = true; break; }
}
+ else if (launcherTrip) available = LauncherStock(recipient);
else foreach (IEntity magazine : m_Stock) if (magazine && !m_Spent.Contains(magazine) && Contains(m_Truck.Entity, magazine)) { available = true; break; }
if (m_Spent.Count() >= 96) return false;
if (!available) return false;
@@ -279,12 +303,13 @@
if (slot < 0) slot = 0; // Isolated legacy fixture uses the first convoy's range.
int token = 100000 + slot * 128 + m_Trip + 1;
if (!m_HQ.ReserveLogistics(recipient.Group, token)) { m_Recipient = null; m_Donor = null; return false; }
- m_MineTrip = mineTrip; m_MineRecipientSlot = -1; m_Receiving = null; m_EmptyMineReplacement = false;
+ m_MineTrip = mineTrip; m_LauncherTrip = launcherTrip; m_MineRecipientSlot = -1; m_Receiving = null; m_EmptyMineReplacement = 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;
string purpose = "stop existing ammo truck and report position";
if (mineTrip) purpose += "; one finite PMN4 top-up for an existing carrier";
+ if (launcherTrip) purpose += "; finite compatible magazine for a reported depleted launcher";
m_Order = m_HQ.Downlink(donor.Group, "INFANTRY SUPPLY HOLD", purpose, m_ReportId);
if (!m_Order) Finish("truck radio downlink unavailable");
else Audit("existing patrol reserved; truck hold awaits radio delivery");
@@ -306,6 +331,31 @@
if (item && !m_MineStock.Contains(item)) m_MineStock.Insert(item);
}
return true;
+ }
+ // Compatibility-only preflight of original own carriers; HQ still uses the
+ // delivered report for demand and positions. No item is reserved or moved here.
+ protected bool LauncherStock(ERR_HQUnit recipient)
+ {
+ if (!recipient || !recipient.StatusSampler || m_Stock.Count() > 96) return false;
+ int strength = recipient.StatusSampler.InitialStrength();
+ if (strength < 1 || strength > 32) return false;
+ int checks;
+ foreach (IEntity magazine : m_Stock)
+ {
+ if (!magazine || m_Spent.Contains(magazine) || !Contains(m_Truck.Entity, magazine)) continue;
+ for (int slot = 0; slot < strength; slot++)
+ {
+ if (++checks > 96 * 32) return false;
+ IEntity actor = recipient.StatusSampler.InitialMember(slot);
+ if (!actor || !recipient.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)) continue;
+ if (ERR_InfantryAmmoTransfer.Compatible(actor, magazine, true)) return true;
+ }
+ }
+ return false;
+ }
+ bool TryPlanLauncher(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
+ {
+ return TryPlan(recipient, donor, demand, donorStatus, now, false, true);
}
bool TryPlanMines(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
{
@@ -345,10 +395,10 @@
m_Order = null;
// A late native move must not race another truck's newer report. Retain both
// exact claims until this one submitted identity proves custody or deletion.
- if (m_MineMove && m_MineMove.Unresolved()) { m_State = 7; return; }
+ if ((m_MineMove && m_MineMove.Unresolved()) || (m_LauncherTrip && m_Move && m_Move.Unresolved())) { m_State = 7; return; }
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; m_MineTrip = false; m_MineRecipientSlot = -1;
+ m_Recipient = null; m_Donor = null; m_Order = null; m_Receiving = null; m_State = 1; m_MineTrip = false; m_LauncherTrip = false; m_MineRecipientSlot = -1;
}
protected void TransferMine(float now)
{
@@ -414,14 +464,19 @@
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;
+ if (m_LauncherTrip)
+ {
+ if (!local.Valid(1) || !local.LauncherComplete || local.Members != 1 || local.LauncherMuzzles <= 0
+ || local.LauncherRounds != 0 || local.LauncherChambers != 0) continue;
+ }
+ else 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;
+ if (!magazine || m_Spent.Contains(magazine) || !Contains(m_Truck.Entity, magazine) || !ERR_InfantryAmmoTransfer.Compatible(actor, magazine, m_LauncherTrip)) continue;
ERR_FiniteMagazineReservation reservation = new ERR_FiniteMagazineReservation(m_Stock, m_Spent, magazine);
if (!reservation.Reserve()) continue;
m_Receiving = actor;
- m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, actor, m_Recipient.Group, magazine, true);
+ m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, actor, m_Recipient.Group, magazine, !m_LauncherTrip, m_LauncherTrip);
m_Move.Tick(now);
if (m_Move.Submitted()) { reservation.Commit(); return; }
// No native operation exists yet: approach geometry, stopped state or
--- a/tests/test_hq_ammo_reporting.py
+++ b/tests/test_hq_ammo_reporting.py
@@ -34,7 +34,7 @@
struct ERR_HQAmmoSnapshot{
bool MineComplete=false;int MineMembers=0,ReadyMines=0;
bool Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0;
- int LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
+ int LauncherMuzzles=0,LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
'''+body(A,' bool Valid(')+r'''
};
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Weapons=1,Muzzles=1,Magazines=1,Rounds=100,Capacity=100,Chambers=0,LowMagazines=0;
--- a/tests/test_hq_ammo_sampler.py
+++ b/tests/test_hq_ammo_sampler.py
@@ -12,10 +12,11 @@
code=code.replace('BaseWeaponComponent.Cast(weaponEntity.FindComponent(BaseWeaponComponent))','weaponEntity->weapon')
code=code.replace('BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))','item->magazine')
code=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)', 'ERR_HQAmmoSnapshot* ',code)
+code=code.replace('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : null)')
code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
for name in ('EWeaponType','EMuzzleType','Replication','ERR_Actors'):code=code.replace(name+'.',name+'::')
code=code.replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
-for name in ('weapon','muzzle','magazine','actor','group','result','inventory','manager','loaded','copy'):
+for name in ('weapon','muzzle','reloadable','magazine','actor','group','result','inventory','manager','loaded','copy'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('GetMagazineWell().','GetMagazineWell()->')
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code)
@@ -30,7 +31,7 @@
void Insert(T n){this->push_back(n);}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}};
struct Entity;struct Muzzle;struct Magazine;struct Weapon;struct Group;struct Inventory;struct Manager;
using IEntity=Entity*;using SCR_AIGroup=Group*;using BaseWeaponComponent=Weapon*;
-using BaseMuzzleComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
+using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
using InventoryStorageManagerComponent=Inventory*;using BaseWeaponManagerComponent=Manager*;
struct EWeaponType{static constexpr int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,
WT_ROCKETLAUNCHER=5,WT_GRENADELAUNCHER=6,WT_FRAGGRENADE=7,WT_SMOKEGRENADE=8;};
@@ -39,7 +40,7 @@
struct Magazine{IEntity owner=nullptr;Well* well=nullptr;int rounds=0,capacity=30;
IEntity GetOwner(){return owner;}Well* GetMagazineWell(){return well;}
int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
-struct Muzzle{Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
+struct Muzzle{bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable(){return disposable;}Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
int GetMuzzleType(){return kind;}Well* GetMagazineWell(){return well;}
int GetBarrelsCount(){return barrels;}bool IsBarrelChambered(int){return chamber;}
Magazine* GetMagazine(){return magazine;}};
@@ -87,7 +88,14 @@
Muzzle launcher;launcher.kind=EMuzzleType::MT_UGLMuzzle;launcher.well=&rocketWell;launcher.magazine=&unrelated;
weapon.muzzles.Insert(&launcher);auto mixed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&mixed);
assert(mixed.Complete&&mixed.LauncherComplete&&mixed.SmallArmsRounds==0&&mixed.LauncherRounds==1);
- assert(mixed.LauncherMagazines==1&&mixed.KnownSmallArmsEmpty()); // launcher stock is not rifle stock
+ assert(mixed.LauncherMagazines==1&&mixed.LauncherMuzzles==1&&mixed.KnownSmallArmsEmpty());
+ auto launcherWire=mixed.Snapshot();mixed.LauncherMuzzles=0;assert(launcherWire->LauncherMuzzles==1);
+ launcher.magazine=nullptr;inv.items.clear();auto expended=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&expended);
+ assert(expended.LauncherComplete&&expended.LauncherMuzzles==1&&expended.LauncherRounds==0&&expended.LauncherMagazines==0);
+ launcher.disposable=true;auto disposable=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&disposable);assert(!disposable.LauncherComplete&&disposable.LauncherMuzzles==0);launcher.disposable=false;
+ launcher.inMag=true;launcher.canReload=false;auto singleUse=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&singleUse);assert(!singleUse.LauncherComplete&&singleUse.LauncherMuzzles==0);launcher.inMag=false;launcher.canReload=true;
+ auto badMuzzles=expended;badMuzzles.LauncherMuzzles=-1;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=33;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=32;assert(badMuzzles.Valid(1));
+ launcher.magazine=&unrelated;inv.items={&loadedEntity,&spareEntity,&unrelatedEntity}; // launcher stock is not rifle stock
launcher.well=nullptr;launcher.magazine=nullptr;auto loose=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&loose);
assert(loose.Complete&&!loose.LauncherComplete);weapon.muzzles={&muzzle};
// Empty/unavailable native launcher muzzles affect only launcher completeness.
--- a/tests/test_hq_multiconvoy_supply.py
+++ b/tests/test_hq_multiconvoy_supply.py
@@ -8,7 +8,7 @@
methods='\n'.join(body(p,s) for s in ('void PlanResupply(float now)','bool PlanResupplyTask(ERR_InfantryResupply supply, float now)'))
methods=methods.replace('null','nullptr').replace('foreach (','for (')
methods=methods.replace('ERR_HQUnit donor;','ERR_HQUnit donor=nullptr;')
-for name in ('m_Resupply','supply','task','unit','donor','recipient','mounted','mineRecipient','m_Watch'):
+for name in ('m_Resupply','supply','task','unit','donor','recipient','mounted','mineRecipient','launcherRecipient','m_Watch'):
methods=methods.replace(name+'.',name+'->')
methods=methods.replace('task->Vehicles[1].','task->Vehicles[1]->')
run(r'''
@@ -24,10 +24,11 @@
struct Vehicle{int Group=0;};
struct Task{bool Stopped=false,Ready=true;Array<Vehicle*> Vehicles;};using ERR_ConvoyTask=Task*;
struct Supply{
- int state=1,calls=0,accepted=0,mountedCalls=0,mountedAccepted=0;bool infantry=true,mounted=false,mines=false;int mineCalls=0,mineAccepted=0;Task*task=nullptr;Unit* recipient=nullptr;Entry* demand=nullptr;Entry* donorReport=nullptr;
+ int state=1,calls=0,accepted=0,mountedCalls=0,mountedAccepted=0;int launcherCalls=0,launcherAccepted=0;bool launcher=false;bool infantry=true,mounted=false,mines=false;int mineCalls=0,mineAccepted=0;Task*task=nullptr;Unit* recipient=nullptr;Entry* demand=nullptr;Entry* donorReport=nullptr;
int State(){return state;}Task*Task(){return task;}
bool TryPlan(Unit*r,Unit*,Entry*d,Entry*s,float){calls++;if(!infantry||r!=recipient||!d||!s||d!=demand||s!=donorReport)return false;accepted++;state=2;return true;}
bool TryPlanMounted(Unit*r,Unit*,Entry*d,Entry*s,float){mountedCalls++;if(!mounted||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mountedAccepted++;state=6;return true;}
+ bool TryPlanLauncher(Unit*r,Unit*,Entry*d,Entry*s,float){launcherCalls++;if(!launcher||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;launcherAccepted++;state=2;return true;}
bool TryPlanMines(Unit*r,Unit*,Entry*d,Entry*s,float){mineCalls++;if(!mines||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mineAccepted++;state=2;return true;}
};using ERR_InfantryResupply=Supply*;
struct Pool{Array<Supply*> services;int Count(){return services.Count();}Supply*At(int i){return services[i];}};
@@ -43,6 +44,10 @@
watch.delivered={{"a",&aStatus},{"b",&bStatus},{"recipient",&demand}};pool.services={&first,&second};hq.m_Resupply=&pool;hq.m_Watch=&watch;hq.m_Units={&da,&db,&recipient};}
};
int main(){
+ {Fixture f;f.first.infantry=false;f.first.launcher=true;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.launcherAccepted==1&&f.first.mineCalls==0&&f.second.calls==0);}
+ {Fixture f;f.first.launcher=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.launcherCalls==0);}
+ {Fixture f;f.first.infantry=false;f.first.mounted=true;f.first.launcher=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==1&&f.first.launcherCalls==0);}
+
{Fixture f;f.first.infantry=false;f.first.mounted=false;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mineAccepted==1&&f.first.mountedCalls>0&&f.second.calls==0);}
{Fixture f;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.mineCalls==0);}
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==1&&f.first.mineCalls==0);}
--- a/tests/test_hq_watch.py
+++ b/tests/test_hq_watch.py
@@ -136,15 +136,17 @@
int main(){
// Physical mine evidence is detached at sampling, then copied again only on delivered radio.
ERR_HQAmmoSnapshot original;original.Complete=true;original.Members=4;
- original.MineComplete=true;original.MineMembers=4;original.ReadyMines=3;
+ original.LauncherComplete=true;original.LauncherMuzzles=2;original.MineComplete=true;original.MineMembers=4;original.ReadyMines=3;
ERR_HQStatusReport mineWire;mineWire.AmmoState=original.Snapshot();
- original.ReadyMines=0;original.MineComplete=false;original.MineMembers=0;
+ original.LauncherMuzzles=0;original.ReadyMines=0;original.MineComplete=false;original.MineMembers=0;
assert(mineWire.AmmoState!=&original&&mineWire.AmmoState->ReadyMines==3&&mineWire.AmmoState->MineComplete&&mineWire.AmmoState->MineMembers==4);
+ assert(mineWire.AmmoState->LauncherMuzzles==2);
RadioIntake radio;StatusUnit sender;RouteWire route;sender.Uplink=&route;sender.PendingStatus=&mineWire;
radio.Deliver(&sender,1000);assert(!radio.m_Watch.Count()&&sender.PendingStatus==&mineWire); // still travelling
route.result=-1;radio.Deliver(&sender,2000);assert(!radio.m_Watch.Count()&&!sender.PendingStatus&&!sender.Uplink); // interrupted route cannot deliver
sender.Uplink=&route;sender.PendingStatus=&mineWire;route.result=1;radio.Deliver(&sender,3000);
auto mines=radio.m_Watch.Find(mineWire.SourceId);assert(mines&&mines->AmmoState!=mineWire.AmmoState&&mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete&&mines->AmmoState->MineMembers==4);
+ assert(mines->AmmoState->LauncherMuzzles==2);mineWire.AmmoState->LauncherMuzzles=0;assert(mines->AmmoState->LauncherMuzzles==2);
mineWire.AmmoState->ReadyMines=0;mineWire.AmmoState->MineComplete=false;assert(mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete);
// Relabelled stale observations and old radio arrivals cannot overwrite the latest delivered evidence.
mineWire.MessageId=2;sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,4000);
@@ -158,6 +160,9 @@
mineWire.AmmoState->MineMembers=4;mineWire.AmmoState->ReadyMines=513;
assert(!radio.m_Watch.Ingest(&mineWire,6000)&&mines->MessageId==2); // impossible finite count
mineWire.AmmoState->ReadyMines=0;assert(radio.m_Watch.Ingest(&mineWire,6000)&&mines->AmmoState->MineComplete&&mines->AmmoState->ReadyMines==0);
+ mineWire.MessageId=4;mineWire.ObservedAt=6000;mineWire.AmmoState->LauncherMuzzles=-1;
+ assert(!radio.m_Watch.Ingest(&mineWire,7000)&&mines->MessageId==3);
+ mineWire.AmmoState->LauncherMuzzles=129;assert(!radio.m_Watch.Ingest(&mineWire,7000));
// Mounted payload obeys the same delivered chronology and is deep copied.
Watch mountedWatch;ERR_HQStatusReport mountedReport;ERR_HQMountedAmmoSnapshot mountedState;
mountedState.Complete=true;mountedState.Weapons=1;mountedState.Muzzles=1;mountedState.Magazines=1;
--- a/tests/test_infantry_ammo_transfer.py
+++ b/tests/test_infantry_ammo_transfer.py
@@ -12,6 +12,7 @@
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('class ','struct ')
for src,dst in {
'm_Donor.FindComponent(VehicleHelicopterSimulation)':'m_Donor->helicopter',
+ 'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->magazine',
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
@@ -25,15 +26,16 @@
'InventoryStorageManagerComponent.Cast(m_Donor.FindComponent(InventoryStorageManagerComponent))':'m_Donor->inventory',
'InventoryStorageManagerComponent.Cast(m_Recipient.FindComponent(InventoryStorageManagerComponent))':'m_Recipient->inventory',
}.items():code=code.replace(src,dst)
+code=code.replace('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : null)')
code=code.replace('ref ERR_InfantryAmmoCallback','ERR_InfantryAmmoCallback*').replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
code=code.replace('void ERR_InfantryAmmoTransfer(','ERR_InfantryAmmoTransfer(').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
for name in ('ERR_InfantryAmmoTransferState','ERR_InfantryAmmoAccess','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_ForceRules','Replication','SCR_Global','ERR_Actors','SCR_FactionManager','ERR_ConvoyRules','CompartmentAccessComponent','EWeaponType','EMuzzleType','EStoragePurpose','vector'):
code=code.replace(name+'.',name+'::')
-for name in ('m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Source','m_Destination','m_Storage','m_Callback','magazine','manager','weapon','muzzle','entity','item','storage','inventory','incoming','empty','source','destination','m_ReturnCallback','donorRecord','recipientRecord','utility','usage','physics'):
+for name in ('m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Source','m_Destination','m_Storage','m_OriginalSlot','m_Callback','magazine','reloadable','loaded','loadedItem','loadedSlot','manager','weapon','muzzle','entity','item','storage','inventory','incoming','empty','source','destination','m_ReturnCallback','donorRecord','recipientRecord','utility','usage','physics'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('GetMagazineWell().','GetMagazineWell()->').replace('GetParentSlot().','GetParentSlot()->').replace('Forces().','Forces()->').replace('m_VehicleMgr.','m_VehicleMgr->').replace('GetPilotCompartmentSlot().','GetPilotCompartmentSlot()->')
-code=re.sub(r'\b(bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageManagerComponent|BaseInventoryStorageComponent|ERR_GameMode\*|ERR_InfantryAmmoCallback\*) (\w+);',r'\1 \2{};',code)
+code=re.sub(r'\b(bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageSlot|InventoryStorageManagerComponent|BaseInventoryStorageComponent|ERR_GameMode\*|ERR_InfantryAmmoCallback\*) (\w+);',r'\1 \2{};',code)
code=code.replace('\n}\n','\n};\n')
callback=callback.replace('class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback','').strip()[1:-1]
callbacks=[]
@@ -63,8 +65,8 @@
struct vector{float x=0,y=0,z=0;float Length(){return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static float Distance(vector a,vector b){return vector{a.x-b.x,a.y-b.y,a.z-b.z}.Length();}};
struct Entity;struct Group;struct Inventory;struct Storage;struct Item;struct Slot;struct Magazine;struct Weapon;struct Muzzle;struct Manager;struct Utility;struct Usage;struct Physics_;struct Record;
using IEntity=Entity*;using Vehicle=Entity*;using SCR_AIGroup=Group*;using ERR_ForceRecord=Record*;
-using InventoryStorageManagerComponent=Inventory*;using BaseInventoryStorageComponent=Storage*;using InventoryItemComponent=Item*;using BaseMagazineComponent=Magazine*;
-using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;
+using InventoryStorageSlot=Slot*;using InventoryStorageManagerComponent=Inventory*;using BaseInventoryStorageComponent=Storage*;using InventoryItemComponent=Item*;using BaseMagazineComponent=Magazine*;
+using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;
using SCR_AIGroupUtilityComponent=Utility*;using SCR_AIVehicleUsageComponent=Usage*;using Physics=Physics_*;
enum ERR_InfantryAmmoTransferState{FAILED=-1,PENDING,SUCCEEDED};
struct ERR_InfantryAmmoTransfer;
@@ -73,12 +75,12 @@
struct EWeaponType{static const int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,WT_GRENADELAUNCHER=5,WT_ROCKETLAUNCHER=6;};
struct EMuzzleType{static const int MT_BaseMuzzle=0,MT_UGLMuzzle=1,MT_RPGMuzzle=2;};struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
struct Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
-struct Magazine{Well*well=nullptr;int rounds=30,capacity=30;bool used=false;bool IsUsed(){return used;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
-struct Muzzle{Well*well=nullptr;int kind=0;Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
+struct Magazine{IEntity owner=nullptr;IEntity GetOwner(){return owner;}Well*well=nullptr;int rounds=30,capacity=30;bool used=false;bool IsUsed(){return used;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
+struct Muzzle{bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable(){return disposable;}Magazine*loaded=nullptr;Magazine*GetMagazine(){return loaded;}Well*well=nullptr;int kind=0;Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
struct Weapon{IEntity owner=nullptr;Array<Muzzle*>muzzles;int kind=1;IEntity GetOwner(){return owner;}int GetWeaponType(){return kind;}void GetMuzzlesList(Array<Muzzle*>&a){a=muzzles;}};
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>&a){a=weapons;}};
struct Storage{IEntity owner=nullptr;IEntity GetOwner(){return owner;}};
-struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;Storage*GetStorage(){return storage;}IEntity GetOccupant(){return occupant;}};
+struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}Storage*GetStorage(){return storage;}IEntity GetOccupant(){return occupant;}};
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
struct Physics_{vector velocity,angular;vector GetVelocity(){return velocity;}vector GetAngularVelocity(){return angular;}};
struct Usage{Slot*pilot=nullptr;Slot*GetPilotCompartmentSlot(){return pilot;}};
@@ -114,7 +116,7 @@
truck.physics=&physics;truck.usage=&usage;usage.pilot=&pilot;pilot.occupant=&driver;driver.group=&donorGroup;driver.vehicle=&truck;spareDriver.group=&donorGroup;spareDriver.vehicle=&truck;
recipient.group=&recipientGroup;recipient.position.x=4;truck.inventory=&source;source.owner=&truck;recipient.inventory=&destination;destination.owner=&recipient;
sourceStorage.owner=&truck;targetStorage.owner=&recipient;source.storages={&sourceStorage};destination.storages={&targetStorage};destination.selected=&targetStorage;
- source.items={&magEntity};magEntity.item=&item;item.slot=&fromSlot;fromSlot.storage=&sourceStorage;toSlot.storage=&targetStorage;magEntity.magazine=&magazine;magazine.well=&well;
+ source.items={&magEntity};magEntity.item=&item;item.slot=&fromSlot;fromSlot.storage=&sourceStorage;fromSlot.attached=&magEntity;toSlot.storage=&targetStorage;magEntity.magazine=&magazine;magazine.well=&well;
recipient.weapons=&weapons;weapons.weapons={&rifle};rifle.weapon=&weapon;weapon.owner=&rifle;weapon.muzzles={&muzzle};muzzle.well=&well;}
ERR_InfantryAmmoTransfer Job(){return ERR_InfantryAmmoTransfer(&mode,&truck,&donorGroup,&recipient,&recipientGroup,&magEntity,true);}
void Move(){source.items.RemoveItem(&magEntity);destination.items.Insert(&magEntity);item.slot=&toSlot;}
@@ -127,11 +129,11 @@
destination.items={&empty,&secondEmpty};destination.selected=nullptr;source.selected=&sourceStorage;}
void Return(bool capacity=true){destination.items.RemoveItem(&empty);source.items.Insert(&empty);emptyItem.slot=&emptyTo;if(capacity)destination.selected=&targetStorage;}
};
-struct ERR_HQAmmoSnapshot{bool Complete=true,LauncherComplete=true;int SmallArmsMuzzles=1,SmallArmsRounds=0,SmallArmsChambers=0;};
+struct ERR_HQAmmoSnapshot{bool Complete=true,LauncherComplete=true;int Members=1,LauncherMuzzles=1,LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;bool Valid(int n){return n==Members;}int SmallArmsMuzzles=1,SmallArmsRounds=0,SmallArmsChambers=0;};
struct ERR_HQAmmoSampler{static void Inspect(IEntity actor,SCR_AIGroup group,ERR_HQAmmoSnapshot* stock){assert(actor&&actor->group==group&&stock);}};
struct SupplyTruck{IEntity Entity;SCR_AIGroup Group;};
struct SupplyRecipient{SCR_AIGroup Group;};
-struct Coordinator{ERR_GameMode* m_Mode;SupplyTruck* m_Truck;SupplyRecipient* m_Recipient;
+struct Coordinator{bool m_LauncherTrip=false;ERR_GameMode* m_Mode;SupplyTruck* m_Truck;SupplyRecipient* m_Recipient;
ERR_InfantryAmmoTransfer* m_Move=nullptr;Array<IEntity>m_Members,m_Stock,m_Spent,m_Served;IEntity m_Receiving=nullptr;
int m_Delivered=0,m_TripDelivered=0,finishes=0,audits=0;
void Finish(string){finishes++;if(m_Move)m_Move->Stop();}
@@ -143,7 +145,34 @@
SupplyFixture():supplyTruck{&truck,&donorGroup},supplyRecipient{&recipientGroup}{
c.m_Mode=&mode;c.m_Truck=&supplyTruck;c.m_Recipient=&supplyRecipient;c.m_Members={&recipient};c.m_Stock={&magEntity};}
};
+struct LauncherFixture:SupplyFixture{Entity loaded;Magazine empty;Item loadedItem;Slot loadedSlot;
+ LauncherFixture(){c.m_LauncherTrip=true;weapon.kind=EWeaponType::WT_GRENADELAUNCHER;muzzle.kind=EMuzzleType::MT_UGLMuzzle;
+ rifle.parent=&recipient;loaded.parent=&rifle;loaded.magazine=&empty;empty.owner=&loaded;empty.well=&well;empty.rounds=0;muzzle.loaded=&empty;loaded.item=&loadedItem;loadedItem.slot=&loadedSlot;loadedSlot.container=&muzzle;loadedSlot.attached=&loaded;}
+};
int main(){
+ // Failed callback settles only exact original unchanged custody; unknown never does.
+ {LauncherFixture f;f.source.callback=2;f.c.Transfer(0);assert(f.c.m_Move->Submitted()&&!f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1);f.c.Transfer(1000);assert(f.source.calls==1&&f.c.m_Spent.Count()==1);}
+ for(int bad=0;bad<7;bad++){LauncherFixture f;f.c.Transfer(0);assert(f.c.m_Move->Unresolved());
+ if(bad!=0)f.source.last->OnFailed();
+ if(bad==1)f.item.slot=nullptr;
+ if(bad==2)f.fromSlot.attached=nullptr;
+ if(bad==3)f.sourceStorage.owner=&f.foreign;
+ if(bad==4)f.destination.items.Insert(&f.magEntity);
+ if(bad==5)f.truck.inventory=&f.destination;
+ if(bad==6)f.source.items.RemoveItem(&f.magEntity);
+ assert(f.c.m_Move->Unresolved());f.c.m_Move->Stop();assert(f.c.m_Move->Unresolved());}
+
+ {LauncherFixture f;f.muzzle.loaded=nullptr;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity,true));f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Move->Submitted());}
+
+ {LauncherFixture f;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity,true));
+ f.c.Transfer(0);assert(f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Spent.Count()==1&&f.source.calls==1);
+ f.Move();f.source.last->OnComplete();f.c.Transfer(1000);f.c.Transfer(2000);assert(f.c.m_Delivered==1&&f.source.calls==1&&f.c.m_Served.Count()==1);}
+ for(int bad=0;bad<8;bad++){LauncherFixture f;if(bad==0)f.magazine.used=true;if(bad==1)f.muzzle.well=nullptr;if(bad==2)f.muzzle.disposable=true;if(bad==3)f.empty.well=nullptr;if(bad==4)f.loadedSlot.attached=nullptr;if(bad==5)f.rifle.parent=nullptr;if(bad==6)f.empty.capacity=0;if(bad==7){f.muzzle.inMag=true;f.muzzle.canReload=false;}
+ f.c.Transfer(0);assert(!f.c.m_Move&&f.source.calls==0&&f.c.m_Spent.IsEmpty());}
+ {LauncherFixture f;f.source.accept=false;f.c.Transfer(0);assert(f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1);f.c.Transfer(1000);assert(f.source.calls==1);f.Move();f.source.last->OnComplete();assert(!f.c.m_Move->Unresolved());}
+ {LauncherFixture f;auto t=ERR_InfantryAmmoTransfer(&f.mode,&f.truck,&f.donorGroup,&f.recipient,&f.recipientGroup,&f.magEntity,false,true);
+ f.source.effect=[&](){t.Stop();f.Move();};f.source.callback=1;assert(t.Tick(0)==FAILED&&!t.Unresolved()&&f.source.calls==1);assert(t.Tick(1)==FAILED);}
+
// A capacity recovery uses one exact unused empty, separately proving both native operations.
{Fixture f;Entity empty;Magazine emptyMag;Item emptyItem;Slot emptyFrom{&f.targetStorage},emptyTo{&f.sourceStorage};
emptyMag.well=&f.well;emptyMag.rounds=0;empty.magazine=&emptyMag;empty.item=&emptyItem;emptyItem.slot=&emptyFrom;
--- a/tests/test_infantry_resupply_coordinator.py
+++ b/tests/test_infantry_resupply_coordinator.py
@@ -50,7 +50,7 @@
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
struct Service {
- HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;bool m_MineTrip=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
+ HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;bool m_MineTrip=false,m_LauncherTrip=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}int mineTransfers=0;void TransferMine(float){mineTransfers++;}
@@ -60,6 +60,9 @@
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=&order;hq.next=&next;}
};
int main(){
+ {Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.m_LauncherTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld);
+ f.s.Tick(200000);assert(f.s.m_State==7&&f.hq.token==7&&f.s.transfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld&&!f.s.m_Move);}
+
{Fixture f;Leaf leaf;f.s.m_MineMove=&leaf;f.s.m_MineTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_Recipient);
f.s.Tick(200000);assert(f.hq.token==7&&f.s.m_State==7&&f.s.transfers==0&&f.s.mineTransfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_MineMove);}
{Fixture f;f.s.m_State=5;f.s.m_MineTrip=true;f.s.Tick(0);assert(f.s.mineTransfers==1&&f.s.transfers==0);}
--- a/tests/test_infantry_resupply_planning.py
+++ b/tests/test_infantry_resupply_planning.py
@@ -13,7 +13,7 @@
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord','ERR_SupplyConvoyTask'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
- for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_InfantryMineTransfer','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
+ for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_InfantryMineTransfer','ERR_InfantryAmmoTransfer','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
s=s.replace(name+'.',name+'::')
s=s.replace('ERR_SupplyConvoyTask.Cast','ERR_SupplyConvoyTask::Cast').replace('FindComponent(BaseMagazineComponent)','HasMagazine()')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('InventoryStorageManagerComponent.Cast','InventoryStorageManagerComponent::Cast').replace('FindComponent(InventoryStorageManagerComponent)','FindComponent()')
@@ -24,10 +24,11 @@
for name in ('Forces()','GetPrefabData()','Convoys()'):s=s.replace(name+'.',name+'->')
s=s.replace('ERR_SupplyConvoyTask* supply', 'Convoy* supply')
s=s.replace('bool designated;','bool designated=false;')
+ s=s.replace('int checks;', 'int checks=0;')
s=s.replace('bool available;','bool available=false;').replace('bool living;','bool living=false;')
return s
-rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair(', 'static bool MinePair('))
-methods='\n'.join(cpp(body(P,x)) for x in ('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'bool TryPlanMines(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
+rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair(', 'static bool MinePair(', 'static bool LauncherNeed(', 'static bool LauncherPair('))
+methods='\n'.join(cpp(body(P,x)) for x in ('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'protected bool LauncherStock(', 'bool TryPlanLauncher(', 'bool TryPlanMines(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
reserve=cpp(body(H,'bool ReserveLogistics('))+cpp(body(H,'void ReleaseLogistics('))
unique=cpp(body(I,'static int AppendUnique('))+cpp(body(I,'static int Items('))
run(r'''
@@ -59,7 +60,7 @@
struct ERR_Inventory{
'''+unique+r'''
};
-struct Ammo{bool MineComplete=true;int MineMembers=2,ReadyMines=1;bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
+struct Ammo{bool LauncherComplete=true;int LauncherMuzzles=1,LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;bool MineComplete=true;int MineMembers=2,ReadyMines=1;bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
struct Entry{bool Closed=false;int Count=2,Fit=2,MessageId=1;float ObservedAt=0;Vec Position;string SourceId;Ammo* AmmoState=nullptr;};using ERR_HQWatchEntry=Entry*;
struct ERR_InfantryResupplyRules{
'''+rules+r'''
@@ -90,7 +91,8 @@
Route* Downlink(Group*,string,string,int){downlinks++;return routeAvailable?&route:nullptr;}
'''+reserve+r'''
};
-struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;MineBatch*m_MineBatch=nullptr;bool m_MineTrip=false,m_MineStockCaptured=false,m_EmptyMineReplacement=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
+struct ERR_InfantryAmmoTransfer{static inline IEntity only=nullptr;static inline int checks=0;static inline bool compatible=true;static bool Compatible(IEntity actor,IEntity item,bool launcher){checks++;return actor&&item&&launcher&&compatible&&(!only||item==only);}};
+struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;MineBatch*m_MineBatch=nullptr;bool m_LauncherTrip=false,m_MineTrip=false,m_MineStockCaptured=false,m_EmptyMineReplacement=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
Array<IEntity>m_Stock,m_MineStock,m_Spent,m_Members,m_Served;Map<int>*m_Consumed=new Map<int>();Map<float>*m_NextAttempt=new Map<float>();int m_ConvoySlot=-1;float m_NextTick=0;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;bool m_Stopped=false;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;string reason;bool Current(){return !m_Stopped&&Replication::IsServer()&&m_HQ->active;}void Audit(string why){reason=why;}
'''+methods+r'''
@@ -110,6 +112,17 @@
void Unchanged(){assert(s.m_Trip==0&&s.m_Consumed->Get("recipient")==0&&s.m_NextAttempt->Get("recipient")==0&&recipient.LogisticsReservation==0&&truck.SupplyToken==0&&hq.downlinks==0);}
};
int main(){
+ {Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);
+ ERR_InfantryAmmoTransfer::only=&f.items[95];ERR_InfantryAmmoTransfer::checks=0;
+ assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));
+ assert(ERR_InfantryAmmoTransfer::checks==191);ERR_InfantryAmmoTransfer::only=nullptr;}
+
+ {Fixture f;f.s.Stock(0);ERR_InfantryAmmoTransfer::compatible=false;assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)&&f.truck.SupplyToken==0);ERR_InfantryAmmoTransfer::compatible=true;}
+
+ {Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;f.ammo.LauncherMagazines=0;assert(!f.Plan());assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_LauncherTrip&&!f.s.m_MineTrip);}
+ for(int bad=0;bad<7;bad++){Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;if(bad==0)f.ammo.LauncherComplete=false;if(bad==1)f.ammo.LauncherMuzzles=0;if(bad==2)f.ammo.LauncherRounds=1;if(bad==3)f.ammo.LauncherChambers=1;if(bad==4)f.ammo.Members=1;if(bad==5)f.demand.ObservedAt=-1;if(bad==6)f.demand.ObservedAt=1;
+ assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_State==1&&f.truck.SupplyToken==0);}
+
// Additional mounted stock callbacks belong to the same initial freeze barrier.
{Fixture f;Cargo mounted;mounted.Finished=false;f.convoy.supply=true;f.convoy.Cargo.Insert(&mounted);
f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());
--- a/tests/test_mounted_supply_integration.py
+++ b/tests/test_mounted_supply_integration.py
@@ -22,7 +22,7 @@
bool Begin(ERR_HQUnit*,ERR_HQUnit*,ERR_HQWatchEntry*,ERR_HQWatchEntry*,float){begins++;busy=allow;return allow;}
bool Busy(){return busy;}bool Unresolved(){return unresolved;}void Tick(float){ticks++;}
};
-struct Service{HQ*m_HQ=nullptr;Mode*m_Mode=nullptr;Convoy*m_Convoy=nullptr;int m_ConvoySlot=0,m_State=1;
+struct Service{bool m_LauncherTrip=false;HQ*m_HQ=nullptr;Mode*m_Mode=nullptr;Convoy*m_Convoy=nullptr;int m_ConvoySlot=0,m_State=1;
Stock*m_Stock=nullptr,*m_Spent=nullptr;Consumed*m_Consumed=nullptr;Cooldown*m_NextAttempt=nullptr;Leaf*m_Move=nullptr,*m_MineMove=nullptr;ERR_MountedResupply*m_Mounted=nullptr;
void Finish(const char*){m_State=1;}bool current=true,valid=true,arrived=false;bool Current(){return current;}bool TruckValid(bool allowArrived=false){return valid&&(!arrived||allowArrived);}
'''+methods+r'''