Fix faction ammunition resupply and settings recovery
Wait for both convoy cargo phases and use actual magazine stock. Resolve empty primary weapon ammunition from muzzle metadata and recover cleanly from failed initial settings saves. Update native fixtures, scoped cleanup guards, packaging evidence and validation documentation. Make reviewed fixture tests checkout-relative. All repository checks pass against the isolated staged tree; native resupply completion and multiplayer qualification remain outstanding.
This commit is contained in:
@@ -193,10 +193,12 @@ class ERR_Settings
|
||||
if (checked.Get(option) != expected) return false;
|
||||
}
|
||||
map<string, int> previous;
|
||||
if (Read(m_Path, previous) && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
|
||||
bool hadPrevious = Read(m_Path, previous);
|
||||
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked))
|
||||
{
|
||||
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
|
||||
return false;
|
||||
}
|
||||
foreach (string option : Keys())
|
||||
@@ -206,6 +208,7 @@ class ERR_Settings
|
||||
if (checked.Get(option) != expected)
|
||||
{
|
||||
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,19 @@ class ERR_AirAmmoMission
|
||||
weapon.GetMuzzlesList(muzzles);
|
||||
foreach (BaseMuzzleComponent muzzle : muzzles)
|
||||
{
|
||||
if (!muzzle || muzzle.GetMuzzleType() != EMuzzleType.MT_BaseMuzzle || !muzzle.GetMagazineWell()) continue;
|
||||
BaseMagazineComponent magazine = muzzle.GetMagazine();
|
||||
if (magazine && magazine.GetOwner().GetPrefabData()) return magazine.GetOwner().GetPrefabData().GetPrefabName();
|
||||
IEntity owner = null;
|
||||
if (magazine) owner = magazine.GetOwner();
|
||||
if (owner && owner.GetPrefabData())
|
||||
{
|
||||
ResourceName loaded = owner.GetPrefabData().GetPrefabName();
|
||||
if (!loaded.IsEmpty()) return loaded;
|
||||
}
|
||||
// A depleted soldier may have removed the last magazine. The carried
|
||||
// weapon still identifies its ammunition; an auxiliary launcher does not.
|
||||
ResourceName fallback = muzzle.GetDefaultMagazineOrProjectileName();
|
||||
if (!fallback.IsEmpty()) return fallback;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
|
||||
@@ -48,6 +48,7 @@ class ERR_InfantryResupply
|
||||
protected int m_Delivered;
|
||||
protected int m_TripDelivered;
|
||||
protected float m_Deadline;
|
||||
protected float m_StockDeadline = -1;
|
||||
protected float m_HeldAt;
|
||||
protected float m_NextTick;
|
||||
protected bool m_Stopped;
|
||||
@@ -124,16 +125,23 @@ class ERR_InfantryResupply
|
||||
if (!m_Convoy || !m_Convoy.Ready || m_Convoy.Vehicles.Count() != 6) return;
|
||||
m_Truck = m_Convoy.Vehicles[1];
|
||||
if (!TruckValid()) { Stop(); return; }
|
||||
// Cargo has a later, separately bounded lifecycle than native crew admission.
|
||||
// Supply convoys first request faction weapons, then their matching magazines.
|
||||
if (m_StockDeadline < 0) m_StockDeadline = now + 90000;
|
||||
if (now >= m_StockDeadline) { Stop(); return; }
|
||||
if (!m_Convoy.CargoIssued) return;
|
||||
ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);
|
||||
if (supply && !supply.MagazinesIssued) return;
|
||||
foreach (ERR_ConvoyCargoCallback operation : m_Convoy.Cargo)
|
||||
if (!operation.Finished) { if (now >= m_Convoy.Deadline) Stop(); return; }
|
||||
if (!operation.Finished) return;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Truck.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { Stop(); return; }
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
if (items.Count() > 128) { Stop(); return; }
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.Magazine()) continue;
|
||||
if (m_Stock.Count() >= 48) { Stop(); return; }
|
||||
if (!item || !item.FindComponent(BaseMagazineComponent)) continue;
|
||||
if (m_Stock.Count() >= 96) { Stop(); return; }
|
||||
m_Stock.Insert(item);
|
||||
}
|
||||
m_State = 1; Audit("truck magazine identities captured once after startup; absent/looted stock is never replaced");
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_InfantryResupply
|
||||
{
|
||||
// Read-only fixture view of the coordinator's one-time stock snapshot.
|
||||
bool ERR_InfantryFixtureStock(IEntity magazine)
|
||||
{
|
||||
return ERR_NativeInfantryTransferProbe.Enabled() && !m_Stopped && m_State == 1 && magazine
|
||||
&& m_Stock.Contains(magazine) && !m_Spent.Contains(magazine);
|
||||
}
|
||||
}
|
||||
// Controlled native inventory boundary only: one existing actor is positioned
|
||||
// beside the existing convoy truck. This does not prove HQ rendezvous/navigation.
|
||||
modded class ERR_HQ
|
||||
@@ -65,9 +74,50 @@ class ERR_NativeInfantryTransferProbe
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
if (items.Count() > 128) return -1;
|
||||
int count;
|
||||
foreach (IEntity item : items) if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_ConvoyResources.Magazine()) count++;
|
||||
foreach (IEntity item : items) if (item && BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))) count++;
|
||||
return count;
|
||||
}
|
||||
// -1 invalid producer, 0 pending startup, 1 finite completed requests.
|
||||
protected int CargoReady(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped) return -1;
|
||||
if (!task.Ready) return 0;
|
||||
if (task.Vehicles.Count() != 6 || task.Cargo.Count() > 104) return -1;
|
||||
if (!task.CargoIssued) return 0;
|
||||
ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(task);
|
||||
if (supply && !supply.MagazinesIssued) return 0;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo)
|
||||
{
|
||||
if (!operation) return -1;
|
||||
if (!operation.Finished) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
protected bool SelectMagazine(ERR_HQ hq, array<IEntity> stock)
|
||||
{
|
||||
if (!hq || !hq.Resupply() || stock.Count() > 128) return false;
|
||||
foreach (IEntity candidate : stock)
|
||||
{
|
||||
if (!candidate || !BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent))) continue;
|
||||
if (!hq.Resupply().ERR_InfantryFixtureStock(candidate)) continue;
|
||||
m_Unit = hq.ERR_InfantryFixtureRecipient(candidate);
|
||||
if (!m_Unit) continue;
|
||||
for (int slot = 0; slot < m_Unit.StatusSampler.InitialStrength(); slot++)
|
||||
{
|
||||
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
|
||||
if (!actor || !m_Unit.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)
|
||||
|| !ERR_InfantryAmmoTransfer.Compatible(actor, candidate)) continue;
|
||||
InventoryStorageManagerComponent recipient = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!recipient || !recipient.FindStorageForItem(candidate, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
|
||||
m_Actor = actor; m_Magazine = candidate; return true;
|
||||
}
|
||||
// A changed recipient must not retain this fixture's reservation while the
|
||||
// remaining original magazines are checked for a compatible original actor.
|
||||
hq.ReleaseLogistics(m_Unit.Group, TOKEN);
|
||||
m_Unit = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool Has(IEntity owner, IEntity item)
|
||||
{
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
|
||||
@@ -157,33 +207,18 @@ class ERR_NativeInfantryTransferProbe
|
||||
if (m_State == 0)
|
||||
{
|
||||
ERR_ConvoyTask task = hq.ConvoyTask();
|
||||
if (!task || !task.Ready) return 0;
|
||||
if (!Check(!task.Stopped && task.Vehicles.Count() == 6 && task.CargoIssued && task.Cargo.Count() == 56,
|
||||
"existing automatic convoy and finite startup cargo are ready")) return -1;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo) if (!operation || !operation.Finished) return 0;
|
||||
if (!task) return 0;
|
||||
int cargo = CargoReady(task);
|
||||
if (cargo == 0) return 0;
|
||||
if (!Check(cargo == 1, "existing automatic convoy and bounded startup cargo completed both producer phases")) return -1;
|
||||
if (!hq.Resupply() || hq.Resupply().State() != 1) return 0;
|
||||
m_Truck = task.Vehicles[1];
|
||||
if (!Check(m_Truck.CrewValid(m_Mode) && m_Truck.SupplyToken == 0 && !m_Truck.Combat.Held,
|
||||
"original ammo truck is available for isolated inventory fixture")) return -1;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Truck.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!Check(inventory != null, "original truck inventory remains available")) return -1;
|
||||
array<IEntity> stock = {}; ERR_Inventory.Items(inventory, stock);
|
||||
foreach (IEntity candidate : stock)
|
||||
{
|
||||
if (!candidate || !candidate.GetPrefabData() || candidate.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.Magazine()) continue;
|
||||
m_Unit = hq.ERR_InfantryFixtureRecipient(candidate);
|
||||
if (!m_Unit) break;
|
||||
m_Magazine = candidate;
|
||||
for (int slot = 0; slot < m_Unit.StatusSampler.InitialStrength(); slot++)
|
||||
{
|
||||
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
|
||||
if (!actor || !m_Unit.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)
|
||||
|| !ERR_InfantryAmmoTransfer.Compatible(actor, candidate)) continue;
|
||||
InventoryStorageManagerComponent recipient = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!recipient || !recipient.FindStorageForItem(candidate, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
|
||||
m_Actor = actor; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!Check(m_Unit && m_Actor && m_Magazine, "existing original patrol actor and compatible original truck magazine selected")) return -1;
|
||||
if (!Check(SelectMagazine(hq, stock), "existing original patrol actor and compatible captured truck magazine selected")) return -1;
|
||||
m_OriginalLedger = m_Mode.Forces().Count();
|
||||
m_Truck.SupplyToken = TOKEN; m_Truck.SupplyHeld = true; m_Truck.ClearOrder();
|
||||
m_State = 1; return 0;
|
||||
|
||||
@@ -14,6 +14,7 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRSmokeDone;
|
||||
protected bool m_ERRSpawnPreloadRequested;
|
||||
protected bool m_ERRSpawnPreloadUIObserved;
|
||||
protected bool m_ERRTestEscapeRoute;
|
||||
protected bool m_ERRTestLostHelicopter;
|
||||
protected bool m_ERRTestPlayerDeath;
|
||||
protected bool m_ERRTestRadioNetwork;
|
||||
@@ -73,6 +74,7 @@ modded class ERR_GameMode
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmoke", enabled) || enabled != "1") return;
|
||||
string outcome;
|
||||
m_ERRTestLostHelicopter = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "lost-helicopter";
|
||||
m_ERRTestEscapeRoute = outcome == "escape-route";
|
||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestHQAwareness = outcome == "hq-awareness";
|
||||
@@ -96,6 +98,9 @@ modded class ERR_GameMode
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy || m_ERRTestInfantryTransfer;
|
||||
if (m_ERRTestLocalHandoff) m_ERRTestPlayerDeath = true;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
// Full-route fixtures include menu checks, two bounded intel batches and
|
||||
// a fresh 30-second defence after interruption. Keep one fixed total cap.
|
||||
if (outcome == "" || outcome == "victory" || m_ERRTestLostHelicopter) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 300000;
|
||||
if (m_ERRTestLocalHandoff) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
|
||||
if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;
|
||||
if (outcome == "supply-convoys") m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000;
|
||||
@@ -115,6 +120,17 @@ modded class ERR_GameMode
|
||||
Print("ERR SMOKE: scripted integration test enabled; gameplay acceptance remains separate");
|
||||
}
|
||||
|
||||
// The route-only diagnostic keeps actual clue collection and radio/extraction
|
||||
// checks, while the existing victory fixture retains administrator stress.
|
||||
protected bool ERR_CheckRouteExtras(float now)
|
||||
{
|
||||
if (m_ERRTestEscapeRoute) return true;
|
||||
int adminMenu = ERR_ProbeAdminMenu(now);
|
||||
if (adminMenu < 0) return ERR_Check(false, "integrated admin menu");
|
||||
if (adminMenu == 0) return false;
|
||||
return ERR_CheckIntelReroll(now);
|
||||
}
|
||||
|
||||
// Ordinary victory fixture additionally exercises the actual administrator
|
||||
// reroll against native enemy inventories. Scripted, not remote UI evidence.
|
||||
protected bool ERR_CheckIntelReroll(float now)
|
||||
@@ -602,6 +618,7 @@ modded class ERR_GameMode
|
||||
if (m_ERRTestInfantryTransfer)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "infantry transfer fixture opens prison before waiting for normal mission AI")) return;
|
||||
m_ERRInfantryTransfer = new ERR_NativeInfantryTransferProbe(this);
|
||||
OnStop().Insert(m_ERRInfantryTransfer.Stop);
|
||||
m_ERRSmokeStep = 108; return;
|
||||
@@ -718,10 +735,7 @@ modded class ERR_GameMode
|
||||
}
|
||||
else if (m_ERRSmokeStep == 3)
|
||||
{
|
||||
int adminMenu = ERR_ProbeAdminMenu(now);
|
||||
if (adminMenu < 0) { ERR_Check(false, "integrated admin menu"); return; }
|
||||
if (adminMenu == 0) return;
|
||||
if (!ERR_CheckIntelReroll(now)) return;
|
||||
if (!ERR_CheckRouteExtras(now)) return;
|
||||
if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2, "carried intel identifies radio through intel service")) return;
|
||||
if (!ERR_Check(!TuneRadio(m_ERRSmokePlayer, m_ERRSmokeRadio), "radio rejects distant player")) return;
|
||||
ERR_MovePlayer(m_ERRSmokeRadio.Position() + "2 0 0");
|
||||
|
||||
Reference in New Issue
Block a user