enum ERR_MortarSupplyState { FAILED = -1, PENDING, READY } class ERR_MortarSupplyRules { static int CycleGate(bool commandOpen, int confirmedShots, int behaviorShots, int limit, int supplyState) { if (!commandOpen) return -1; if (confirmedShots <= behaviorShots) return 0; if (confirmedShots < limit) return supplyState; return 1; } } class ERR_MortarInitialItem : ScriptedInventoryOperationCallback { bool Finished; bool Succeeded; protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } } protected override void OnComplete() { Finish(true); } protected override void OnFailed() { Finish(false); } protected override void OnAllComplete() { Finish(true); } protected override void OnAllFailed() { Finish(false); } void Rejected() { Finish(false); } } // One finite allocation, after the ENTIRE original support crew exists. Failed // or stopped setup retains physical assets and retires only its pending spawn intent. class ERR_MortarReadiness { protected ERR_MortarBattery m_Battery; protected ref array m_Members = {}; protected ref array m_Operations = {}; protected bool m_Allocated; protected bool m_Ready; protected bool m_Stopped; protected bool m_Retired; protected int m_RequestedShells; void ERR_MortarReadiness(ERR_MortarBattery battery) { m_Battery = battery; } static ERR_MortarSupplyState Population(int physical, int gunners, int operators, bool complete, bool expired) { if (physical > 4 || gunners > 3 || operators > 1) return ERR_MortarSupplyState.FAILED; if (physical == 4 && gunners == 3 && operators == 1 && complete && !expired) return ERR_MortarSupplyState.READY; if (expired) return ERR_MortarSupplyState.FAILED; return ERR_MortarSupplyState.PENDING; } static bool AllocationValid(int shells) { return shells >= 0 && shells <= 9; } int RequestedShells() { return m_RequestedShells; } bool Ready() { return m_Ready && !m_Stopped; } void Stop() { if (m_Stopped) return; m_Stopped = true; if (m_Ready || m_Retired || !m_Battery || !m_Battery.Group || !Replication.IsServer()) return; m_Retired = true; // As in Core initial guard cleanup: stopping the current request alone does // not retire the native group's still-authored future expansion intent. RetireGroup(m_Battery.Group); } static void RetireGroup(SCR_AIGroup group) { if (!group || !Replication.IsServer()) return; group.SetSpawnImmediately(false); group.m_aUnitPrefabSlots = new array(); if (!GetGame()) return; ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld()); if (world) world.PurgeSpawnRequestsForGroup(group); } protected ERR_MortarSupplyState Fail(string reason) { m_Battery.SpawnFailed(reason); Stop(); return ERR_MortarSupplyState.FAILED; } protected bool Allocate() { m_Allocated = true; // before native requests; never refill or retry a failure foreach (IEntity actor : m_Members) { if (actor == m_Battery.RadioOperator) continue; InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent)); int existing = m_Battery.InventoryShells(inventory); if (!AllocationValid(existing)) return false; if (ERR_HQRadio.Capability(actor) == 0 && !inventory.TrySpawnPrefabToStorage("{E1A5D4B878AA8980}Prefabs/Items/Equipment/Radios/Radio_R148.et")) m_Battery.Audit("RADIO EQUIP", "optional crew handset did not fit; dedicated manpack remains required"); for (int i = existing; i < 9; i++) { ERR_MortarInitialItem operation = new ERR_MortarInitialItem(); m_Operations.Insert(operation); m_RequestedShells++; if (!inventory.TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL, purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: operation)) { operation.Rejected(); return false; } } } return true; } ERR_MortarSupplyState Poll(float now) { if (m_Stopped || !Replication.IsServer() || !m_Battery || m_Battery.Stopped()) return ERR_MortarSupplyState.FAILED; if (m_Ready) return ERR_MortarSupplyState.READY; SCR_AIGroup group = m_Battery.Group; ERR_GameMode mode = ERR_GameMode.Get(); if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed"); DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent)); if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed"); bool expired = now < m_Battery.SpawnedAt || now - m_Battery.SpawnedAt >= 90000; array agents = {}; group.GetAgents(agents); array physical = {}; int gunners, operators; IEntity operatorActor; foreach (AIAgent agent : agents) { if (!agent || !agent.GetControlledEntity()) continue; IEntity actor = agent.GetControlledEntity(); if (physical.Contains(actor)) return Fail("initial support group reports duplicate physical actor"); physical.Insert(actor); if (!ERR_Actors.CanFight(actor) || SCR_FactionManager.SGetFaction(actor) != mode.EnemyFaction() || !actor.GetPrefabData()) return Fail("initial support actor unavailable or has wrong faction/resource"); ResourceName prefab = actor.GetPrefabData().GetPrefabName(); if (prefab == ERR_MortarBattery.CREW) gunners++; else if (prefab == ERR_MortarBattery.RADIO_OPERATOR) { operators++; operatorActor = actor; } else return Fail("initial support actor does not belong to authored crew catalog"); } ERR_MortarSupplyState population = Population(physical.Count(), gunners, operators, group.IsExpandComplete(), expired); if (population == ERR_MortarSupplyState.FAILED) return Fail("initial four-person support population incomplete/invalid within 90 seconds"); if (population == ERR_MortarSupplyState.PENDING) return population; if (m_Allocated) { foreach (IEntity original : m_Members) if (!physical.Contains(original)) return Fail("initial support member replaced after finite allocation"); } foreach (IEntity member : physical) { InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(member.FindComponent(InventoryStorageManagerComponent)); if (!inventory) return ERR_MortarSupplyState.PENDING; if (!m_Allocated && member != operatorActor && m_Battery.InventoryShells(inventory) < 9 && !inventory.CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT)) return ERR_MortarSupplyState.PENDING; } if (ERR_HQRadio.Capability(operatorActor) != 2) return ERR_MortarSupplyState.PENDING; // Preserve the existing earliest activation delay, but it is not readiness. if (now - m_Battery.SpawnedAt < 10000) return ERR_MortarSupplyState.PENDING; if (!m_Allocated) { if (!group.GetGroupUtilityComponent() || !group.GetGroupUtilityComponent().m_VehicleMgr) return ERR_MortarSupplyState.PENDING; SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Battery.Gun.FindComponent(SCR_AIVehicleUsageComponent)); if (!usage || !group.GetGroupUtilityComponent().m_VehicleMgr.TryAddVehicle(usage)) return Fail("native support gun registration failed"); m_Members.Copy(physical); m_Battery.RadioOperator = operatorActor; if (!Allocate()) return Fail("finite initial support shell allocation rejected; no retry"); return ERR_MortarSupplyState.PENDING; // never accept mutation-frame readiness } foreach (ERR_MortarInitialItem operation : m_Operations) { if (!operation.Finished) return ERR_MortarSupplyState.PENDING; if (!operation.Succeeded) return Fail("finite initial support shell operation failed; no retry"); } foreach (IEntity equipped : m_Members) { InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(equipped.FindComponent(InventoryStorageManagerComponent)); int shells = m_Battery.InventoryShells(inventory); if (equipped == operatorActor) { if (shells != 0) return Fail("dedicated initial manpack operator unexpectedly carries mortar shells"); } else if (shells != 9) return ERR_MortarSupplyState.PENDING; } if (m_Battery.CountShells() != 27) return ERR_MortarSupplyState.PENDING; m_Ready = true; group.ActivateAI(); foreach (AIAgent readyAgent : agents) { if (readyAgent) { readyAgent.SetPermanentLOD(0); readyAgent.ActivateAI(); } } m_Battery.Audit("CREW READY", string.Format("four original physical members, one manpack, 27 confirmed HE; new shell requests=%1; allocation permanently closed", m_RequestedShells)); return ERR_MortarSupplyState.READY; } }