206 lines
7.7 KiB
C
206 lines
7.7 KiB
C
// 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();
|
|
}
|
|
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;
|
|
void ERR_BindEnemySkill(ERR_GameMode mode, SCR_AIGroup group)
|
|
{
|
|
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, GetOwner())) return;
|
|
if (!m_ERRSkillMode) m_ERRPreviousSkill = GetAISkill();
|
|
m_ERRSkillMode = mode; m_ERRSkillGroup = group;
|
|
SetAISkill(GetAISkill());
|
|
}
|
|
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())) skill = ERR_EnemyEquipmentRules.Floor(skill);
|
|
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;
|
|
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<IEntity> 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 (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
|
|
{
|
|
SCR_AIGroup Group;
|
|
int Scans;
|
|
float ReadyAt;
|
|
}
|
|
|
|
class ERR_EnemyEquipment
|
|
{
|
|
protected ERR_GameMode m_Mode;
|
|
protected ref array<ref ERR_EnemyEquipmentGroup> m_Groups = {};
|
|
protected ref array<ref ERR_EnemyEquipmentEntry> m_Entries = {};
|
|
protected float m_NextCheck;
|
|
protected bool m_Stopped;
|
|
void ERR_EnemyEquipment(ERR_GameMode mode) { m_Mode = mode; }
|
|
void RegisterGroup(SCR_AIGroup group)
|
|
{
|
|
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;
|
|
m_Groups.Insert(state);
|
|
}
|
|
void UnregisterGroup(SCR_AIGroup group)
|
|
{
|
|
foreach (ERR_EnemyEquipmentGroup state : m_Groups) if (state.Group == group) state.Scans = 5;
|
|
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<AIAgent> 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.Combat = SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent));
|
|
if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, state.Group);
|
|
m_Entries.Insert(entry);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
entry.Check(m_Mode);
|
|
}
|
|
}
|
|
void Stop()
|
|
{
|
|
if (m_Stopped) return;
|
|
m_Stopped = true;
|
|
foreach (ERR_EnemyEquipmentEntry entry : m_Entries) entry.Stop(m_Mode);
|
|
m_Groups.Clear();
|
|
}
|
|
}
|