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

482 lines
40 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c
@@ -154,6 +154,7 @@
}
class ERR_EnemyMineGrant
{
+ protected bool m_Designated;
protected bool m_Rolled;
protected bool m_Finished;
protected int m_Checks;
@@ -161,7 +162,7 @@
protected ref ERR_EnemyEquipmentCallback m_Operation;
// A terminal grant may still retain an accepted native insertion after timeout.
// Resupply cannot race that callback or restart the initial allowance.
- bool ReadyForResupply() { return m_Finished && (!m_Operation || m_Operation.Finished); }
+ bool ReadyForResupply(bool replacement = false) { return m_Finished && (!m_Operation || m_Operation.Finished) && (!replacement || m_Designated); }
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
{
if (m_Finished) return;
@@ -170,6 +171,7 @@
{
m_Rolled = true;
if (!ERR_EnemyLoadoutRules.Selected(mode.Settings().Runtime("antiPersonnelMineChancePercent"), Math.RandomInt(0, 100))) { m_Finished = true; return; }
+ m_Designated = true; // Original startup selection persists; it never grants another startup allowance.
m_Remaining = 2;
}
if (++m_Checks > 10) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "startup deadline; pending native operation will not be duplicated"); return; }
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c
@@ -141,12 +141,12 @@
}
foreach (ERR_EnemyEquipmentEntry entry : m_Entries) if (entry.Group == group) entry.Stop(m_Mode);
}
- bool MineSupplyReady(SCR_AIGroup group, IEntity actor)
+ bool MineSupplyReady(SCR_AIGroup group, IEntity actor, bool replacement = false)
{
if (m_Stopped || m_Entries.Count() > 2048 || !ERR_EnemyEquipmentRules.Owned(m_Mode, group, actor)) return false;
foreach (ERR_EnemyEquipmentEntry entry : m_Entries)
if (entry && entry.Actor == actor && entry.Group == group)
- return !entry.Released && entry.Mines && entry.Mines.ReadyForResupply();
+ return !entry.Released && entry.Mines && entry.Mines.ReadyForResupply(replacement);
return false;
}
protected bool Known(IEntity actor)
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -347,9 +347,9 @@
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.LogisticsReservation;
return 0;
}
- bool MineSupplyReady(SCR_AIGroup group, IEntity actor)
- {
- return Replication.IsServer() && MissionActive() && m_Equipment && m_Equipment.MineSupplyReady(group, actor);
+ bool MineSupplyReady(SCR_AIGroup group, IEntity actor, bool replacement = false)
+ {
+ return Replication.IsServer() && MissionActive() && m_Equipment && m_Equipment.MineSupplyReady(group, actor, replacement);
}
bool ReserveLogistics(SCR_AIGroup group, int token)
{
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
@@ -19,7 +19,7 @@
return Fresh(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
&& recipient.AmmoState && recipient.AmmoState.Valid(recipient.Count)
&& recipient.AmmoState.MineComplete && recipient.AmmoState.MineMembers == recipient.Count
- && recipient.AmmoState.ReadyMines == 1 && vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
+ && recipient.AmmoState.ReadyMines <= 1 && vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
}
static bool Pair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
{
@@ -44,6 +44,7 @@
protected bool m_MineStockCaptured;
protected bool m_MineTrip;
protected int m_MineRecipientSlot = -1;
+ protected bool m_EmptyMineReplacement;
protected int m_MinesDelivered;
protected ref array<IEntity> m_Members = {};
protected ref array<IEntity> m_Served = {};
@@ -208,6 +209,19 @@
if (mineTrip)
{
if (!ERR_InfantryResupplyRules.MinePair(demand, donorStatus, now)) return false;
+ // Empty reports authorize only an original startup-designated engineer.
+ // This reads retained entitlement, never live positions/inventory for planning.
+ if (demand.AmmoState.ReadyMines == 0)
+ {
+ bool designated;
+ for (int member = 0; member < recipient.StatusSampler.InitialStrength() && member < 32; member++)
+ {
+ IEntity original = recipient.StatusSampler.InitialMember(member);
+ if (original && recipient.StatusSampler.CanAttribute(member, original)
+ && m_HQ.MineSupplyReady(recipient.Group, original, true)) { designated = true; break; }
+ }
+ if (!designated) return false;
+ }
}
else if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;
if (demand.SourceId != recipient.ForceSourceId || donorStatus.SourceId != donor.ForceSourceId) return false;
@@ -235,7 +249,7 @@
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_MineTrip = mineTrip; 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;
@@ -325,22 +339,27 @@
if (!actor || !m_Members.Contains(actor) || !m_Recipient.StatusSampler.CanAttribute(slot, actor, false)) continue;
if (!ERR_Actors.CanFight(actor) || vector.DistanceXZ(actor.GetOrigin(), m_Truck.Entity.GetOrigin()) > 4) continue;
if (!m_HQ.MineSupplyReady(m_Recipient.Group, actor)) continue;
- if (!ERR_InfantryMineTransfer.Carrier(actor, m_Recipient.Group)) continue;
+ IEntity carried;
+ int count = ERR_InfantryMineTransfer.CarriedCount(actor, m_Recipient.Group, carried);
+ if (count != 1 && (count != 0 || !m_HQ.MineSupplyReady(m_Recipient.Group, actor, true))) continue;
+ m_EmptyMineReplacement = count == 0;
m_Receiving = actor; m_MineRecipientSlot = slot; break;
}
if (!m_Receiving) return;
}
- if (!m_HQ.MineSupplyReady(m_Recipient.Group, m_Receiving)) return;
+ if (!m_HQ.MineSupplyReady(m_Recipient.Group, m_Receiving, m_EmptyMineReplacement)) return;
+ IEntity carried;
+ int count = ERR_InfantryMineTransfer.CarriedCount(m_Receiving, m_Recipient.Group, carried);
if (!m_Recipient.StatusSampler.CanAttribute(m_MineRecipientSlot, m_Receiving, false)
- || !ERR_InfantryMineTransfer.Carrier(m_Receiving, m_Recipient.Group))
- { Finish("original one-mine carrier lost or no longer has exactly one usable mine"); return; }
+ || (m_EmptyMineReplacement && count != 0) || (!m_EmptyMineReplacement && count != 1))
+ { Finish("original mine recipient lost or its pinned zero/one stock changed"); return; }
foreach (IEntity mine : m_MineStock)
{
if (!ERR_InfantryMineTransfer.ReadyMine(mine) || m_Spent.Contains(mine) || !Contains(m_Truck.Entity, mine)) continue;
ERR_FiniteMagazineReservation reservation = new ERR_FiniteMagazineReservation(m_MineStock, m_Spent, mine);
if (!reservation.Reserve()) continue;
m_MineMove = new ERR_InfantryMineTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, m_Receiving, m_Recipient.Group,
- mine, reservation, m_Recipient.StatusSampler, m_MineRecipientSlot);
+ mine, reservation, m_Recipient.StatusSampler, m_MineRecipientSlot, m_EmptyMineReplacement);
m_MineMove.Tick(now);
// The leaf commits before the one native move. Only an unsubmitted refusal
// may release its tentative claim while the same carrier approaches/retries.
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryMineTransfer.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryMineTransfer.c
@@ -7,7 +7,7 @@
protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false); }
}
-// One finite original PMN4 tops up an existing original carrier from one to two.
+// One finite original PMN4 tops up a carrier, or replaces an original designated engineer's empty stock.
// The coordinator owns radio authorization and the shared truck/recipient claim.
class ERR_InfantryMineTransfer
{
@@ -18,6 +18,7 @@
protected SCR_AIGroup m_RecipientGroup;
protected IEntity m_Mine;
protected IEntity m_CarriedMine;
+ protected bool m_EmptyReplacement;
protected IEntity m_Driver;
protected ref ERR_ForceRecord m_DonorRecord;
protected ref ERR_ForceRecord m_RecipientRecord;
@@ -38,11 +39,11 @@
protected ERR_InfantryAmmoTransferState m_State = ERR_InfantryAmmoTransferState.PENDING;
protected string m_Reason;
- void ERR_InfantryMineTransfer(ERR_GameMode mode, Vehicle donorVehicle, SCR_AIGroup donorGroup, IEntity recipientActor, SCR_AIGroup recipientGroup, IEntity exactMine, ERR_FiniteMagazineReservation reservation, ERR_HQStatusSampler originalRoster, int originalSlot)
+ void ERR_InfantryMineTransfer(ERR_GameMode mode, Vehicle donorVehicle, SCR_AIGroup donorGroup, IEntity recipientActor, SCR_AIGroup recipientGroup, IEntity exactMine, ERR_FiniteMagazineReservation reservation, ERR_HQStatusSampler originalRoster, int originalSlot, bool emptyReplacement = false)
{
m_Mode = mode; m_Donor = donorVehicle; m_DonorGroup = donorGroup;
m_Recipient = recipientActor; m_RecipientGroup = recipientGroup; m_Mine = exactMine;
- m_Reservation = reservation; m_Roster = originalRoster; m_Slot = originalSlot;
+ m_Reservation = reservation; m_Roster = originalRoster; m_Slot = originalSlot; m_EmptyReplacement = emptyReplacement;
if (!mode || !mode.Forces() || !donorVehicle || !recipientActor || !originalRoster) return;
m_DonorRecord = mode.Forces().FindGroup(donorGroup); m_RecipientRecord = mode.Forces().FindGroup(recipientGroup);
m_Strength = originalRoster.InitialStrength();
@@ -65,31 +66,38 @@
}
// Exclusion is used only after submission: the incoming item may already be
// carried, but the exact pre-existing one must still prove carrier entitlement.
- static IEntity Carrier(IEntity actor, SCR_AIGroup group, IEntity exclude = null)
- {
- if (!Replication.IsServer() || !group || group.IsSlave() || group.GetPlayerCount(true) > 0) return null;
+ static int CarriedCount(IEntity actor, SCR_AIGroup group, out IEntity found, IEntity exclude = null)
+ {
+ found = null;
+ if (!Replication.IsServer() || !group || group.IsSlave() || group.GetPlayerCount(true) > 0) return -1;
if (!ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group
- || CompartmentAccessComponent.GetVehicleIn(actor)) return null;
+ || CompartmentAccessComponent.GetVehicleIn(actor)) return -1;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
- if (!inventory || inventory.GetOwner() != actor) return null;
+ if (!inventory || inventory.GetOwner() != actor) return -1;
array<IEntity> items = {}; inventory.GetItems(items);
- if (items.Count() > 128) return null;
- array<IEntity> seen = {}; IEntity found;
+ if (items.Count() > 128) return -1;
+ array<IEntity> seen = {}; int count;
foreach (IEntity item : items)
{
- if (!item || !item.GetPrefabData()) return null;
+ if (!item || !item.GetPrefabData()) return -1;
if (seen.Contains(item)) continue;
seen.Insert(item);
if (item == exclude || item.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) continue;
InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item
- || !ERR_InfantryAmmoTransfer.StoredUnder(item, actor)) return null;
+ || !ERR_InfantryAmmoTransfer.StoredUnder(item, actor)) return -1;
if (!item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)
- || !item.FindComponent(SCR_PressureTriggerComponent) || !item.FindComponent(DamageManagerComponent)) return null;
+ || !item.FindComponent(SCR_PressureTriggerComponent) || !item.FindComponent(DamageManagerComponent)) return -1;
if (!ReadyMine(item)) continue;
- if (found) return null;
+ count++;
found = item;
}
+ return count;
+ }
+ static IEntity Carrier(IEntity actor, SCR_AIGroup group, IEntity exclude = null)
+ {
+ IEntity found;
+ if (CarriedCount(actor, group, found, exclude) != 1) return null;
return found;
}
bool Submitted() { return m_Submitted; }
@@ -177,10 +185,17 @@
if (!m_Source || !m_Destination || m_Source == m_Destination) return false;
if (m_Donor.FindComponent(InventoryStorageManagerComponent) != m_Source
|| m_Recipient.FindComponent(InventoryStorageManagerComponent) != m_Destination) return false;
- if (!m_CarriedMine || !ReadyMine(m_Mine)) return false;
+ if (!ReadyMine(m_Mine)) return false;
IEntity exclude;
if (m_Submitted) exclude = m_Mine;
- if (Carrier(m_Recipient, m_RecipientGroup, exclude) != m_CarriedMine) return false;
+ if (m_EmptyReplacement)
+ {
+ ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
+ if (!hq || !hq.MineSupplyReady(m_RecipientGroup, m_Recipient, true)) return false;
+ IEntity found;
+ if (CarriedCount(m_Recipient, m_RecipientGroup, found, exclude) != 0) return false;
+ }
+ else if (!m_CarriedMine || Carrier(m_Recipient, m_RecipientGroup, exclude) != m_CarriedMine) return false;
Physics physics = m_Donor.GetPhysics();
if (!physics) return false;
float speed = physics.GetVelocity().Length(); float angular = physics.GetAngularVelocity().Length();
--- a/tests/test_enemy_loadout.py
+++ b/tests/test_enemy_loadout.py
@@ -1,7 +1,8 @@
"""Execute production finite launcher/fallback/mine controllers with native boundaries."""
from policy_harness import ROOT, run, body
-import re
-source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'
+import re,os
+from pathlib import Path
+source=Path(os.environ.get('ERR_LOADOUT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'))
s=source.read_text();s=s[s.index('class ERR_EnemyLauncherGrant'):]
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
s=s.replace('ResourceName','string').replace('array<IEntity>','Array<IEntity*>')
@@ -61,8 +62,9 @@
assert(!ERR_EnemyLoadoutRules::Selected(-1,0)&&!ERR_EnemyLoadoutRules::Selected(101,0));
ERR_GameMode mode;Math::roll=99;
Soldier off;ERR_EnemyLauncherGrant noaa;ERR_EnemyMineGrant nomines;mode.settings.aa=mode.settings.mines=0;
- noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);
+ noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);assert(nomines.ReadyForResupply()&&!nomines.ReadyForResupply(true));
mode.settings.aa=mode.settings.mines=100;
+ nomines.Check(&mode,&off.group,&off.actor);assert(!nomines.ReadyForResupply(true)&&off.inv.requests==0);
Soldier good;ERR_EnemyLauncherGrant aa;aa.Check(&mode,&good.group,&good.actor);assert(good.inv.last=="IGLA");
IEntity igla;igla.prefab="IGLA";good.Add(igla,true);good.inv.Complete(true);aa.Check(&mode,&good.group,&good.actor);assert(aa.m_Finished&&good.inv.requests==1);
good.mgr.weapons.clear();good.inv.items.clear();for(int n=0;n<20;n++)aa.Check(&mode,&good.group,&good.actor);assert(good.inv.requests==1);
@@ -87,12 +89,14 @@
miner.inv.items.clear();for(int n=0;n<20;n++)mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);
Soldier full;ERR_EnemyMineGrant existing;full.Add(mine1,false);full.Add(mine2,false);existing.Check(&mode,&full.group,&full.actor);assert(existing.m_Finished&&full.inv.requests==0);
Soldier dropped;ERR_EnemyMineGrant finite;finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);assert(finite.m_Finished&&dropped.inv.requests==2);
- Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1);
+ Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1&&!noLoose.ReadyForResupply(true));
+ noSpace.inv.Complete(false);assert(noLoose.ReadyForResupply(true)); // Finished refusal settles uncertainty; no extra startup request.
Soldier timeoutActor;ERR_EnemyMineGrant timeoutGrant;
timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);
for(int i=0;i<12;i++)timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);
- assert(timeoutGrant.m_Finished&&!timeoutGrant.ReadyForResupply()&&timeoutActor.inv.requests==1);
- timeoutActor.inv.Complete(true);assert(timeoutGrant.ReadyForResupply());
+ assert(timeoutGrant.m_Finished&&!timeoutGrant.ReadyForResupply()&&!timeoutGrant.ReadyForResupply(true)&&timeoutActor.inv.requests==1);
+ timeoutActor.inv.Complete(true);assert(timeoutGrant.ReadyForResupply()&&timeoutGrant.ReadyForResupply(true));
+ mode.settings.mines=0;assert(timeoutGrant.ReadyForResupply(true)); // Current setting cannot confer or revoke original designation.
timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);assert(timeoutActor.inv.requests==1);
}
--- a/tests/test_enemy_equipment.py
+++ b/tests/test_enemy_equipment.py
@@ -36,7 +36,7 @@
void Check(ERR_GameMode*){checks++;}void Stop(){stops++;}
};
struct ERR_EnemyRadio{ERR_EnemyRadio(SCR_AIGroup*,int){}void Register(IEntity*){}void Check(ERR_GameMode*){}void Stop(){}};
-struct MineGrant{bool settled=false;bool ReadyForResupply(){return settled;}};
+struct MineGrant{bool settled=false,designated=false;bool ReadyForResupply(bool replacement=false){return settled&&(!replacement||designated);}};
struct ERR_EnemyEquipmentEntry{
MineGrant* Mines=nullptr;
ERR_EnemyOptics* Optics=nullptr;bool Complete=false;
@@ -53,9 +53,9 @@
assert(entry->Complete&&entry->Optics->checks==1&&combat.refreshes==1); // Stinger is already complete; optics has its own startup completion.
MineGrant grant;entry->Mines=&grant;IEntity foreign;SCR_AIGroup otherGroup;
assert(!service.MineSupplyReady(&group,&soldier));grant.settled=true;
- assert(service.MineSupplyReady(&group,&soldier));assert(!service.MineSupplyReady(&otherGroup,&soldier));assert(!service.MineSupplyReady(&group,&foreign));
+ assert(service.MineSupplyReady(&group,&soldier));assert(!service.MineSupplyReady(&group,&soldier,true));grant.designated=true;assert(service.MineSupplyReady(&group,&soldier,true));assert(!service.MineSupplyReady(&otherGroup,&soldier,true));assert(!service.MineSupplyReady(&otherGroup,&soldier));assert(!service.MineSupplyReady(&group,&foreign));
soldier.eligible=false;assert(!service.MineSupplyReady(&group,&soldier));soldier.eligible=true;
- entry->Released=true;assert(!service.MineSupplyReady(&group,&soldier));entry->Released=false;
+ entry->Released=true;assert(!service.MineSupplyReady(&group,&soldier));assert(!service.MineSupplyReady(&group,&soldier,true));entry->Released=false;
entry->Mines=nullptr;assert(!service.MineSupplyReady(&group,&soldier));entry->Mines=&grant;
service.m_Stopped=true;assert(!service.MineSupplyReady(&group,&soldier));service.m_Stopped=false;
service.m_Entries.resize(2049,nullptr);assert(!service.MineSupplyReady(&group,&soldier));service.m_Entries.resize(1);
--- a/tests/test_infantry_mine_transfer.py
+++ b/tests/test_infantry_mine_transfer.py
@@ -12,12 +12,13 @@
stored=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c',' static bool StoredUnder(')
def cpp(s):
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('override ','').replace('ref ','').replace('class ','struct ').replace('foreach (','for (').replace('array<','Array<').replace('null','nullptr')
+ s=s.replace('out IEntity found','IEntity& found').replace('ERR_HQ.Cast(m_Mode.Command())','m_Mode.Command()')
s=s.replace('void ERR_InfantryMineTransfer(', 'ERR_InfantryMineTransfer(').replace('void ERR_FiniteMagazineReservation(', 'ERR_FiniteMagazineReservation(')
for typ in ('InventoryStorageManagerComponent','InventoryItemComponent','SCR_AIVehicleUsageComponent','SCR_PressureTriggerComponent'):
s=re.sub(typ+r'\.Cast\((\w+)\.FindComponent\('+typ+r'\)\)',r'\1.'+{'InventoryStorageManagerComponent':'inventory','InventoryItemComponent':'item','SCR_AIVehicleUsageComponent':'usage','SCR_PressureTriggerComponent':'trigger'}[typ],s)
for typ,field in [('InventoryStorageManagerComponent','inventory'),('SCR_AntiPersonnelMineCollisionHandlerComponent','collision'),('SCR_PressureTriggerComponent','trigger'),('DamageManagerComponent','damage')]:
s=re.sub(r'(\w+)\.FindComponent\('+typ+r'\)',r'\1.'+field,s)
- for typ in ('ERR_GameMode','ERR_ForceRecord','ERR_HQStatusSampler','InventoryStorageManagerComponent','InventoryItemComponent','InventoryStorageSlot','BaseInventoryStorageComponent','SCR_AIVehicleUsageComponent','SCR_PressureTriggerComponent','SCR_AIGroupUtilityComponent','Physics','ERR_InfantryMineTransfer','ERR_InfantryMineCallback','ERR_FiniteMagazineReservation'):
+ for typ in ('ERR_HQ','ERR_GameMode','ERR_ForceRecord','ERR_HQStatusSampler','InventoryStorageManagerComponent','InventoryItemComponent','InventoryStorageSlot','BaseInventoryStorageComponent','SCR_AIVehicleUsageComponent','SCR_PressureTriggerComponent','SCR_AIGroupUtilityComponent','Physics','ERR_InfantryMineTransfer','ERR_InfantryMineCallback','ERR_FiniteMagazineReservation'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=s.replace('Array<BaseInventoryStorageComponent>','Array<BaseInventoryStorageComponent*>').replace('Array<IEntity> attempted','Array<IEntity>* attempted')
for field in ('m_Original','m_Spent'):
@@ -25,7 +26,7 @@
s=s.replace('Array<IEntity> original, Array<IEntity> spent','Array<IEntity>* original, Array<IEntity>* spent')
for ns in ('Replication','SCR_Global','ERR_GameMode','ERR_Actors','ERR_EnemyEquipmentRules','ERR_ConvoyRules','SCR_FactionManager','CompartmentAccessComponent','ERR_InfantryAmmoTransfer','ERR_InfantryAmmoAccess','ERR_ForceRules','ERR_EnemyLoadoutCatalog','ERR_InfantryAmmoTransferState','EStoragePurpose'):
s=s.replace(ns+'.',ns+'::')
- for var in ('m_Mine','exactMine','mode','donorVehicle','recipientActor','originalRoster','usage','item','actor','group','inventory','carried','trigger','m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Roster','m_DonorRecord','m_RecipientRecord','m_Source','m_Destination','m_Reservation','m_Callback','m_FromSlot','m_FromStorage','m_Storage','slot','storage','utility','physics','Transfer','entity'):
+ for var in ('hq','m_Mine','exactMine','mode','donorVehicle','recipientActor','originalRoster','usage','item','actor','group','inventory','carried','trigger','m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Roster','m_DonorRecord','m_RecipientRecord','m_Source','m_Destination','m_Reservation','m_Callback','m_FromSlot','m_FromStorage','m_Storage','slot','storage','utility','physics','Transfer','entity'):
s=re.sub(r'\b'+var+r'\.',var+'->',s)
for call in ('Forces()','GetPilotCompartmentSlot()','GetPrefabData()','GetParentSlot()'):
s=s.replace(call+'.',call+'->')
@@ -60,7 +61,8 @@
struct Group{bool slave=false;int players=0,faction=1;SCR_AIGroupUtilityComponent*utility=nullptr;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}SCR_AIGroupUtilityComponent*GetGroupUtilityComponent(){return utility;}};
struct ERR_ForceRecord{SCR_AIGroup Group=nullptr;bool Physical=true,Eliminated=false;};
struct Ledger{SCR_AIGroup donor=nullptr,recipient=nullptr;ERR_ForceRecord*d=nullptr,*r=nullptr;ERR_ForceRecord*FindGroup(SCR_AIGroup g){return g==donor?d:g==recipient?r: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 ERR_HQ{bool designated=true;bool MineSupplyReady(SCR_AIGroup,IEntity,bool replacement=false){return !replacement||designated;}};
+struct ERR_GameMode{ERR_HQ hq;bool command=true;ERR_HQ*Command(){return command?&hq:nullptr;}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 ERR_HQStatusSampler{SCR_AIGroup group=nullptr;IEntity actor=nullptr;bool valid=true;int strength=1;bool OwnsRoster(SCR_AIGroup g){return g==group;}int InitialStrength(){return strength;}IEntity InitialMember(int i){return i==0?actor:nullptr;}bool CanAttribute(int i,IEntity a,bool){return valid&&i==0&&a==actor;}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct SCR_Global{inline static bool edit=false;static bool IsEditMode(){return edit;}};
@@ -95,10 +97,19 @@
from.storage=&sourceStorage;from.attached=&incoming;to.storage=&destStorage;ownSlot.storage=&destStorage;ownSlot.attached=&original;extraSlot.storage=&destStorage;extraSlot.attached=&extra;
incoming.prefab=&prefab;incoming.trigger=&inTrigger;incoming.item=&inItem;inItem.slot=&from;original.prefab=&prefab;original.trigger=&ownTrigger;original.item=&ownItem;ownItem.slot=&ownSlot;extra.prefab=&prefab;extra.trigger=&extraTrigger;extra.item=&extraItem;extraItem.slot=&extraSlot;
source.items={&incoming};destination.items={&original};roster.group=&recipientGroup;roster.actor=&actor;stock={&incoming};assert(reservation.Reserve());}
- ERR_InfantryMineTransfer Job(){return ERR_InfantryMineTransfer(&mode,&truck,&donorGroup,&actor,&recipientGroup,&incoming,&reservation,&roster,0);}
+ ERR_InfantryMineTransfer Job(bool empty=false){return ERR_InfantryMineTransfer(&mode,&truck,&donorGroup,&actor,&recipientGroup,&incoming,&reservation,&roster,0,empty);}
void Move(){source.items.clear();destination.items.Insert(&incoming);from.attached=nullptr;to.attached=&incoming;inItem.slot=&to;}
};
int main(){
+ // Authoritative designation plus an inspected empty inventory permits exactly one finite replacement.
+ {Fixture f;f.destination.items.clear();auto t=f.Job(true);assert(t.Tick(0)==PENDING&&t.Submitted());f.Move();f.source.last->OnComplete();assert(t.Tick(1)==SUCCEEDED&&f.destination.items.Count()==1&&f.source.calls==1);}
+ for(int k=0;k<7;k++){Fixture f;f.destination.items.clear();
+ if(k==0)f.mode.hq.designated=false;if(k==1)f.destination.items.Insert(nullptr);if(k==2)f.destination.items.Insert(&f.original);if(k==3)f.destination.owner=&f.extra;if(k==4)f.destination.items.resize(129,&f.original);if(k==5){f.original.damage=false;f.destination.items.Insert(&f.original);}if(k==6)f.mode.command=false;
+ auto t=f.Job(true);assert(t.Tick(0)==FAILED&&!t.Submitted()&&f.source.calls==0);}
+ // Empty/one mode never switches after dispatch; a foreign new mine or revoked entitlement aborts and retains uncertainty.
+ for(int k=0;k<2;k++){Fixture f;f.destination.items.clear();auto t=f.Job(true);t.Tick(0);if(k==0)f.destination.items.Insert(&f.original);if(k==1){f.mode.hq.designated=false;}assert(t.Tick(1)==FAILED&&t.Unresolved()&&f.source.calls==1);f.Move();f.source.last->OnComplete();assert(!t.Unresolved()&&t.Tick(2)==FAILED);}
+ {Fixture f;f.destination.items.clear();auto t=f.Job(true);t.Tick(0);t.Stop();f.Move();f.source.last->OnComplete();assert(!t.Unresolved()&&t.Tick(1)==FAILED&&f.source.calls==1&&!f.reservation.ReleaseUnsubmitted());}
+
{Fixture f;auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Submitted()&&t.Unresolved()&&f.source.calls==1);assert(f.reservation.CommittedFor(&f.incoming)&&!f.reservation.ReleaseUnsubmitted());
f.source.last->OnComplete();assert(t.Tick(1)==PENDING&&t.Unresolved());f.Move();assert(t.Tick(2)==SUCCEEDED&&!t.Unresolved());assert(f.destination.items.Count()==2);t.Tick(3);assert(f.source.calls==1);}
// Partially constructed contexts never bind later or reach native mutation.
@@ -134,4 +145,4 @@
{Fixture f;auto t=f.Job();t.Tick(0);f.roster.valid=false;assert(t.Tick(1)==FAILED&&t.Unresolved());f.Move();f.source.last->OnComplete();assert(!t.Unresolved()&&t.Tick(2)==FAILED);}
}
'''.replace(';if(', ';\n if('))
-print('PASS: actual finite PMN4 one-to-two entitlement, pinned original ownership, shared commit, physical custody, callback quarantine/deletion and no retry')
+print('PASS: actual finite PMN4 designated-zero/one entitlement, pinned original ownership, shared commit, physical custody, callback quarantine/deletion and no retry')
--- a/tests/test_infantry_mine_coordinator.py
+++ b/tests/test_infantry_mine_coordinator.py
@@ -18,23 +18,24 @@
#include <cmath>
using string=std::string;
struct Vec{float x=0;static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
-struct Group{};struct Mode{};struct Entity{bool ready=false,fight=true,owned=true,grant=true;int mines=1;Vec pos;Vec GetOrigin(){return pos;}};using IEntity=Entity*;
+struct Group{};struct Mode{};struct Entity{bool ready=false,fight=true,owned=true,grant=true,designated=false;int mines=1;Vec pos;Vec GetOrigin(){return pos;}};using IEntity=Entity*;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}};
'''+translated_reservation()+r'''
struct Sampler{Array<IEntity>original;int InitialStrength(){return original.Count();}IEntity InitialMember(int i){return original[i];}bool CanAttribute(int i,IEntity x,bool){return i>=0&&i<InitialStrength()&&original[i]==x&&x->owned;}};
struct Unit{struct Group*Group;Sampler*StatusSampler;};struct Truck{IEntity Entity;struct Group*Group;};
-struct HQ{bool MineSupplyReady(Group*,IEntity x){return x&&x->grant;}};
+struct HQ{bool MineSupplyReady(Group*,IEntity x,bool replacement=false){return x&&x->grant&&(!replacement||x->designated);}};
struct ERR_Actors{static bool CanFight(IEntity x){return x&&x->fight;}};
struct ERR_InfantryMineTransfer{
static inline bool accept=true;static inline int made=0;IEntity item,actor;ERR_FiniteMagazineReservation*r;bool submitted=false;int result=0;
- ERR_InfantryMineTransfer(Mode*,IEntity,Group*,IEntity a,Group*,IEntity i,ERR_FiniteMagazineReservation*q,Sampler*,int):item(i),actor(a),r(q){made++;}
+ ERR_InfantryMineTransfer(Mode*,IEntity,Group*,IEntity a,Group*,IEntity i,ERR_FiniteMagazineReservation*q,Sampler*,int,bool empty=false):item(i),actor(a),r(q){assert(a->mines==(empty?0:1));made++;}
static bool ReadyMine(IEntity x){return x&&x->ready;}
+ static int CarriedCount(IEntity x,Group*,IEntity& found){found=x;return x?x->mines:-1;}
static IEntity Carrier(IEntity x,Group*){return x&&x->mines==1?x:nullptr;}
int Tick(float){if(accept&&!submitted){assert(r->Commit());submitted=true;}return result;}
bool Submitted(){return submitted;}
};
struct Service{
- HQ*m_HQ;Mode*m_Mode;Unit*m_Recipient;Truck*m_Truck;IEntity m_Receiving=nullptr;int m_MineRecipientSlot=-1,m_MinesDelivered=0,finishes=0;ERR_InfantryMineTransfer*m_MineMove=nullptr;
+ HQ*m_HQ;Mode*m_Mode;Unit*m_Recipient;Truck*m_Truck;IEntity m_Receiving=nullptr;bool m_EmptyMineReplacement=false;int m_MineRecipientSlot=-1,m_MinesDelivered=0,finishes=0;ERR_InfantryMineTransfer*m_MineMove=nullptr;
Array<IEntity>m_Members,m_MineStock,m_Spent,cargo;
bool Contains(IEntity,IEntity x){return cargo.Contains(x);}void Finish(string){finishes++;}
'''+s+r'''
@@ -43,7 +44,11 @@
Fixture():sampler{{&actor,&other}},unit{&g,&sampler},donor{&truck,&d}{mine.ready=second.ready=true;s.m_HQ=&h;s.m_Mode=&mode;s.m_Recipient=&unit;s.m_Truck=&donor;s.m_Members={&actor,&other};s.m_MineStock={&mine,&second};s.cargo=s.m_MineStock;ERR_InfantryMineTransfer::accept=true;}
};
int main(){
- // Never grant mines to zero/two-mine members or to unsettled initial grants.
+ {Fixture f;f.actor.mines=0;f.actor.designated=true;f.s.TransferMine(0);assert(f.s.m_MineMove&&f.s.m_MineMove->actor==&f.actor&&f.s.m_EmptyMineReplacement&&f.s.m_Spent.Count()==1);}
+ {Fixture f;f.actor.mines=-1;f.actor.designated=true;f.other.mines=0;f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.empty());}
+ {Fixture f;f.actor.mines=0;f.actor.designated=true;ERR_InfantryMineTransfer::accept=false;f.s.TransferMine(0);assert(f.s.m_Receiving==&f.actor&&f.s.m_EmptyMineReplacement);f.actor.mines=1;ERR_InfantryMineTransfer::accept=true;f.s.TransferMine(1);assert(!f.s.m_MineMove&&f.s.finishes==1&&f.s.m_Spent.empty());}
+
+ // Never grant mines to undesignated zero/two-mine members or to unsettled initial grants.
for(int refusal=0;refusal<6;refusal++){Fixture f;f.other.mines=0;
if(refusal==0)f.actor.mines=0;if(refusal==1)f.actor.mines=2;if(refusal==2)f.actor.grant=false;if(refusal==3)f.actor.owned=false;if(refusal==4)f.actor.pos.x=5;if(refusal==5)f.actor.fight=false;
f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.empty()&&!f.s.m_Receiving);}
@@ -62,16 +67,16 @@
{Fixture f;f.s.TransferMine(0);auto*leaf=f.s.m_MineMove;leaf->result=-1;f.s.TransferMine(1000);assert(f.s.m_MineMove==leaf&&f.s.finishes==1&&f.s.m_Spent.Count()==1);}
}
'''.replace('if(refusal==0)f.actor.mines=0;if(refusal==1)f.actor.mines=2;if(refusal==2)f.actor.grant=false;if(refusal==3)f.actor.owned=false;if(refusal==4)f.actor.pos.x=5;if(refusal==5)f.actor.fight=false;', 'if(refusal==0){f.actor.mines=0;}if(refusal==1){f.actor.mines=2;}if(refusal==2){f.actor.grant=false;}if(refusal==3){f.actor.owned=false;}if(refusal==4){f.actor.pos.x=5;}if(refusal==5){f.actor.fight=false;}'))
-print('PASS: actual one-mine original carrier selection, settled startup, shared finite claims, no refund after submission')
+print('PASS: actual designated zero/one original carrier selection, settled startup, shared finite claims, no refund after submission')
h=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
w=body(h,'bool MineSupplyReady(').replace('SCR_AIGroup group','Group* group').replace('Replication.','Replication::').replace('m_Equipment.','m_Equipment->')
run(r'''
#include <cassert>
struct Group{};struct Item{};using IEntity=Item*;
-struct Equipment{bool ready=false;Group*group=nullptr;IEntity actor=nullptr;bool MineSupplyReady(Group*g,IEntity a){return ready&&g==group&&a==actor;}};
+struct Equipment{bool ready=false,designated=false;Group*group=nullptr;IEntity actor=nullptr;bool MineSupplyReady(Group*g,IEntity a,bool replacement=false){return ready&&g==group&&a==actor&&(!replacement||designated);}};
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct HQ{Equipment*m_Equipment=nullptr;bool active=true;bool MissionActive(){return active;}
'''+w+r'''};
-int main(){Group group,other;Item actor;Equipment e;e.group=&group;e.actor=&actor;HQ h;assert(!h.MineSupplyReady(&group,&actor));h.m_Equipment=&e;assert(!h.MineSupplyReady(&group,&actor));e.ready=true;assert(h.MineSupplyReady(&group,&actor));assert(!h.MineSupplyReady(&other,&actor));h.active=false;assert(!h.MineSupplyReady(&group,&actor));h.active=true;Replication::server=false;assert(!h.MineSupplyReady(&group,&actor));}
+int main(){Group group,other;Item actor;Equipment e;e.group=&group;e.actor=&actor;HQ h;assert(!h.MineSupplyReady(&group,&actor));h.m_Equipment=&e;assert(!h.MineSupplyReady(&group,&actor));e.ready=true;assert(h.MineSupplyReady(&group,&actor));assert(!h.MineSupplyReady(&group,&actor,true));e.designated=true;assert(h.MineSupplyReady(&group,&actor,true));assert(!h.MineSupplyReady(&other,&actor,true));assert(!h.MineSupplyReady(&other,&actor));h.active=false;assert(!h.MineSupplyReady(&group,&actor));h.active=true;Replication::server=false;assert(!h.MineSupplyReady(&group,&actor));}
''')
print('PASS: actual HQ startup grant readiness forwarding is authoritative and fail-closed')
--- a/tests/test_infantry_resupply_planning.py
+++ b/tests/test_infantry_resupply_planning.py
@@ -23,6 +23,7 @@
s=s.replace(name+'.',name+'->')
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('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('))
@@ -85,10 +86,11 @@
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity x){return x&&x->readyMine;}};
struct Route{};struct Leaf{bool unresolved=false;void Stop(){}bool Unresolved(){return unresolved;}};struct Mounted{bool busy=false;bool Busy(){return busy;}void Stop(){}};
struct HQ{Mode* m_Mode;Array<ERR_HQUnit>m_Units;Convoy* convoy=nullptr;ConvoyService convoys;ConvoyService* Convoys(){return &convoys;}Route route;bool active=true,routeAvailable=true;int downlinks=0;bool MissionActive(){return active;}Convoy* ConvoyTask(){return convoy;}
+ bool designated=false;bool MineSupplyReady(Group*,IEntity,bool replacement=false){return !replacement||designated;}
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;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
+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;
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'''
@@ -179,9 +181,10 @@
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.items[0].prefab.name="NATO_556_STANAG";
f.items[96].magazine=false;f.inventory.items.Insert(&f.items[96]);f.s.Stock(0);
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Count()==96&&f.s.m_Stock.Contains(&f.items[0])&&!f.s.m_Stock.Contains(&f.items[96]));}
- // Mine demand is a detached known-one report, never a grant to zero or unknown.
+ // Detached known-zero demand also requires settled original designation; unknown never authorizes.
{Fixture f;MineBatch batch;f.convoy.supply=true;f.convoy.MineStock=&batch;f.items[48].readyMine=true;batch.items={&f.items[48]};f.inventory.items.Insert(&f.items[48]);f.s.Stock(0);
for(int n: {0,2,3}){f.ammo.ReadyMines=n;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
+ f.ammo.ReadyMines=0;f.hq.designated=true;f.one.released=f.two.released=true;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.one.released=f.two.released=false;assert(f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.s.Finish("reset");f.s.m_Consumed->data.clear();f.s.m_NextAttempt->data.clear();f.hq.designated=false;
f.ammo.ReadyMines=1;f.ammo.MineComplete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.ammo.MineComplete=true;
batch.complete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));batch.complete=true;
assert(f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_MineTrip&&f.s.m_MineStock.Count()==1);}
--- a/tests/test_enemy_radio.py
+++ b/tests/test_enemy_radio.py
@@ -39,7 +39,7 @@
float worldTime=0;
struct SCR_AICombatComponent{void ERR_BindEnemySkill(ERR_GameMode*,Group*){} void ERR_RefreshEnemySkill(){}};
struct ERR_EnemyOptics{ERR_EnemyOptics(IEntity,Group*){}void Check(ERR_GameMode*){}};
-struct MineGrant{bool ReadyForResupply(){return false;}};
+struct MineGrant{bool ReadyForResupply(bool replacement=false){return false;}};
struct ERR_EnemyEquipmentEntry{MineGrant*Mines=nullptr;IEntity Actor=nullptr;struct Group*Group=nullptr;SCR_AICombatComponent*Combat=nullptr;ERR_EnemyOptics*Optics=nullptr;bool Released=false;int SkillChecks=0;
void Check(ERR_GameMode*){}void Stop(ERR_GameMode*){Released=true;}};
'''