--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c @@ -12,6 +12,7 @@ ref ERR_SiteDefinition Definition; ERR_LocationComponent Location; IEntity Container; + ref array DepotMagazines = {}; // Exact authored startup identities; never refreshed from live inventory. SCR_AIGroup Group; ref ERR_SiteRuntime Responders; string ExpectedGuardFaction; @@ -56,6 +57,30 @@ { if (index < 0 || index >= m_Sites.Count()) return null; return m_Sites[index]; + } + // Optional ground loading consumes only retained original camp magazines. + int DepotMagazineCount(ERR_SiteRuntime site) + { + if (!Replication.IsServer() || m_Stopped || m_State != ERR_PrepareResult.READY || !site || !m_Sites.Contains(site)) return 0; + if (!site.Definition || site.Definition.Kind != ERR_LocationKind.OUTPOST || site.Definition.RevealOnEscape) return 0; + if (!site.Container || !Owns(site.Container) || !site.Responders || !site.Responders.Group) return 0; + if (site.DepotMagazines.Count() > 96) return 0; + return site.DepotMagazines.Count(); + } + void DepotStock(ERR_SiteRuntime site, out array items) + { + items = {}; + int count = DepotMagazineCount(site); + for (int i = 0; i < count; i++) items.Insert(site.DepotMagazines[i]); + } + IEntity DepotMagazine(ERR_SiteRuntime site, int index) + { + if (index < 0 || index >= DepotMagazineCount(site)) return null; + return site.DepotMagazines[index]; + } + bool DepotOriginal(ERR_SiteRuntime site, IEntity item) + { + return item && DepotMagazineCount(site) > 0 && site.DepotMagazines.Contains(item); } bool Owns(IEntity entity) { return entity && m_Entities.Contains(entity); } // Snapshot only nearby mission-owned objects. Distant sites must not block @@ -389,6 +414,8 @@ document.SetTarget(stock.TargetId); } ERR_StockPlacement placement = new ERR_StockPlacement(); + if (spec.Kind == ERR_LocationKind.OUTPOST && !spec.RevealOnEscape && stock.TargetId.IsEmpty() + && item.FindComponent(BaseMagazineComponent) && site.DepotMagazines.Count() < 96) site.DepotMagazines.Insert(item); placement.Item = item; placement.Inventory = inventory; m_Placements.Insert(placement); --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c @@ -0,0 +1,69 @@ +// Optional depot work runs after all existing tactical/logistics planners. +modded class ERR_HQ +{ + protected ref ERR_GroundDepotLoading m_GroundDepot; + int GroundSupplyCount() { if (!m_Resupply) return 0; return m_Resupply.Count(); } + ERR_InfantryResupply GroundSupplyAt(int index) { if (!m_Resupply) return null; return m_Resupply.At(index); } + ERR_HQUnit GroundUnit(SCR_AIGroup group) + { + if (!group) return null; + foreach (ERR_HQUnit unit : m_Units) if (unit && unit.Group == group) return unit; + return null; + } + bool ReserveGroundDepot(ERR_HQUnit crew, ERR_HQUnit donor, ERR_InfantryResupply service, int token) + { + if (!Replication.IsServer() || m_Stopping || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return false; + if (m_Mode.Command() != this) return false; + if (token <= 300000 || token > 300096 || !crew || !donor || crew == donor || !m_Units.Contains(crew)) return false; + if (!service || !service.DepotNeeded() || !MountedLogisticsEligible(donor)) return false; + ERR_ConvoyVehicle truck = service.DepotTruck(); + if (!truck || truck.Group != donor.Group || truck.SupplyToken > 0) return false; + if (crew.HomeGuard || crew.ReservedForSupport || crew.Task || crew.PendingTask || crew.PendingCheckin || crew.PendingConvoyOrder) return false; + if (!ReserveLogistics(crew.Group, token)) return false; + // Single authoritative call, no native mutation between validation and claims. + donor.LogisticsReservation = token; truck.SupplyToken = token; + return true; + } + void ReleaseGroundDepot(ERR_HQUnit crew, ERR_HQUnit donor, ERR_ConvoyVehicle truck, int token) + { + if (!Replication.IsServer() || token <= 300000 || token > 300096) return; + // These retained exact unit objects outlive base Stop's m_Units.Clear(). + if (crew && crew.LogisticsReservation == token) crew.LogisticsReservation = 0; + if (donor && donor.LogisticsReservation == token) donor.LogisticsReservation = 0; + if (truck && truck.SupplyToken == token) { truck.SupplyToken = 0; truck.SupplyHeld = false; } + } + override string Status() + { + string result = super.Status(); + if (m_GroundDepot) result += "\n" + m_GroundDepot.Status(); + return result; + } + override void Tick(float now) + { + super.Tick(now); + if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Mode.Command() != this) return; + if (m_GroundDepot) m_GroundDepot.Tick(now); + if (m_Stopping || !m_Mode.Active() || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return; + if (!m_GroundDepot) m_GroundDepot = new ERR_GroundDepotLoading(this, m_Mode); + m_GroundDepot.Plan(now); + } + protected void CancelGroundDepotPoll() + { + if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(PollGroundDepot); + } + protected void PollGroundDepot() + { + CancelGroundDepotPoll(); + if (!m_Stopping || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Mode.Command() != this) return; + if (!m_GroundDepot || !GetGame() || !GetGame().GetWorld() || !GetGame().GetCallqueue()) return; + m_GroundDepot.Tick(GetGame().GetWorld().GetWorldTime()); + if (m_GroundDepot.Unresolved()) GetGame().GetCallqueue().CallLater(PollGroundDepot, 1000, false); + } + override void Stop() + { + if (m_GroundDepot) m_GroundDepot.Stop(); + super.Stop(); + PollGroundDepot(); + } + void ~ERR_HQ() { CancelGroundDepotPoll(); } +} --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c @@ -0,0 +1,352 @@ +// Native storage anchors only; no invented interaction point or remote move. +class ERR_GroundDepotAccess +{ + protected IEntity m_Root; + protected IEntity m_Actor; + protected bool Filter(notnull IEntity entity) + { + return !ERR_ConvoyRules.PartOf(entity, m_Root) && !ERR_ConvoyRules.PartOf(entity, m_Actor); + } + static bool Points(IEntity root, out array points) + { + points = {}; + if (!root) return false; + ActionsManagerComponent manager = ActionsManagerComponent.Cast(root.FindComponent(ActionsManagerComponent)); + if (!manager || !manager.IsEnabled()) return false; + array contexts = {}; manager.GetContextList(contexts); + if (contexts.IsEmpty() || contexts.Count() > 128) return false; + foreach (UserActionContext context : contexts) + { + if (!context) return false; + array actions = {}; context.GetActionsList(actions); + if (actions.Count() > 64) return false; + bool storage; + foreach (BaseUserAction action : actions) + if (action && action.GetOwner() == root && (SCR_OpenStorageAction.Cast(action) || SCR_OpenVehicleStorageAction.Cast(action))) storage = true; + if (!storage) continue; + vector transform[4]; + if (!context.GetTransformationWorld(transform) || !ERR_ForceRules.FiniteVector(transform[3])) return false; + points.Insert(transform[3]); + } + return !points.IsEmpty(); + } + bool Check(IEntity root, IEntity actor) + { + ChimeraCharacter character = ChimeraCharacter.Cast(actor); + if (!character || !GetGame() || !GetGame().GetWorld()) return false; + array points; + if (!Points(root, points)) return false; + vector start = character.AimingPosition(); + if (!ERR_ForceRules.FiniteVector(start)) return false; + m_Root = root; m_Actor = actor; + foreach (vector point : points) + { + float distance = vector.Distance(start, point); + if (!(distance >= 0.05 && distance <= 3)) continue; + TraceParam trace = new TraceParam(); trace.Start = start; trace.End = point; + trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS; trace.LayerMask = TRACE_LAYER_CAMERA; + if (GetGame().GetWorld().TraceMove(trace, Filter) == 1) return true; + } + return false; + } + static bool Clear(IEntity root, IEntity actor) + { + ERR_GroundDepotAccess access = new ERR_GroundDepotAccess(); return access.Check(root, actor); + } +} + +// Opportunistic camp -> original responder -> original ammo truck. One job +// mission-wide; no new force, warehouse, stock, native inventory retry or refill. +class ERR_GroundDepotLoading +{ + protected ERR_HQ m_HQ; + protected ERR_GameMode m_Mode; + protected ref array m_Original = {}; + protected ref array m_Spent = {}; + protected bool m_Captured; + protected bool m_Stopped; + protected int m_Attempts; + protected int m_Delivered; + protected int m_State; // 0 idle,1 truck radio,2 stopped report,3 porter radio,4 pickup,5 delivery,6 returning,7 quarantine + protected int m_Token; + protected float m_Deadline; + protected float m_HeldAt = -1; + protected float m_NextPlan; + protected float m_NextTick; + protected float m_Last = -1; + protected ERR_SiteRuntime m_Site; + protected IEntity m_Cache; + protected InventoryStorageManagerComponent m_CacheInventory; + protected IEntity m_Worker; + protected SCR_AIUtilityComponent m_Utility; + protected int m_WorkerSlot; + protected ref ERR_HQUnit m_Crew; + protected ref ERR_HQUnit m_Donor; + protected ref ERR_ForceRecord m_CrewRecord; + protected ERR_InfantryResupply m_Service; + protected ERR_ConvoyTask m_Task; + protected ERR_ConvoyVehicle m_Truck; + protected IEntity m_Item; + protected vector m_CachePosition; + protected vector m_TruckPosition; + protected ref ERR_HQRoute m_Order; + protected ref SCR_AIBehaviorBase m_Action; + protected ref ERR_GroundDepotTransfer m_Move; + protected ref ERR_FiniteMagazineReservation m_Reservation; + protected bool m_PickedUp; + protected bool m_DeliveryClaimed; + protected bool m_Imported; + void ERR_GroundDepotLoading(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; } + bool Busy() { return m_State != 0; } + bool Unresolved() { return m_Move && m_Move.Unresolved(); } + string Status() { return string.Format("Ground depot: one job, state=%1; delivered=%2; attempts=%3/96; original sources=%4", m_State, m_Delivered, m_Attempts, m_Original.Count()); } + protected bool Active() + { + return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.Command() == m_HQ; + } + protected void Note(string reason) + { + if (m_HQ) m_HQ.Journal().Write("GROUND DEPOT", reason, m_Token); + } + protected static bool Present(IEntity entity) + { + if (!entity) return false; + DamageManagerComponent damage = DamageManagerComponent.Cast(entity.FindComponent(DamageManagerComponent)); + return !damage || !damage.IsDestroyed(); + } + protected bool Stationary() + { + if (!m_Truck || !m_Truck.Entity || !m_Truck.Entity.GetPhysics()) return false; + Physics physics = m_Truck.Entity.GetPhysics(); + float speed = physics.GetVelocity().Length(); float angular = physics.GetAngularVelocity().Length(); + return speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1; + } + protected bool Quiet(ERR_HQUnit unit) + { + return unit && unit.Group && unit.Utility && unit.StatusSampler && !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 WorkerValid() + { + if (!Quiet(m_Crew) || !m_Worker || !m_Utility || !m_Utility.m_AIInfo || !ERR_Actors.CanFight(m_Worker)) return false; + if (!m_Crew.StatusSampler.OwnsRoster(m_Crew.Group) || !m_Crew.StatusSampler.CanAttribute(m_WorkerSlot, m_Worker, false)) return false; + if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, m_Crew.Group, m_Worker) || CompartmentAccessComponent.GetVehicleIn(m_Worker)) return false; + SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Worker)); + return agent && agent.m_UtilityComponent == m_Utility && m_CrewRecord && m_Mode.Forces().FindGroup(m_Crew.Group) == m_CrewRecord + && m_CrewRecord.Physical && !m_CrewRecord.Eliminated; + } + protected bool Valid() + { + if (!Active() || !m_Service || m_Service.Task() != m_Task || !m_Service.DepotCurrent() || m_Service.DepotTruck() != m_Truck) return false; + if (!m_Task || !m_Task.Receipt || !m_Task.Receipt.Consumed || m_Task.Vehicles.Count() != 6 || m_Task.Vehicles[1] != m_Truck) return false; + ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Task); + if (!supply || !supply.Tactics || supply.Tactics.State() != 0 || !m_Truck.Combat || m_Truck.Combat.Held) return false; + if (!Quiet(m_Donor) || m_Donor.Group != m_Truck.Group || !m_HQ.ValidateBoundVehicle(m_Donor.Group, m_Truck.Entity)) return false; + if (!m_Site || !m_Mode.Sites() || !m_Site.Definition || m_Mode.Sites().Find(m_Site.Definition.Id) != m_Site) return false; + if (m_Site.Container != m_Cache || !Present(m_Cache) || m_Cache.FindComponent(InventoryStorageManagerComponent) != m_CacheInventory) return false; + if (!m_Site.Responders || m_Site.Responders.Group != m_Crew.Group || !m_Mode.Sites().DepotOriginal(m_Site, m_Item)) return false; + if (vector.Distance(m_Cache.GetOrigin(), m_CachePosition) > 2 || !WorkerValid()) return false; + return m_Token > 300000 && m_Truck.SupplyToken == m_Token && m_Crew.LogisticsReservation == m_Token && m_Donor.LogisticsReservation == m_Token; + } + protected void Capture() + { + if (m_Captured || !m_Mode.Sites()) return; + m_Captured = true; + for (int i = 0; i < m_Mode.Sites().Count() && i < 64; i++) + { + ERR_SiteRuntime site = m_Mode.Sites().At(i); + array originals = {}; m_Mode.Sites().DepotStock(site, originals); + foreach (IEntity item : originals) + { + if (m_Original.Count() >= 96) return; + if (item && !m_Original.Contains(item)) m_Original.Insert(item); + } + } + } + protected bool Stored(IEntity item, IEntity owner) + { + if (!item || !owner || !ERR_InfantryAmmoTransfer.StoredUnder(item, owner)) return false; + InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent)); + if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item) return false; + InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent)); + if (!inventory || inventory.GetOwner() != owner) return false; + array items = {}; ERR_Inventory.Items(inventory, items); + return items.Count() <= 128 && items.Contains(item); + } + protected bool SelectWorker(ERR_HQUnit crew) + { + if (!Quiet(crew) || crew.HomeGuard || crew.ReservedForSupport || crew.LogisticsReservation > 0) return false; + if (crew.Assignment || crew.Task || crew.PendingTask || crew.PendingOrder || crew.Downlink || crew.Waypoint) return false; + int count = crew.StatusSampler.InitialStrength(); + if (count < 1 || count > 32 || !crew.StatusSampler.OwnsRoster(crew.Group)) return false; + for (int slot = 0; slot < count; slot++) + { + IEntity actor = crew.StatusSampler.InitialMember(slot); + if (!actor || !crew.StatusSampler.CanAttribute(slot, actor, false) || !ERR_Actors.CanFight(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue; + SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor)); + if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue; + m_Worker = actor; m_WorkerSlot = slot; m_Utility = agent.m_UtilityComponent; return true; + } + return false; + } + void Plan(float now) + { + if (!Active() || Busy() || !ERR_ForceRules.Finite(now) || now < m_NextPlan || m_Attempts >= 96) return; + m_NextPlan = now + 5000; Capture(); + for (int index = 0; index < m_HQ.GroundSupplyCount() && index < 20; index++) + { + ERR_InfantryResupply service = m_HQ.GroundSupplyAt(index); + if (!service || !service.DepotNeeded()) continue; + ERR_ConvoyVehicle truck = service.DepotTruck(); + if (!truck || truck.SupplyToken > 0) continue; + ERR_HQUnit donor = m_HQ.GroundUnit(truck.Group); + if (!Quiet(donor)) continue; + ERR_HQWatchEntry report = m_HQ.Watch().Find(donor.ForceSourceId); + if (!ERR_InfantryResupplyRules.Fresh(report, now)) continue; + for (int siteIndex = 0; siteIndex < m_Mode.Sites().Count() && siteIndex < 64; siteIndex++) + { + ERR_SiteRuntime site = m_Mode.Sites().At(siteIndex); + if (!site || !site.Definition || m_Mode.Sites().DepotMagazineCount(site) <= 0) continue; + // The camp is an authored own location; only the truck position is reported. + if (vector.DistanceXZ(site.Definition.Position, report.Position) > 150) continue; + ERR_HQUnit crew = m_HQ.GroundUnit(site.Responders.Group); + if (!SelectWorker(crew)) continue; + for (int itemIndex = 0; itemIndex < m_Mode.Sites().DepotMagazineCount(site) && itemIndex < 96; itemIndex++) + { + IEntity item = m_Mode.Sites().DepotMagazine(site, itemIndex); + if (!m_Original.Contains(item) || m_Spent.Contains(item) || !ERR_GroundDepotTransfer.Magazine(item) || !Stored(item, site.Container)) continue; + // Match a real carried rifle; source camp stock cannot become arbitrary cargo. + if (!ERR_InfantryAmmoTransfer.Compatible(m_Worker, item)) continue; + int token = 300000 + m_Attempts + 1; + if (!m_HQ.ReserveGroundDepot(crew, donor, service, token)) continue; + m_Reservation = new ERR_FiniteMagazineReservation(m_Original, m_Spent, item); + if (!m_Reservation.Reserve()) { m_HQ.ReleaseGroundDepot(crew, donor, truck, token); continue; } + m_Attempts++; m_Token = token; m_Crew = crew; m_Donor = donor; m_Service = service; m_Task = service.Task(); m_Truck = truck; + m_Site = site; m_Cache = site.Container; m_CachePosition = m_Cache.GetOrigin(); m_Item = item; + m_CacheInventory = InventoryStorageManagerComponent.Cast(m_Cache.FindComponent(InventoryStorageManagerComponent)); + m_CrewRecord = m_Mode.Forces().FindGroup(crew.Group); + m_PickedUp = false; m_DeliveryClaimed = false; m_Imported = false; m_HeldAt = -1; + m_Deadline = Math.Min(now + 180000, report.ObservedAt + 240000); m_State = 1; + m_Order = m_HQ.Downlink(donor.Group, "DEPOT HOLD", "hold original truck for one finite camp magazine", report.MessageId); + if (!m_Order) Abort("truck radio unavailable"); else Note("one original camp item reserved; awaiting truck hold radio"); + return; + } + } + } + } + protected void ClearAction() + { + if (m_Action && m_Worker && m_Utility && m_Utility.m_AIInfo) + { + SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Worker)); + if (agent && agent.m_UtilityComponent == m_Utility) + { + array actions = {}; m_Utility.GetActions(actions); + if (actions.Count() <= 128 && actions.Contains(m_Action)) m_Action.Complete(); + } + } + m_Action = null; + } + protected bool Walk(vector position) + { + if (!WorkerValid() || !ERR_ForceRules.FiniteVector(position)) return false; + ClearAction(); m_Action = new SCR_AIMoveIndividuallyBehavior(m_Utility, null, ERR_Actors.Ground(position), 1000, 0, null, 0.5); + m_Utility.AddAction(m_Action); return true; + } + protected void Cleanup() + { + ClearAction(); + if (m_Reservation) m_Reservation.ReleaseUnsubmitted(); + if (m_HQ) m_HQ.ReleaseGroundDepot(m_Crew, m_Donor, m_Truck, m_Token); + m_State = 0; m_Token = 0; m_Order = null; m_Move = null; m_Reservation = null; + m_Crew = null; m_Donor = null; m_Service = null; m_Task = null; m_Truck = null; + m_Worker = null; m_Utility = null; m_Site = null; m_Cache = null; m_Item = null; m_CacheInventory = null; m_CrewRecord = null; + } + protected void Abort(string reason) + { + Note(reason); ClearAction(); m_Order = null; + if (m_Move) m_Move.Stop(); + if (Unresolved()) { m_State = 7; return; } + Cleanup(); + } + void Stop() { if (m_Stopped) return; m_Stopped = true; Abort("depot service stopped; any submitted uncertainty retains original claims"); } + bool TransferValid(ERR_GroundDepotTransfer operation) + { + if (!operation || operation != m_Move || operation.Item() != m_Item || !Valid() || !Stationary() || !m_Truck.SupplyHeld) return false; + if (operation.Delivery()) return m_PickedUp && m_State == 5 && ERR_GroundDepotAccess.Clear(m_Truck.Entity, m_Worker); + return !m_PickedUp && m_State == 4 && ERR_GroundDepotAccess.Clear(m_Cache, m_Worker); + } + bool ClaimDelivery(ERR_GroundDepotTransfer operation) + { + if (!TransferValid(operation) || !operation.Delivery() || m_DeliveryClaimed || !m_Reservation.CommittedFor(m_Item)) return false; + m_DeliveryClaimed = true; return true; + } + bool DeliveredTo(ERR_InfantryResupply service, IEntity item) + { + return !m_Imported && m_State == 5 && service == m_Service && item == m_Item && m_Move && m_Move.Delivery() + && m_Move.Confirmed() && m_Reservation && m_Reservation.CommittedFor(item) && Valid(); + } + void Tick(float now) + { + // Terminal proof only: a late callback cannot resume walking or admit stock. + if (m_State == 7) { if (!Unresolved()) Cleanup(); return; } + if (!Busy()) return; + if (!ERR_ForceRules.Finite(now) || now < m_Last || now >= m_Deadline || !Valid()) { Abort("depot deadline or original ownership lost"); return; } + m_Last = now; + if (now < m_NextTick) return; + m_NextTick = now + 1000; + if (m_State == 1 || m_State == 3) + { + int result = m_Order.Advance(now, m_HQ.Journal()); + if (result == 0) return; + if (result < 0) { Abort("depot radio interrupted"); return; } + m_Order = null; + if (m_State == 1) { m_Truck.SupplyHeld = true; m_Truck.ClearOrder(); m_State = 2; return; } + if (!Walk(m_CachePosition + "2 0 0")) { Abort("original porter cannot walk to camp cache"); return; } + m_State = 4; return; + } + if (m_State == 2) + { + if (!Stationary()) return; + if (m_HeldAt < 0) { m_HeldAt = now; m_Donor.NextStatus = now; return; } + ERR_HQWatchEntry report = m_HQ.Watch().Find(m_Donor.ForceSourceId); + if (!ERR_InfantryResupplyRules.Fresh(report, now) || report.ObservedAt < m_HeldAt) return; + if (vector.DistanceXZ(report.Position, m_Site.Definition.Position) > 150) { Abort("reported stopped truck outside camp service range"); return; } + m_TruckPosition = report.Position + "3 0 0"; + m_Order = m_HQ.Downlink(m_Crew.Group, "DEPOT COURIER", string.Format("carry one existing magazine from camp to stopped truck at %1", m_TruckPosition), report.MessageId); + if (!m_Order) { Abort("porter radio unavailable"); return; } + m_State = 3; return; + } + if (m_State == 6) + { + if (vector.DistanceXZ(m_Worker.GetOrigin(), m_CachePosition) <= 5 + && vector.DistanceXZ(m_Worker.GetOrigin(), m_Truck.Entity.GetOrigin()) >= 6) Cleanup(); + return; + } + if (!Stationary()) { Abort("held truck moved during physical loading"); return; } + if (!m_Move) + { + IEntity source = m_Cache; IEntity destination = m_Worker; + bool delivery = m_State == 5; + if (delivery) { source = m_Worker; destination = m_Truck.Entity; } + IEntity access = m_Cache; if (delivery) access = m_Truck.Entity; + if (!ERR_GroundDepotAccess.Clear(access, m_Worker)) return; + m_Move = new ERR_GroundDepotTransfer(this, source, destination, m_Item, m_Reservation, delivery); + } + int moved = m_Move.Tick(now); + if (moved == 0) return; + if (moved < 0) { Abort("native depot move refused, changed or expired; no submitted retry"); return; } + if (m_State == 4) + { + m_PickedUp = true; m_Move = null; // Retire first-leg proof before custody changes again. + if (!Walk(m_TruckPosition)) { Abort("porter cannot walk to reported truck"); return; } + m_State = 5; return; + } + if (!m_Service.AdmitDepot(this, m_Item)) { Abort("proved physical delivery could not enter finite truck allowance"); return; } + m_Imported = true; m_Delivered++; m_Move = null; + Note("one original camp magazine physically delivered and admitted; porter returning to camp"); + if (!Walk(m_CachePosition + "2 0 0")) { Cleanup(); return; } + m_State = 6; + } +} --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotTransfer.c @@ -0,0 +1,137 @@ +class ERR_GroundDepotCallback : ScriptedInventoryOperationCallback +{ + ERR_GroundDepotTransfer Transfer; + protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); } + protected override void OnAllComplete() { if (Transfer) Transfer.Acknowledge(true); } + protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); } + protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false); } +} + +// One exact native move. Its owner retains the leaf until uncertain custody +// settles; pickup and delivery never share callback state. +class ERR_GroundDepotTransfer +{ + protected ERR_GroundDepotLoading m_Owner; + protected IEntity m_From; + protected IEntity m_To; + protected IEntity m_Item; + protected InventoryStorageManagerComponent m_Source; + protected InventoryStorageManagerComponent m_Destination; + protected InventoryStorageSlot m_FromSlot; + protected BaseInventoryStorageComponent m_FromStorage; + protected BaseInventoryStorageComponent m_ToStorage; + protected ref ERR_FiniteMagazineReservation m_Reservation; + protected ref ERR_GroundDepotCallback m_Callback; + protected bool m_Delivery; + protected bool m_Submitted; + protected int m_Ack; + protected int m_State; + protected int m_Rounds; + protected int m_Capacity; + protected float m_Started = -1; + protected float m_Last = -1; + void ERR_GroundDepotTransfer(ERR_GroundDepotLoading owner, IEntity source, IEntity destination, IEntity item, ERR_FiniteMagazineReservation reservation, bool delivery) + { + m_Owner = owner; m_From = source; m_To = destination; m_Item = item; m_Reservation = reservation; m_Delivery = delivery; + if (!source || !destination || !item) return; + m_Source = InventoryStorageManagerComponent.Cast(source.FindComponent(InventoryStorageManagerComponent)); + m_Destination = InventoryStorageManagerComponent.Cast(destination.FindComponent(InventoryStorageManagerComponent)); + InventoryItemComponent inventoryItem = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent)); + if (inventoryItem) m_FromSlot = inventoryItem.GetParentSlot(); + if (m_FromSlot) m_FromStorage = m_FromSlot.GetStorage(); + BaseMagazineComponent magazine = BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent)); + if (magazine) { m_Rounds = magazine.GetAmmoCount(); m_Capacity = magazine.GetMaxAmmoCount(); } + } + static bool Magazine(IEntity item) + { + if (!item) return false; + DamageManagerComponent damage = DamageManagerComponent.Cast(item.FindComponent(DamageManagerComponent)); + if (damage && damage.IsDestroyed()) return false; + BaseMagazineComponent magazine = BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent)); + return magazine && !magazine.IsUsed() && magazine.GetMagazineWell() && magazine.GetAmmoCount() > 0 + && magazine.GetAmmoCount() <= magazine.GetMaxAmmoCount() && magazine.GetMaxAmmoCount() <= 100000; + } + bool Submitted() { return m_Submitted; } + bool Delivery() { return m_Delivery; } + IEntity Item() { return m_Item; } + void Acknowledge(bool success) + { + if (!m_Submitted) return; + if (!success) m_Ack = -1; + else if (m_Ack == 0) m_Ack = 1; + } + protected bool Managed(InventoryStorageManagerComponent inventory, BaseInventoryStorageComponent storage, IEntity owner) + { + if (!inventory || !storage || !owner || inventory.GetOwner() != owner) return false; + if (!ERR_InfantryAmmoTransfer.StoredUnder(storage.GetOwner(), owner)) return false; + array storages = {}; inventory.GetStorages(storages, EStoragePurpose.PURPOSE_DEPOSIT); + return storages.Count() <= 64 && storages.Contains(storage); + } + protected int Contains(InventoryStorageManagerComponent inventory) + { + if (!inventory || !m_Item) return -1; + array items = {}; inventory.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT); + if (items.Count() > 128) return -1; + if (items.Contains(m_Item)) return 1; + return 0; + } + protected bool Slot(InventoryStorageSlot slot) + { + if (!m_Item || !slot || slot.GetAttachedEntity() != m_Item) return false; + InventoryItemComponent item = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent)); + return item && item.GetParentSlot() == slot; + } + protected bool OriginalProof() + { + return m_FromSlot && m_FromSlot.GetStorage() == m_FromStorage && Managed(m_Source, m_FromStorage, m_From) + && Slot(m_FromSlot) && m_Destination && m_Destination.GetOwner() == m_To + && Contains(m_Source) == 1 && Contains(m_Destination) == 0; + } + bool PhysicalProof() + { + if (!m_Item || !m_Source || !m_From || m_Source.GetOwner() != m_From) return false; + if (!Managed(m_Destination, m_ToStorage, m_To) || Contains(m_Source) != 0 || Contains(m_Destination) != 1) return false; + InventoryItemComponent item = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent)); + return item && item.GetParentSlot() && item.GetParentSlot().GetStorage() == m_ToStorage && Slot(item.GetParentSlot()); + } + bool Unresolved() + { + if (!m_Submitted || !m_Item) return false; + if (m_Ack == 1 && PhysicalProof()) return false; + if (m_Ack < 0 && OriginalProof()) return false; + return true; + } + void Stop() { if (m_State == 0) m_State = -1; } + bool Confirmed() { return m_State == 1 && m_Ack == 1 && PhysicalProof(); } + int Tick(float now) + { + if (m_State != 0) return m_State; + if (!ERR_ForceRules.Finite(now) || now < 0 || now < m_Last) { Stop(); return m_State; } + if (m_Started < 0) m_Started = now; + m_Last = now; + if (now - m_Started >= 8000 || !m_Owner || !m_Owner.TransferValid(this) || !Magazine(m_Item)) { Stop(); return m_State; } + if (!m_From || !m_To || m_From.FindComponent(InventoryStorageManagerComponent) != m_Source + || m_To.FindComponent(InventoryStorageManagerComponent) != m_Destination) { Stop(); return m_State; } + BaseMagazineComponent magazine = BaseMagazineComponent.Cast(m_Item.FindComponent(BaseMagazineComponent)); + if (magazine.GetAmmoCount() != m_Rounds || magazine.GetMaxAmmoCount() != m_Capacity) { Stop(); return m_State; } + if (!m_Submitted) + { + if (!m_Reservation || !OriginalProof()) { Stop(); return m_State; } + m_ToStorage = m_Destination.FindStorageForItem(m_Item, EStoragePurpose.PURPOSE_DEPOSIT); + if (!Managed(m_Destination, m_ToStorage, m_To) || !m_Source.CanMoveItemToStorage(m_Item, m_ToStorage, -1)) { Stop(); return m_State; } + if (!m_Delivery && !m_Reservation.Commit()) { Stop(); return m_State; } + if (!m_Reservation.CommittedFor(m_Item)) { Stop(); return m_State; } + if (m_Delivery && !m_Owner.ClaimDelivery(this)) { Stop(); return m_State; } + m_Callback = new ERR_GroundDepotCallback(); m_Callback.Transfer = this; + m_Submitted = true; + bool accepted = m_Source.TryMoveItemToStorage(m_Item, m_ToStorage, -1, m_Callback); + if (!accepted && m_Ack != 1) { Stop(); return m_State; } + } + // Native callbacks may synchronously cancel the owner or this exact leaf. + if (m_State != 0) return m_State; + if (!m_Owner || !m_Owner.TransferValid(this)) { Stop(); return m_State; } + if (m_Ack < 0) { Stop(); return m_State; } + if (m_Ack == 1 && PhysicalProof()) m_State = 1; + return m_State; + } +} --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c @@ -102,6 +102,32 @@ 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; + 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; + } + 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; --- /dev/null +++ b/tests/test_ground_depot_lifecycle.py @@ -0,0 +1,40 @@ +"""Execute actual modded HQ owner gates, priority order, Stop and terminal polling.""" +import os,re +from pathlib import Path +from policy_harness import ROOT,body,run +base=Path(os.environ.get('ERR_DEPOT_ROOT',ROOT)) +p=base/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c' +def cpp(s): + s=s.replace('protected ','').replace('override ','').replace('super.','Base::').replace('this)', 'this)') + for n in ('m_Mode','m_GroundDepot'):s=s.replace(n+'.',n+'->') + s=s.replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current') + s=s.replace('ERR_Phase.RADIO_SEARCH','2').replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('GetWorld().','GetWorld()->') + s=s.replace('Remove(PollGroundDepot)', 'Remove(0)').replace('CallLater(PollGroundDepot, 1000, false)', 'CallLater(0, 1000, false)') + return s +methods='\n'.join(cpp(body(p,t)) for t in ('override void Tick(', 'override void Stop(', 'protected void CancelGroundDepotPoll(', 'protected void PollGroundDepot(')) +run(r''' +#include +#include +std::vector events;bool server=true;struct HQ; +struct Mode{HQ*owner=nullptr;bool active=true;int phase=2;bool Active(){return active;}int Phase(){return phase;}HQ*Command(){return owner;}};Mode*current=nullptr; +struct Queue{int pending=0,calls=0;void Remove(int){pending=0;}void CallLater(int,int ms,bool repeat){assert(ms==1000&&!repeat);pending=1;calls++;}}; +struct World{float GetWorldTime(){return 500;}};struct Game{Queue queue;World world;Queue*GetCallqueue(){return &queue;}World*GetWorld(){return &world;}};Game game;Game*GetGame(){return &game;} +struct ERR_GroundDepotLoading{static inline int created=0;bool stopped=false,unknown=false;int plans=0,ticks=0;ERR_GroundDepotLoading(HQ*,Mode*){created++;}void Tick(float){ticks++;events.push_back(3);}void Plan(float){assert(!stopped);plans++;events.push_back(4);}void Stop(){stopped=true;events.push_back(1);}bool Unresolved(){return unknown;}}; +struct Base{bool m_Stopping=false;void Tick(float){events.push_back(2);}void Stop(){events.push_back(2);m_Stopping=true;}}; +struct HQ:Base{Mode*m_Mode=nullptr;ERR_GroundDepotLoading*m_GroundDepot=nullptr;METHODS}; +struct F{HQ h;Mode mode;F(){server=true;game.queue={};current=&mode;mode.owner=&h;h.m_Mode=&mode;events.clear();}}; +int main(){ + {F f;server=false;f.h.Tick(0);assert(!f.h.m_GroundDepot&&events==std::vector{2});} + {F f;current=nullptr;f.h.Tick(0);assert(!f.h.m_GroundDepot);} + {F f;f.mode.owner=nullptr;f.h.Tick(0);assert(!f.h.m_GroundDepot);} + {F f;f.mode.active=false;f.h.Tick(0);assert(!f.h.m_GroundDepot);} + {F f;f.mode.phase=1;f.h.Tick(0);assert(!f.h.m_GroundDepot);} + {F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;assert(d&&d->plans==1&&events==std::vector({2,4}));events.clear();f.h.Tick(1);assert(f.h.m_GroundDepot==d&&events==std::vector({2,3,4}));} + {F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;events.clear();f.h.Stop();assert(d->stopped&&events==std::vector({1,2,3})&&game.queue.pending==1);int plans=d->plans;f.h.Stop();assert(game.queue.pending==1&&d->plans==plans);d->unknown=false;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->plans==plans);} + {F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;f.mode.owner=nullptr;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);} + {F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;server=false;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);} + {F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;current=nullptr;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);} +} +'''.replace('METHODS',methods)) +assert 'void ~ERR_HQ() { CancelGroundDepotPoll(); }' in p.read_text() +print('PASS: actual depot HQ lifecycle, existing planner priority, server/mode/owner gates, idempotent polling and owner replacement') --- /dev/null +++ b/tests/test_ground_depot_loading.py @@ -0,0 +1,102 @@ +"""Actual depot planner/state transitions and final finite import; native boundaries doubled.""" +import os,re +from pathlib import Path +from policy_harness import ROOT,body,run +BASE=Path(os.environ.get('ERR_DEPOT_ROOT',ROOT)) +P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c' +S=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c' +source=P.read_text() +valid=body(P,'protected bool Valid(') +assert 'm_Service.DepotCurrent()' in valid and 'DepotAvailable()' not in valid and 'DepotNeeded()' not in valid +for forbidden in ('Teleport(', 'TrySpawn', 'SetAmmoCount', 'SetOrigin(', 'SetTransform('):assert forbidden not in source + +def cpp(s): + s=s.replace('protected ','').replace('ref ','').replace('null','nullptr') + for t in ('ERR_HQUnit','ERR_InfantryResupply','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_SiteRuntime','ERR_GroundDepotTransfer','ERR_GroundDepotLoading','ERR_FiniteMagazineReservation'): + s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s) + s=s.replace('ERR_FiniteMagazineReservation(m_Original, m_Spent, item)','ERR_FiniteMagazineReservation(&m_Original, &m_Spent, item)') + for ns in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_GroundDepotTransfer','ERR_InfantryAmmoTransfer','ERR_GroundDepotAccess','Math'): + s=s.replace(ns+'.',ns+'::') + for n in ('m_HQ','m_Mode','m_Service','m_Move','m_Reservation','m_Order','m_Truck','m_Crew','m_Donor','m_Worker','m_Site','m_Cache','service','truck','donor','report','site','crew','operation'): + s=re.sub(r'\b'+n+r'\.',n+'->',s) + s=s.replace('Sites().','Sites()->').replace('Watch().','Watch()->').replace('Forces().','Forces()->') + s=s.replace('site->Responders.Group','site->Responders->Group').replace('site->Definition.Position','site->Definition->Position').replace('m_Site->Definition.Position','m_Site->Definition->Position') + s=s.replace('m_Truck->Entity.GetOrigin()', 'm_Truck->Entity->GetOrigin()') + s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('string.Format','Format') + s=s.replace('InventoryStorageManagerComponent.Cast(m_Cache->FindComponent(InventoryStorageManagerComponent))','m_Cache->inventory') + s=s.replace('"3 0 0"','Vec{3}').replace('"2 0 0"','Vec{2}') + return s +methods='\n'.join(cpp(body(P,x)) for x in ('void Plan(', 'void Tick(', 'protected void Abort(', 'void Stop(', 'bool TransferValid(', 'bool ClaimDelivery(', 'bool DeliveredTo(')) +admit=cpp(body(S,'bool AdmitDepot(')).replace('job.','job->').replace('m_Stock.Insert(item); Audit(', 'm_Stock.Insert(item); Audit(') +run(r''' +#include +#include +#include +#include +#include +using string=std::string; +templatestruct Array:std::vector{using std::vector::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}}; +struct Vec{float x=0;Vec operator+(Vec b){return {x+b.x};}static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}}; +templatestring Format(string s,T...){return s;} +struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}}; +struct Math{static float Min(float a,float b){return std::min(a,b);}}; +struct Group{};struct Inventory{};struct Entity{Vec pos;Inventory*inventory=nullptr;bool stock=true,compatible=true;Vec GetOrigin(){return pos;}};using IEntity=Entity*; +struct Definition{Vec Position;};struct ERR_SiteRuntime{struct Definition*Definition=nullptr;ERR_SiteRuntime*Responders=nullptr;struct Group*Group=nullptr;IEntity Container=nullptr;Arrayitems;}; +struct Sites{Arraysites;int Count(){return sites.Count();}ERR_SiteRuntime*At(int i){return sites[i];}int DepotMagazineCount(ERR_SiteRuntime*s){return s?s->items.Count():0;}IEntity DepotMagazine(ERR_SiteRuntime*s,int i){return s->items[i];}}; +struct ERR_HQUnit{struct Group*Group=nullptr;int ForceSourceId=0,LogisticsReservation=0;float NextStatus=0;bool eligible=true;}; +struct ERR_ConvoyTask{}; +struct ERR_ConvoyVehicle{struct Group*Group=nullptr;IEntity Entity=nullptr;int SupplyToken=0;bool SupplyHeld=false;int clears=0;void ClearOrder(){clears++;}}; +struct ERR_HQWatchEntry{int SourceId=2,MessageId=1;float ObservedAt=0;Vec Position;bool valid=true;}; +struct ERR_InfantryResupplyRules{static bool Fresh(ERR_HQWatchEntry*r,float n){return r&&r->valid&&n>=r->ObservedAt&&n-r->ObservedAt<=120000;}}; +struct Watch{ERR_HQWatchEntry report;ERR_HQWatchEntry*Find(int){return &report;}}; +struct Ledger{void*FindGroup(Group*){return this;}}; +struct Mode{struct Sites sites;Ledger ledger;struct Sites*Sites(){return &sites;}Ledger*Forces(){return &ledger;}}; +struct ERR_GroundDepotLoading;struct ERR_InfantryResupply; +struct ERR_FiniteMagazineReservation{Array*original,*spent;IEntity item;bool committed=false,closed=false;ERR_FiniteMagazineReservation(Array*a,Array*b,IEntity i):original(a),spent(b),item(i){}bool Reserve(){if(!item||!original->Contains(item)||spent->Contains(item)||spent->Count()>=96)return false;spent->Insert(item);return true;}bool ReleaseUnsubmitted(){if(closed||committed)return false;closed=true;spent->RemoveItem(item);return true;}bool CommittedFor(IEntity i){return committed&&i==item&&spent->Contains(i);}}; +struct Route{int outcome=1,calls=0;int Advance(float,void*){calls++;return outcome;}}; +struct HQ{Arrayservices;ERR_HQUnit*crew=nullptr,*donor=nullptr;struct Watch watch;Route route;bool claim=true,radio=true;int orders=0,releases=0,claims=0; + int GroundSupplyCount(){return services.Count();}ERR_InfantryResupply*GroundSupplyAt(int i){return services[i];}ERR_HQUnit*GroundUnit(Group*g){return crew&&crew->Group==g?crew:donor&&donor->Group==g?donor:nullptr;}struct Watch*Watch(){return &watch;}void*Journal(){return this;} + bool ReserveGroundDepot(ERR_HQUnit*,ERR_HQUnit*,ERR_InfantryResupply*,int); + void ReleaseGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_ConvoyVehicle*t,int token){releases++;if(c&&c->LogisticsReservation==token)c->LogisticsReservation=0;if(d&&d->LogisticsReservation==token)d->LogisticsReservation=0;if(t&&t->SupplyToken==token){t->SupplyToken=0;t->SupplyHeld=false;}} + Route*Downlink(Group*,string,string,int){orders++;return radio?&route:nullptr;} +}; +struct ERR_InfantryAmmoTransfer{static bool Compatible(IEntity a,IEntity i){return a&&i&&i->compatible;}}; +struct ERR_GroundDepotAccess{static inline bool clear=true;static bool Clear(IEntity,IEntity){return clear;}}; +struct ERR_GroundDepotTransfer{static inline int constructed=0;bool delivery=false,submitted=true,unknown=true,confirmed=false;int result=0;IEntity item;ERR_FiniteMagazineReservation*r; + ERR_GroundDepotTransfer(ERR_GroundDepotLoading*,IEntity,IEntity,IEntity i,ERR_FiniteMagazineReservation*q,bool d):delivery(d),item(i),r(q){constructed++;r->committed=true;} + static bool Magazine(IEntity i){return i&&i->stock;}bool Delivery(){return delivery;}IEntity Item(){return item;}bool Unresolved(){return submitted&&unknown;}bool Confirmed(){return confirmed;}void Stop(){result=-1;}int Tick(float){return result;} +}; +struct ERR_InfantryResupply{bool available=true,needed=true;int m_DepotImports=0;ERR_ConvoyVehicle*truck=nullptr;ERR_ConvoyTask task;Arraym_Stock,m_Spent;ERR_ConvoyVehicle*m_Truck=nullptr; + bool DepotAvailable(){return available&&m_Stock.Count()<96;}bool DepotNeeded(){return DepotAvailable()&&needed;}ERR_ConvoyVehicle*DepotTruck(){return available?truck:nullptr;}ERR_ConvoyTask*Task(){return &task;}bool Contains(IEntity,IEntity i){return i!=nullptr;}void Audit(string){}bool AdmitDepot(ERR_GroundDepotLoading*,IEntity); +}; +struct ERR_GroundDepotLoading{HQ*m_HQ;Mode*m_Mode;Arraym_Original,m_Spent;bool active=true,valid=true,stationary=true,canWalk=true,m_Stopped=false,m_Captured=false,m_PickedUp=false,m_DeliveryClaimed=false,m_Imported=false;int m_State=0,m_Attempts=0,m_Delivered=0,m_Token=0,m_WorkerSlot=0,walks=0,cleanups=0,notes=0;float m_NextPlan=0,m_Deadline=0,m_Last=-1,m_NextTick=0,m_HeldAt=-1;Vec m_CachePosition,m_TruckPosition; + ERR_HQUnit*m_Crew=nullptr,*m_Donor=nullptr;ERR_SiteRuntime*m_Site=nullptr;IEntity m_Worker=nullptr,m_Cache=nullptr,m_Item=nullptr;Inventory*m_CacheInventory=nullptr;void*m_CrewRecord=nullptr;ERR_InfantryResupply*m_Service=nullptr;ERR_ConvoyTask*m_Task=nullptr;ERR_ConvoyVehicle*m_Truck=nullptr;Route*m_Order=nullptr;ERR_GroundDepotTransfer*m_Move=nullptr;ERR_FiniteMagazineReservation*m_Reservation=nullptr; + bool Active(){return active&&!m_Stopped;}bool Busy(){return m_State!=0;}bool Unresolved(){return m_Move&&m_Move->Unresolved();}bool Valid(){return Active()&&valid&&(!m_Service||m_Service->available);}bool Stationary(){return stationary;}bool Quiet(ERR_HQUnit*u){return u&&u->eligible;}bool SelectWorker(ERR_HQUnit*u){return Quiet(u)&&u->LogisticsReservation==0;}bool Stored(IEntity,IEntity){return true;}void Capture(){m_Captured=true;}void Note(string){notes++;}void ClearAction(){}bool Walk(Vec){walks++;return canWalk;} + void Cleanup(){cleanups++;if(m_Reservation)m_Reservation->ReleaseUnsubmitted();m_HQ->ReleaseGroundDepot(m_Crew,m_Donor,m_Truck,m_Token);m_State=0;m_Token=0;m_Move=nullptr;m_Order=nullptr;} + METHODS +}; +ADMISSION +bool HQ::ReserveGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_InfantryResupply*s,int token){if(!claim||c->LogisticsReservation||d->LogisticsReservation||s->truck->SupplyToken)return false;claims++;c->LogisticsReservation=d->LogisticsReservation=s->truck->SupplyToken=token;return true;} +struct F{Mode mode;HQ hq;ERR_GroundDepotLoading job;Group cg,dg;ERR_HQUnit crew,donor;Definition def;ERR_SiteRuntime site,responders;Entity worker,cache,truck,item;Inventory inv;ERR_ConvoyVehicle vehicle;ERR_InfantryResupply service; + F(){truck.pos={20};crew.Group=&cg;donor.Group=&dg;hq.crew=&crew;hq.donor=&donor;vehicle.Group=&dg;vehicle.Entity=&truck;service.truck=service.m_Truck=&vehicle;hq.services={&service};site.Definition=&def;site.Responders=&responders;responders.Group=&cg;site.Container=&cache;site.items={&item};cache.inventory=&inv;mode.sites.sites={&site};job.m_HQ=&hq;job.m_Mode=&mode;job.m_Worker=&worker;job.m_Original={&item};ERR_GroundDepotAccess::clear=true;} + void Start(){job.Plan(0);assert(job.m_State==1&&hq.claims==1&&hq.orders==1);} + void Reach(){Start();job.Tick(0);assert(job.m_State==2&&vehicle.SupplyHeld&&vehicle.clears==1);job.Tick(1000);assert(donor.NextStatus==1000&&job.walks==0);job.Tick(2000);assert(job.m_State==2);hq.watch.report.ObservedAt=3000;job.Tick(3000);assert(job.m_State==3&&hq.orders==2&&job.walks==0);job.Tick(4000);assert(job.m_State==4&&job.walks==1);} +}; +int main(){ + // No new stock or commands on missing capacity/camp/worker/report/claim. + for(int k=0;k<8;k++){F f;if(k==0)f.service.available=false;if(k==1)f.site.items.clear();if(k==2)f.crew.eligible=false;if(k==3)f.hq.watch.report.valid=false;if(k==4)f.hq.claim=false;if(k==5)f.item.compatible=false;if(k==6)f.job.m_Original.clear();if(k==7)f.service.needed=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0&&f.job.m_Spent.empty());} + {F f;f.Start();f.hq.route.outcome=-1;f.job.Tick(0);assert(!f.job.Busy()&&f.job.walks==0&&!f.vehicle.SupplyHeld&&f.job.m_Spent.empty());} + // Complete actual state sequence; first proof retires before delivery callback. + {F f;f.Reach();f.job.Tick(5000);auto*pickup=f.job.m_Move;assert(pickup&&!pickup->Delivery());pickup->unknown=false;pickup->confirmed=true;pickup->result=1;f.job.Tick(6000);assert(f.job.m_State==5&&!f.job.m_Move&&f.job.walks==2);f.job.Tick(7000);auto*delivery=f.job.m_Move;assert(delivery&&delivery!=pickup&&delivery->Delivery());delivery->unknown=false;delivery->confirmed=true;delivery->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==1&&f.job.m_Delivered==1&&f.job.m_State==6);f.job.Tick(9000);assert(!f.job.Busy()&&!f.vehicle.SupplyHeld&&f.job.m_Spent.Count()==1);} + for(bool second:{false,true}){F f;f.Reach();f.job.Tick(5000);if(second){f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);}auto*leaf=f.job.m_Move;int walks=f.job.walks;f.job.Stop();assert(f.job.m_State==7&&f.vehicle.SupplyHeld&&f.crew.LogisticsReservation>0);f.job.Tick(200000);assert(f.job.m_State==7&&f.service.m_Stock.empty()&&f.job.walks==walks);leaf->unknown=false;leaf->confirmed=true;f.job.Tick(200001);assert(!f.job.Busy()&&f.service.m_Stock.empty()&&f.job.m_Spent.Count()==1&&!f.vehicle.SupplyHeld);f.job.Plan(300000);assert(!f.job.Busy());} + // Expiry never extends the job; late proof does not adopt cargo. + {F f;f.Reach();f.job.Tick(5000);f.job.Tick(f.job.m_Deadline);assert(f.job.m_State==7);f.job.m_Move->unknown=false;f.job.Tick(999999);assert(!f.job.Busy()&&f.service.m_Stock.empty());} + // Target lifetime96 allowance: no eviction of spent/deleted identities. + {F f;f.service.m_Stock.resize(96,nullptr);f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0);} + // Filling the final lifetime slot does not invalidate the return phase. + {F f;f.service.m_Stock.resize(95,nullptr);f.Reach();f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;f.job.m_Move->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==96&&f.job.m_State==6);f.truck.pos={3};f.job.Tick(9000);assert(f.job.m_State==6&&f.vehicle.SupplyHeld);f.truck.pos={20};f.job.Tick(10000);assert(!f.job.Busy());} + // A completed physical move cannot import wrong/previously-spent/foreign identity. + {F f;f.Reach();f.job.Tick(5000);auto*l=f.job.m_Move;l->unknown=false;l->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;Entity foreign;assert(!f.service.AdmitDepot(&f.job,&foreign));f.service.m_Spent.Insert(&f.item);assert(!f.service.AdmitDepot(&f.job,&f.item));} +} +'''.replace('METHODS',methods).replace('ADMISSION',admit.replace('bool AdmitDepot(', 'bool ERR_InfantryResupply::AdmitDepot('))) +print('PASS: actual depot radio/planning/two-leg state transitions, fixed deadline, late proof quarantine, finite import and no adoption after Stop')