Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

354 lines
18 KiB
C

// Decisions consumed by the finite original-item controller below.
class ERR_EngineerMineRules
{
// Straight XZ corridors are conservative local checks, not native route proofs.
static bool CorridorClear(vector start, vector end, array<vector> priorSites)
{
if (!ERR_ForceRules.FiniteVector(start) || !ERR_ForceRules.FiniteVector(end)) return false;
if (!priorSites) return true;
if (priorSites.Count() > 32) return false;
float dx = end[0] - start[0], dz = end[2] - start[2];
float squared = dx * dx + dz * dz;
if (!ERR_ForceRules.Finite(squared)) return false;
foreach (vector site : priorSites)
{
if (!ERR_ForceRules.FiniteVector(site)) return false;
float along = 0;
if (squared > 0)
{
float projection = (site[0] - start[0]) * dx + (site[2] - start[2]) * dz;
if (!ERR_ForceRules.Finite(projection)) return false;
along = projection / squared;
if (!ERR_ForceRules.Finite(along)) return false;
along = Math.Clamp(along, 0, 1);
}
float x = start[0] + along * dx - site[0], z = start[2] + along * dz - site[2];
float clearance = x * x + z * z;
if (!ERR_ForceRules.Finite(clearance) || clearance <= 400) return false;
}
return true;
}
static bool WithinWorkArea(vector post, vector actor)
{
if (!ERR_ForceRules.FiniteVector(post) || !ERR_ForceRules.FiniteVector(actor)) return false;
float distance = vector.DistanceXZ(post, actor);
return ERR_ForceRules.Finite(distance) && distance <= 60;
}
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 ref array<vector> m_PriorSites = {};
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 float m_NextArmLog;
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; }
bool BlocksNextPost(ERR_ForceRecord record)
{
if (!record || record != m_Record) return false;
if (m_Begun && !Finished()) return true;
// A late terminal callback or exact deletion settles only the old job.
// Destroyed but surviving items and uncertain native refusals remain pinned.
return m_Removal && !m_Removal.Result && m_Mine;
}
protected bool Refuse() { Stop(); return false; }
protected bool Quiet()
{
return m_Unit && !m_Unit.InfantryCombat.Held && !ERR_HunterCombat.Active(m_Group);
}
// Separate delivered-HQ-order authority from a checkpoint's original hold order.
// Shared custody, roster, combat, deadline and arming checks remain unchanged.
protected bool OrderValid()
{
if (!m_Task || m_Unit.Task != m_Task || !m_Assignment || m_Unit.Assignment != m_Assignment) return false;
if (!m_Task.RoadIntercept || m_Task.ReleaseOnly || m_Task.Investigation() || !m_Task.SecuritySite.IsEmpty()) return false;
if (m_Unit.PendingTask || m_Unit.PendingOrder || vector.Distance(m_Task.Destination, m_Rally) > 0.01) return false;
SCR_AIGroupUtilityComponent groupUtility = m_Group.GetGroupUtilityComponent();
return groupUtility && groupUtility.IsPositionAllowed(m_Position);
}
protected void OnArmed() {}
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 || !OrderValid()) return false;
if (m_Unit.ReservedForSupport || m_Unit.LogisticsReservation > 0 || m_Group.IsSlave() || m_Group.GetPlayerCount(true) > 0) return false;
if (!m_Mode.EnemyFaction() || m_Group.GetFaction() != m_Mode.EnemyFaction() || !m_Unit.StatusSampler) return false;
if (!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;
vector actorPosition = m_Actor.GetOrigin();
if (!ERR_EngineerMineRules.WithinWorkArea(m_Rally, actorPosition) || !ERR_EngineerMineRules.WithinWorkArea(m_Rally, m_Position)) return false;
if (!ERR_EngineerMineRules.CorridorClear(actorPosition, actorPosition, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Position, m_Position, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Rally, m_Rally, m_PriorSites)) 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, array<vector> priorSites = null)
{
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 (priorSites)
{
if (priorSites.Count() > 32) return Refuse();
foreach (vector prior : priorSites)
{
if (!ERR_ForceRules.FiniteVector(prior)) return Refuse();
m_PriorSites.Insert(prior);
}
}
if (!SelectActor() || !Owned() || !Quiet()) return Refuse();
if (!ERR_EngineerMineRules.CorridorClear(m_Actor.GetOrigin(), m_Position, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Position, m_Rally, m_PriorSites)) 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)) { ERR_HQ.Event("ENGINEER MINE", "placement deadline reached before arming"); Stop(); return; }
m_LastTick = now;
if (!Owned() || !MineValid()) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: original soldier, item or HQ order no longer eligible"); Stop(); return; }
if (!Quiet()) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: squad entered combat"); Stop(); return; }
if (m_Task && now >= m_Task.ExpiresAt) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: road-post order expired"); 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) { ERR_HQ.Event("ENGINEER MINE", "native inventory refused mine removal"); Stop(); return; }
return;
}
if (m_State == 2)
{
if (!m_Removal || m_Removal.Result < 0) { ERR_HQ.Event("ENGINEER MINE", "native inventory mine-removal callback failed"); 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))
{
if (now >= m_NextArmLog)
{
m_NextArmLog = now + 15000;
if (m_FriendlyNear) ERR_HQ.Event("ENGINEER MINE", "arming delayed: friendly or unknown-affiliation unit within 20m");
else ERR_HQ.Event("ENGINEER MINE", "arming delayed: loose mine has not settled at placement site");
}
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);
OnArmed();
ERR_HQ.Event("ENGINEER MINE", "original PMN4 armed after engineer withdrawal and physical friendly clearance");
}
Stop();
}
}