// Owned AI startup-equipment adaptation; source hashes in docs/ai-equipment-migration.md. // HQ supplies the physical groups. No independent spawn/order timers exist here. class ERR_EnemyEquipmentRules { static bool Eligible(ERR_GameMode mode, SCR_AIGroup group, IEntity actor) { if (!Replication.IsServer() || SCR_Global.IsEditMode() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return false; if (!group || !actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group) return false; SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent(); return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && group.GetFaction() == mode.EnemyFaction() && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group); } static EAISkill Floor(EAISkill skill) { if (skill < EAISkill.EXPERT) return EAISkill.EXPERT; return skill; } } // The binding is explicit and mission-local; initialization of ordinary AI is untouched. modded class SCR_AICombatComponent { protected ERR_GameMode m_ERRSkillMode; protected SCR_AIGroup m_ERRSkillGroup; protected EAISkill m_ERRPreviousSkill; protected EAISkill m_ERRRequestedSkill; void ERR_BindEnemySkill(ERR_GameMode mode, SCR_AIGroup group) { if (!ERR_EnemyEquipmentRules.Eligible(mode, group, GetOwner())) return; if (!m_ERRSkillMode) { m_ERRPreviousSkill = GetAISkill(); m_ERRRequestedSkill = GetAISkill(); } m_ERRSkillMode = mode; m_ERRSkillGroup = group; ERR_RefreshEnemySkill(); } void ERR_RefreshEnemySkill() { if (!m_ERRSkillMode || !ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())) return; EAISkill skill = ERR_EnemyGunnerSkill.Profile(m_ERRRequestedSkill, ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())); if (GetAISkill() != skill) super.SetAISkill(skill); } void ERR_ReleaseEnemySkill(ERR_GameMode mode) { if (!m_ERRSkillMode || m_ERRSkillMode != mode) return; m_ERRSkillMode = null; m_ERRSkillGroup = null; super.SetAISkill(m_ERRPreviousSkill); } override void SetAISkill(EAISkill skill) { if (m_ERRSkillMode && ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())) { m_ERRRequestedSkill = skill; skill = ERR_EnemyGunnerSkill.Profile(skill, ERR_EnemyGunnerSkill.OriginalGunner(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())); } super.SetAISkill(skill); } override void ResetAISkill() { super.ResetAISkill(); if (m_ERRSkillMode) SetAISkill(GetAISkill()); } } class ERR_EnemyEquipmentCallback : 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); } } class ERR_EnemyEquipmentEntry { static const ResourceName LAUNCHER = "{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et"; static const int MAX_CHECKS = 5; IEntity Actor; SCR_AIGroup Group; SCR_AICombatComponent Combat; ref ERR_EnemyOptics Optics; int Checks; int SkillChecks; bool Rolled; bool Selected; bool Submitted; bool Complete; bool Released; ref ERR_EnemyEquipmentCallback Operation; protected bool Finish(string result) { Complete = true; if (Selected) ERR_HQ.Event("STINGER", result); return true; } bool Check(ERR_GameMode mode) { if (Complete) return true; if (!ERR_EnemyEquipmentRules.Eligible(mode, Group, Actor)) return Finish("startup equipment cancelled: actor no longer eligible"); Checks++; if (!Rolled) { int oneIn = mode.Settings().Get("stingerChanceOneIn"); Rolled = true; if (oneIn > 0) Selected = Math.RandomInt(0, oneIn) == 0; } if (!Selected) return Finish("not selected"); BaseWeaponManagerComponent weapons = BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent)); InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent)); if (!weapons || !inventory) { if (Checks >= MAX_CHECKS) return Finish("inventory unavailable after bounded startup checks"); return false; } array equipped = {}; weapons.GetWeaponsList(equipped); foreach (IEntity item : equipped) { if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == LAUNCHER) return Finish("physical FIM-92 observed in weapon slot; no replacement will be issued"); } // A successful callback may precede a changed slot; dropping/looting that item // before observation must not cause a second grant. Failure permits a bounded retry. if (Submitted && Operation && Operation.Finished && Operation.Succeeded) return Finish("native insertion completed; equipment will not be replenished"); if (Checks >= MAX_CHECKS) return Finish("startup deadline reached; unresolved insertion quarantined for this actor"); if (Submitted && Operation && !Operation.Finished) return false; Operation = new ERR_EnemyEquipmentCallback(); Submitted = true; if (!inventory.TrySpawnPrefabToStorage(LAUNCHER, null, -1, EStoragePurpose.PURPOSE_WEAPON_PROXY, Operation)) Submitted = false; return false; } void Stop(ERR_GameMode mode) { Complete = true; Released = true; if (Optics) Optics.Stop(); if (Combat) Combat.ERR_ReleaseEnemySkill(mode); Combat = null; // Do not interpret Stop as cancellation of an accepted native insertion. // The service is terminal and retains this exact actor's completed entry. } } class ERR_EnemyEquipmentGroup { ref ERR_EnemyRadio Radio; SCR_AIGroup Group; int Scans; float ReadyAt; } class ERR_EnemyEquipment { protected ERR_GameMode m_Mode; protected ref array m_Groups = {}; protected ref array m_Entries = {}; protected float m_NextCheck; protected bool m_Stopped; void ERR_EnemyEquipment(ERR_GameMode mode) { m_Mode = mode; } void RegisterGroup(SCR_AIGroup group, int radioId = 0, bool fitRadios = true) { if (m_Stopped || !Replication.IsServer() || !group || m_Groups.Count() >= 128) return; foreach (ERR_EnemyEquipmentGroup existing : m_Groups) if (existing.Group == group) return; ERR_EnemyEquipmentGroup state = new ERR_EnemyEquipmentGroup(); state.Group = group; state.ReadyAt = GetGame().GetWorld().GetWorldTime() + 2000; if (fitRadios && radioId > 0) state.Radio = new ERR_EnemyRadio(group, radioId); m_Groups.Insert(state); } void UnregisterGroup(SCR_AIGroup group) { foreach (ERR_EnemyEquipmentGroup state : m_Groups) { if (state.Group != group) continue; state.Scans = 5; if (state.Radio) state.Radio.Stop(); } foreach (ERR_EnemyEquipmentEntry entry : m_Entries) if (entry.Group == group) entry.Stop(m_Mode); } protected bool Known(IEntity actor) { foreach (ERR_EnemyEquipmentEntry entry : m_Entries) if (entry.Actor == actor) return true; return false; } void Tick(float now) { if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || now < m_NextCheck) return; m_NextCheck = now + 3000; foreach (ERR_EnemyEquipmentGroup state : m_Groups) { if (state.Scans >= 5 || now < state.ReadyAt) continue; state.Scans++; if (!state.Group) continue; array agents = {}; state.Group.GetAgents(agents); foreach (AIAgent agent : agents) { if (!agent || m_Entries.Count() >= 2048) continue; IEntity actor = agent.GetControlledEntity(); if (Known(actor) || !ERR_EnemyEquipmentRules.Eligible(m_Mode, state.Group, actor)) continue; ERR_EnemyEquipmentEntry entry = new ERR_EnemyEquipmentEntry(); entry.Actor = actor; entry.Group = state.Group; entry.Optics = new ERR_EnemyOptics(actor, state.Group); entry.Combat = SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent)); if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, state.Group); m_Entries.Insert(entry); if (state.Radio) state.Radio.Register(actor); } } foreach (ERR_EnemyEquipmentEntry entry : m_Entries) { if (entry.Released) continue; if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; } // Combat may arrive after the group's initial membership notification. if (!entry.Combat && entry.SkillChecks < 5) { entry.SkillChecks++; entry.Combat = SCR_AICombatComponent.Cast(entry.Actor.FindComponent(SCR_AICombatComponent)); if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, entry.Group); } if (entry.Combat) entry.Combat.ERR_RefreshEnemySkill(); entry.Check(m_Mode); if (entry.Optics) entry.Optics.Check(m_Mode); } foreach (ERR_EnemyEquipmentGroup state : m_Groups) if (state.Radio && now >= state.ReadyAt) state.Radio.Check(m_Mode); } void Stop() { if (m_Stopped) return; m_Stopped = true; foreach (ERR_EnemyEquipmentEntry entry : m_Entries) entry.Stop(m_Mode); foreach (ERR_EnemyEquipmentGroup state : m_Groups) if (state.Radio) state.Radio.Stop(); m_Groups.Clear(); } }