Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,13 @@
Finite launcher resupply proposal (no target writes/native runs)
Apply fix.patch; 4 production files and 8 existing regression programs. No check.py registration required. baseline.json captures before working/index SHA256; source-manifest.json freezes proposed files. apply-check.log records successful check against current target.
Delivered complete ammunition reports now carry LauncherMuzzles: supported positive reloadable magazine-well presence even when a consumed round no longer has an inventory identity. Snapshot copies it; Valid bounds it to Members*32. Demand needs fresh full-member launcher sampling, positive supported muzzles, zero aggregate rounds and zero chambers. Unsupported/disposable/non-reloadable configurations cannot establish demand. Partial sampling remains unknown. Rifle and mounted planning run first, then launcher, then mine supply.
Only original existing positive-ammo finite truck magazines are eligible, using exact native magazine-well match, immutable original carrier attribution, native inventory access/custody and shared reservations/spent/replay/cooldown. Availability scans at most96*32 actor/magazine pairs; native runtime performance is unmeasured. Loaded identity, when present, requires exact reciprocal muzzle parent slot; native null storage is supported. Empty muzzle is valid when supported well and both reloadability checks pass. Launcher trips do not return speculative empty fired-round entities. No stock creation or replenishment changed.
Submitted unknown launcher moves keep exact truck and recipient claims through Stop. Inline Stop or ownership revocation cannot become success after native return. Exact destination proof/deletion resolves retained uncertainty without resubmission/refund. A terminal negative callback also settles only when pinned original source/destination managers remain unchanged and the item is still exclusively in its exact reciprocal original managed source slot. Detached, foreign, duplicate, unknown or changed-manager evidence cannot settle refusal.
Eleven focused actual-method programs pass (final-results.json and final-*.log). Prior original-Compatible and original-HQ-loop negative controls compiled and failed runtime assertions (red-controls.json). Earlier failed C++ adapter logs are retained separately; final-* logs are corrected current-source outcomes. Independent final schema review is requested from hq_memory_review. Native engine, actual projectile firing lifetime, full-suite and multiplayer acceptance remain untested here.
Installed evidence is retained in sdk-review.json/sdk-files.json, source hashes sdk-evidence.json. RPG7/GP25/M203 authored round resources are Projectile entities WITH native MagazineComponent and wells, not generated/default-only guesses. RPG75 is explicitly disposable/nonreloadable. Exact fired-round deletion timing is unknown and is not assumed. Existing production IssueMagazines already requests each admitted cargo weapon muzzle's default stock; this patch changes none of that random initial availability.
@@ -0,0 +1,16 @@
from pathlib import Path
P=Path('/tmp/erri-launcher-resupply-1140');R=Path('/home/jakub/devel/escapistsrereintroduced')
for name in ['test_mounted_supply_integration.py','test_hq_multiconvoy_supply.py']:
src=R/'tests'/name;s=src.read_text();(P/'baseline/tests'/name).write_text(s)
if name=='test_mounted_supply_integration.py':s=s.replace('struct Service{HQ*','struct Service{bool m_LauncherTrip=false;HQ*')
else:
s=s.replace("'mineRecipient','m_Watch'", "'mineRecipient','launcherRecipient','m_Watch'")
s=s.replace('bool infantry=true,', 'int launcherCalls=0,launcherAccepted=0;bool launcher=false;bool infantry=true,')
s=s.replace(' bool TryPlanMines(',''' 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(''')
s=s.replace('int main(){','''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);}
''')
(P/'tests'/name).write_text(s)
@@ -0,0 +1,268 @@
enum ERR_InfantryAmmoTransferState { FAILED = -1, PENDING, SUCCEEDED }
class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback
{
ERR_InfantryAmmoTransfer Transfer;
bool Returning;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true, Returning); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false, Returning); }
protected override void OnAllComplete() { if (Transfer) Transfer.Acknowledge(true, Returning); }
protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false, Returning); }
}
// 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_RecycleEmpty;
protected bool m_LauncherMagazine;
protected IEntity m_ReturnMagazine;
protected BaseInventoryStorageComponent m_ReturnStorage;
protected ref ERR_InfantryAmmoCallback m_ReturnCallback;
protected bool m_ReturnSubmitted;
protected bool m_ReturnAccepted;
protected bool m_ReturnDone;
protected int m_ReturnAcknowledgement;
protected float m_PhaseStarted = -1;
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, 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, 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;
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, IEntity exact = null)
{
if (!exact) exact = m_Magazine;
if (!inventory || !exact) return -1;
array<IEntity> items = {}; inventory.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
if (items.Count() > 512) return -1;
if (items.Contains(exact)) return 1;
return 0;
}
protected bool Proof(IEntity exact, InventoryStorageManagerComponent source, InventoryStorageManagerComponent destination, BaseInventoryStorageComponent storage, IEntity owner, int acknowledgement)
{
if (!exact || acknowledgement != 1 || Ownership(source, exact) != 0 || Ownership(destination, exact) != 1 || !ManagedStorage(destination, storage, owner)) return false;
InventoryItemComponent item = InventoryItemComponent.Cast(exact.FindComponent(InventoryItemComponent));
return item && item.GetParentSlot() && item.GetParentSlot().GetStorage() == storage;
}
protected bool PhysicalProof()
{
return Proof(m_Magazine, m_Source, m_Destination, m_Storage, m_Recipient, m_Acknowledgement);
}
protected bool ReturnProof()
{
return Proof(m_ReturnMagazine, m_Destination, m_Source, m_ReturnStorage, m_Donor, m_ReturnAcknowledgement);
}
void Acknowledge(bool success, bool returning = false)
{
if (returning)
{
if (m_ReturnAcknowledgement == 0)
{
if (success) m_ReturnAcknowledgement = 1;
else m_ReturnAcknowledgement = -1;
}
return;
}
if (m_Acknowledgement != 0) return;
if (success) m_Acknowledgement = 1;
else m_Acknowledgement = -1;
}
// Both native operations remain observable after Stop. Returning an empty
// magazine never proves delivery or grants authority for a later full one.
bool Unresolved()
{
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();
if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;
return !PhysicalProof();
}
// Any native submission consumes this incoming identity at the coordinator,
// even when only the empty return was attempted and delivery never started.
bool Submitted() { return m_Submitted || m_ReturnSubmitted; }
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 <= 15)) return false;
if (distance > 4 && !m_Donor.FindComponent(VehicleHelicopterSimulation)) return false;
if (!Compatible(m_Recipient, m_Magazine, m_LauncherMagazine)) return false;
m_Driver = driver; m_DonorRecord = donorRecord; m_RecipientRecord = recipientRecord;
return true;
}
// One optional capacity recovery, using only an unused exact empty identity.
protected bool ReturnEmpty()
{
if (!m_RecycleEmpty || m_ReturnSubmitted || m_ReturnMagazine) return false;
BaseMagazineComponent incoming = BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent));
if (!incoming || !incoming.GetMagazineWell()) return false;
array<IEntity> items = {}; m_Destination.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
if (items.Count() > 128) return false;
foreach (IEntity candidate : items)
{
if (!candidate || candidate == m_Magazine) continue;
BaseMagazineComponent empty = BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent));
if (!empty || empty.IsUsed() || empty.GetAmmoCount() != 0 || empty.GetMaxAmmoCount() <= 0
|| !empty.GetMagazineWell() || empty.GetMagazineWell().Type() != incoming.GetMagazineWell().Type()) continue;
if (Ownership(m_Destination, candidate) != 1 || Ownership(m_Source, candidate) != 0) continue;
InventoryItemComponent item = InventoryItemComponent.Cast(candidate.FindComponent(InventoryItemComponent));
if (!item || !item.GetParentSlot() || !ManagedStorage(m_Destination, item.GetParentSlot().GetStorage(), m_Recipient)) continue;
BaseInventoryStorageComponent storage = m_Source.FindStorageForItem(candidate, EStoragePurpose.PURPOSE_DEPOSIT);
if (!ManagedStorage(m_Source, storage, m_Donor) || !m_Destination.CanMoveItemToStorage(candidate, storage, -1)) continue;
m_ReturnMagazine = candidate; m_ReturnStorage = storage;
m_ReturnCallback = new ERR_InfantryAmmoCallback(); m_ReturnCallback.Transfer = this; m_ReturnCallback.Returning = true;
m_ReturnSubmitted = true;
m_ReturnAccepted = m_Destination.TryMoveItemToStorage(candidate, storage, -1, m_ReturnCallback);
if (m_ReturnAcknowledgement == 1) m_ReturnAccepted = true;
return true;
}
return false;
}
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; m_PhaseStarted = now; }
if (now < m_Started || now < m_PhaseStarted || now - m_Started >= 16000 || now - m_PhaseStarted >= 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_ReturnSubmitted && !m_ReturnDone)
{
if (!m_ReturnAccepted || m_ReturnAcknowledgement < 0) return Fail("native empty return rejected; neither identity will be retried");
if (!ReturnProof()) return ERR_InfantryAmmoTransferState.PENDING;
m_ReturnDone = true; m_PhaseStarted = now;
// Separate slow tick allows loss of HQ authority to stop before delivery.
return ERR_InfantryAmmoTransferState.PENDING;
}
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 (m_Storage && !ManagedStorage(m_Destination, m_Storage, m_Recipient)) return Fail("recipient storage ownership unavailable");
bool capacity = m_Storage && m_Source.CanMoveItemToStorage(m_Magazine, m_Storage, -1);
if (!ERR_InfantryAmmoAccess.Clear(m_Donor, m_Recipient)) return Fail("no unobstructed physical reach to an authored donor storage access point");
if (!capacity)
{
if (!m_ReturnDone && ReturnEmpty()) return ERR_InfantryAmmoTransferState.PENDING;
return Fail("recipient has no native compatible available storage after at most one empty return");
}
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;
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,50 @@
{
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c": {
"working_sha256": "966068555a6724e620a2c377b98d8dc02f141466c5cdd44636f86cb1cfe9ac78",
"index_sha256": "966068555a6724e620a2c377b98d8dc02f141466c5cdd44636f86cb1cfe9ac78"
},
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c": {
"working_sha256": "401a29ec31712d113c203d6975e44aed7bb3166025454f4950a4c7cb6f5ad6eb",
"index_sha256": "401a29ec31712d113c203d6975e44aed7bb3166025454f4950a4c7cb6f5ad6eb"
},
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c": {
"working_sha256": "1f129b979abcb86ee10622e2866aa987807f070f2ab2d818af015e12a8bcfd8c",
"index_sha256": "1f129b979abcb86ee10622e2866aa987807f070f2ab2d818af015e12a8bcfd8c"
},
"tests/test_hq_multiconvoy_supply.py": {
"working_sha256": "c96a941e75673a2c95adee83b57a84d3d8e2af772e96b197f85d78a90d469a2e",
"index_sha256": "c96a941e75673a2c95adee83b57a84d3d8e2af772e96b197f85d78a90d469a2e"
},
"tests/test_mounted_supply_integration.py": {
"working_sha256": "2f365cd9918bf39457fab046b8e046392f7b35e2b2eff45a0d2ed7d6ffee956a",
"index_sha256": "2f365cd9918bf39457fab046b8e046392f7b35e2b2eff45a0d2ed7d6ffee956a"
},
"tests/test_infantry_resupply_coordinator.py": {
"working_sha256": "65fa119778289175766ba7fdecd9e5685199736dffecc32056692121cbc6a682",
"index_sha256": "65fa119778289175766ba7fdecd9e5685199736dffecc32056692121cbc6a682"
},
"tests/test_infantry_resupply_planning.py": {
"working_sha256": "211e3d0ee20242f271afc46885e1ba7845ff2406d663bfe5118f40578cd5d709",
"index_sha256": "211e3d0ee20242f271afc46885e1ba7845ff2406d663bfe5118f40578cd5d709"
},
"tests/test_infantry_ammo_transfer.py": {
"working_sha256": "e1172dda6350d2f8b89166ba63995cd1590435930ba220cd38b022307103a306",
"index_sha256": "e1172dda6350d2f8b89166ba63995cd1590435930ba220cd38b022307103a306"
},
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c": {
"working_sha256": "258ad63c9dbf221f2d8402e93feb7b4d51897417954abf477004b5302e819bc3",
"index_sha256": "258ad63c9dbf221f2d8402e93feb7b4d51897417954abf477004b5302e819bc3"
},
"tests/test_hq_watch.py": {
"working_sha256": "5bf0debdeecb8f7fff6be94cc3772acfa948b839771d309291b1720b249b2890",
"index_sha256": "5bf0debdeecb8f7fff6be94cc3772acfa948b839771d309291b1720b249b2890"
},
"tests/test_hq_ammo_reporting.py": {
"working_sha256": "7cdf673d137a5c016630dd61d21d29f860469519c56303a12e19ee9630e1664f",
"index_sha256": "7cdf673d137a5c016630dd61d21d29f860469519c56303a12e19ee9630e1664f"
},
"tests/test_hq_ammo_sampler.py": {
"working_sha256": "4f59b80291ec6f636a0ba5f748689f2664d3a1e44e74c2f709fb73c8019c7572",
"index_sha256": "4f59b80291ec6f636a0ba5f748689f2664d3a1e44e74c2f709fb73c8019c7572"
}
}
@@ -0,0 +1,327 @@
"""Execute the actual finite native magazine transaction; engine behavior remains separate."""
from pathlib import Path
import os,re
from policy_harness import ROOT,run,body
from test_finite_magazine_reservation import translated_reservation
p=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))
s=p.read_text()
for forbidden in ('TrySpawn','DeleteEntity','SetOrigin','SetTransform','TryInsert','TryRemove','SetAmmoCount'):
assert forbidden not in s,forbidden
callback=s[s.index('class ERR_InfantryAmmoCallback'):s.index('// One exact physical')]
code=s[s.index('class ERR_InfantryAmmoTransfer\n'):]
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',
'InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))':'entity->item',
'InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent))':'m_Magazine->item',
'InventoryItemComponent.Cast(exact.FindComponent(InventoryItemComponent))':'exact->item',
'InventoryItemComponent.Cast(candidate.FindComponent(InventoryItemComponent))':'candidate->item',
'BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent))':'m_Magazine->magazine',
'BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent))':'candidate->magazine',
'SCR_AIVehicleUsageComponent.Cast(m_Donor.FindComponent(SCR_AIVehicleUsageComponent))':'m_Donor->usage',
'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','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=code.replace('\n}\n','\n};\n')
callback=callback.replace('class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback','').strip()[1:-1]
callbacks=[]
for name in ('OnComplete','OnFailed','OnAllComplete','OnAllFailed'):
m=re.search(r'protected override void '+name+r'\(\) (\{[^\n]+\})',callback)
callbacks.append('void ERR_InfantryAmmoCallback::'+name+'() '+m[1].replace('Transfer.','Transfer->'))
# Integrate the actual coordinator transfer selector with this actual leaf.
coordinator_path=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
transfer=body(coordinator_path,'protected void Transfer(float now)').replace('protected ','').replace('null','nullptr').replace('foreach (','for (')
transfer=transfer.replace('ERR_HQAmmoSnapshot local','ERR_HQAmmoSnapshot* local')
for name in ('ERR_Actors','ERR_HQAmmoSampler','ERR_InfantryAmmoTransfer','vector'):
transfer=transfer.replace(name+'.',name+'::')
transfer=transfer.replace('ERR_FiniteMagazineReservation reservation', 'ERR_FiniteMagazineReservation* reservation').replace('new ERR_FiniteMagazineReservation(m_Stock, m_Spent,', 'new ERR_FiniteMagazineReservation(&m_Stock, &m_Spent,')
for name in ('m_Move','actor','m_Truck','m_Recipient','local','reservation'):
transfer=transfer.replace(name+'.',name+'->')
transfer=transfer.replace('Entity.GetOrigin()', 'Entity->GetOrigin()')
pre=r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>
#include <limits>
#include <functional>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T t){this->push_back(t);}void RemoveItem(T t){this->erase(std::remove(this->begin(),this->end(),t),this->end());}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
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 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;
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnFailed(){}virtual void OnAllComplete(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
struct ERR_InfantryAmmoCallback:ScriptedInventoryOperationCallback{ERR_InfantryAmmoTransfer*Transfer=nullptr;bool Returning=false;void OnComplete()override;void OnFailed()override;void OnAllComplete()override;void OnAllFailed()override;};
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{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;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;}};
struct VehicleManager{IEntity vehicle=nullptr;IEntity FindVehicle(IEntity v){return vehicle==v?v:nullptr;}};
struct Utility{VehicleManager*m_VehicleMgr=nullptr;bool military=true;bool IsMilitary(){return military;}};
struct Group{int faction=1,players=0;bool slave=false;Utility*utility=nullptr;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}bool IsSlave(){return slave;}Utility*GetGroupUtilityComponent(){return utility;}};
struct Entity{bool helicopter=false;bool alive=true,awake=true,player=false,captured=false;int faction=1;Group*group=nullptr;IEntity vehicle=nullptr,parent=nullptr;
Inventory*inventory=nullptr;Magazine*magazine=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;Usage*usage=nullptr;Physics_*physics=nullptr;vector position;
IEntity GetParent(){return parent;}Physics_*GetPhysics(){return physics;}vector GetOrigin(){return position;}};
struct Record{SCR_AIGroup Group=nullptr;bool Physical=true,Eliminated=false;};
struct Ledger{Array<Record*>records;Record*FindGroup(SCR_AIGroup g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;Ledger*ledger=nullptr;static ERR_GameMode*Get(){return current;}bool Active(){return active;}Ledger*Forces(){return ledger;}int EnemyFaction(){return 1;}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->alive&&a->awake;}static bool Alive(IEntity a){return a&&a->alive;}};
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a?a->faction:-1;}};
struct CompartmentAccessComponent{static IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}};
struct ERR_ConvoyRules{static bool Captured(Vehicle v,SCR_AIGroup g){return v->captured||g->players>0;}static bool PartOf(IEntity a,IEntity root){return a==root;}};
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,SCR_AIGroup g,IEntity a){return m&&g&&a&&a->alive&&a->awake&&!a->player&&a->group==g&&g->players==0&&g->faction==m->EnemyFaction()&&g->utility&&g->utility->military;}};
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}};
struct ERR_InfantryAmmoAccess{static inline bool clear=true;static inline int checks=0;static bool Clear(Vehicle donor,IEntity recipient){assert(donor&&recipient);checks++;return clear;}};
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,callback=0;ScriptedInventoryOperationCallback*last=nullptr;std::function<void()> effect;
IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a,int purpose){assert(purpose==1);a=items;}void GetStorages(Array<Storage*>&a,int purpose){assert(purpose==1);a=storages;}
Storage*FindStorageForItem(IEntity,int purpose){assert(purpose==1);return selected;}
bool CanMoveItemToStorage(IEntity,Storage*,int slot){assert(slot==-1);return allow;}
bool TryMoveItemToStorage(IEntity,Storage*,int slot,ScriptedInventoryOperationCallback*cb){assert(slot==-1);calls++;last=cb;if(effect)effect();if(callback==1){cb->OnComplete();}if(callback==2){cb->OnFailed();}return accept;}};
'''
main=r'''
struct Fixture{ERR_GameMode mode;Ledger ledger;Group donorGroup,recipientGroup,otherGroup;Record donorRecord,recipientRecord;Entity truck,recipient,driver,spareDriver,magEntity,rifle,foreign;
VehicleManager vehicleManager;Utility donorUtility,recipientUtility;Physics_ physics;Usage usage;Slot pilot,fromSlot,toSlot;Storage sourceStorage,targetStorage;Inventory source,destination;Item item;Magazine magazine;Manager weapons;Weapon weapon;Muzzle muzzle;Well well;
Fixture(){ERR_GameMode::current=&mode;mode.ledger=&ledger;donorRecord.Group=&donorGroup;recipientRecord.Group=&recipientGroup;ledger.records={&donorRecord,&recipientRecord};
donorGroup.utility=&donorUtility;recipientGroup.utility=&recipientUtility;donorUtility.m_VehicleMgr=&vehicleManager;vehicleManager.vehicle=&truck;
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;
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;}
};
struct EmptyFixture:Fixture{
Entity empty,secondEmpty;Magazine emptyMag;Item emptyItem,secondItem;Slot emptyFrom,emptyTo;
EmptyFixture(){emptyMag.well=&well;emptyMag.rounds=0;empty.magazine=&emptyMag;empty.item=&emptyItem;
emptyFrom.storage=&targetStorage;emptyTo.storage=&sourceStorage;emptyItem.slot=&emptyFrom;
secondEmpty.magazine=&emptyMag;secondEmpty.item=&secondItem;secondItem.slot=&emptyFrom;
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 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{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();}
void Audit(string){audits++;}
bool Contains(IEntity owner,IEntity item){return owner&&owner->inventory&&owner->inventory->items.Count()<=128&&owner->inventory->items.Contains(item);}
'''+transfer+r'''
};
struct SupplyFixture:Fixture{SupplyTruck supplyTruck;SupplyRecipient supplyRecipient;Coordinator c;
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(){
{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;
f.destination.items={&empty};f.destination.selected=nullptr;f.source.selected=&f.sourceStorage;
auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Submitted()&&t.Unresolved()&&f.destination.calls==1&&f.source.calls==0);
auto returning=f.destination.last;returning->OnComplete();assert(t.Tick(1)==PENDING&&f.source.calls==0&&t.Unresolved());
f.destination.items.clear();f.source.items.Insert(&empty);emptyItem.slot=&emptyTo;f.destination.selected=&f.targetStorage;
assert(t.Tick(2)==PENDING&&!t.Unresolved()&&f.source.calls==0);assert(t.Tick(3)==PENDING&&f.source.calls==1&&t.Unresolved());
returning->OnAllComplete();returning->OnFailed();assert(t.Tick(4)==PENDING&&t.Unresolved());
f.Move();assert(t.Tick(5)==PENDING);f.source.last->OnComplete();assert(t.Tick(6)==SUCCEEDED&&!t.Unresolved());
assert(f.destination.calls==1&&f.source.calls==1);}
// Ground coordinator reserves the incoming identity as soon as reverse native movement is submitted.
// A failed/ambiguous return never releases it to try a second empty or another incoming magazine.
for(int outcome=0;outcome<3;outcome++){EmptyFixture f;SupplyTruck truck{&f.truck,&f.donorGroup};SupplyRecipient recipient{&f.recipientGroup};Coordinator c;
c.m_Mode=&f.mode;c.m_Truck=&truck;c.m_Recipient=&recipient;c.m_Members={&f.recipient};c.m_Stock={&f.magEntity};
if(outcome==0){f.destination.accept=false;}
c.Transfer(0);assert(c.m_Move&&c.m_Move->Submitted()&&c.m_Spent.Count()==1&&c.m_Receiving==&f.recipient&&c.m_Delivered==0);
if(outcome==1){f.Return(false);f.destination.last->OnComplete();c.Transfer(1000);}
c.Transfer(outcome==2?8000:2000);assert(c.finishes==1&&c.m_Spent.Contains(&f.magEntity)&&c.m_Delivered==0&&f.destination.calls==1&&f.source.calls==0);
assert(c.m_Move->Unresolved()==(outcome==2));}
{EmptyFixture f;SupplyTruck truck{&f.truck,&f.donorGroup};SupplyRecipient recipient{&f.recipientGroup};Coordinator c;
c.m_Mode=&f.mode;c.m_Truck=&truck;c.m_Recipient=&recipient;c.m_Members={&f.recipient};c.m_Stock={&f.magEntity};
c.Transfer(0);f.Return();f.destination.last->OnComplete();c.Transfer(1000);assert(c.m_Delivered==0&&f.source.calls==0);
c.Transfer(2000);assert(c.m_Delivered==0&&f.source.calls==1);f.Move();f.source.last->OnComplete();c.Transfer(3000);c.Transfer(4000);
assert(c.m_Delivered==1&&c.m_Served.Count()==1&&c.m_Spent.Count()==1&&f.source.calls==1&&f.destination.calls==1&&f.source.items.Contains(&f.empty));}
// Default callers (including air delivery) retain the original one-way behavior.
{EmptyFixture f;ERR_InfantryAmmoTransfer t(&f.mode,&f.truck,&f.donorGroup,&f.recipient,&f.recipientGroup,&f.magEntity);
assert(t.Tick(0)==FAILED&&!t.Submitted()&&f.destination.calls==0&&f.source.calls==0);}
// No return when capacity already exists, even with carried empties.
{EmptyFixture f;f.destination.selected=&f.targetStorage;auto t=f.Job();assert(t.Tick(0)==PENDING&&f.source.calls==1&&f.destination.calls==0);}
// Unused, zero-round, exact-well, exclusive recipient storage ownership and donor space are required.
for(int invalid=0;invalid<12;invalid++){EmptyFixture f;f.destination.items={&f.empty};Well other;other.type=2;
if(invalid==0)f.emptyMag.used=true;if(invalid==1)f.emptyMag.rounds=1;if(invalid==2)f.emptyMag.rounds=-1;
if(invalid==3)f.emptyMag.capacity=0;if(invalid==4)f.emptyMag.well=&other;if(invalid==5)f.emptyMag.well=nullptr;
if(invalid==6)f.emptyItem.slot=&f.emptyTo;if(invalid==7)f.source.items.Insert(&f.empty);
if(invalid==8)f.source.selected=nullptr;if(invalid==9)f.destination.allow=false;
if(invalid==10){for(int i=0;i<129;i++)f.destination.items.Insert(&f.empty);}if(invalid==11)ERR_InfantryAmmoAccess::clear=false;
auto t=f.Job();assert(t.Tick(0)==FAILED&&!t.Submitted()&&!t.Unresolved()&&f.destination.calls==0&&f.source.calls==0);
ERR_InfantryAmmoAccess::clear=true;}
// Return proof needs exact destination slot as well as callback; no second candidate is ever moved.
{EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return();f.emptyItem.slot=&f.emptyFrom;f.destination.last->OnComplete();
assert(t.Tick(1)==PENDING&&t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);
f.emptyItem.slot=&f.emptyTo;assert(t.Tick(2)==PENDING&&!t.Unresolved());assert(t.Tick(3)==PENDING&&f.source.calls==1&&f.destination.calls==1);}
// Inline success callbacks override a false API result for the reverse operation, but require physical proof.
{EmptyFixture f;f.destination.accept=false;f.destination.callback=1;f.destination.effect=[&](){f.Return();};auto t=f.Job();
assert(t.Tick(0)==PENDING&&t.Submitted()&&!t.Unresolved());assert(t.Tick(1)==PENDING&&f.source.calls==0);
f.source.accept=false;f.source.callback=1;f.source.effect=[&](){f.Move();};assert(t.Tick(2)==SUCCEEDED&&f.source.calls==1&&f.destination.calls==1);}
// Every attempted reverse move burns the incoming identity, including synchronous rejection.
for(int failure=0;failure<3;failure++){EmptyFixture f;if(failure==0)f.destination.accept=false;if(failure==1)f.destination.callback=2;
auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Submitted()&&f.destination.calls==1&&f.source.calls==0);
assert(t.Tick(failure==2?8000:1)==FAILED);assert(t.Unresolved()==(failure==2));
for(int i=0;i<3;i++){t.Tick(9000+i);}assert(f.destination.calls==1&&f.source.calls==0);}
// Stop before, during, or after confirmed return can never launch the incoming move.
for(int stopAt=0;stopAt<3;stopAt++){EmptyFixture f;auto t=f.Job();if(stopAt==0){t.Stop();assert(t.Tick(0)==FAILED&&!t.Submitted());continue;}
t.Tick(0);if(stopAt==2){f.Return();f.destination.last->OnComplete();assert(t.Tick(1)==PENDING&&!t.Unresolved());}
t.Stop();assert(t.Tick(2)==FAILED&&t.Submitted()&&f.source.calls==0);if(stopAt==1){assert(t.Unresolved());f.Return();f.destination.last->OnAllComplete();}
assert(!t.Unresolved()&&t.Tick(3)==FAILED&&f.source.calls==0&&f.destination.calls==1);}
// Confirmed return cannot trigger a second empty or bypass revalidated incoming custody/capacity/access.
for(int changed=0;changed<6;changed++){EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return(changed!=0);f.destination.last->OnComplete();assert(t.Tick(1)==PENDING);
if(changed==1)f.source.items.RemoveItem(&f.magEntity);if(changed==2)f.destination.items.Insert(&f.magEntity);
if(changed==3)f.truck.captured=true;if(changed==4)f.recipient.player=true;if(changed==5)ERR_InfantryAmmoAccess::clear=false;
assert(t.Tick(2)==FAILED&&t.Submitted()&&!t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);
assert(t.Tick(3)==FAILED&&f.destination.calls==1);ERR_InfantryAmmoAccess::clear=true;}
// Original owner/driver/ledger loss while a return is accepted quarantines that exact operation.
for(int changed=0;changed<4;changed++){EmptyFixture f;auto t=f.Job();t.Tick(0);
if(changed==0)f.truck.captured=true;if(changed==1)f.recipient.player=true;if(changed==2)f.pilot.occupant=&f.spareDriver;if(changed==3)f.donorRecord.Eliminated=true;
assert(t.Tick(1)==FAILED&&t.Unresolved());f.destination.last->OnFailed();assert(!t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);}
// Both native phases are bounded independently, with no renewal while waiting for callbacks.
{EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return();f.destination.last->OnComplete();assert(t.Tick(7999)==PENDING);
assert(t.Tick(8000)==PENDING&&f.source.calls==1);assert(t.Tick(15999)==FAILED&&t.Unresolved());}
// XZ approach can precede valid vertical reach, stopped state, capacity or clear access.
// None of those no-submission outcomes consumes cargo or ends the trip; the same exact item can later submit once.
for(int obstruction=0;obstruction<5;obstruction++){SupplyFixture f;
if(obstruction==0){f.recipient.position.y=3;}if(obstruction==1){f.physics.velocity.x=1;}
if(obstruction==2){f.destination.selected=nullptr;}if(obstruction==3){f.source.allow=false;}
if(obstruction==4){ERR_InfantryAmmoAccess::clear=false;}
for(int tick=0;tick<3;tick++){f.c.Transfer(tick*1000);assert(f.source.calls==0&&f.c.m_Spent.IsEmpty()&&!f.c.m_Move&&!f.c.m_Receiving&&f.c.finishes==0);}
f.recipient.position.y=0;f.physics.velocity.x=0;f.destination.selected=&f.targetStorage;f.source.allow=true;ERR_InfantryAmmoAccess::clear=true;
f.c.Transfer(3000);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move&&f.c.m_Move->Submitted());
f.Move();f.source.last->OnComplete();f.c.Transfer(4000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&f.recipient)&&f.source.calls==1&&f.c.m_Spent.Count()==1);
}
// A capacity-blocked first member cannot prevent another original member from receiving this magazine.
{SupplyFixture f;Fixture second;ERR_GameMode::current=&f.mode;
second.recipient.group=&f.recipientGroup;second.recipientRecord.Group=&f.recipientGroup;
f.c.m_Members.Insert(&second.recipient);f.destination.selected=nullptr;
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Receiving==&second.recipient&&f.c.m_Spent.Count()==1&&f.c.finishes==0);
f.source.items.clear();second.destination.items={&f.magEntity};f.item.slot=&second.toSlot;f.source.last->OnComplete();
f.c.Transfer(1000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&second.recipient)&&!f.c.m_Served.Contains(&f.recipient));}
// A native API may complete both physical movement and callback inline. Account once on the next slow tick.
{SupplyFixture f;f.source.callback=1;f.source.effect=[&](){f.Move();};f.c.Transfer(0);
assert(f.source.calls==1&&f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Delivered==0&&f.c.m_Spent.Count()==1);
f.c.Transfer(1000);f.c.Transfer(2000);assert(f.c.m_Delivered==1&&f.c.m_Served.Count()==1&&f.source.calls==1&&f.c.finishes==0);}
// Crossing the native submission boundary burns the exact identity even if native refusal is synchronous.
for(int refusal=0;refusal<2;refusal++){SupplyFixture f;if(refusal==0){f.source.accept=false;}else{f.source.callback=2;}
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move->Submitted());
f.c.Transfer(1000);assert(f.c.finishes==1&&f.c.m_Spent.Count()==1&&f.source.calls==1);
}
// Accepted but unresolved loss stays retained/spent; changing geometry cannot cause a second native move.
{SupplyFixture f;f.c.Transfer(0);f.recipient.position.x=8;f.c.Transfer(1000);
assert(f.c.finishes==1&&f.c.m_Move&&f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1&&f.source.calls==1);}
// Several equivalent magazines do not multiply a blocked actor's expensive preflight work on one slow tick.
{SupplyFixture f;Entity another=f.magEntity;f.source.items.Insert(&another);f.c.m_Stock.Insert(&another);
ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;f.c.Transfer(0);
assert(ERR_InfantryAmmoAccess::checks==1&&f.source.calls==0&&f.c.m_Spent.IsEmpty());ERR_InfantryAmmoAccess::clear=true;}
// A blocked native access check happens before submission and is terminal.
{Fixture f;auto t=f.Job();ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;
assert(t.Tick(0)==FAILED&&!t.Submitted()&&!t.Unresolved()&&f.source.calls==0&&ERR_InfantryAmmoAccess::checks==1);
ERR_InfantryAmmoAccess::clear=true;assert(t.Tick(1)==FAILED&&f.source.calls==0&&ERR_InfantryAmmoAccess::checks==1);}
// Once native move is accepted, later geometry cannot cause a second operation or pretend cancellation.
{Fixture f;auto t=f.Job();ERR_InfantryAmmoAccess::checks=0;assert(t.Tick(0)==PENDING&&ERR_InfantryAmmoAccess::checks==1);
ERR_InfantryAmmoAccess::clear=false;assert(t.Tick(1)==PENDING&&t.Unresolved()&&f.source.calls==1&&ERR_InfantryAmmoAccess::checks==1);
f.Move();f.source.last->OnComplete();assert(t.Tick(2)==SUCCEEDED);ERR_InfantryAmmoAccess::clear=true;}
// Read-only selection follows the native inherited-well direction, excluding launcher muzzles and weapons.
{Fixture f;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));Well derived;derived.type=2;derived.parent=1;f.magazine.well=&derived;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.magazine.well=&f.well;f.muzzle.well=&derived;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.muzzle.well=&f.well;f.muzzle.kind=EMuzzleType::MT_UGLMuzzle;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.muzzle.kind=0;f.weapon.kind=EWeaponType::WT_ROCKETLAUNCHER;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));}
// Exactly one source move; neither a callback alone nor physical movement alone is a successful transaction.
{Fixture f;auto t=f.Job();assert(t.Tick(100)==PENDING&&f.source.calls==1&&t.Unresolved());f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
f.Move();assert(t.Tick(102)==SUCCEEDED&&!t.Unresolved());t.Stop();assert(t.Tick(103)==SUCCEEDED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();assert(t.Tick(101)==PENDING&&t.Unresolved());f.source.last->OnAllComplete();assert(t.Tick(102)==SUCCEEDED);}
// Exact destination slot and known donor absence are mandatory even after success callback.
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();f.item.slot=&f.fromSlot;f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
f.item.slot=&f.toSlot;for(int i=0;i<513;i++)f.source.items.Insert(&f.foreign);assert(t.Tick(102)==PENDING&&t.Unresolved());}
// Late callbacks resolve retained reservations after Stop without Tick or further operations; the stopped task stays failed.
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();assert(t.Unresolved());f.Move();f.source.last->OnAllComplete();assert(!t.Unresolved());assert(t.Tick(2)==FAILED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();f.source.last->OnAllFailed();assert(!t.Unresolved());assert(t.Tick(1)==FAILED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(0);f.source.last->OnComplete();f.source.last->OnFailed();assert(t.Unresolved());f.Move();assert(!t.Unresolved()&&t.Tick(1)==SUCCEEDED);}
// Failure/rejection and the 8s deadline never submit a second move.
for(int failure=0;failure<3;failure++){Fixture f;if(failure==0)f.source.accept=false;if(failure==1)f.source.callback=2;auto t=f.Job();t.Tick(100);
if(failure==2){assert(t.Tick(8100)==FAILED&&t.Unresolved());}else assert(t.Tick(101)==FAILED&&!t.Unresolved());
for(int i=0;i<10;i++)t.Tick(8200+i);assert(f.source.calls==1);}
// Invalid physical state before submission spends no native action; the terminal object cannot revive.
for(int invalid=0;invalid<25;invalid++){Fixture f;auto t=f.Job();
if(invalid==0)Replication::server=false;if(invalid==1)SCR_Global::editor=true;if(invalid==2)f.mode.active=false;
if(invalid==3)f.truck.alive=false;if(invalid==4)f.recipient.alive=false;if(invalid==5)f.recipient.awake=false;if(invalid==6)f.recipient.player=true;
if(invalid==7)f.recipient.vehicle=&f.truck;if(invalid==8)f.truck.captured=true;if(invalid==9)f.donorGroup.players=1;
if(invalid==10)f.recipient.group=&f.otherGroup;if(invalid==11)f.donorRecord.Physical=false;if(invalid==12)f.recipientRecord.Eliminated=true;
if(invalid==13)f.vehicleManager.vehicle=&f.foreign;if(invalid==14)f.driver.alive=false;if(invalid==15)f.driver.vehicle=nullptr;
if(invalid==16)f.physics.velocity.x=.251f;if(invalid==17)f.physics.angular.x=.101f;if(invalid==18)f.recipient.position.x=4.001f;
if(invalid==19)f.truck.faction=2;if(invalid==20)f.driver.faction=2;if(invalid==21)f.recipientGroup.slave=true;
if(invalid==22)f.magazine.rounds=0;if(invalid==23)f.magazine.rounds=31;if(invalid==24)f.physics.velocity.x=std::numeric_limits<float>::quiet_NaN();
assert(t.Tick(100)==FAILED&&f.source.calls==0&&!t.Unresolved());Replication::server=true;SCR_Global::editor=false;assert(t.Tick(200)==FAILED&&f.source.calls==0);}
// Native stock/storage identity and capacity guards reject shared, foreign, oversized and inaccessible inventory.
for(int invalid=0;invalid<9;invalid++){Fixture f;auto t=f.Job();if(invalid==0)f.source.items.clear();if(invalid==1)f.destination.items={&f.magEntity};
if(invalid==2)f.fromSlot.storage=&f.targetStorage;if(invalid==3)f.sourceStorage.owner=&f.foreign;if(invalid==4)f.destination.selected=nullptr;
if(invalid==5)f.targetStorage.owner=&f.foreign;if(invalid==6)f.source.allow=false;if(invalid==7)f.destination.storages.clear();if(invalid==8){for(int i=0;i<65;i++)f.destination.storages.Insert(&f.targetStorage);}
assert(t.Tick(0)==FAILED&&f.source.calls==0);}
// Conditions and original native driver/ledger identities remain strict while an accepted move is pending.
for(int changed=0;changed<4;changed++){Fixture f;auto t=f.Job();t.Tick(100);Record replacement=f.recipientRecord;
if(changed==0)f.pilot.occupant=&f.spareDriver;if(changed==1)f.ledger.records[1]=&replacement;if(changed==2)f.physics.velocity.x=1;if(changed==3)f.recipient.position.x=8;
assert(t.Tick(101)==FAILED&&t.Unresolved());f.source.last->OnFailed();assert(!t.Unresolved()&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(100);assert(t.Tick(99)==FAILED&&t.Unresolved());}
// Clothing storage ownership follows native inventory slots and bounds cycles.
{Fixture f;Entity vest;Item vestItem;Slot vestSlot;Storage body;body.owner=&f.recipient;vestSlot.storage=&body;vestItem.slot=&vestSlot;vest.item=&vestItem;f.targetStorage.owner=&vest;
auto t=f.Job();assert(t.Tick(0)==PENDING);f.Move();f.source.last->OnComplete();assert(t.Tick(1)==SUCCEEDED);
vestSlot.storage=&f.targetStorage;assert(!ERR_InfantryAmmoTransfer::StoredUnder(&vest,&f.recipient));}
}
'''
# Brace tiny fixture statements to retain -Werror without suppressing useful compiler diagnostics.
main=re.sub(r'if\((invalid|changed|failure)==(\d+)\)([^;{}]+);',r'if(\1==\2){\3;}',main)
main=main.replace('for(int i=0;i<10;i++)t.Tick(8200+i);','for(int i=0;i<10;i++){t.Tick(8200+i);}')
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
print('PASS: actual bounded empty-return and finite delivery; phase-isolated callbacks, exact ownership proof, stop quarantine and original actor/vehicle boundaries')
@@ -0,0 +1,293 @@
enum ERR_InfantryAmmoTransferState { FAILED = -1, PENDING, SUCCEEDED }
class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback
{
ERR_InfantryAmmoTransfer Transfer;
bool Returning;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true, Returning); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false, Returning); }
protected override void OnAllComplete() { if (Transfer) Transfer.Acknowledge(true, Returning); }
protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false, Returning); }
}
// 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_RecycleEmpty;
protected bool m_LauncherMagazine;
protected IEntity m_ReturnMagazine;
protected BaseInventoryStorageComponent m_ReturnStorage;
protected ref ERR_InfantryAmmoCallback m_ReturnCallback;
protected bool m_ReturnSubmitted;
protected bool m_ReturnAccepted;
protected bool m_ReturnDone;
protected int m_ReturnAcknowledgement;
protected float m_PhaseStarted = -1;
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, 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, 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)
{
if (!rifle) continue;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent));
if (!weapon || weapon.GetOwner() != rifle) continue;
EWeaponType kind = weapon.GetWeaponType();
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 (!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;
}
}
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, IEntity exact = null)
{
if (!exact) exact = m_Magazine;
if (!inventory || !exact) return -1;
array<IEntity> items = {}; inventory.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
if (items.Count() > 512) return -1;
if (items.Contains(exact)) return 1;
return 0;
}
protected bool Proof(IEntity exact, InventoryStorageManagerComponent source, InventoryStorageManagerComponent destination, BaseInventoryStorageComponent storage, IEntity owner, int acknowledgement)
{
if (!exact || acknowledgement != 1 || Ownership(source, exact) != 0 || Ownership(destination, exact) != 1 || !ManagedStorage(destination, storage, owner)) return false;
InventoryItemComponent item = InventoryItemComponent.Cast(exact.FindComponent(InventoryItemComponent));
return item && item.GetParentSlot() && item.GetParentSlot().GetStorage() == storage;
}
protected bool PhysicalProof()
{
return Proof(m_Magazine, m_Source, m_Destination, m_Storage, m_Recipient, m_Acknowledgement);
}
protected bool ReturnProof()
{
return Proof(m_ReturnMagazine, m_Destination, m_Source, m_ReturnStorage, m_Donor, m_ReturnAcknowledgement);
}
void Acknowledge(bool success, bool returning = false)
{
if (returning)
{
if (m_ReturnAcknowledgement == 0)
{
if (success) m_ReturnAcknowledgement = 1;
else m_ReturnAcknowledgement = -1;
}
return;
}
if (m_Acknowledgement != 0) return;
if (success) m_Acknowledgement = 1;
else m_Acknowledgement = -1;
}
// Both native operations remain observable after Stop. Returning an empty
// magazine never proves delivery or grants authority for a later full one.
bool Unresolved()
{
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();
if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;
return !PhysicalProof();
}
// Any native submission consumes this incoming identity at the coordinator,
// even when only the empty return was attempted and delivery never started.
bool Submitted() { return m_Submitted || m_ReturnSubmitted; }
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 <= 15)) return false;
if (distance > 4 && !m_Donor.FindComponent(VehicleHelicopterSimulation)) return false;
if (!Compatible(m_Recipient, m_Magazine, m_LauncherMagazine)) return false;
m_Driver = driver; m_DonorRecord = donorRecord; m_RecipientRecord = recipientRecord;
return true;
}
// One optional capacity recovery, using only an unused exact empty identity.
protected bool ReturnEmpty()
{
if (!m_RecycleEmpty || m_ReturnSubmitted || m_ReturnMagazine) return false;
BaseMagazineComponent incoming = BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent));
if (!incoming || !incoming.GetMagazineWell()) return false;
array<IEntity> items = {}; m_Destination.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
if (items.Count() > 128) return false;
foreach (IEntity candidate : items)
{
if (!candidate || candidate == m_Magazine) continue;
BaseMagazineComponent empty = BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent));
if (!empty || empty.IsUsed() || empty.GetAmmoCount() != 0 || empty.GetMaxAmmoCount() <= 0
|| !empty.GetMagazineWell() || empty.GetMagazineWell().Type() != incoming.GetMagazineWell().Type()) continue;
if (Ownership(m_Destination, candidate) != 1 || Ownership(m_Source, candidate) != 0) continue;
InventoryItemComponent item = InventoryItemComponent.Cast(candidate.FindComponent(InventoryItemComponent));
if (!item || !item.GetParentSlot() || !ManagedStorage(m_Destination, item.GetParentSlot().GetStorage(), m_Recipient)) continue;
BaseInventoryStorageComponent storage = m_Source.FindStorageForItem(candidate, EStoragePurpose.PURPOSE_DEPOSIT);
if (!ManagedStorage(m_Source, storage, m_Donor) || !m_Destination.CanMoveItemToStorage(candidate, storage, -1)) continue;
m_ReturnMagazine = candidate; m_ReturnStorage = storage;
m_ReturnCallback = new ERR_InfantryAmmoCallback(); m_ReturnCallback.Transfer = this; m_ReturnCallback.Returning = true;
m_ReturnSubmitted = true;
m_ReturnAccepted = m_Destination.TryMoveItemToStorage(candidate, storage, -1, m_ReturnCallback);
if (m_ReturnAcknowledgement == 1) m_ReturnAccepted = true;
return true;
}
return false;
}
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; m_PhaseStarted = now; }
if (now < m_Started || now < m_PhaseStarted || now - m_Started >= 16000 || now - m_PhaseStarted >= 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_ReturnSubmitted && !m_ReturnDone)
{
if (!m_ReturnAccepted || m_ReturnAcknowledgement < 0) return Fail("native empty return rejected; neither identity will be retried");
if (!ReturnProof()) return ERR_InfantryAmmoTransferState.PENDING;
m_ReturnDone = true; m_PhaseStarted = now;
// Separate slow tick allows loss of HQ authority to stop before delivery.
return ERR_InfantryAmmoTransferState.PENDING;
}
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 (m_Storage && !ManagedStorage(m_Destination, m_Storage, m_Recipient)) return Fail("recipient storage ownership unavailable");
bool capacity = m_Storage && m_Source.CanMoveItemToStorage(m_Magazine, m_Storage, -1);
if (!ERR_InfantryAmmoAccess.Clear(m_Donor, m_Recipient)) return Fail("no unobstructed physical reach to an authored donor storage access point");
if (!capacity)
{
if (!m_ReturnDone && ReturnEmpty()) return ERR_InfantryAmmoTransferState.PENDING;
return Fail("recipient has no native compatible available storage after at most one empty return");
}
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;
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,32 @@
/tmp/err-policy-7562cy7g/check.cpp: In function ‘int main()’:
/tmp/err-policy-7562cy7g/check.cpp:48:3: error: ‘Fixture’ was not declared in this scope
48 | {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);
| ^~~~~~~
/tmp/err-policy-7562cy7g/check.cpp:48:13: error: ‘Leaf’ was not declared in this scope
48 | {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);
| ^~~~
/tmp/err-policy-7562cy7g/check.cpp:48:23: error: ‘f’ was not declared in this scope
48 | {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);
| ^
/tmp/err-policy-7562cy7g/check.cpp:48:35: error: ‘leaf’ was not declared in this scope
48 | {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);
| ^~~~
infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed
Traceback (most recent call last):
File "/tmp/erri-launcher-resupply-1140/tests/test_infantry_resupply_coordinator.py", line 111, in <module>
run(r'''
~~~^^^^^
#include <cassert>
^^^^^^^^^^^^^^^^^^
...<31 lines>...
}
^
'''.replace('METHOD',s))
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-7562cy7g/check.cpp', '-o', '/tmp/err-policy-7562cy7g/check']' returned non-zero exit status 1.
@@ -0,0 +1,13 @@
{
"test_hq_ammo_sampler.py": 0,
"test_infantry_ammo_transfer.py": 0,
"test_infantry_resupply_planning.py": 0,
"test_infantry_resupply_coordinator.py": 0,
"test_mounted_supply_integration.py": 0,
"test_hq_multiconvoy_supply.py": 0,
"test_hq_ammo_reporting.py": 0,
"test_hq_watch.py": 0,
"test_infantry_mine_coordinator.py": 0,
"test_infantry_supply_pool.py": 0,
"test_hq_status_sampler.py": 0
}
@@ -0,0 +1 @@
PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures requested check-ins and non-urgent detached carried-mine status
@@ -0,0 +1 @@
PASS: actual own-inventory ammo inspection deduplicates loaded/carried/shared wells, separates chambers/launchers, freezes scalar snapshots and treats unsupported/capped/foreign inventory as unknown
@@ -0,0 +1 @@
PASS: actual HQ rotates finite donors, skips unavailable/contacted trucks, uses delivered reports and dispatches at most one trip per planning cadence
@@ -0,0 +1 @@
PASS: own initial roster, explicit retained corpse evidence, unconscious/missing/ref transfer guards, fixed strength, no replacement adoption, detached samples, surviving radio sender, immutable convoy root/ledger, mounted capture release and lost-root unknown
@@ -0,0 +1 @@
PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence, detached/validated mounted payload replacement 256-record cap and detached carried-mine radio delivery/chronology
@@ -0,0 +1 @@
PASS: actual bounded empty-return and finite delivery; phase-isolated callbacks, exact ownership proof, stop quarantine and original actor/vehicle boundaries
@@ -0,0 +1,2 @@
PASS: actual designated zero/one original carrier selection, settled startup, shared finite claims, no refund after submission
PASS: actual HQ startup grant readiness forwarding is authoritative and fail-closed
@@ -0,0 +1,2 @@
infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed
PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership
@@ -0,0 +1 @@
PASS: actual convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup
@@ -0,0 +1 @@
PASS: actual finite convoy supply pool; immutable slots, shared demand state, sparse/capped discovery and permanent shutdown
@@ -0,0 +1 @@
PASS: actual shared mounted stock/history ownership, atomic exact HQ pair claim, and bounded stopped-owner proof polling
@@ -0,0 +1,29 @@
from pathlib import Path
import json,hashlib,subprocess
p=Path('/tmp/erri-launcher-resupply-1140'); root=Path('/home/jakub/devel/escapistsrereintroduced')
f=p/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c';s=f.read_text().replace('if (++checks > 128) return false;', 'if (++checks > 96 * 32) return false;').replace('magazine, true, m_LauncherTrip);','magazine, !m_LauncherTrip, m_LauncherTrip);');f.write_text(s)
f=p/'tests/test_infantry_resupply_planning.py';s=f.read_text().replace('static inline bool compatible=true;', 'static inline IEntity only=nullptr;static inline int checks=0;static inline bool compatible=true;').replace('return actor&&item&&launcher&&compatible;', 'checks++;return actor&&item&&launcher&&compatible&&(!only||item==only);');s=s.replace('int main(){','''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;}
''',1);f.write_text(s)
f=p/'tests/test_hq_ammo_sampler.py';s=f.read_text().replace('launcher.disposable=false;','''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));''');f.write_text(s)
rel='tests/test_hq_watch.py';f=p/rel;orig=(root/rel).read_text();b=p/'baseline'/rel;b.parent.mkdir(parents=True,exist_ok=True);b.write_text(orig)
s=orig.replace('original.MineComplete=true;', 'original.LauncherComplete=true;original.LauncherMuzzles=2;original.MineComplete=true;',1).replace('original.ReadyMines=0;', 'original.LauncherMuzzles=0;original.ReadyMines=0;',1)
s=s.replace('RadioIntake radio;StatusUnit sender;', 'assert(mineWire.AmmoState->LauncherMuzzles==2);\n RadioIntake radio;StatusUnit sender;',1)
s=s.replace('mineWire.AmmoState->ReadyMines=0;mineWire.AmmoState->MineComplete=false;', 'assert(mines->AmmoState->LauncherMuzzles==2);mineWire.AmmoState->LauncherMuzzles=0;assert(mines->AmmoState->LauncherMuzzles==2);\n mineWire.AmmoState->ReadyMines=0;mineWire.AmmoState->MineComplete=false;',1)
s=s.replace('// Mounted payload obeys', '''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''');f.write_text(s)
meta=json.loads((p/'baseline.json').read_text())
for f in (p/'tests').glob('test_*.py'):
rel=str(f.relative_to(p));base=p/'baseline'/rel
if not base.exists():base.write_bytes((root/rel).read_bytes())
if rel not in meta:
indexed=subprocess.check_output(['git','show',':'+rel],cwd=root)
meta[rel]={'working_sha256':hashlib.sha256(base.read_bytes()).hexdigest(),'index_sha256':hashlib.sha256(indexed).hexdigest()}
(p/'baseline.json').write_text(json.dumps(meta,indent=2)+'\n')
@@ -0,0 +1,707 @@
--- 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'''
@@ -0,0 +1,12 @@
check: /tmp/err-policy-yv5hrq37/check.cpp:448: int main(): Assertion `ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity,true)' failed.
Traceback (most recent call last):
File "/tmp/erri-launcher-resupply-1140/tests/test_infantry_ammo_transfer.py", line 323, in <module>
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
subprocess.run([str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['/tmp/err-policy-yv5hrq37/check']' died with <Signals.SIGABRT: 6>.
@@ -0,0 +1,19 @@
check: /tmp/err-policy-gec2651y/check.cpp:66: int main(): Assertion `f.first.launcherAccepted==1&&f.first.mineCalls==0&&f.second.calls==0' failed.
Traceback (most recent call last):
File "/tmp/erri-launcher-resupply-1140/tests/test_hq_multiconvoy_supply.py", line 14, in <module>
run(r'''
~~~^^^^^
#include <cassert>
^^^^^^^^^^^^^^^^^^
...<54 lines>...
}
^
'''.replace('METHODS',methods).replace('Task*Task()','struct Task*Task()').replace('Task*task=','struct Task*task='))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
subprocess.run([str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['/tmp/err-policy-gec2651y/check']' died with <Signals.SIGABRT: 6>.
@@ -0,0 +1,12 @@
from pathlib import Path
import difflib,json,hashlib,subprocess
p=Path('/tmp/erri-launcher-resupply-1140');root=Path('/home/jakub/devel/escapistsrereintroduced');paths=sorted([*p.glob('addons/**/*.c'),*p.glob('tests/test_*.py')]);parts=[];manifest={}
for f in paths:
rel=str(f.relative_to(p));before=(p/'baseline'/rel).read_text();after=f.read_text()
if before==after:continue
assert (root/rel).read_text()==before,rel+' target changed since baseline'
parts.extend(difflib.unified_diff(before.splitlines(True),after.splitlines(True),fromfile='a/'+rel,tofile='b/'+rel))
manifest[rel]=hashlib.sha256(f.read_bytes()).hexdigest()
(p/'fix.patch').write_text(''.join(parts));(p/'source-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
r=subprocess.run(['git','apply','--check',str(p/'fix.patch')],cwd=root,text=True,capture_output=True);(p/'apply-check.log').write_text(r.stdout+r.stderr);assert r.returncode==0,r.stderr
print(len(manifest),'changed files; apply check passed')
@@ -0,0 +1 @@
PASS: actual convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup
@@ -0,0 +1,89 @@
from pathlib import Path
import hashlib,json,subprocess
R=Path('/home/jakub/devel/escapistsrereintroduced'); P=Path('/tmp/erri-launcher-resupply-1140'); names=['ERR_InfantryResupply.c','ERR_InfantryAmmoTransfer.c','ERR_HQ.c'];baseline={}
for name in names:
rel='addons/core/Scripts/Game/ERR/10_AI/'+name;src=R/rel
for dest in (P/rel,P/'baseline'/rel):dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(src.read_bytes())
baseline[rel]={'working_sha256':hashlib.sha256(src.read_bytes()).hexdigest(),'index_sha256':hashlib.sha256(subprocess.check_output(['git','show',':'+rel],cwd=R)).hexdigest()}
(P/'baseline.json').write_text(json.dumps(baseline,indent=2)+'\n')
p=P/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c';s=p.read_text();s=s.replace('protected bool m_RecycleEmpty;','protected bool m_RecycleEmpty;\n protected bool m_LauncherMagazine;')
s=s.replace('bool recycleEmpty = false)','bool recycleEmpty = false, bool launcherMagazine = false)').replace('m_RecycleEmpty = recycleEmpty;','m_RecycleEmpty = recycleEmpty; m_LauncherMagazine = launcherMagazine;')
s=s.replace('static bool Compatible(IEntity actor, IEntity exactMagazine)','static bool Compatible(IEntity actor, IEntity exactMagazine, bool launcherMagazine = false)')
s=s.replace('array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);','if (launcherMagazine && magazine.IsUsed()) return false;\n array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);')
s=s.replace('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;''')
s=s.replace('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;
// Projectile-only/disposable and entirely missing magazine configurations
// are outside this finite magazine slice; never synthesize a projectile.
BaseMagazineComponent loaded = muzzle.GetMagazine();
if (!loaded || !loaded.GetOwner() || !loaded.GetMagazineWell() || loaded.GetMaxAmmoCount() <= 0) continue;
if (!StoredUnder(loaded.GetOwner(), rifle)) continue;
if (loaded.GetMagazineWell().Type() != muzzle.GetMagazineWell().Type()) continue;
if (magazine.GetMagazineWell().Type() != muzzle.GetMagazineWell().Type()) continue;
}''')
s=s.replace('if (!Compatible(m_Recipient, m_Magazine))','if (!Compatible(m_Recipient, m_Magazine, m_LauncherMagazine))')
s=s.replace('if (m_ReturnAccepted &&', 'if ((m_ReturnAccepted || (m_LauncherMagazine && m_ReturnSubmitted)) &&').replace('if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;', 'if (m_LauncherMagazine && m_Submitted) return m_Magazine && !PhysicalProof();\n if (!m_Accepted || !m_Magazine || m_Acknowledgement < 0) return false;')
s=s.replace('if (PhysicalProof())\n {', '''// 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())
{''')
p.write_text(s)
p=P/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c';s=p.read_text();anchor=' static bool MinePair('
s=s.replace(anchor,''' // A zero aggregate without a supported physical magazine is not launcher evidence.
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.LauncherMagazines > 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;
}
'''+anchor)
s=s.replace('protected bool m_MineTrip;','protected bool m_MineTrip;\n protected bool m_LauncherTrip;')
s=s.replace('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");
}''')
s=s.replace('if (m_MineTrip) reason = "mine top-up: " + reason;', 'if (m_MineTrip) reason = "mine top-up: " + reason;\n if (m_LauncherTrip) reason = "depleted launcher magazine: " + reason;')
s=s.replace('float now, bool mineTrip = false)','float now, bool mineTrip = false, bool launcherTrip = false)')
s=s.replace('if (m_Truck.Combat.Held || m_Truck.SupplyToken > 0 || !recipient || !donor)', 'if ((mineTrip && launcherTrip) || m_Truck.Combat.Held || m_Truck.SupplyToken > 0 || !recipient || !donor)')
s=s.replace('else if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;', '''else if (launcherTrip)
{
if (!ERR_InfantryResupplyRules.LauncherPair(demand, donorStatus, now)) return false;
}
else if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;''')
s=s.replace('m_MineTrip = mineTrip; m_MineRecipientSlot', 'm_MineTrip = mineTrip; m_LauncherTrip = launcherTrip; m_MineRecipientSlot')
s=s.replace('if (mineTrip) purpose += "; one finite PMN4 top-up for an existing carrier";', 'if (mineTrip) purpose += "; one finite PMN4 top-up for an existing carrier";\n if (launcherTrip) purpose += "; finite compatible magazine for a reported depleted launcher";')
s=s.replace(' bool TryPlanMines(',''' 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(''')
s=s.replace('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; }')
s=s.replace('m_State = 1; m_MineTrip = false;', 'm_State = 1; m_MineTrip = false; m_LauncherTrip = false;')
s=s.replace('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.LauncherMagazines <= 0
|| local.LauncherRounds != 0 || local.LauncherChambers != 0) continue;
}
else if (!local.Complete || local.SmallArmsMuzzles <= 0 || local.SmallArmsRounds + local.SmallArmsChambers > 30) continue;''')
s=s.replace('ERR_InfantryAmmoTransfer.Compatible(actor, magazine)', 'ERR_InfantryAmmoTransfer.Compatible(actor, magazine, m_LauncherTrip)')
s=s.replace('actor, m_Recipient.Group, magazine, true);','actor, m_Recipient.Group, magazine, true, m_LauncherTrip);')
p.write_text(s)
p=P/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c';s=p.read_text().replace(' // Optional finite mines are last:', ''' // 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:''');p.write_text(s)
@@ -0,0 +1,55 @@
from pathlib import Path
import hashlib,json,subprocess
P=Path('/tmp/erri-launcher-resupply-1140');R=Path('/home/jakub/devel/escapistsrereintroduced');D=P/'addons/core/Scripts/Game/ERR/10_AI';rel='addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c';s=(R/rel).read_text();(P/'baseline'/rel).write_text(s)
b=json.loads((P/'baseline.json').read_text());b[rel]={'working_sha256':hashlib.sha256((R/rel).read_bytes()).hexdigest(),'index_sha256':hashlib.sha256(subprocess.check_output(['git','show',':'+rel],cwd=R)).hexdigest()};(P/'baseline.json').write_text(json.dumps(b,indent=2)+'\n')
s=s.replace(' int LauncherRounds;',' int LauncherMuzzles;\n int LauncherRounds;').replace('copy.LauncherRounds = LauncherRounds;', 'copy.LauncherMuzzles = LauncherMuzzles; copy.LauncherRounds = LauncherRounds;')
s=s.replace(' if (LauncherRounds < 0', ' if (LauncherMuzzles < 0 || LauncherMuzzles > Members * 32) return false;\n if (LauncherRounds < 0')
s=s.replace('else launchers.Insert(muzzle);', '''else
{
launchers.Insert(muzzle);
if (muzzle.GetMagazineWell() && !muzzle.IsDisposable()) result.LauncherMuzzles++;
else result.LauncherComplete = false;
}''');(P/rel).write_text(s)
p=D/'ERR_InfantryResupply.c';s=p.read_text().replace('A zero aggregate without a supported physical magazine is not launcher evidence.', 'Only positively sampled reloadable magazine-well muzzles establish launcher demand.')
s=s.replace('entry.AmmoState.LauncherMagazines > 0','entry.AmmoState.LauncherMuzzles > 0').replace('local.LauncherMagazines <= 0','local.LauncherMuzzles <= 0');p.write_text(s)
p=D/'ERR_InfantryAmmoTransfer.c';s=p.read_text();s=s.replace(''' // Projectile-only/disposable and entirely missing magazine configurations
// are outside this finite magazine slice; never synthesize a projectile.
BaseMagazineComponent loaded = muzzle.GetMagazine();
if (!loaded || !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;''', ''' // 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;
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;
}''');p.write_text(s)
for name in ('test_infantry_ammo_transfer.py','test_infantry_resupply_planning.py'):
p=P/'tests'/name;s=p.read_text().replace('LauncherMagazines=1,','LauncherMuzzles=1,LauncherMagazines=1,')
if name=='test_infantry_ammo_transfer.py':
s=s.replace('struct Muzzle{Magazine*', 'struct Muzzle{bool disposable=false;bool IsDisposable(){return disposable;}Magazine*')
s=s.replace('if(bad==2)f.muzzle.loaded=nullptr;', 'if(bad==2)f.muzzle.disposable=true;')
s=s.replace('int main(){', '''int main(){
{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());}
''',1)
else:s=s.replace('if(bad==1)f.ammo.LauncherMagazines=0;', 'if(bad==1)f.ammo.LauncherMuzzles=0;').replace('f.ammo.SmallArmsRounds=999;assert(!f.Plan());', 'f.ammo.SmallArmsRounds=999;f.ammo.LauncherMagazines=0;assert(!f.Plan());')
p.write_text(s)
name='test_hq_ammo_sampler.py';s=(R/'tests'/name).read_text();(P/'baseline/tests'/name).write_text(s)
s=s.replace('struct Muzzle{Well*', 'struct Muzzle{bool disposable=false;bool IsDisposable(){return disposable;}Well*')
s=s.replace('assert(mixed.LauncherMagazines==1&&mixed.KnownSmallArmsEmpty());', '''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.magazine=&unrelated;inv.items={&loadedEntity,&spareEntity,&unrelatedEntity};''')
# names above depend on existing exact fixture names; use original variable spelling.
print([l for l in s.splitlines() if 'Entity actor' in l or 'Entity loaded' in l]);(P/'tests'/name).write_text(s)
@@ -0,0 +1,14 @@
[
{
"control": "old-compatible",
"returncode": 1,
"runtime_assertion_failure": true,
"compilation_error": false
},
{
"control": "old-hq",
"returncode": 1,
"runtime_assertion_failure": true,
"compilation_error": false
}
]
@@ -0,0 +1,42 @@
from pathlib import Path
p=Path('/tmp/erri-launcher-resupply-1140');f=p/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c';s=f.read_text();(p/'before-refusal.c').write_text(s)
s=s.replace(' protected BaseInventoryStorageComponent m_Storage;', ' protected BaseInventoryStorageComponent m_Storage;\n protected InventoryStorageSlot m_OriginalSlot;\n protected BaseInventoryStorageComponent m_OriginalStorage;')
s=s.replace(' protected bool ReturnProof()',''' // 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()''')
s=s.replace('return m_Magazine && !PhysicalProof();', 'return m_Magazine && !PhysicalProof() && !RefusedAtSource();')
s=s.replace(' m_Callback = new ERR_InfantryAmmoCallback(); m_Callback.Transfer = this;', ''' 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;''');f.write_text(s)
f=p/'tests/test_infantry_ammo_transfer.py';s=f.read_text();(p/'before-refusal-test.py').write_text(s)
s=s.replace("'m_Storage','m_Callback'", "'m_Storage','m_OriginalSlot','m_Callback'")
s=s.replace('bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageManagerComponent|', 'bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageSlot|InventoryStorageManagerComponent|')
s=s.replace('fromSlot.storage=&sourceStorage;', 'fromSlot.storage=&sourceStorage;fromSlot.attached=&magEntity;')
s=s.replace('int main(){', '''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());}
''',1);f.write_text(s)
@@ -0,0 +1,20 @@
from pathlib import Path
P=Path('/tmp/erri-launcher-resupply-1140');R=Path('/home/jakub/devel/escapistsrereintroduced');d=P/'addons/core/Scripts/Game/ERR/10_AI'
p=d/'ERR_InfantryAmmoTransfer.c';s=p.read_text().replace('if (muzzle.IsDisposable()) continue;', '''if (muzzle.IsDisposable()) continue;
SCR_MuzzleInMagComponent reloadable = SCR_MuzzleInMagComponent.Cast(muzzle);
if (reloadable && !reloadable.CanBeReloaded()) continue;''');p.write_text(s)
p=d/'ERR_HQAmmoSampler.c';s=p.read_text().replace('if (muzzle.GetMagazineWell() && !muzzle.IsDisposable()) result.LauncherMuzzles++;', '''SCR_MuzzleInMagComponent reloadable = SCR_MuzzleInMagComponent.Cast(muzzle);
bool supported = muzzle.GetMagazineWell() && !muzzle.IsDisposable();
if (reloadable && !reloadable.CanBeReloaded()) supported = false;
if (supported) result.LauncherMuzzles++;''');p.write_text(s)
for n in ['test_infantry_ammo_transfer.py','test_hq_ammo_sampler.py']:
p=P/'tests'/n;s=p.read_text().replace("'magazine','loaded','loadedItem'", "'magazine','reloadable','loaded','loadedItem'").replace("'weapon','muzzle','magazine','actor'", "'weapon','muzzle','reloadable','magazine','actor'")
# Translate only the native cast boundary.
marker="code=code.replace('array<'" if n=='test_hq_ammo_sampler.py' else "code=code.replace('ref ERR_InfantryAmmoCallback'"
pos=s.index(marker);s=s[:pos]+"code=code.replace('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : nullptr)')\n"+s[pos:]
s=s.replace('using BaseMuzzleComponent=Muzzle*;', 'using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;')
s=s.replace('bool disposable=false;bool IsDisposable()', 'bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable()')
if n=='test_infantry_ammo_transfer.py':
s=s.replace('bad<7;', 'bad<8;').replace('if(bad==6)f.empty.capacity=0;', 'if(bad==6)f.empty.capacity=0;if(bad==7){f.muzzle.inMag=true;f.muzzle.canReload=false;}')
p.write_text(s)
n='test_hq_ammo_reporting.py';s=(R/'tests'/n).read_text();(P/'baseline/tests'/n).write_text(s);s=s.replace('LauncherRounds=', 'LauncherMuzzles=0,LauncherRounds=',1);(P/'tests'/n).write_text(s)
@@ -0,0 +1,13 @@
import os,subprocess,json
from pathlib import Path
p=Path('/tmp/erri-launcher-resupply-1140');root=Path('/home/jakub/devel/escapistsrereintroduced');env=os.environ.copy();env.update(PYTHONDONTWRITEBYTECODE='1',PYTHONPATH=str(p/'tests')+':'+str(root/'tests'))
for key,name in [('ERR_INFANTRY_TRANSFER_SOURCE','ERR_InfantryAmmoTransfer.c'),('ERR_INFANTRY_RESUPPLY_SOURCE','ERR_InfantryResupply.c'),('ERR_HQ_SOURCE','ERR_HQ.c'),('ERR_AMMO_SOURCE','ERR_HQAmmoSampler.c')]:env[key]=str(p/'addons/core/Scripts/Game/ERR/10_AI'/name)
results={}
for name in ['test_hq_ammo_sampler.py','test_infantry_ammo_transfer.py','test_infantry_resupply_planning.py','test_infantry_resupply_coordinator.py','test_mounted_supply_integration.py','test_hq_multiconvoy_supply.py','test_hq_ammo_reporting.py','test_hq_watch.py','test_infantry_mine_coordinator.py','test_infantry_supply_pool.py','test_hq_status_sampler.py']:
f=p/'tests'/name
if not f.exists():f=root/'tests'/name
r=subprocess.run(['python3',str(f)],env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
(p/('final-'+name+'.log')).write_text(r.stdout);results[name]=r.returncode;print(name,r.returncode,flush=True)
if r.returncode:print(r.stdout[-5000:])
(p/'final-results.json').write_text(json.dumps(results,indent=2)+'\n')
assert not any(results.values())
@@ -0,0 +1,4 @@
{
"/tmp/erri-launcher-sdk-review/review.json": "e250680fc7258da65b9cf22bec2776130c23ef3c8d599c1ed81ae07debdbd77b",
"/tmp/erri-launcher-sdk-review/files.json": "f1c411d434b04a19c7a8fb5c8bafec1a67a24c1cf5c602710b24e88c8f9be152"
}
@@ -0,0 +1,242 @@
[
{
"path": "Prefabs/Props/Military/Posters/PosterMilitary_USSR_01_RPG7_V1.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "7a83c7ccad975e2f8f88e4c950b34bc94b915a9d7b56afc83d4e7d390381d3c6"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Grenade_HEDP_M433.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "9f844c35ec648de2deecc9caa31f2c76d0d103ff5415bbeed1589216b9d44197"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Grenade_HEDP_M433_Penetrator.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "a980aa10985198f436dfdc5634f152006e72e7abe726d3c2d12c85af60039065"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Grenade_HEDP_M433_Spall.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "c305528da65ce2116711d29721a8adeda4da68e679d992e7a48f5678869ae1d7"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Grenade_HE_VOG25.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "f8d15fecd55338ef0bdbf1ac431b5cfc5fc97e0ea725c0a5865224eb521ec381"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VL.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "8cd7efec0e969a8a475249d8c9611bfacc4098d6ba3a83d8da01eba7e6132c32"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VL_Penetrator.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "ce50ffe77cf669a085216e908bb7925395a1e5cd1e6792ee4b3885a62cb3b6bc"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VL_Spall.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "d21ac8de32696747ed928c5ec7d4259c27abd3e6f2838481c05fa6cd09769386"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VM.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "de5d58fb7ea86233c991d8127401db301c15c08f67053116d6c497d1c78d4d15"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VM_Penetrator.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "f1c117a733311d33c15f1f75e488817b4abdc5b31ca65ab050543e14f866e496"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VM_Spall.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "4c0975fac8387adad4188b0138c02277c0f8efa89eadb62974cb5c6698a8e1e3"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VR.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "a920dbbe8bdecda239dcb4c9b51a1db612dc85d778e06bc51877ca55b7908792"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VR_Penetrator.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "97ac544c60327103b817dd123bc1959876a371a727290e5e430db3913b220344"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VR_Spall.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "0a0e024ff30df03264a7b56b16b9cfa14e0e1baebde7d8dd19be8cf33c7e4a07"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_RPG75.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "994840ab40805787f845daedf7e4374b7bf31e99db2cb8bc3bff53967ab5c0f3"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_RPG75_Penetrator.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "636a7afd57ebbdab54401b34171626265f9123f913f7d12fe67e4a80bf83c61c"
},
{
"path": "Prefabs/Weapons/Ammo/Ammo_Rocket_RPG75_Spall.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "4c0975fac8387adad4188b0138c02277c0f8efa89eadb62974cb5c6698a8e1e3"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_GP25.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "583085be9199a5400a22b8c8a00d6a4a551f61cca0dfe4ae045f30685e4c9869"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "8b8a5a23142ac2289f49ae74e866070f7557e299341d1eda47d322c69cdf6e16"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_long.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "85e3da02143360866b113eab303c3170f860e1db623054bcdecf7e8c6a7b3493"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_long_OliveGreen_Sand_Stripes.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "3eaffd9dce72d38762d1469ddbdf52fb13ad64bd6a0941b6d8f867f81890c1ac"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_long_OliveGreen_Solid.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "661eab68d3a80e0b7c00a369ab67ddee0c3d66171f63b81d233e4a22fcea4bf3"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_short.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "ca801708c4c9bb40228568207f4d15c3639cde5c03c43c88592752e3ef552cb9"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_short_OliveGreen_Sand_Stripes.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "1025526f51c01db81f504aec72bda45aad37db9d5dcc12b3c5baa43fcbb16575"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_short_OliveGreen_Solid.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "c56589d45387631eeff5da0ead2e9626ae462ff38c3ae76b02f196d4f9b4fd4f"
},
{
"path": "Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_short_Tutorial.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "8ef35b98f3d80925913f99fd4a8969253455af32221256bfa1e07341e0aabae5"
},
{
"path": "Prefabs/Weapons/Core/Ammo_GrenadeLauncher_Base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "74e7fea5f594e699734da2079456e553c0240820dde216fd78e4dab42f30b234"
},
{
"path": "Prefabs/Weapons/Core/Ammo_Rocket_Base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "12609566a239dce9f7419f58f40f9e9a0288aaba183e4515e4cae003b09bcba3"
},
{
"path": "Prefabs/Weapons/Core/Launcher_Base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "e0f9516cc84110973b4b4db04e884405cfc456edbfcd24eeec8aad1d754efba1"
},
{
"path": "Prefabs/Weapons/Core/Magazine_Base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "21807d11d5a91733f91eac8e507f36fe72caa3979355020768014a81745ad383"
},
{
"path": "Prefabs/Weapons/Core/UGL_Base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "0a66f3ea781dbfc6f20acf14206f323b2c4403201586d4b2dca408dde4b214ec"
},
{
"path": "Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "ffbf0767dd922cbdf93255dac46f6a4baca6b62a3bc5ed691363b8caea64e0f0"
},
{
"path": "Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7_base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "cb829d2032945964e418db5e6e752a1cd0c2e07777e976ef047a49a9bf06a8b6"
},
{
"path": "Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7_PGO7.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "820969cf1ccab107726af8d1b919cffbd545167134a0b4abaacfaf5054d6bee5"
},
{
"path": "Prefabs/Weapons/Launchers/RPG75/Launcher_RPG75.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "8808768c102db6254adbdf5d3efaf9383dd5f2c1dba00628ddcbe0fd933c9443"
},
{
"path": "Prefabs/Weapons/Launchers/RPG75/Launcher_RPG75_base.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "31d3a1bb7c4fb5aa45df8dcbe42a227eecfd6ea8b6ad47af32cc916d41dd36e5"
},
{
"path": "Prefabs/Weapons/Rifles/AK74/Rifle_AK74N_GP25.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "5ef939f733d030c581f271d06b5e6f2dc62b16720c36dd46aad53d7ecd040df1"
},
{
"path": "Prefabs/Weapons/Rifles/AK74/Rifle_AK74_GP25.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "2b188c394c09ab581cc5ee4912facac9ee098bfb2cab52d14cd71b0855205c2d"
},
{
"path": "Prefabs/Weapons/Rifles/AK74/Variants/Rifle_AK74N_PBS4_GP25.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "f2a6bbc25fb1a18ff2653e7f2fc231a5059c5e65a0d8d8f65d724230126d5819"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Rifle_M16A2_carbine_M203.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "05bf78cfe60f5ed93db0b6dc4be416f1fbdc1ab73ae1f6318992418ccd5e6824"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Rifle_M16A2_carbine_M203_Tutorial.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "034e88a40e20f1de2d24948f2cf8887ae8d4bb93c912b2db59d2143d954484f6"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Rifle_M16A2_M203.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "316ca68a91ff83b7be3872f5d915d37fe2ce1a26cdd422957944a1cb5c26b458"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Variants/Rifle_M16A2_carbine_M203_OliveGreen_Sand_Stripes.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "dca6b2f987eb036f0e5545a7540ea7b2db8ebaee877bcf91560624d1ad4a9765"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Variants/Rifle_M16A2_carbine_M203_OliveGreen_Solid.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "0740f8339234e439524d2715970644dea1e32609e454ba9d67aea9da83f8ae37"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Variants/Rifle_M16A2_M203_OliveGreen_Sand_Stripes.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "08a4853da85da078b2527ee5ff16065062180d3292cb2ce84500498d99daeb51"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Variants/Rifle_M16A2_M203_OliveGreen_Solid.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "382ed88099fe66118569d5d2adb4794c1f10ce705f9e07a889d613efecb77293"
},
{
"path": "Prefabs/Weapons/Rifles/M16/Variants/Rifle_M16A2_suppressor_M203_OliveGreen_Sand_Stripes.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "e0e1c3137d798a2c61ca887e7c83ab071b79a8d3e8921a99c05bfbc7fb8918f9"
},
{
"path": "Prefabs/Weapons/Warheads/Warhead_HEAT_RPG75.et",
"pak": "/home/jakub/.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak",
"sha256": "0dae3a61678ff5561f22472c440771f15946ba3eb919c8d81e4359a7d99303dc"
}
]
@@ -0,0 +1,68 @@
{
"scope": "Read-only installed vanilla source/prefab inspection, no engine launch or successor edits",
"prefab_manifest": "files.json",
"script_hashes": {
"/tmp/erri-vanilla-stock/scripts/Game/generated/Weapon/BaseMuzzleComponent.c": "a5662f439b2169db923e90b09d5cfa3505db51a06b15d2ac6429b4113490fcd1",
"/tmp/erri-vanilla-stock/scripts/Game/generated/Weapon/BaseMagazineComponent.c": "0162c6bf856e4daf19559d775ffb57b321ba494c32951923b09c1f255dca6bb6",
"/tmp/erri-vanilla-stock/scripts/Game/generated/Weapon/MuzzleInMagComponent.c": "5cc80fc54f2bbebe528eafebd1635ff843bff7435895bbe277f0c791bdd7cff1",
"/tmp/erri-vanilla-stock/scripts/Game/Components/Weapons/SCR_MuzzleInMagComponent.c": "25fdd35e07cb20d6de5f5b749e4c75511e7b62f507f3700d603f18e131b48665",
"/tmp/erri-vanilla-stock/scripts/Game/Inventory/SCR_CharacterInventoryStorageComponent.c": "0df5169d5e3ea2dbbf78066ce77a0dc827a23ccf47a00693077ecab577042190",
"/tmp/erri-vanilla-stock/scripts/Game/Inventory/SCR_InventoryStorageManagerComponent.c": "37799972155e2a460c1b9cf66d9be3a54ab7dc2fcfcc33b8d40672973c342023",
"/tmp/erri-vanilla-stock/scripts/Game/AI/Components/SCR_AICombatComponent.c": "79919aaf106d2d79caa83c948586cf6c3d284bd4a1f8db2e92179a1921f25cee",
"/tmp/erri-vanilla-stock/scripts/Game/UI/Inventory/SCR_InventorySlotUI.c": "3bcb4cbccfb5be7836c07d57bfcdcfff975a0511374fc7b1361722d346afcde5"
},
"findings": [
{
"fact": "RPG7 is a reloadable projectile-containing muzzle with authored magazine well independent of loaded item.",
"references": [
"Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7_base.et:87 SCR_MuzzleInMagComponent",
"Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7_base.et:116 MagazineWellRPG7",
"Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7.et:27 AmmoTemplate Ammo_Rocket_PG7VM.et",
"Components/Weapons/SCR_MuzzleInMagComponent.c:8 default CanBeReloaded=1"
]
},
{
"fact": "GP25 and M203 similarly author separate wells and AmmoTemplate on MuzzleInMagComponent.",
"references": [
"Prefabs/Weapons/Attachments/Underbarrel/UGL_GP25.et:123 MagazineWellRU_GP;169 Ammo_Grenade_HE_VOG25",
"Prefabs/Weapons/Attachments/Underbarrel/UGL_M203_base.et:101 MagazineWellUS_UGL;143 Ammo_Grenade_HEDP_M433"
]
},
{
"fact": "All three actual round prefabs are Projectile entities with MagazineComponent and corresponding well; RPG7 rocket base MaxAmmo1. A general rejection of Projectile entity type would reject legitimate resupply rounds.",
"references": [
"Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VM.et:34",
"Prefabs/Weapons/Core/Ammo_Rocket_Base.et:42",
"Prefabs/Weapons/Ammo/Ammo_Grenade_HE_VOG25.et:35",
"Prefabs/Weapons/Ammo/Ammo_Grenade_HEDP_M433.et:34"
]
},
{
"fact": "Supported reload and native resupply eligibility inspect muzzle well/resource without requiring GetMagazine nonnull.",
"references": [
"Inventory/SCR_CharacterInventoryStorageComponent.c:1125-1153",
"Inventory/SCR_InventoryStorageManagerComponent.c:1505-1509",
"generated/Weapon/BaseMuzzleComponent.c:40-43"
]
},
{
"fact": "Disposable weapons require exclusion: RPG75 has Disposable1 and CanBeReloaded0; native AI and resupply use respective gates.",
"references": [
"Prefabs/Weapons/Launchers/RPG75/Launcher_RPG75_base.et:169-175",
"AI/Components/SCR_AICombatComponent.c:526-528",
"Inventory/SCR_InventoryStorageManagerComponent.c:1505-1507"
]
}
],
"unproven": [
"Exact fired-round entity deletion timing versus native projectile reuse and transient GetMagazine lifetime are engine internals, not established by these scripts. No native firing claim.",
"SetAmmoCount(0) on an installed round is not proven equivalent to actual firing/chamber removal.",
"Native UI uses IsCurrentBarrelChambered for MuzzleInMag current-magazine visibility (SCR_InventorySlotUI.c:958); this supports treating empty chamber independently, not a proof of entity destruction."
],
"implementation_constraints": [
"Match existing finite stocked entity BaseMagazineComponent.GetMagazineWell to exact owned eligible muzzle well; no required empty-magazine identity for projectile-fed launcher admission.",
"Refuse IsDisposable or SCR_MuzzleInMagComponent !CanBeReloaded.",
"Never synthesize stock from default resource merely because default getter can return a projectile; validate existing physical stock identity, custody and positive ammo/capacity.",
"Do not implement empty return for fired projectile rounds based on mock retained-empty assumptions."
]
}
@@ -0,0 +1,14 @@
{
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c": "52e601a0571b4942c9d4d0ce820727df8498febd6726efbd7da0c5eff7041b58",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c": "8bb0ec1422887feaa340307d3c60bae50fc82d424fce6390876e580ff8b75e8b",
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c": "463f8a355aeac40e5ee80981b09e8561fae1b44e179facf911a0fecbfc7722fe",
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c": "b01fa948f204d49acbfbdfc5fa641b1d914e0eb91081e92a93816ae52ae88b1e",
"tests/test_hq_ammo_reporting.py": "bdd4a30440501a2861aca80c951098c48cf999023eaaabd9a2eef82174f8173d",
"tests/test_hq_ammo_sampler.py": "e9c2d147eb1547be471df7459f1be815cdc9ef0a64c9a4ae4ef58936424dbc55",
"tests/test_hq_multiconvoy_supply.py": "e58d1d6eba48b2c5c6073d54c76302b2a7250ad22176fa94c386e1bc2ce65588",
"tests/test_hq_watch.py": "51d5a728b0be83ff957c0df61289d14f90aba109b49365e457c9b200d5ede36a",
"tests/test_infantry_ammo_transfer.py": "7929033fe661de87f9ca72bc33d64fe996ac8b8e3630a17b4210a6f498b2930f",
"tests/test_infantry_resupply_coordinator.py": "f9cf7d583a25a12cec9f5f80539b2590449560331f4ef34e33ea2db283c5d54a",
"tests/test_infantry_resupply_planning.py": "9929ee0f5a3272c348ee329355b6ba67fdcb745fc9ce1f377f6c989eae91bf9f",
"tests/test_mounted_supply_integration.py": "f52acf7a16c9d5273894eb5a4afe7657a7c97de2ed72ba06671d25e163eea5d7"
}
@@ -0,0 +1,45 @@
from pathlib import Path
P=Path('/tmp/erri-launcher-resupply-1140');d=P/'addons/core/Scripts/Game/ERR/10_AI'
p=d/'ERR_InfantryAmmoTransfer.c';s=p.read_text().replace('if (!StoredUnder(loaded.GetOwner(), rifle)) 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;''');p.write_text(s)
p=d/'ERR_InfantryResupply.c';s=p.read_text().replace(' else foreach (IEntity magazine : m_Stock)', ' else if (launcherTrip) available = LauncherStock(recipient);\n else foreach (IEntity magazine : m_Stock)')
s=s.replace(' bool TryPlanLauncher(',''' // 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 > 128) 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(''');p.write_text(s)
p=P/'tests/test_infantry_ammo_transfer.py';s=p.read_text().replace("'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':", "'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',\n 'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':")
s=s.replace("'magazine','loaded','manager'", "'magazine','loaded','loadedItem','loadedSlot','manager'")
s=s.replace('using InventoryStorageManagerComponent=Inventory*;', 'using InventoryStorageSlot=Slot*;using InventoryStorageManagerComponent=Inventory*;')
s=s.replace('struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;', 'struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}')
s=s.replace('struct LauncherFixture:SupplyFixture{Entity loaded;Magazine empty;', 'struct LauncherFixture:SupplyFixture{Entity loaded;Magazine empty;Item loadedItem;Slot loadedSlot;')
s=s.replace('empty.rounds=0;muzzle.loaded=&empty;', 'empty.rounds=0;muzzle.loaded=&empty;loaded.item=&loadedItem;loadedItem.slot=&loadedSlot;loadedSlot.container=&muzzle;loadedSlot.attached=&loaded;')
s=s.replace('if(bad==4)f.loaded.parent=nullptr;', 'if(bad==4)f.loadedSlot.attached=nullptr;')
p.write_text(s)
p=P/'tests/test_infantry_resupply_planning.py';s=p.read_text().replace("'bool TryPlanLauncher(',", "'protected bool LauncherStock(', 'bool TryPlanLauncher(',")
s=s.replace("'ERR_InfantryMineTransfer','ERR_ConvoyRules'", "'ERR_InfantryMineTransfer','ERR_InfantryAmmoTransfer','ERR_ConvoyRules'")
s=s.replace("s=s.replace('bool available;'", "s=s.replace('int checks;', 'int checks=0;')\n s=s.replace('bool available;'")
s=s.replace('struct Service{', 'struct ERR_InfantryAmmoTransfer{static inline bool compatible=true;static bool Compatible(IEntity actor,IEntity item,bool launcher){return actor&&item&&launcher&&compatible;}};\nstruct Service{')
s=s.replace('int main(){', '''int main(){
{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;}
''')
p.write_text(s)
@@ -0,0 +1 @@
PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures requested check-ins and non-urgent detached carried-mine status
@@ -0,0 +1,23 @@
/tmp/err-policy-h70ccic1/check.cpp: In static member function ‘static void ERR_HQAmmoSampler::Inspect(IEntity, SCR_AIGroup, ERR_HQAmmoSnapshot*)’:
/tmp/err-policy-h70ccic1/check.cpp:178:70: error: ‘nullptrptr’ was not declared in this scope; did you mean ‘nullptr_t’?
178 | SCR_MuzzleInMagComponent reloadable = (muzzle->inMag ? muzzle : nullptrptr);
| ^~~~~~~~~~
| nullptr_t
Traceback (most recent call last):
File "/tmp/erri-launcher-resupply-1140/tests/test_hq_ammo_sampler.py", line 24, in <module>
run(r'''
~~~^^^^^
#include <cassert>
^^^^^^^^^^^^^^^^^^
...<140 lines>...
}
^
''')
^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-h70ccic1/check.cpp', '-o', '/tmp/err-policy-h70ccic1/check']' returned non-zero exit status 1.
@@ -0,0 +1 @@
PASS: actual HQ rotates finite donors, skips unavailable/contacted trucks, uses delivered reports and dispatches at most one trip per planning cadence
@@ -0,0 +1 @@
PASS: own initial roster, explicit retained corpse evidence, unconscious/missing/ref transfer guards, fixed strength, no replacement adoption, detached samples, surviving radio sender, immutable convoy root/ledger, mounted capture release and lost-root unknown
@@ -0,0 +1 @@
PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence, detached/validated mounted payload replacement 256-record cap and detached carried-mine radio delivery/chronology
@@ -0,0 +1,16 @@
/tmp/err-policy-98sorxmd/check.cpp: In static member function ‘static bool ERR_InfantryAmmoTransfer::Compatible(IEntity, IEntity, bool)’:
/tmp/err-policy-98sorxmd/check.cpp:181:70: error: ‘nullptrptr’ was not declared in this scope; did you mean ‘nullptr_t’?
181 | SCR_MuzzleInMagComponent reloadable = (muzzle->inMag ? muzzle : nullptrptr);
| ^~~~~~~~~~
| nullptr_t
Traceback (most recent call last):
File "/tmp/erri-launcher-resupply-1140/tests/test_infantry_ammo_transfer.py", line 326, in <module>
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-98sorxmd/check.cpp', '-o', '/tmp/err-policy-98sorxmd/check']' returned non-zero exit status 1.
@@ -0,0 +1,2 @@
PASS: actual designated zero/one original carrier selection, settled startup, shared finite claims, no refund after submission
PASS: actual HQ startup grant readiness forwarding is authoritative and fail-closed
@@ -0,0 +1,2 @@
infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed
PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership
@@ -0,0 +1 @@
PASS: actual convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup
@@ -0,0 +1 @@
PASS: actual finite convoy supply pool; immutable slots, shared demand state, sparse/capped discovery and permanent shutdown
@@ -0,0 +1 @@
PASS: actual shared mounted stock/history ownership, atomic exact HQ pair claim, and bounded stopped-owner proof polling
@@ -0,0 +1,41 @@
from pathlib import Path
P=Path('/tmp/erri-launcher-resupply-1140');R=Path('/home/jakub/devel/escapistsrereintroduced');D=P/'tests';D.mkdir(exist_ok=True)
for name in ['test_infantry_ammo_transfer.py','test_infantry_resupply_planning.py','test_infantry_resupply_coordinator.py']:
s=(R/'tests'/name).read_text();bp=P/'baseline/tests'/name;bp.parent.mkdir(parents=True,exist_ok=True);bp.write_text(s)
if name=='test_infantry_ammo_transfer.py':
s=s.replace("'magazine','manager','weapon','muzzle'","'magazine','loaded','manager','weapon','muzzle'")
s=s.replace('struct Magazine{Well*well=', 'struct Magazine{IEntity owner=nullptr;IEntity GetOwner(){return owner;}Well*well=')
s=s.replace('struct Muzzle{Well*well=', 'struct Muzzle{Magazine*loaded=nullptr;Magazine*GetMagazine(){return loaded;}Well*well=')
s=s.replace('int SmallArmsMuzzles=1,', 'int Members=1,LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;bool Valid(int n){return n==Members;}int SmallArmsMuzzles=1,')
s=s.replace('struct Coordinator{ERR_GameMode*', 'struct Coordinator{bool m_LauncherTrip=false;ERR_GameMode*')
s=s.replace('int main(){', '''struct LauncherFixture:SupplyFixture{Entity loaded;Magazine empty;
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;}
};
int main(){
{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<7;bad++){LauncherFixture f;if(bad==0)f.magazine.used=true;if(bad==1)f.muzzle.well=nullptr;if(bad==2)f.muzzle.loaded=nullptr;if(bad==3)f.empty.well=nullptr;if(bad==4)f.loaded.parent=nullptr;if(bad==5)f.rifle.parent=nullptr;if(bad==6)f.empty.capacity=0;
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);}
''')
elif name=='test_infantry_resupply_planning.py':
s=s.replace("'static bool MinePair(')","'static bool MinePair(', 'static bool LauncherNeed(', 'static bool LauncherPair(')")
s=s.replace("'bool TryPlanMines(',", "'bool TryPlanLauncher(', 'bool TryPlanMines(',")
s=s.replace('struct Ammo{', 'struct Ammo{bool LauncherComplete=true;int LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;')
s=s.replace('bool m_MineTrip=false,','bool m_LauncherTrip=false,m_MineTrip=false,')
s=s.replace('int main(){','''int main(){
{Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;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.LauncherMagazines=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);}
''')
else:
s=s.replace('m_MineTrip=false','m_MineTrip=false,m_LauncherTrip=false')
s=s.replace('int main(){','''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);}
''')
(D/name).write_text(s)
@@ -0,0 +1 @@
PASS: actual bounded empty-return and finite delivery; phase-isolated callbacks, exact ownership proof, stop quarantine and original actor/vehicle boundaries