Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
276 lines
14 KiB
C
276 lines
14 KiB
C
// Decisions consumed by the finite original-item controller below.
|
|
class ERR_EngineerMineRules
|
|
{
|
|
static bool Within(float started, float previous, float now)
|
|
{
|
|
return ERR_ForceRules.Finite(started) && ERR_ForceRules.Finite(previous) && ERR_ForceRules.Finite(now)
|
|
&& started >= 0 && previous >= started && now >= previous && now - started < 180000;
|
|
}
|
|
static float PrepareSince(float since, float now, bool near, bool engaged)
|
|
{
|
|
if (!ERR_ForceRules.Finite(now) || now < 0 || !near || engaged) return -1;
|
|
if (!ERR_ForceRules.Finite(since) || since < 0 || since > now) return now;
|
|
return since;
|
|
}
|
|
static bool Prepared(float since, float now)
|
|
{
|
|
return ERR_ForceRules.Finite(since) && ERR_ForceRules.Finite(now) && since >= 0 && now - since >= 10000;
|
|
}
|
|
static bool MayArm(bool grounded, bool ownerValid, float distance, bool friendlyNear)
|
|
{
|
|
return grounded && ownerValid && ERR_ForceRules.Finite(distance) && distance >= 20 && !friendlyNear;
|
|
}
|
|
}
|
|
|
|
// Callback records only terminal evidence. It never moves, arms or retries items.
|
|
class ERR_EngineerMineRemoval : ScriptedInventoryOperationCallback
|
|
{
|
|
int Result;
|
|
protected void Finish(int result) { if (!Result) Result = result; }
|
|
protected override void OnComplete() { Finish(1); }
|
|
protected override void OnAllComplete() { Finish(1); }
|
|
protected override void OnFailed() { Finish(-1); }
|
|
protected override void OnAllFailed() { Finish(-1); }
|
|
}
|
|
|
|
// One original soldier, one carried PMN4, one accepted HQ road-interception post.
|
|
// Loose cancelled items remain ordinary unarmed loot. Armed mines retain native
|
|
// lifetime; a round's world reset owns their destruction, not this controller.
|
|
class ERR_EngineerMine
|
|
{
|
|
protected ERR_GameMode m_Mode;
|
|
protected ERR_HQUnit m_Unit;
|
|
protected SCR_AIGroup m_Group;
|
|
protected ref ERR_HQTask m_Task;
|
|
protected ref ERR_HQReport m_Assignment;
|
|
protected ref ERR_ForceRecord m_Record;
|
|
protected string m_SourceId;
|
|
protected int m_Strength;
|
|
protected IEntity m_Actor;
|
|
protected int m_Slot;
|
|
protected IEntity m_Mine;
|
|
protected SCR_PressureTriggerComponent m_Trigger;
|
|
protected SCR_InventoryStorageManagerComponent m_Inventory;
|
|
protected SCR_AIUtilityComponent m_Utility;
|
|
protected ref SCR_AIBehaviorBase m_Action;
|
|
protected ref ERR_EngineerMineRemoval m_Removal;
|
|
protected ref array<IEntity> m_Attempted;
|
|
protected ref array<IEntity> m_IntelFolders = {};
|
|
protected vector m_Position;
|
|
protected vector m_Rally;
|
|
protected float m_Started = -1;
|
|
protected float m_LastTick = -1;
|
|
protected float m_PreparedAt = -1;
|
|
protected float m_NextClearance;
|
|
protected int m_State; // 0 approach,1 continuous preparation,2 removal pending,3 withdraw,4 finished
|
|
protected int m_Checked;
|
|
protected bool m_FriendlyNear;
|
|
protected bool m_Begun;
|
|
protected bool m_Stopped;
|
|
|
|
void ERR_EngineerMine(array<IEntity> attempted) { m_Attempted = attempted; }
|
|
bool Finished() { return m_Stopped || m_State == 4; }
|
|
protected bool Refuse() { Stop(); return false; }
|
|
protected bool Quiet()
|
|
{
|
|
return m_Unit && !m_Unit.InfantryCombat.Held && !ERR_HunterCombat.Active(m_Group);
|
|
}
|
|
protected bool Owned()
|
|
{
|
|
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return false;
|
|
if (!m_Unit || !m_Group || m_Unit.Group != m_Group || !m_Task || m_Unit.Task != m_Task) return false;
|
|
if (!m_Assignment || m_Unit.Assignment != m_Assignment || !m_Task.RoadIntercept || m_Task.ReleaseOnly) return false;
|
|
if (m_Task.Investigation() || !m_Task.SecuritySite.IsEmpty() || m_Unit.PendingTask || m_Unit.PendingOrder) return false;
|
|
if (m_Unit.ReservedForSupport || m_Unit.LogisticsReservation > 0 || m_Group.IsSlave() || m_Group.GetPlayerCount(true) > 0) return false;
|
|
SCR_AIGroupUtilityComponent groupUtility = m_Group.GetGroupUtilityComponent();
|
|
if (m_Started < 0 && (!groupUtility || !groupUtility.IsPositionAllowed(m_Position))) return false;
|
|
if (!m_Mode.EnemyFaction() || m_Group.GetFaction() != m_Mode.EnemyFaction() || !m_Unit.StatusSampler) return false;
|
|
if (vector.Distance(m_Task.Destination, m_Rally) > 0.01 || !m_Mode.Forces() || !m_Record) return false;
|
|
if (m_Mode.Forces().FindGroup(m_Group) != m_Record || m_Record.Group != m_Group || !m_Record.Physical || m_Record.Eliminated) return false;
|
|
if (m_Record.SourceId != m_SourceId || m_Unit.ForceSourceId != m_SourceId || m_Record.InitialStrength != m_Strength) return false;
|
|
if (!m_Unit.StatusSampler.OwnsRoster(m_Group) || m_Unit.StatusSampler.InitialStrength() != m_Strength) return false;
|
|
if (!m_Actor || !ERR_Actors.CanFight(m_Actor) || ERR_Actors.Player(m_Actor) || CompartmentAccessComponent.GetVehicleIn(m_Actor)) return false;
|
|
if (!m_Unit.StatusSampler.CanAttribute(m_Slot, m_Actor, false) || ERR_Actors.Group(m_Actor) != m_Group) return false;
|
|
if (SCR_FactionManager.SGetFaction(m_Actor) != m_Mode.EnemyFaction()) return false;
|
|
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Actor));
|
|
if (!agent || !m_Utility || agent.m_UtilityComponent != m_Utility || !m_Utility.m_AIInfo) return false;
|
|
if (!m_Inventory || m_Actor.FindComponent(SCR_InventoryStorageManagerComponent) != m_Inventory) return false;
|
|
return true;
|
|
}
|
|
protected bool MineValid()
|
|
{
|
|
if (!ERR_Actors.Alive(m_Mine) || !m_Mine.GetPrefabData() || m_Mine.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) return false;
|
|
if (!m_Trigger || m_Mine.FindComponent(SCR_PressureTriggerComponent) != m_Trigger || m_Trigger.IsActivated()) return false;
|
|
return m_Attempted && m_Attempted.Contains(m_Mine);
|
|
}
|
|
protected bool Carried()
|
|
{
|
|
if (!MineValid() || !m_Inventory) return false;
|
|
array<IEntity> items = {}; ERR_Inventory.Items(m_Inventory, items);
|
|
return items.Count() <= 128 && items.Contains(m_Mine) && ERR_InfantryAmmoTransfer.StoredUnder(m_Mine, m_Actor);
|
|
}
|
|
protected bool Grounded()
|
|
{
|
|
if (!MineValid() || !m_Inventory) return false;
|
|
InventoryItemComponent item = InventoryItemComponent.Cast(m_Mine.FindComponent(InventoryItemComponent));
|
|
if (!item || item.GetParentSlot() || m_Mine.GetParent()) return false;
|
|
array<IEntity> items = {}; ERR_Inventory.Items(m_Inventory, items);
|
|
return items.Count() <= 128 && !items.Contains(m_Mine);
|
|
}
|
|
protected bool Placed()
|
|
{
|
|
if (!Grounded() || vector.Distance(m_Mine.GetOrigin(), m_Position) > 1) return false;
|
|
Physics physics = m_Mine.GetPhysics();
|
|
return physics && physics.GetVelocity().LengthSq() <= 0.25;
|
|
}
|
|
protected void ClearAction()
|
|
{
|
|
if (m_Action && m_Actor && m_Utility && m_Utility.m_AIInfo)
|
|
{
|
|
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Actor));
|
|
if (agent && agent.m_UtilityComponent == m_Utility)
|
|
{
|
|
array<ref AIActionBase> actions = {}; m_Utility.GetActions(actions);
|
|
if (actions.Count() <= 128 && actions.Contains(m_Action)) m_Action.Complete();
|
|
}
|
|
}
|
|
m_Action = null;
|
|
}
|
|
protected void Move(vector destination)
|
|
{
|
|
ClearAction();
|
|
m_Action = new SCR_AIMoveIndividuallyBehavior(m_Utility, null, destination, 1000, 0, null, 1);
|
|
m_Utility.AddAction(m_Action);
|
|
}
|
|
protected void WaitAtMine()
|
|
{
|
|
ClearAction();
|
|
m_Action = new SCR_AIWaitBehavior(m_Utility, null);
|
|
m_Action.SetPriority(1000);
|
|
m_Utility.AddAction(m_Action);
|
|
}
|
|
protected bool CheckFriendly(IEntity entity)
|
|
{
|
|
if (++m_Checked > 256) { m_FriendlyNear = true; return false; }
|
|
if (!entity || (!ChimeraCharacter.Cast(entity) && !Vehicle.Cast(entity))) return true;
|
|
DamageManagerComponent damage = DamageManagerComponent.Cast(entity.FindComponent(DamageManagerComponent));
|
|
if (damage && damage.IsDestroyed()) return true;
|
|
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(entity.FindComponent(FactionAffiliationComponent));
|
|
Faction faction;
|
|
if (affiliation) faction = affiliation.GetAffiliatedFaction();
|
|
if (faction && m_Mode.EnemyFaction().IsFactionEnemy(faction)) return true;
|
|
// Unconscious allies and unknown affiliations both prevent arming.
|
|
m_FriendlyNear = true;
|
|
return false;
|
|
}
|
|
protected bool SelectActor()
|
|
{
|
|
if (!m_Unit.StatusSampler.OwnsRoster(m_Group) || m_Strength < 1 || m_Strength > 32) return false;
|
|
for (int slot = 0; slot < m_Strength; slot++)
|
|
{
|
|
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
|
|
if (!actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
|
|
if (!m_Unit.StatusSampler.CanAttribute(slot, actor, false) || vector.DistanceXZ(actor.GetOrigin(), m_Rally) > 20) continue;
|
|
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
|
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
|
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(actor.FindComponent(SCR_InventoryStorageManagerComponent));
|
|
if (!inventory) continue;
|
|
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
|
if (items.Count() > 128) continue;
|
|
foreach (IEntity mine : items)
|
|
{
|
|
if (!ERR_Actors.Alive(mine) || m_Attempted.Contains(mine) || !mine.GetPrefabData() || mine.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) continue;
|
|
if (!ERR_InfantryAmmoTransfer.StoredUnder(mine, actor)) continue;
|
|
SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(mine.FindComponent(SCR_PressureTriggerComponent));
|
|
if (!trigger || trigger.IsActivated() || trigger.IsActivatedByDefault()) continue;
|
|
m_Actor = actor; m_Slot = slot; m_Mine = mine; m_Trigger = trigger;
|
|
m_Inventory = inventory; m_Utility = agent.m_UtilityComponent;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
bool Begin(ERR_GameMode mode, ERR_HQUnit unit, ERR_HQTask task, vector minePosition, float now)
|
|
{
|
|
if (m_Begun || m_Stopped) return false;
|
|
m_Begun = true;
|
|
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return Refuse();
|
|
if (!unit || !unit.Group || !unit.StatusSampler || !task || unit.Task != task || !unit.Assignment) return Refuse();
|
|
if (!ERR_ForceRules.Finite(now) || now < 0 || !ERR_ForceRules.FiniteVector(minePosition)) return Refuse();
|
|
if (!m_Attempted || m_Attempted.Count() >= 256 || !mode.Forces()) return Refuse();
|
|
m_Mode = mode; m_Unit = unit; m_Task = task; m_Assignment = unit.Assignment; m_Group = unit.Group;
|
|
m_SourceId = unit.ForceSourceId; m_Strength = unit.StatusSampler.InitialStrength(); m_Record = mode.Forces().FindGroup(m_Group);
|
|
m_Position = minePosition; m_Rally = task.Destination;
|
|
float distance = vector.DistanceXZ(m_Position, m_Rally);
|
|
if (!ERR_ForceRules.FiniteVector(m_Rally) || distance < 25 || distance > 60 || now >= task.ExpiresAt) return Refuse();
|
|
if (!SelectActor() || !Owned() || !Quiet()) return Refuse();
|
|
// The shared exact-item tombstone survives pickup, transfer and late callbacks.
|
|
m_Attempted.Insert(m_Mine);
|
|
if (!Carried()) return Refuse();
|
|
if (m_Mode.MineIntel()) m_Mode.MineIntel().Candidates(m_Actor, m_IntelFolders);
|
|
m_Started = now; m_LastTick = now; m_State = 0;
|
|
Move(m_Position);
|
|
ERR_HQ.Event("ENGINEER MINE", "original carried PMN4 assigned; one approach and finite 180s deadline");
|
|
return true;
|
|
}
|
|
void Stop()
|
|
{
|
|
if (m_Stopped) return;
|
|
m_Stopped = true; m_State = 4;
|
|
ClearAction();
|
|
// Retain removal evidence and shared item quarantine. No late completion can arm.
|
|
}
|
|
void Tick(float now)
|
|
{
|
|
if (!m_Begun || Finished()) return;
|
|
if (!ERR_EngineerMineRules.Within(m_Started, m_LastTick, now)) { Stop(); return; }
|
|
m_LastTick = now;
|
|
if (!Owned() || !Quiet() || !MineValid() || now >= m_Task.ExpiresAt) { Stop(); return; }
|
|
if (m_State <= 1)
|
|
{
|
|
if (!Carried()) { Stop(); return; }
|
|
bool near = vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) <= 3;
|
|
m_PreparedAt = ERR_EngineerMineRules.PrepareSince(m_PreparedAt, now, near, false);
|
|
if (m_State == 0 && near) { m_State = 1; WaitAtMine(); }
|
|
if (!ERR_EngineerMineRules.Prepared(m_PreparedAt, now)) return;
|
|
// Set state and callback BEFORE native submission; never repeat the removal.
|
|
m_Removal = new ERR_EngineerMineRemoval(); m_State = 2;
|
|
bool accepted = m_Inventory.TryRemoveItemFromInventory(m_Mine, null, m_Removal);
|
|
if (!accepted && !m_Removal.Result) { Stop(); return; }
|
|
return;
|
|
}
|
|
if (m_State == 2)
|
|
{
|
|
if (!m_Removal || m_Removal.Result < 0) { Stop(); return; }
|
|
// Observed transfer ends our custody; a later nearby drop cannot restore it.
|
|
// Original inventory ownership may still settle after the native callback.
|
|
InventoryItemComponent item = InventoryItemComponent.Cast(m_Mine.FindComponent(InventoryItemComponent));
|
|
bool attached = m_Mine.GetParent() || (item && item.GetParentSlot());
|
|
if (attached && !ERR_InfantryAmmoTransfer.StoredUnder(m_Mine, m_Actor)) { Stop(); return; }
|
|
if (!m_Removal.Result || !Grounded()) return;
|
|
if (vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) > 3) { Stop(); return; }
|
|
// A callback proves removal, not continued custody of a distant loose item.
|
|
if (vector.Distance(m_Mine.GetOrigin(), m_Actor.GetOrigin()) > 3
|
|
|| vector.Distance(m_Mine.GetOrigin(), m_Position) > 3) { Stop(); return; }
|
|
m_Mine.SetOrigin(m_Position);
|
|
m_State = 3; Move(m_Rally);
|
|
return;
|
|
}
|
|
if (m_State != 3 || !Grounded() || vector.Distance(m_Mine.GetOrigin(), m_Position) > 1) { Stop(); return; }
|
|
if (now < m_NextClearance || vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) < 20) return;
|
|
m_NextClearance = now + 1000;
|
|
m_Checked = 0; m_FriendlyNear = false;
|
|
if (!GetGame() || !GetGame().GetWorld()) { Stop(); return; }
|
|
GetGame().GetWorld().QueryEntitiesBySphere(m_Position, 20, CheckFriendly, queryFlags: EQueryEntitiesFlags.ALL);
|
|
if (!ERR_EngineerMineRules.MayArm(Placed(), Owned() && Quiet(), vector.DistanceXZ(m_Actor.GetOrigin(), m_Position), m_FriendlyNear)) return;
|
|
m_Trigger.SetUser(m_Actor);
|
|
m_Trigger.ActivateTrigger();
|
|
if (m_Trigger.IsActivated())
|
|
{
|
|
if (m_Mode.MineIntel()) m_Mode.MineIntel().Seal(m_Actor, m_Mine, m_Position, m_IntelFolders);
|
|
ERR_HQ.Event("ENGINEER MINE", "original PMN4 armed after engineer withdrawal and physical friendly clearance");
|
|
}
|
|
Stop();
|
|
}
|
|
}
|