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:
+592
@@ -0,0 +1,592 @@
|
||||
// HQ-owned ground rendezvous using delivered positions and existing finite cargo.
|
||||
class ERR_InfantryResupplyRules
|
||||
{
|
||||
static bool Fresh(ERR_HQWatchEntry entry, float now)
|
||||
{
|
||||
return entry && !entry.Closed && entry.Count > 0 && entry.Fit > 0
|
||||
&& ERR_ForceRules.Finite(now) && entry.ObservedAt >= 0 && now >= entry.ObservedAt
|
||||
&& now - entry.ObservedAt <= 120000 && ERR_ForceRules.FiniteVector(entry.Position);
|
||||
}
|
||||
static bool Need(ERR_HQWatchEntry entry, float now)
|
||||
{
|
||||
return Fresh(entry, now) && entry.AmmoState && entry.AmmoState.Valid(entry.Count)
|
||||
&& entry.AmmoState.Complete && entry.AmmoState.Members == entry.Count
|
||||
&& entry.AmmoState.SmallArmsMuzzles > 0
|
||||
&& entry.AmmoState.SmallArmsRounds + entry.AmmoState.SmallArmsChambers <= entry.Count * 30;
|
||||
}
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
static bool Pair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
|
||||
{
|
||||
return Need(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
|
||||
&& vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_InfantryResupply
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyTask m_Convoy;
|
||||
protected int m_ConvoySlot = -1;
|
||||
protected ERR_ConvoyVehicle m_Truck;
|
||||
protected ERR_HQUnit m_Recipient;
|
||||
protected ERR_HQUnit m_Donor;
|
||||
protected ref array<IEntity> m_Stock = {};
|
||||
protected ref array<IEntity> m_Spent = {};
|
||||
protected ref array<IEntity> m_MineStock = {};
|
||||
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;
|
||||
protected ref array<IEntity> m_Members = {};
|
||||
protected ref array<IEntity> m_Served = {};
|
||||
protected ref map<string, int> m_Consumed = new map<string, int>();
|
||||
protected ref map<string, float> m_NextAttempt = new map<string, float>();
|
||||
protected ref ERR_HQRoute m_Order;
|
||||
protected ref ERR_InfantryAmmoTransfer m_Move;
|
||||
protected ref ERR_InfantryMineTransfer m_MineMove;
|
||||
protected ref ERR_MountedResupply m_Mounted;
|
||||
protected IEntity m_Receiving;
|
||||
protected AIWaypoint m_MoveWaypoint;
|
||||
protected AIWaypoint m_HoldWaypoint;
|
||||
protected vector m_Rendezvous;
|
||||
protected int m_State; // 0 stock startup, 1 idle, 2 truck downlink, 3 stopped status, 4 infantry downlink, 5 transfer, 7 unresolved item custody
|
||||
protected int m_Trip;
|
||||
protected int m_Token;
|
||||
protected int m_ReportId;
|
||||
protected int m_Delivered;
|
||||
protected int m_TripDelivered;
|
||||
protected float m_Deadline;
|
||||
protected float m_StockDeadline = -1;
|
||||
protected float m_HeldAt;
|
||||
protected float m_NextTick;
|
||||
protected bool m_Stopped;
|
||||
protected string m_Reason;
|
||||
void ERR_InfantryResupply(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
|
||||
// Bind before startup; every coordinator retains its exact finite convoy identity.
|
||||
bool Bind(ERR_ConvoyTask task, int slot, map<string, int> consumed, map<string, float> nextAttempt)
|
||||
{
|
||||
if (!task || !consumed || !nextAttempt || !m_HQ || !m_HQ.Convoys() || slot < 0 || slot >= 20
|
||||
|| slot >= m_HQ.Convoys().Count() || m_HQ.Convoys().At(slot) != task) return false;
|
||||
if (m_Stopped || m_Convoy || m_ConvoySlot >= 0 || m_State != 0 || m_Trip != 0 || m_NextTick != 0
|
||||
|| m_StockDeadline >= 0 || !m_Stock.IsEmpty() || m_Move || m_Truck) return false;
|
||||
m_Convoy = task; m_ConvoySlot = slot; m_Consumed = consumed; m_NextAttempt = nextAttempt;
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Task() { return m_Convoy; }
|
||||
int State()
|
||||
{
|
||||
if (m_Mounted && m_Mounted.Busy()) return 6;
|
||||
return m_State;
|
||||
}
|
||||
ERR_MountedResupply Mounted() { return m_Mounted; }
|
||||
bool Unresolved() { return (m_Move && m_Move.Unresolved()) || (m_MineMove && m_MineMove.Unresolved()) || (m_Mounted && m_Mounted.Unresolved()); }
|
||||
protected void PollTransfers(float now)
|
||||
{
|
||||
if (m_Mounted) m_Mounted.Tick(now);
|
||||
if (m_Move && !m_Move.Unresolved() && (m_State <= 1 || m_State == 7))
|
||||
{
|
||||
m_Move = null;
|
||||
if (m_State == 7) Finish("magazine custody settled; original claims released without retry or refund");
|
||||
}
|
||||
if (m_MineMove && !m_MineMove.Unresolved() && (m_State <= 1 || m_State == 7))
|
||||
{
|
||||
m_MineMove = null;
|
||||
if (m_State == 7) Finish("mine custody settled; original claims released without retry or refund");
|
||||
}
|
||||
}
|
||||
int Delivered() { return m_Delivered; }
|
||||
int MinesDelivered() { return m_MinesDelivered; }
|
||||
int StockCount() { return m_Stock.Count(); }
|
||||
// Admission uses the existing array object so an already-created mounted
|
||||
// coordinator observes this exact finite addition without a replacement ledger.
|
||||
protected int m_DepotImports;
|
||||
protected int m_DepotMineImports;
|
||||
bool DepotCurrent()
|
||||
{
|
||||
return Current() && m_State == 1 && !m_Move && !m_MineMove && !Unresolved()
|
||||
&& (!m_Mounted || !m_Mounted.Busy()) && TruckValid(true);
|
||||
}
|
||||
bool DepotAvailable() { return DepotCurrent() && m_Stock.Count() < 96 && m_Spent.Count() < 96; }
|
||||
bool DepotNeeded()
|
||||
{
|
||||
if (!DepotAvailable()) return false;
|
||||
int committed;
|
||||
foreach (IEntity item : m_Spent)
|
||||
if (item && m_Stock.Contains(item)) committed++;
|
||||
return committed > m_DepotImports;
|
||||
}
|
||||
// Mine imports replace only proved outgoing original mine commitments.
|
||||
// Keep the startup batch binding and both lifetime identity/spent ceilings.
|
||||
bool DepotMineAvailable()
|
||||
{
|
||||
return DepotCurrent() && CaptureMines() && m_MineStock.Count() < 96 && m_Spent.Count() < 96;
|
||||
}
|
||||
bool DepotMineNeeded()
|
||||
{
|
||||
if (!DepotMineAvailable()) return false;
|
||||
int committed;
|
||||
foreach (IEntity item : m_Spent)
|
||||
if (item && m_MineStock.Contains(item)) committed++;
|
||||
return committed > m_DepotMineImports;
|
||||
}
|
||||
bool AdmitDepotMine(ERR_GroundDepotLoading job, IEntity item)
|
||||
{
|
||||
if (!job || !DepotMineNeeded() || !ERR_InfantryMineTransfer.ReadyMine(item)
|
||||
|| m_MineStock.Contains(item) || m_Stock.Contains(item) || m_Spent.Contains(item)) return false;
|
||||
if (!job.DeliveredTo(this, item, true) || !Contains(m_Truck.Entity, item)
|
||||
|| !ERR_InfantryAmmoTransfer.StoredUnder(item, m_Truck.Entity)) return false;
|
||||
InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
|
||||
if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item) return false;
|
||||
m_MineStock.Insert(item); m_DepotMineImports++;
|
||||
Audit("one exact finite camp PMN4 admitted after proved physical courier delivery");
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyVehicle DepotTruck()
|
||||
{
|
||||
if (!DepotCurrent()) return null;
|
||||
return m_Truck;
|
||||
}
|
||||
bool AdmitDepot(ERR_GroundDepotLoading job, IEntity item)
|
||||
{
|
||||
if (!job || !DepotAvailable() || !item || m_Stock.Contains(item) || m_Spent.Contains(item)) return false;
|
||||
if (!job.DeliveredTo(this, item) || !Contains(m_Truck.Entity, item)) return false;
|
||||
m_Stock.Insert(item); m_DepotImports++; Audit("one exact finite camp magazine admitted after proved physical courier delivery");
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active();
|
||||
}
|
||||
protected bool Contains(IEntity owner, IEntity item)
|
||||
{
|
||||
if (!owner || !item) return false;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return false;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
return items.Count() <= 128 && items.Contains(item);
|
||||
}
|
||||
protected bool ConvoyCurrent()
|
||||
{
|
||||
if (!m_HQ || !m_Convoy) return false;
|
||||
if (m_ConvoySlot < 0) return m_HQ.ConvoyTask() == m_Convoy;
|
||||
return m_HQ.Convoys() && m_ConvoySlot < m_HQ.Convoys().Count() && m_HQ.Convoys().At(m_ConvoySlot) == m_Convoy;
|
||||
}
|
||||
protected bool TruckValid(bool allowArrived = false)
|
||||
{
|
||||
if (!m_Convoy || m_Convoy.Stopped || !m_Convoy.Ready || !m_Convoy.Receipt || !m_Convoy.Receipt.Consumed
|
||||
|| !ConvoyCurrent() || m_Convoy.Vehicles.Count() != 6 || m_Convoy.Vehicles[1] != m_Truck) return false;
|
||||
if (!m_Truck || m_Truck.Index != 1 || !m_Truck.Registered || m_Truck.Released || (m_Truck.Arrived && !allowArrived)
|
||||
|| !m_Truck.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(m_Truck.Entity, m_Truck.Group)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(m_Truck.Group);
|
||||
return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
|
||||
&& m_Convoy.Receipt.Records.Count() >= 6 && record.Id == m_Convoy.Receipt.Records[1];
|
||||
}
|
||||
protected bool Quiet(ERR_HQUnit unit)
|
||||
{
|
||||
return unit && unit.Group && unit.Utility && !unit.Group.IsSlave() && unit.Group.GetPlayerCount(true) == 0
|
||||
&& unit.Group.GetFaction() == m_Mode.EnemyFaction()
|
||||
&& (!unit.Utility.m_Perception || unit.Utility.m_Perception.m_aTargets.IsEmpty());
|
||||
}
|
||||
protected bool Roster()
|
||||
{
|
||||
if (!m_Recipient || !Quiet(m_Recipient) || !m_Recipient.StatusSampler.OwnsRoster(m_Recipient.Group)) return false;
|
||||
int strength = m_Recipient.StatusSampler.InitialStrength();
|
||||
if (strength < 1 || strength > 32) return false;
|
||||
array<AIAgent> agents = {}; m_Recipient.Group.GetAgents(agents);
|
||||
if (agents.IsEmpty() || agents.Count() > 32) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !m_Members.Contains(agent.GetControlledEntity())) return false;
|
||||
bool living;
|
||||
foreach (IEntity actor : m_Members)
|
||||
{
|
||||
if (!actor || !ERR_Actors.Alive(actor)) continue;
|
||||
// Returning to the group cannot undo a sampler's permanent release of an
|
||||
// original slot after transfer or player control during this trip.
|
||||
bool attributed = false;
|
||||
for (int slot = 0; slot < strength; slot++)
|
||||
{
|
||||
if (m_Recipient.StatusSampler.InitialMember(slot) != actor) continue;
|
||||
attributed = m_Recipient.StatusSampler.CanAttribute(slot, actor);
|
||||
break;
|
||||
}
|
||||
if (!attributed) return false;
|
||||
if (ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Recipient.Group
|
||||
|| SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction() || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
|
||||
if (ERR_Actors.CanFight(actor)) living = true;
|
||||
}
|
||||
return living;
|
||||
}
|
||||
protected void Stock(float now)
|
||||
{
|
||||
if (!m_Convoy) m_Convoy = m_HQ.ConvoyTask();
|
||||
if (m_Convoy && !ConvoyCurrent()) { Stop(); return; }
|
||||
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;
|
||||
if (supply) m_MineBatch = supply.MineStock;
|
||||
foreach (ERR_ConvoyCargoCallback operation : m_Convoy.Cargo)
|
||||
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.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");
|
||||
}
|
||||
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 ((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)
|
||||
{
|
||||
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 (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;
|
||||
bool available;
|
||||
if (mineTrip)
|
||||
{
|
||||
if (!CaptureMines()) return false;
|
||||
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 available = CompatibleStock(recipient, false);
|
||||
if (m_Spent.Count() >= 96) return false;
|
||||
if (!available) return false;
|
||||
m_Members.Clear();
|
||||
for (int i = 0; i < recipient.StatusSampler.InitialStrength(); i++)
|
||||
{
|
||||
IEntity actor = recipient.StatusSampler.InitialMember(i);
|
||||
if (actor && recipient.StatusSampler.CanAttribute(i, actor)) m_Members.Insert(actor);
|
||||
}
|
||||
m_Recipient = recipient; m_Donor = donor;
|
||||
if (!Roster()) { m_Recipient = null; m_Donor = null; return false; }
|
||||
// Disjoint bounded token range from the existing mortar courier's trip IDs.
|
||||
int slot = m_ConvoySlot;
|
||||
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_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");
|
||||
return true;
|
||||
}
|
||||
// Optional mine startup must never delay normal magazine readiness. Pin the
|
||||
// original batch during Stock(), then copy its closed exact identities once.
|
||||
protected bool CaptureMines()
|
||||
{
|
||||
ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);
|
||||
if (!supply || !m_MineBatch || supply.MineStock != m_MineBatch || !m_MineBatch.Complete()) return false;
|
||||
if (m_MineStockCaptured) return !m_MineStock.IsEmpty();
|
||||
m_MineStockCaptured = true;
|
||||
int count = m_MineBatch.Count();
|
||||
if (count < 0 || count > 2) return false;
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
IEntity item = m_MineBatch.At(index);
|
||||
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 CompatibleStock(ERR_HQUnit recipient, bool launcherMagazine)
|
||||
{
|
||||
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, launcherMagazine)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool LauncherStock(ERR_HQUnit recipient)
|
||||
{
|
||||
return CompatibleStock(recipient, true);
|
||||
}
|
||||
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)
|
||||
{
|
||||
return TryPlan(recipient, donor, demand, donorStatus, now, true);
|
||||
}
|
||||
// Share exact frozen inventory identities and global radio replay state.
|
||||
bool TryPlanMounted(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
|
||||
{
|
||||
if (!Current() || m_State != 1 || m_Move || m_MineMove || m_ConvoySlot < 0 || !TruckValid(true)) return false;
|
||||
if (m_Mounted && m_Mounted.Busy()) return false;
|
||||
if (!m_Mounted) m_Mounted = new ERR_MountedResupply(m_HQ, m_Mode, m_Convoy, m_ConvoySlot, m_Stock, m_Spent, m_Consumed, m_NextAttempt);
|
||||
return m_Mounted.Begin(recipient, donor, demand, donorStatus, now);
|
||||
}
|
||||
protected bool Waypoints()
|
||||
{
|
||||
m_MoveWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
m_HoldWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
if (!m_MoveWaypoint || !m_HoldWaypoint) return false;
|
||||
// The reported rendezvous is 3 m from the donor report. A 2 m move
|
||||
// radius allowed arrival 5 m away, beyond the transfer's physical reach.
|
||||
m_MoveWaypoint.SetCompletionRadius(0.5); m_HoldWaypoint.SetCompletionRadius(0.5);
|
||||
m_Recipient.Group.AddWaypointAt(m_HoldWaypoint, 0); m_Recipient.Group.AddWaypointAt(m_MoveWaypoint, 0);
|
||||
m_Recipient.Group.ActivateAI(); return true;
|
||||
}
|
||||
protected void ClearWaypoints()
|
||||
{
|
||||
if (m_MoveWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_MoveWaypoint); ERR_Actors.DeleteSpawned(m_MoveWaypoint); }
|
||||
if (m_HoldWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_HoldWaypoint); ERR_Actors.DeleteSpawned(m_HoldWaypoint); }
|
||||
m_MoveWaypoint = null; m_HoldWaypoint = null;
|
||||
}
|
||||
protected void Finish(string reason)
|
||||
{
|
||||
Audit(reason);
|
||||
if (m_Move) m_Move.Stop();
|
||||
if (m_MineMove) m_MineMove.Stop();
|
||||
ClearWaypoints();
|
||||
m_Order = null;
|
||||
// A late native move must not race another truck's newer report. Retain both
|
||||
// exact claims until the existing transfer leaf reports that its operation has settled.
|
||||
if ((m_MineMove && m_MineMove.Unresolved()) || (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_LauncherTrip = false; m_MineRecipientSlot = -1;
|
||||
}
|
||||
protected void TransferMine(float now)
|
||||
{
|
||||
if (m_MineMove)
|
||||
{
|
||||
int result = m_MineMove.Tick(now);
|
||||
if (result == 0) return;
|
||||
if (result < 0) { Finish("native mine transfer failed or expired; exact submitted identity stays spent"); return; }
|
||||
m_MineMove = null; m_MinesDelivered++;
|
||||
Finish("one exact finite PMN4 confirmed in the original carrier inventory"); return;
|
||||
}
|
||||
if (!m_Receiving)
|
||||
{
|
||||
int strength = m_Recipient.StatusSampler.InitialStrength();
|
||||
for (int slot = 0; slot < strength && slot < 32; slot++)
|
||||
{
|
||||
IEntity actor = m_Recipient.StatusSampler.InitialMember(slot);
|
||||
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;
|
||||
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, 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)
|
||||
|| (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, 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.
|
||||
if (m_MineMove.Submitted()) return;
|
||||
reservation.ReleaseUnsubmitted(); m_MineMove = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
protected void Transfer(float now)
|
||||
{
|
||||
if (m_Move)
|
||||
{
|
||||
int result = m_Move.Tick(now);
|
||||
if (result == 0) return;
|
||||
if (result < 0) { Finish("native transfer rejected, expired or lost authority; no item retry"); return; }
|
||||
m_Move = null; m_Delivered++; m_TripDelivered++; m_Served.Insert(m_Receiving); m_Receiving = null;
|
||||
Audit("exact finite magazine confirmed in original soldier inventory");
|
||||
if (m_TripDelivered >= 6) { Finish("six-magazine trip allowance reached"); return; }
|
||||
}
|
||||
foreach (IEntity actor : m_Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(actor) || m_Served.Contains(actor) || vector.DistanceXZ(actor.GetOrigin(), m_Truck.Entity.GetOrigin()) > 4) continue;
|
||||
ERR_HQAmmoSnapshot local = new ERR_HQAmmoSnapshot(); local.Complete = true; local.LauncherComplete = true;
|
||||
ERR_HQAmmoSampler.Inspect(actor, m_Recipient.Group, local);
|
||||
if (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, 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, !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
|
||||
// capacity may improve while the same bounded rendezvous remains valid.
|
||||
// Keep every submitted identity spent, including a synchronous rejection.
|
||||
reservation.ReleaseUnsubmitted(); m_Move = null; m_Receiving = null;
|
||||
break; // At most one unsubmitted preflight per member on this slow tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!ERR_ForceRules.Finite(now) || now < 0) return;
|
||||
// Stopped services retain uncertain leaf refs until exact late proof settles.
|
||||
if (m_Stopped) { PollTransfers(now); return; }
|
||||
if (!Current() || now < m_NextTick) return;
|
||||
m_NextTick = now + 1000;
|
||||
PollTransfers(now);
|
||||
if (m_Mounted && m_Mounted.Busy()) return;
|
||||
if (m_State == 7) return;
|
||||
if (m_State == 0) { Stock(now); return; }
|
||||
if (m_State == 1)
|
||||
{
|
||||
if (m_Move && !m_Move.Unresolved()) m_Move = null;
|
||||
return;
|
||||
}
|
||||
if (now >= m_Deadline || !TruckValid() || !Quiet(m_Donor) || !Roster() || m_Truck.Combat.Held
|
||||
|| m_HQ.LogisticsToken(m_Recipient.Group) != m_Token)
|
||||
{ Finish("rendezvous expired, contact appeared or original ownership changed"); return; }
|
||||
if (m_Truck.SupplyToken != m_Token) { Finish("truck hold authority lost"); return; }
|
||||
if (m_State == 2)
|
||||
{
|
||||
int delivered = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (delivered == 0) return;
|
||||
if (delivered < 0 || m_Truck.SupplyToken != m_Token) { Finish("truck hold downlink interrupted or reservation lost"); return; }
|
||||
m_Truck.SupplyHeld = true; m_Truck.ClearOrder(); m_HeldAt = -1;
|
||||
m_Order = null; m_State = 3; Audit("truck received stop; waiting for delivered stopped-position report"); return;
|
||||
}
|
||||
if (m_State == 3)
|
||||
{
|
||||
// The delivered hold asks the original crew to report only after physically stopping.
|
||||
if (m_HeldAt < 0)
|
||||
{
|
||||
Physics physics = m_Truck.Entity.GetPhysics();
|
||||
if (!physics) return;
|
||||
float speed = physics.GetVelocity().Length();
|
||||
float angular = physics.GetAngularVelocity().Length();
|
||||
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1)) return;
|
||||
m_HeldAt = now; m_Donor.NextStatus = now; return;
|
||||
}
|
||||
ERR_HQWatchEntry stopped = m_HQ.Watch().Find(m_Donor.ForceSourceId);
|
||||
if (!ERR_InfantryResupplyRules.Fresh(stopped, now) || stopped.ObservedAt < m_HeldAt) return;
|
||||
m_Rendezvous = stopped.Position + "3 0 0";
|
||||
m_Order = m_HQ.Downlink(m_Recipient.Group, "INFANTRY SUPPLY RENDEZVOUS", string.Format("walk to reported stopped ammo truck at %1", m_Rendezvous), m_ReportId);
|
||||
if (!m_Order) { Finish("infantry radio downlink unavailable"); return; }
|
||||
m_State = 4; return;
|
||||
}
|
||||
if (m_State == 4)
|
||||
{
|
||||
int delivered = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (delivered == 0) return;
|
||||
if (delivered < 0 || !Waypoints()) { Finish("infantry order interrupted or native waypoint creation failed"); return; }
|
||||
m_Order = null; m_State = 5; Audit("infantry received reported rendezvous; native movement and physical transfer required");
|
||||
}
|
||||
if (m_State == 5)
|
||||
{
|
||||
if (m_MineTrip) TransferMine(now);
|
||||
else Transfer(now);
|
||||
}
|
||||
}
|
||||
string Status()
|
||||
{
|
||||
string detail = string.Format("Ground supply: state=%1 trips=%2 infantry moved=%3 stock=%4 spent=%5", State(), m_Trip, m_Delivered, m_Stock.Count(), m_Spent.Count());
|
||||
return detail + string.Format("; mines moved=%1 allowance=%2; %3", m_MinesDelivered, m_MineStock.Count(), m_Reason);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
if (m_Mounted) m_Mounted.Stop();
|
||||
Finish("service stopped; original forces and all remaining cargo retained"); m_Stopped = true;
|
||||
}
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
"""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_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|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=[]
|
||||
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;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;}
|
||||
};
|
||||
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.owner=&loaded;empty.well=&well;empty.rounds=0;muzzle.loaded=∅loaded.item=&loadedItem;loadedItem.slot=&loadedSlot;loadedSlot.container=&muzzle;loadedSlot.attached=&loaded;}
|
||||
};
|
||||
int main(){
|
||||
|
||||
// The shared force claim is necessary: independent rifle leaves can otherwise
|
||||
// submit the same still-carried empty to two different trucks. Native acceptance
|
||||
// is delayed here; this proves submission interference, not an engine outcome.
|
||||
{EmptyFixture first;Fixture second;ERR_GameMode::current=&first.mode;
|
||||
first.ledger.records.Insert(&second.donorRecord);second.source.selected=&second.sourceStorage;
|
||||
auto old=first.Job();assert(old.Tick(0)==PENDING&&old.Unresolved());
|
||||
assert(old.m_ReturnMagazine==&first.empty&&old.m_ReturnStorage==&first.sourceStorage);
|
||||
old.Stop();assert(old.Unresolved()&&old.Tick(180001)==FAILED);
|
||||
ERR_InfantryAmmoTransfer next(&first.mode,&second.truck,&second.donorGroup,&first.recipient,&first.recipientGroup,&second.magEntity,true);
|
||||
assert(next.Tick(180001)==PENDING&&next.Unresolved());
|
||||
assert(first.destination.calls==2&&next.m_ReturnMagazine==old.m_ReturnMagazine);
|
||||
assert(next.m_ReturnStorage==&second.sourceStorage&&next.m_ReturnStorage!=old.m_ReturnStorage);
|
||||
assert(first.destination.items.Contains(&first.empty)&&first.source.calls==0&&second.source.calls==0);
|
||||
// No duplicate object is invented: one physical completion can settle only its
|
||||
// original donor proof; the competing outstanding submission remains unknown.
|
||||
first.Return();old.m_ReturnCallback->OnComplete();assert(!old.Unresolved()&&next.Unresolved());
|
||||
assert(old.Tick(180002)==FAILED&&first.source.calls==0&&second.source.calls==0);}
|
||||
// Rifle moves must obey the same irreversible cancellation boundary as launchers.
|
||||
// Native inventory processing may synchronously invoke mission/ownership events.
|
||||
for(int loss=0;loss<4;loss++){Fixture f;auto t=f.Job();
|
||||
f.source.effect=[&](){if(loss==0)t.Stop();if(loss==1)f.truck.captured=true;if(loss==2)f.mode.active=false;if(loss==3)f.recipient.group=&f.otherGroup;f.Move();};
|
||||
f.source.callback=1;assert(t.Tick(0)==FAILED&&t.Submitted()&&!t.Unresolved()&&f.source.calls==1);
|
||||
assert(t.Tick(1)==FAILED&&f.source.calls==1);}
|
||||
// Callback without custody after cancellation retains uncertainty until the
|
||||
// original physical identity arrives, without resurrecting success or retry.
|
||||
{Fixture f;auto t=f.Job();f.source.effect=[&](){t.Stop();};f.source.callback=1;
|
||||
assert(t.Tick(0)==FAILED&&t.Submitted()&&t.Unresolved());f.Move();assert(!t.Unresolved());assert(t.Tick(1)==FAILED&&f.source.calls==1);}
|
||||
// Coordinator must never book a cancelled native rifle transfer as delivery.
|
||||
{SupplyFixture f;f.source.effect=[&](){f.c.m_Move->Stop();f.Move();};f.source.callback=1;
|
||||
f.c.Transfer(0);assert(f.c.m_Spent.Count()==1&&f.c.m_Delivered==0);f.c.Transfer(1000);
|
||||
assert(f.c.finishes==1&&f.c.m_Delivered==0&&f.c.m_Spent.Count()==1&&f.source.calls==1);}
|
||||
// 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;
|
||||
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')
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
"""Actual coordinator decisions/transitions with native boundaries doubled; no engine proof."""
|
||||
import os,re,sys
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
|
||||
sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
SOURCE=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',REPO/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
def method(signature):
|
||||
s=body(SOURCE,signature).replace('protected ','').replace('null','nullptr')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_InfantryResupplyRules.','ERR_InfantryResupplyRules::').replace('ERR_Actors.','ERR_Actors::')
|
||||
s=s.replace(' + "3 0 0"',' + Vec{3,0,0}')
|
||||
s=re.sub(r'string\.Format\("walk to reported stopped ammo truck at %1", m_Rendezvous\)', '"rendezvous"', s)
|
||||
for kind in ['ERR_HQWatchEntry','Physics']:
|
||||
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
|
||||
# Enforce native entity/reference member access becomes C++ pointer member access.
|
||||
for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_MineMove','m_Mounted','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ['AmmoState','Group','Combat','Entity']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Watch().','Watch()->').replace('GetVelocity().','GetVelocity().').replace('GetAngularVelocity().','GetAngularVelocity().')
|
||||
return s
|
||||
rules='\n'.join(method(x) for x in ['static bool Fresh','static bool Need','static bool Pair'])
|
||||
methods='\n'.join(method(x) for x in ['protected void PollTransfers','protected void ClearWaypoints','protected void Finish','void Tick(float now)','void Stop()'])
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
struct Vec {float x=0,y=0,z=0; float Length(){return std::sqrt(x*x+y*y+z*z);} static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);} Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};} string ToString(){return "position";}};
|
||||
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);} static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
|
||||
struct Ammo {bool Complete=true,valid=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int){return valid;}};
|
||||
struct ERR_HQWatchEntry {bool Closed=false;int Count=2,Fit=2,SourceId=1;float ObservedAt=0;Vec Position;Ammo* AmmoState=nullptr;};
|
||||
struct ERR_InfantryResupplyRules {
|
||||
RULES
|
||||
};
|
||||
struct Waypoint {int id;};
|
||||
struct Group {std::vector<Waypoint*> points;void RemoveWaypoint(Waypoint* p){points.erase(std::remove(points.begin(),points.end(),p),points.end());}};
|
||||
struct ERR_Actors {static inline int deleted=0;static void DeleteSpawned(Waypoint*){deleted++;}};
|
||||
struct Physics {Vec linear,angular;Vec GetVelocity(){return linear;} Vec GetAngularVelocity(){return angular;}};
|
||||
struct Entity {Physics* physics=nullptr;Physics* GetPhysics(){return physics;}};
|
||||
struct Combat {bool Held=false;};
|
||||
struct Truck {int SupplyToken=7,clears=0;bool SupplyHeld=false;struct Combat* Combat=nullptr;struct Entity* Entity=nullptr;void ClearOrder(){clears++;}};
|
||||
struct Unit {struct Group* Group=nullptr;int ForceSourceId=1;float NextStatus=-1;};
|
||||
struct Leaf {int stops=0;bool unresolved=true;void Stop(){stops++;}bool Unresolved(){return unresolved;}};
|
||||
struct Mounted{bool busy=false;int ticks=0,stops=0;bool Busy(){return busy;}void Tick(float){ticks++;}void Stop(){stops++;}};
|
||||
struct Route {int result=0,advances=0;int Advance(float,int){advances++;return result;}};
|
||||
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,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++;}
|
||||
METHODS
|
||||
};
|
||||
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
|
||||
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=ℴ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);}
|
||||
|
||||
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
|
||||
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
|
||||
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
|
||||
demand.ObservedAt=1;assert(!ERR_InfantryResupplyRules::Fresh(&demand,0));demand.ObservedAt=0;
|
||||
ammo.SmallArmsChambers=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.SmallArmsChambers=0;
|
||||
ammo.Complete=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Complete=true;
|
||||
ammo.Members=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Members=2;
|
||||
ammo.valid=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.valid=true;
|
||||
donor.SourceId=1;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));donor.SourceId=2;donor.Position.x=201;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));
|
||||
demand.Position.x=std::numeric_limits<float>::quiet_NaN();assert(!ERR_InfantryResupplyRules::Need(&demand,0));demand.Position.x=0;
|
||||
{Fixture f;f.s.Tick(0);assert(f.s.m_State==2&&!f.truck.SupplyHeld&&f.truck.clears==0&&f.s.waypointCalls==0);f.order.result=1;f.s.Tick(1000);assert(f.s.m_State==3&&f.truck.SupplyHeld&&f.truck.clears==1&&f.s.m_Order==nullptr);f.physics.linear.x=1;f.s.Tick(2000);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);f.physics.linear.x=0;f.s.Tick(3000);assert(f.s.m_HeldAt==3000&&f.donor.NextStatus==3000);ERR_HQWatchEntry report;report.Position={20,0,5};report.ObservedAt=2999;f.hq.watch.entry=&report;f.s.Tick(4000);assert(f.hq.downlinks==0);report.ObservedAt=3000;f.s.Tick(5000);assert(f.s.m_State==4&&f.hq.downlinks==1&&f.s.m_Rendezvous.x==23);f.s.Tick(6000);assert(f.s.m_State==4&&f.s.waypointCalls==0&&f.s.transfers==0);f.next.result=1;f.s.Tick(7000);assert(f.s.m_State==5&&f.s.waypointCalls==1&&f.s.transfers==1);}
|
||||
{Fixture f;f.order.result=-1;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==0&&f.truck.SupplyToken==0&&!f.truck.SupplyHeld&&f.truck.clears==0);}
|
||||
{Fixture f;f.s.m_State=4;f.order.result=-1;f.s.Tick(0);assert(f.s.waypointCalls==0&&f.s.transfers==0&&f.hq.token==0);}
|
||||
{Fixture f;f.hq.token=99;f.truck.SupplyToken=99;f.truck.SupplyHeld=true;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==99&&f.truck.SupplyToken==99&&f.truck.SupplyHeld);}
|
||||
{Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_State==7&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);leaf.unresolved=false;f.s.Tick(6000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_Move);}
|
||||
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.Finish("timeout");f.s.Tick(0);assert(f.s.m_Move==&leaf);leaf.unresolved=false;f.s.Tick(1000);assert(f.s.m_Move==nullptr);}
|
||||
{Fixture f;f.s.m_State=3;f.physics.linear.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
|
||||
{Fixture f;f.s.m_State=3;f.physics.angular.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
|
||||
{Fixture f;f.s.m_Deadline=0;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&f.hq.token==0);}
|
||||
// Waiting for physical transfer never extends the original bounded rendezvous deadline.
|
||||
{Fixture f;f.s.m_State=5;f.s.m_Deadline=1000;f.s.Tick(0);assert(f.s.transfers==1&&f.s.m_State==5);
|
||||
f.s.Tick(1000);assert(f.s.transfers==1&&f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld);}
|
||||
|
||||
{Fixture f;f.s.quiet=false;f.order.result=1;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&!f.truck.SupplyHeld);}
|
||||
{Fixture f;f.s.m_State=4;f.order.result=1;f.s.waypoints=false;f.s.Tick(0);assert(f.s.m_State==1&&f.s.waypointCalls==1&&f.s.transfers==0&&f.hq.token==0);}
|
||||
{Fixture f;f.s.m_State=3;f.s.m_HeldAt=100;ERR_HQWatchEntry report;report.ObservedAt=100;f.hq.watch.entry=&report;f.hq.next=nullptr;f.s.Tick(1000);assert(f.s.m_State==1&&f.hq.downlinks==1&&f.s.waypointCalls==0&&f.hq.token==0);}
|
||||
|
||||
}
|
||||
'''.replace('RULES',rules).replace('METHODS',methods)
|
||||
run(code)
|
||||
print('infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed')
|
||||
|
||||
# Recheck permanent initial-roster eligibility during an active trip.
|
||||
src=SOURCE
|
||||
s=body(src,'protected bool Roster()').replace('protected ','')
|
||||
s=s.replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
|
||||
s=s.replace('foreach (AIAgent agent : agents)','for (Agent* agent : agents)').replace('foreach (IEntity actor : m_Members)','for (Actor* actor : m_Members)')
|
||||
for x in ['m_Recipient','agent']:s=s.replace(x+'.',x+'->')
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Group.','Group->')
|
||||
for x in ['ERR_Actors','SCR_FactionManager','CompartmentAccessComponent']:s=s.replace(x+'.',x+'::')
|
||||
s=s.replace('m_Mode.','m_Mode->').replace('bool living;', 'bool living = false;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
|
||||
struct Group;
|
||||
struct Actor{bool alive=true,player=false,fight=true;Group* group=nullptr;int faction=1;};
|
||||
struct Agent{Actor* actor;Actor* GetControlledEntity(){return actor;}};
|
||||
struct Group{Array<Agent*> agents;void GetAgents(Array<Agent*>& out){out=agents;}};
|
||||
struct Sampler{Group* group;bool released=false;int strength=1;Array<Actor*> original;int InitialStrength(){return strength;}Actor* InitialMember(int slot){return slot>=0&&slot<(int)original.size()?original[slot]:nullptr;}bool OwnsRoster(Group* g){return g==group;}bool CanAttribute(int,Actor*){return !released;}};
|
||||
struct Unit{struct Group* Group;Sampler* StatusSampler;};
|
||||
struct Mode{int EnemyFaction(){return 1;}};
|
||||
struct ERR_Actors{static bool Alive(Actor* a){return a->alive;}static bool Player(Actor* a){return a->player;}static struct Group* Group(Actor* a){return a->group;}static bool CanFight(Actor* a){return a->fight;}};
|
||||
struct SCR_FactionManager{static int SGetFaction(Actor* a){return a->faction;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(Actor*){return false;}};
|
||||
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
|
||||
METHOD
|
||||
};
|
||||
int main(){
|
||||
Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
|
||||
// Permanent sampler release survives returning to the physical group.
|
||||
assert(!service.Roster());sampler.released=false;assert(service.Roster());
|
||||
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
|
||||
Actor foreign;foreign.group=&group;sampler.original={&foreign};assert(!service.Roster());
|
||||
// Initial roster order, not current GetAgents order, determines the slot.
|
||||
sampler.strength=2;sampler.original={&foreign,&soldier};assert(service.Roster());
|
||||
soldier.player=true;assert(!service.Roster());soldier.player=false;
|
||||
soldier.group=nullptr;assert(!service.Roster());soldier.group=&group;
|
||||
soldier.alive=false;assert(!service.Roster());soldier.alive=true;
|
||||
Agent extra{&foreign};group.agents.push_back(&extra);assert(!service.Roster());
|
||||
}
|
||||
'''.replace('METHOD',s))
|
||||
print('PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership')
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
"""Execute real finite-stock/planning/reservation bodies; native inventory/group APIs are doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
A=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))
|
||||
I=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Inventory.c'
|
||||
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('notnull ','').replace('null','nullptr')
|
||||
s=s.replace('map<string, int>', 'Map<int>*').replace('map<string, float>', 'Map<float>*')
|
||||
s=s.replace('array<IEntity> items = {};','Array<IEntity> items;').replace('array<IEntity> raw = {};','Array<IEntity> raw;').replace('array<AIAgent> agents = {};','Array<AIAgent> agents;')
|
||||
s=s.replace('out array<IEntity> items','Array<IEntity>& items').replace('array<IEntity> items','Array<IEntity>& items')
|
||||
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_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()')
|
||||
for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','m_MineMove','m_MineBatch','m_Mounted','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine','supply','m_Consumed','m_NextAttempt'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Receipt','Entity','Group','Utility','StatusSampler','AmmoState','Combat','m_Perception'):
|
||||
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('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(', 'static bool LauncherNeed(', 'static bool LauncherPair('))
|
||||
stock_method=('protected bool CompatibleStock(',) if 'protected bool CompatibleStock(' in P.read_text() else ()
|
||||
methods='\n'.join(cpp(body(P,x)) for x in stock_method+('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('))
|
||||
compatible=body(A,'static bool Compatible(')
|
||||
for a,b in {
|
||||
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->ammoComp',
|
||||
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
|
||||
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
|
||||
'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',
|
||||
'SCR_MuzzleInMagComponent.Cast(muzzle)':'(muzzle->inMag ? muzzle : nullptr)',
|
||||
}.items():compatible=compatible.replace(a,b)
|
||||
compatible=compatible.replace('array<','Array<').replace('foreach (','for (').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
|
||||
for a in ['EWeaponType','EMuzzleType']:compatible=compatible.replace(a+'.',a+'::')
|
||||
for a in ['magazine','manager','weapon','muzzle','reloadable','loaded','loadedItem','loadedSlot']:compatible=compatible.replace(a+'.',a+'->')
|
||||
compatible=compatible.replace('GetMagazineWell().','GetMagazineWell()->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
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 Clear(){this->clear();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
template<class V>struct Map{std::map<string,V>data;V Get(string k){auto it=data.find(k);return it==data.end()?V{}:it->second;}void Set(string k,V v){data[k]=v;}};
|
||||
struct Vec{float x=0,y=0,z=0;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
|
||||
struct Math{static float Min(float a,float b){return std::min(a,b);}};
|
||||
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct Magazine;struct Manager;struct Weapon;struct Muzzle;struct Item;struct Slot;
|
||||
using BaseMagazineComponent=Magazine*;using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;using InventoryItemComponent=Item*;using InventoryStorageSlot=Slot*;
|
||||
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 Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
|
||||
struct Magazine{IEntity owner=nullptr;Well*well=nullptr;int rounds=30,capacity=30;bool used=false;IEntity GetOwner(){return owner;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}bool IsUsed(){return used;}};
|
||||
struct Muzzle{bool inMag=true,canReload=true,disposable=false;Magazine*loaded=nullptr;Well*well=nullptr;int kind=0;bool CanBeReloaded(){return canReload;}bool IsDisposable(){return disposable;}Magazine*GetMagazine(){return loaded;}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 Slot{Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}};
|
||||
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct InventoryStorageManagerComponent{Array<IEntity>items;void GetItems(Array<IEntity>& out){out=items;}static auto Cast(InventoryStorageManagerComponent* p){return p;}};
|
||||
struct Prefab{string name="magazine";string GetPrefabName(){return name;}};
|
||||
struct Group;using SCR_AIGroup=Group*;
|
||||
struct Entity{Magazine*ammoComp=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;IEntity parent=nullptr;Prefab prefab;bool readyMine=false,magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
||||
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
|
||||
struct Waypoint{};
|
||||
struct Group{Array<AIAgent>agents;bool slave=false;int players=0,faction=1;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}void GetAgents(Array<AIAgent>& out){out=agents;}void RemoveWaypoint(Waypoint*){}};
|
||||
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}static bool Player(IEntity a){return a&&a->player;}static SCR_AIGroup Group(IEntity a){return a->group;}static bool CanFight(IEntity a){return a&&a->alive&&a->fight;}static void DeleteSpawned(Waypoint*){}};
|
||||
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a->faction;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity a){return a->aboard;}};
|
||||
struct ERR_Inventory{
|
||||
'''+unique+r'''
|
||||
};
|
||||
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'''
|
||||
};
|
||||
namespace ERR_ForceKind{const int CONVOY=3;}
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;int Kind=3,Id=2;};
|
||||
struct Ledger{Group* owned=nullptr;ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group* g){return g==owned?&record:nullptr;}};
|
||||
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
|
||||
struct Perception{Array<int>m_aTargets;};
|
||||
struct Utility{Perception* m_Perception=nullptr;};
|
||||
struct Sampler{Group* group;Array<IEntity>original;int InitialStrength(){return original.Count();}IEntity InitialMember(int i){return original[i];}bool CanAttribute(int i,IEntity a){return original[i]==a&&!a->released;}bool OwnsRoster(Group* g){return g==group;}};
|
||||
struct Unit{struct Group* Group=nullptr;struct Utility* Utility=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool HomeGuard=false,eligible=true;int LogisticsReservation=0;void* Downlink=nullptr;void* PendingOrder=nullptr;void* PendingConvoyOrder=nullptr;void* Radio=this;void* Assignment=nullptr;void* Waypoint=nullptr;bool Eligible(Mode*){return eligible&&LogisticsReservation==0;}};using ERR_HQUnit=Unit*;
|
||||
struct Combat{bool Held=false;};
|
||||
struct Truck{int Index=1,SupplyToken=0;bool Registered=true,Released=false,Arrived=false,SupplyHeld=false,crew=true;struct Entity* Entity=nullptr;struct Group* Group=nullptr;struct Combat* Combat=nullptr;bool CrewValid(Mode*){return crew;}};
|
||||
struct Receipt{bool Consumed=true;Array<int>Records={1,2,3,4,5,6};};
|
||||
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
|
||||
struct MineBatch{bool complete=true;Array<IEntity>items;bool Complete(){return complete;}int Count(){return items.Count();}IEntity At(int i){return items[i];}};
|
||||
struct Convoy{MineBatch*MineStock=nullptr;bool supply=false,MagazinesIssued=true,CargoIssued=true,Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
|
||||
using ERR_ConvoyTask=Convoy*;
|
||||
struct ConvoyService{Array<Convoy*>tasks;int Count(){return tasks.Count();}Convoy* At(int index){return index>=0&&index<Count()?tasks[index]:nullptr;}};
|
||||
struct ERR_SupplyConvoyTask:Convoy{static Convoy* Cast(Convoy* task){return task&&task->supply?task:nullptr;}};
|
||||
struct ERR_ConvoyRules{static bool Captured(Entity* e,Group*){return e->captured;}};
|
||||
struct ERR_ConvoyResources{static string Magazine(){return "magazine";}};
|
||||
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 ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity actor){return item&&item->parent==actor;}
|
||||
'''+compatible+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_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'''
|
||||
};
|
||||
struct Fixture{Mode mode;HQ hq;Service s;InventoryStorageManagerComponent inventory;Entity vehicle;std::array<Entity,140>items;std::array<Magazine,140>magazines;Entity rifle,launcher;Manager weapons;Weapon rifleWeapon,launcherWeapon;Muzzle rifleMuzzle,launcherMuzzle;Well well,wrongWell{2,0};Entity one,two;Agent a{&one},b{&two};Group recipients,donors;Utility recipientUtility,donorUtility;Sampler sampler;Unit recipient,donor;Combat combat;Truck truck;Receipt receipt;Cargo cargo;Convoy convoy;Ammo ammo;Entry demand,donorStatus;
|
||||
Fixture():sampler{&recipients,{&one,&two}}{
|
||||
hq.m_Mode=&mode;hq.convoy=&convoy;hq.convoys.tasks={&convoy};s.m_HQ=&hq;s.m_Mode=&mode;
|
||||
one.weapons=&weapons;two.weapons=&weapons;weapons.weapons={&rifle,&launcher};rifle.weapon=&rifleWeapon;rifleWeapon.owner=&rifle;rifleWeapon.muzzles={&rifleMuzzle};rifleMuzzle.well=&well;
|
||||
launcher.weapon=&launcherWeapon;launcherWeapon.owner=&launcher;launcherWeapon.kind=EWeaponType::WT_ROCKETLAUNCHER;launcherWeapon.muzzles={&launcherMuzzle};launcherMuzzle.well=&well;launcherMuzzle.kind=EMuzzleType::MT_RPGMuzzle;launcher.parent=&one;
|
||||
for(int i=0;i<140;i++){items[i].ammoComp=&magazines[i];magazines[i].owner=&items[i];magazines[i].well=&well;}
|
||||
vehicle.inventory=&inventory;vehicle.group=&donors;truck.Entity=&vehicle;truck.Group=&donors;truck.Combat=&combat;
|
||||
mode.ledger.owned=&donors;convoy.Receipt=&receipt;convoy.Vehicles={&truck,&truck,&truck,&truck,&truck,&truck};convoy.Cargo={&cargo};
|
||||
one.group=&recipients;two.group=&recipients;recipients.agents={&a,&b};recipient.Group=&recipients;recipient.Utility=&recipientUtility;recipient.StatusSampler=&sampler;recipient.ForceSourceId="recipient";
|
||||
donor.Group=&donors;donor.Utility=&donorUtility;donor.ForceSourceId="donor";hq.m_Units={&recipient,&donor};
|
||||
demand.SourceId="recipient";demand.AmmoState=&ammo;donorStatus.SourceId="donor";donorStatus.AmmoState=&ammo;
|
||||
for(int i=0;i<48;i++)inventory.items.Insert(&items[i]);
|
||||
}
|
||||
bool Plan(float now=0){return s.TryPlan(&recipient,&donor,&demand,&donorStatus,now);}
|
||||
void Refresh(float now,int id){demand.ObservedAt=now;donorStatus.ObservedAt=now;demand.MessageId=id;}
|
||||
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&&!truck.SupplyHeld&&!convoy.Stopped&&!s.m_Recipient&&!s.m_Donor);}
|
||||
};
|
||||
int main(){
|
||||
// Actual Compatible must reject wrong-well, empty or malformed stock before
|
||||
// any report/cooldown consumption, reservation, downlink or convoy hold.
|
||||
for(int bad=0;bad<5;bad++){Fixture f;for(auto&mag:f.magazines){if(bad==0)mag.well=&f.wrongWell;if(bad==1)mag.rounds=0;if(bad==2)mag.capacity=0;if(bad==3)mag.rounds=31;if(bad==4)mag.well=nullptr;}f.s.Stock(0);assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.rifleMuzzle.well=&f.wrongWell;assert(!f.Plan());f.Unchanged();f.rifleMuzzle.well=&f.well;assert(f.Plan());}
|
||||
// Same delivered recipient report remains available to a later compatible
|
||||
// truck sharing the real per-source consumption/cooldown maps.
|
||||
{Fixture first,second;Map<int> consumed;Map<float> cooldown;second.hq.convoys.tasks={&first.convoy,&second.convoy};second.hq.convoy=&first.convoy;second.hq.m_Units={&first.recipient,&second.donor};
|
||||
assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown));
|
||||
for(auto&mag:first.magazines){mag.well=&first.wrongWell;}first.s.Stock(0);second.s.Stock(0);assert(!first.Plan());first.Unchanged();
|
||||
assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,0));assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000&&first.recipient.LogisticsReservation==100129&&second.hq.downlinks==1&&first.hq.downlinks==0);}
|
||||
// A usable compatible item must still be a surviving unspent frozen identity.
|
||||
{Fixture f;for(auto&mag:f.magazines)mag.rounds=0;f.magazines[48].rounds=30;f.s.Stock(0);f.inventory.items.Insert(&f.items[48]);assert(!f.Plan());f.Unchanged();f.magazines[0].rounds=30;f.s.m_Spent.Insert(&f.items[0]);assert(!f.Plan());f.Unchanged();f.s.m_Spent.Clear();assert(f.Plan());}
|
||||
// Hard 32 original carriers and 96 captured magazines remain fail-closed.
|
||||
{Fixture f;f.s.Stock(0);f.sampler.original.resize(33,&f.one);assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);for(int i=48;i<97;i++){f.s.m_Stock.Insert(&f.items[i]);f.inventory.items.Insert(&f.items[i]);}assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;f.s.Stock(0);assert(f.Plan());}
|
||||
// Small-arms demand cannot borrow launcher compatibility, but the dedicated
|
||||
// launcher path still supports its real empty reloadable muzzle and rejects disposables.
|
||||
{Fixture f;f.weapons.weapons={&f.launcher};f.s.Stock(0);assert(!f.Plan());f.Unchanged();assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
|
||||
{Fixture f;f.weapons.weapons={&f.launcher};f.launcherMuzzle.disposable=true;f.s.Stock(0);assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.Unchanged();}
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);
|
||||
for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;
|
||||
assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));
|
||||
assert(f.s.m_LauncherTrip);}
|
||||
|
||||
{Fixture f;f.s.Stock(0);for(auto&mag:f.magazines)mag.well=&f.wrongWell;assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)&&f.truck.SupplyToken==0);}
|
||||
|
||||
{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());
|
||||
mounted.Finished=true;f.inventory.items.Insert(&f.items[48]);f.s.Stock(1000);
|
||||
assert(f.s.m_State==1&&f.s.m_Stock.Count()==49&&f.s.m_Stock.Contains(&f.items[48]));}
|
||||
// Bind accepts only an exact current slot and non-null shared demand histories.
|
||||
{Fixture f;Map<int> consumed;Map<float> cooldown;Convoy other;
|
||||
assert(!f.s.Bind(nullptr,0,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,-1,&consumed,&cooldown));
|
||||
assert(!f.s.Bind(&f.convoy,20,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,1,&consumed,&cooldown));
|
||||
assert(!f.s.Bind(&other,0,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,0,nullptr,&cooldown));assert(!f.s.Bind(&f.convoy,0,&consumed,nullptr));
|
||||
assert(f.s.Bind(&f.convoy,0,&consumed,&cooldown));assert(f.s.Task()==&f.convoy);assert(!f.s.Bind(&f.convoy,0,&consumed,&cooldown));}
|
||||
// Any prior startup/stock/trip/stop refuses late binding.
|
||||
for(int began=0;began<6;began++){Fixture f;Map<int> consumed;Map<float> cooldown;
|
||||
if(began==0){f.s.Stock(0);}if(began==1){f.s.m_NextTick=1000;}if(began==2){f.s.m_Trip=1;}
|
||||
if(began==3){f.s.m_StockDeadline=90000;}if(began==4){f.s.m_Stopped=true;}if(began==5){f.s.m_State=1;}
|
||||
assert(!f.s.Bind(&f.convoy,0,&consumed,&cooldown));}
|
||||
// Second convoy remains valid independently of canonical first, but never follows replacement/reordering.
|
||||
for(int mutation=0;mutation<3;mutation++){Fixture f;Convoy first,replacement;Map<int> consumed;Map<float> cooldown;
|
||||
f.hq.convoy=&first;f.hq.convoys.tasks={&first,&f.convoy};assert(f.s.Bind(&f.convoy,1,&consumed,&cooldown));
|
||||
f.s.Stock(0);assert(f.s.TruckValid()&&f.s.Task()==&f.convoy&&f.s.m_Stock.Count()==48);
|
||||
if(mutation==0)f.hq.convoys.tasks[1]=&replacement;
|
||||
if(mutation==1)std::swap(f.hq.convoys.tasks[0],f.hq.convoys.tasks[1]);
|
||||
if(mutation==2)f.hq.convoys.tasks.resize(1);
|
||||
assert(!f.s.TruckValid()&&!f.Plan()&&f.s.Task()==&f.convoy);}
|
||||
// Canonical legacy startup also pins its first identity before readiness.
|
||||
{Fixture f;Convoy replacement;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.Task()==&f.convoy);
|
||||
f.hq.convoy=&replacement;f.s.Stock(1000);assert(f.s.m_Stopped&&f.s.Task()==&f.convoy);}
|
||||
// Every bound slot owns a disjoint 128-trip token range, including its last allowed trip.
|
||||
for(int slot=0;slot<20;slot++){Fixture f;Map<int> consumed;Map<float> cooldown;
|
||||
f.hq.convoys.tasks.resize(slot+1,nullptr);f.hq.convoys.tasks[slot]=&f.convoy;assert(f.s.Bind(&f.convoy,slot,&consumed,&cooldown));
|
||||
f.s.Stock(0);assert(f.Plan());assert(f.s.m_Token==100000+slot*128+1);f.s.Finish("test released");
|
||||
f.s.m_Trip=127;f.Refresh(180000,2);assert(f.Plan(180000));assert(f.s.m_Token==100000+(slot+1)*128);
|
||||
f.s.Finish("test released");f.Refresh(360000,3);assert(!f.Plan(360000));}
|
||||
// Separate convoys share report consumption and failed-attempt cooldowns for the same recipient source.
|
||||
{Fixture first,second;Map<int> consumed;Map<float> cooldown;
|
||||
second.hq.convoys.tasks={&first.convoy,&second.convoy};second.hq.convoy=&first.convoy;
|
||||
assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown));
|
||||
first.s.Stock(0);second.s.Stock(0);first.hq.routeAvailable=false;assert(first.Plan());
|
||||
assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000);
|
||||
second.Refresh(180000,1);assert(!second.Plan(180000));second.Refresh(179999,2);assert(!second.Plan(179999));
|
||||
second.Refresh(180000,2);assert(second.Plan(180000));assert(first.s.m_Consumed->Get("recipient")==2&&first.s.m_NextAttempt->Get("recipient")==360000);}
|
||||
// Actual inventory aggregation collapses repeated native storage references, not distinct identical-prefab magazines.
|
||||
{Fixture f;f.inventory.items.Insert(&f.items[0]);f.inventory.items.Insert(nullptr);f.s.Stock(0);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Count()==48);assert(f.s.m_Stock[0]!=f.s.m_Stock[1]);}
|
||||
// Malformed/non-magazine items never become spendable stock; native startup partial cargo stays partial.
|
||||
{Fixture f;f.inventory.items={&f.items[0],&f.items[1],&f.items[2],nullptr};f.items[1].magazine=false;f.items[2].magazine=false;f.items[2].prefab.name="rifle";f.s.Stock(0);assert(f.s.m_Stock.Count()==1&&f.s.m_State==1);}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.s.Stock(89999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());f.s.Stock(90000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);f.cargo.Finished=true;f.inventory.items={&f.items[0]};f.s.Stock(1000);assert(f.s.m_Stock.Count()==1&&!f.s.m_Stopped);}
|
||||
{Fixture f;for(int i=48;i<97;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(auto& item:f.items)f.inventory.items.Insert(&item);f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.vehicle.inventory=nullptr;f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());}
|
||||
{Fixture f;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.receipt.Consumed=false;f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Supply weapons settle after crew deadline; matching ammo must be issued and settled before snapshot.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.s.Stock(25000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(31000);assert(!f.s.m_Stopped&&f.s.m_State==0);f.convoy.MagazinesIssued=true;f.cargo.Finished=false;
|
||||
f.s.Stock(32000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.cargo.Finished=true;
|
||||
f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(33000);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Earliest admission: producer may finish weapons only at original Deadline+30000.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.cargo.Finished=false;f.s.Stock(0);
|
||||
f.s.Stock(59999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());
|
||||
f.convoy.MagazinesIssued=true;f.s.Stock(60000);assert(!f.s.m_Stopped&&f.s.m_State==0);
|
||||
f.cargo.Finished=true;f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(89000);
|
||||
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Missing first-phase submission and missing second phase are bounded by one immutable deadline.
|
||||
for(int phase=0;phase<2;phase++){Fixture f;f.convoy.supply=true;f.convoy.CargoIssued=phase!=0;f.convoy.MagazinesIssued=false;
|
||||
f.s.Stock(25000);f.s.Stock(114999);assert(!f.s.m_Stopped&&f.s.m_StockDeadline==115000&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(115000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Faction catalogs may yield 96 distinct magazine identities; weapons are excluded.
|
||||
{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]));}
|
||||
// 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);}
|
||||
// An accepted unresolved rifle move keeps both original claims across a new
|
||||
// report after the shared180s cooldown. Distinct convoy slots own distinct tokens.
|
||||
{Fixture first,second;Leaf leaf;leaf.unresolved=true;Map<int> consumed;Map<float> cooldown;
|
||||
first.hq.convoys.tasks={&first.convoy,&second.convoy};first.hq.m_Units={&first.recipient,&first.donor,&second.donor};second.s.m_HQ=&first.hq;
|
||||
assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown));
|
||||
first.s.Stock(0);second.s.Stock(0);assert(first.Plan());first.s.m_Move=&leaf;first.s.Finish("accepted native rifle move remains unresolved");
|
||||
assert(first.s.m_Move==&leaf&&leaf.Unresolved());
|
||||
first.Refresh(180001,2);second.donorStatus.ObservedAt=180001;
|
||||
assert(!second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));
|
||||
assert(first.s.m_State==7&&first.recipient.LogisticsReservation==100001&&first.truck.SupplyToken==100001);
|
||||
assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000&&second.s.m_Trip==0);
|
||||
leaf.unresolved=false;first.s.Finish("settled");assert(first.recipient.LogisticsReservation==0);
|
||||
assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));
|
||||
assert(first.recipient.LogisticsReservation==100129&&second.truck.SupplyToken==100129&&first.truck.SupplyToken==0);}
|
||||
// A submitted unknown mine retains the real HQ claim across a newer delivered
|
||||
// report and expired shared cooldown; a second truck cannot race the same actor.
|
||||
{Fixture first,second;Leaf leaf;leaf.unresolved=true;first.s.Stock(0);assert(first.Plan());first.s.m_MineMove=&leaf;first.s.m_MineTrip=true;first.s.Finish("unknown native move");
|
||||
assert(first.s.m_State==7&&first.recipient.LogisticsReservation==100001&&first.truck.SupplyToken==100001);
|
||||
second.s.Stock(0);second.s.m_HQ=&first.hq;first.hq.convoy=&second.convoy;first.hq.convoys.tasks={&second.convoy};second.s.m_Consumed=first.s.m_Consumed;second.s.m_NextAttempt=first.s.m_NextAttempt;
|
||||
first.Refresh(180001,2);second.donorStatus.ObservedAt=180001;
|
||||
assert(!second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));assert(first.recipient.LogisticsReservation==100001);
|
||||
leaf.unresolved=false;first.s.Finish("settled");assert(first.recipient.LogisticsReservation==0);
|
||||
assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));}
|
||||
// Removed stock is unavailable; a different magazine added after capture does not replace its identity.
|
||||
{Fixture f;f.s.Stock(0);f.inventory.items={&f.items[48]};assert(!f.Plan());f.Unchanged();f.inventory.items.Insert(&f.items[0]);assert(f.Plan());assert(f.s.m_Stock.Count()==48&&!f.s.m_Stock.Contains(&f.items[48]));}
|
||||
{Fixture f;f.s.Stock(0);f.s.m_Spent=f.s.m_Stock;assert(!f.Plan());f.Unchanged();}
|
||||
// A delivered report is tied to exact original source identities and original still-owned members.
|
||||
{Fixture f;f.s.Stock(0);f.demand.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.donorStatus.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.one.released=true;assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);}
|
||||
{Fixture f;f.s.Stock(0);Entity foreign;foreign.group=&f.recipients;Agent extra{&foreign};f.recipients.agents.Insert(&extra);assert(!f.Plan());f.Unchanged();}
|
||||
// Real HQ force reservation refuses busy/support-ineligible or silent-radio recipients without consuming the demand.
|
||||
for(int refusal=0;refusal<5;refusal++){Fixture f;f.s.Stock(0);void* owned=&f;
|
||||
if(refusal==0){f.recipient.Assignment=owned;}if(refusal==1){f.recipient.Downlink=owned;}if(refusal==2){f.recipient.Waypoint=owned;}if(refusal==3){f.recipient.Radio=nullptr;}if(refusal==4){f.recipient.eligible=false;}
|
||||
assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);
|
||||
f.recipient.Assignment=nullptr;f.recipient.Downlink=nullptr;f.recipient.Waypoint=nullptr;f.recipient.Radio=&f;f.recipient.eligible=true;
|
||||
assert(f.Plan());assert(f.s.m_Trip==1&&f.s.m_Consumed->Get("recipient")==1&&f.recipient.LogisticsReservation==100001);}
|
||||
{Fixture f;f.s.Stock(0);f.recipient.LogisticsReservation=7;assert(!f.Plan());assert(f.recipient.LogisticsReservation==7&&f.s.m_Trip==0&&f.hq.downlinks==0);}
|
||||
// Failed truck downlink is still a consumed planning attempt; cleanup preserves all cargo and other ownership.
|
||||
{Fixture f;f.s.Stock(0);f.hq.routeAvailable=false;assert(f.Plan());assert(f.s.m_State==1&&f.s.m_Trip==1&&f.s.m_Consumed->Get("recipient")==1&&f.recipient.LogisticsReservation==0&&f.truck.SupplyToken==0&&f.inventory.items.Count()==48);
|
||||
f.hq.routeAvailable=true;f.Refresh(180000,1);assert(!f.Plan(180000)); // same consumed message cannot replay after cooldown
|
||||
f.Refresh(179999,2);assert(!f.Plan(179999));f.Refresh(180000,2);assert(f.Plan(180000));assert(f.s.m_Trip==2&&f.s.m_Deadline==300000);}
|
||||
// A stale-but-still-admissible demand gets only the remaining evidence horizon, never a new full lifetime.
|
||||
{Fixture f;f.s.Stock(0);assert(f.Plan(120000));assert(f.s.m_Deadline==180000);assert(!f.Plan(120000)&&f.hq.downlinks==1);}
|
||||
// Independent guarded failures do not leak reservations or acquire replacement force identities.
|
||||
for(int refusal=0;refusal<7;refusal++){Fixture f;f.s.Stock(0);
|
||||
if(refusal==0){f.recipient.HomeGuard=true;}if(refusal==1){f.truck.Combat->Held=true;}if(refusal==2){f.vehicle.captured=true;}if(refusal==3){f.mode.ledger.record.Eliminated=true;}if(refusal==4){f.donor.PendingConvoyOrder=&f;}if(refusal==5){f.ammo.Complete=false;}if(refusal==6){f.demand.Position.x=201;}
|
||||
assert(!f.Plan());f.Unchanged();}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual Compatible and bounded stock preflight before report/claim consumption, later compatible truck, launcher controls, convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup')
|
||||
Reference in New Issue
Block a user