// Local knowledge of mines laid by this checkpoint service. No mine immunity, // no hostile/player detection, no changes to native pressure/explosion logic. class ERR_MineAvoidanceRules { static bool Clear(vector start, vector end, vector mine, float radius) { if (!ERR_ForceRules.FiniteVector(start) || !ERR_ForceRules.FiniteVector(end) || !ERR_ForceRules.FiniteVector(mine) || !ERR_ForceRules.Finite(radius) || radius < 0) return false; vector delta = end - start; delta[1] = 0; float squared = delta.LengthSq(); if (!ERR_ForceRules.Finite(squared)) return false; float fraction; if (squared > 0.001) fraction = Math.Clamp(vector.Dot(mine - start, delta) / squared, 0, 1); return vector.DistanceXZ(mine, start + delta * fraction) > radius; } // A short chord round the danger circle, chosen toward the original route. // Used repeatedly until that route is clear; never order a straight crossing. static vector Detour(vector start, vector goal, vector mine, int side) { vector away = start - mine; away[1] = 0; if (away.LengthSq() < 0.01) return start; away.Normalize(); if (vector.DistanceXZ(start, mine) < 10) return mine + away * 16; vector tangent = Vector(away[2], 0, -away[0]); if (side < 0) tangent = -tangent; return mine + (away * 0.707107 + tangent * 0.707107) * 16; } } class ERR_KnownMine { IEntity Item; vector Position; bool Live() { if (!ERR_Actors.Alive(Item) || Item.GetParent() || vector.Distance(Item.GetOrigin(), Position) > 1) return false; SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(Item.FindComponent(SCR_PressureTriggerComponent)); return trigger && trigger.IsActivated(); } } class ERR_MineDetour { IEntity Actor; SCR_AIUtilityComponent Utility; ref SCR_AIBehaviorBase Action; vector Goal; vector Step; float Expires; float NextStep; SCR_AIGroup Group; AIWaypoint Waypoint; bool ResumeMove; bool FinalLeg; void Clear() { SCR_ChimeraAIAgent agent; if (Actor) agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(Actor)); if (agent && agent.m_UtilityComponent == Utility && Action) { array actions = {}; Utility.GetActions(actions); if (actions.Contains(Action)) Action.Complete(); } Action = null; } } class ERR_KnownMines { protected ERR_GameMode m_Mode; protected ref array m_Mines = {}; protected ref array m_Detours = {}; protected ref array m_Near = {}; protected bool m_Running, m_Stopped; void ERR_KnownMines(ERR_GameMode mode) { m_Mode = mode; } void Add(IEntity item, vector position) { if (m_Stopped || !item || m_Mines.Count() >= 48) return; foreach (ERR_KnownMine old : m_Mines) if (old.Item == item) return; ERR_KnownMine mine = new ERR_KnownMine(); mine.Item = item; mine.Position = position; if (!mine.Live()) return; m_Mines.Insert(mine); if (!m_Running) { m_Running = true; GetGame().GetCallqueue().CallLater(Tick, 250, true); } } protected bool FriendlyActor(IEntity actor) { if (!ChimeraCharacter.Cast(actor) || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false; Faction faction = SCR_FactionManager.SGetFaction(actor); return faction && m_Mode.EnemyFaction() && !m_Mode.EnemyFaction().IsFactionEnemy(faction); } protected bool Collect(IEntity actor) { if (m_Near.Count() >= 128) return false; if (FriendlyActor(actor) && !m_Near.Contains(actor)) m_Near.Insert(actor); return true; } protected ERR_KnownMine Blocking(vector start, vector goal, float radius) { ERR_KnownMine nearest; float best = float.MAX; foreach (ERR_KnownMine mine : m_Mines) { if (ERR_MineAvoidanceRules.Clear(start, goal, mine.Position, radius)) continue; float distance = vector.DistanceXZ(start, mine.Position); if (distance < best) { best = distance; nearest = mine; } } return nearest; } protected bool SafeStep(vector start, vector end, ERR_KnownMine escaping, IEntity actor) { foreach (ERR_KnownMine mine : m_Mines) { // Inside the safety circle only a strictly outward move is admissible. if (mine == escaping && vector.DistanceXZ(start, mine.Position) < 10) { vector travel = end - start, away = start - mine.Position; if (vector.DotXZ(travel, away) <= 0) return false; } else if (!ERR_MineAvoidanceRules.Clear(start, end, mine.Position, 8)) return false; } if (!ERR_HQRoadIntercept.Standable(end)) return false; // Do not hand native navigation an obstructed straight chord whose actual // detour could bend back through the mine. Wait when this corridor is blocked. TraceBox path = new TraceBox(); path.Start = start + "0 1 0"; path.End = end + "0 1 0"; path.Mins = "-0.35 -0.8 -0.35"; path.Maxs = "0.35 0.8 0.35"; path.Exclude = actor; path.Flags = TraceFlags.WORLD | TraceFlags.ENTS; return GetGame().GetWorld().TraceMove(path, null) == 1; } protected void Steer(ERR_MineDetour detour, ERR_KnownMine mine) { vector start = detour.Actor.GetOrigin(); // A cover point or movement prediction inside the danger circle is not a // destination we can safely reach. Hold outside instead of circling it. if (!ERR_MineAvoidanceRules.Clear(detour.Goal, detour.Goal, mine.Position, 10) && vector.DistanceXZ(start, mine.Position) >= 12) { detour.Clear(); detour.FinalLeg = false; detour.Step = start; detour.NextStep = GetGame().GetWorld().GetWorldTime() + 5000; detour.Action = new SCR_AIWaitBehavior(detour.Utility, null); detour.Action.SetPriority(1100); detour.Utility.AddAction(detour.Action); return; } vector first = ERR_Actors.Ground(ERR_MineAvoidanceRules.Detour(start, detour.Goal, mine.Position, 1)); vector second = ERR_Actors.Ground(ERR_MineAvoidanceRules.Detour(start, detour.Goal, mine.Position, -1)); if (vector.DistanceXZ(second, detour.Goal) < vector.DistanceXZ(first, detour.Goal)) { vector swap = first; first = second; second = swap; } bool safe = SafeStep(start, first, mine, detour.Actor); if (!safe && SafeStep(start, second, mine, detour.Actor)) { first = second; safe = true; } detour.Clear(); detour.FinalLeg = false; detour.NextStep = GetGame().GetWorld().GetWorldTime() + 1000; if (safe) { detour.Step = first; detour.Action = new SCR_AIMoveIndividuallyBehavior(detour.Utility, null, first, 1100, 0, null, 1); } else { // Never deliberately send a guard across a mine when terrain offers no step. detour.Step = start; detour.Action = new SCR_AIWaitBehavior(detour.Utility, null); detour.Action.SetPriority(1100); } detour.Utility.AddAction(detour.Action); } protected void Tick() { if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return; } for (int i = m_Mines.Count() - 1; i >= 0; i--) if (!m_Mines[i].Live()) m_Mines.Remove(i); float now = GetGame().GetWorld().GetWorldTime(); for (int i = m_Detours.Count() - 1; i >= 0; i--) { ERR_MineDetour detour = m_Detours[i]; SCR_ChimeraAIAgent agent; if (detour.Actor) agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(detour.Actor)); if (!FriendlyActor(detour.Actor) || !agent || agent.m_UtilityComponent != detour.Utility || now >= detour.Expires || m_Mines.IsEmpty() || !detour.Group || ERR_Actors.Group(detour.Actor) != detour.Group || detour.Group.GetCurrentWaypoint() != detour.Waypoint) { detour.Clear(); m_Detours.Remove(i); continue; } vector start = detour.Actor.GetOrigin(); ERR_KnownMine obstruction = Blocking(start, detour.Goal, 10); if (!obstruction) { // Native individual moves can be cancelled when the avoidance action wins. // Finish the captured move explicitly; never lose an engineer/HQ foot order // merely because its route needed a short detour. A new group order cancels. if (!detour.ResumeMove || vector.DistanceXZ(start, detour.Goal) < 3) { detour.Clear(); m_Detours.Remove(i); continue; } if (!detour.FinalLeg) { detour.Clear(); detour.FinalLeg = true; detour.Step = detour.Goal; detour.Action = new SCR_AIMoveIndividuallyBehavior(detour.Utility, null, detour.Goal, 1100, 0, null, 1); detour.Utility.AddAction(detour.Action); } continue; } if (now >= detour.NextStep && (detour.FinalLeg || vector.DistanceXZ(start, detour.Step) < 4 || Blocking(start, start, 7))) Steer(detour, obstruction); } if (m_Mines.IsEmpty()) { GetGame().GetCallqueue().Remove(Tick); m_Running = false; return; } m_Near.Clear(); foreach (ERR_KnownMine mine : m_Mines) GetGame().GetWorld().QueryEntitiesBySphere(mine.Position, 22, Collect, queryFlags: EQueryEntitiesFlags.DYNAMIC); foreach (IEntity actor : m_Near) { bool busy; foreach (ERR_MineDetour existing : m_Detours) if (existing.Actor == actor) { busy = true; break; } if (busy || m_Detours.Count() >= 64) continue; SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor)); if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue; vector start = actor.GetOrigin(), goal = start; SCR_AIMoveBehaviorBase movement = SCR_AIMoveBehaviorBase.Cast(agent.m_UtilityComponent.GetCurrentAction()); if (movement) goal = movement.m_vPosition.m_Value; else { Physics physics = actor.GetPhysics(); if (physics) goal = start + physics.GetVelocity() * 4; } ERR_KnownMine obstruction = Blocking(start, goal, 10); if (!obstruction) continue; ERR_MineDetour detour = new ERR_MineDetour(); detour.Actor = actor; detour.Utility = agent.m_UtilityComponent; detour.Goal = goal; detour.Expires = now + 45000; detour.Group = ERR_Actors.Group(actor); if (!detour.Group) continue; detour.Waypoint = detour.Group.GetCurrentWaypoint(); detour.ResumeMove = movement != null; m_Detours.Insert(detour); Steer(detour, obstruction); } } void Stop() { m_Stopped = true; if (GetGame()) GetGame().GetCallqueue().Remove(Tick); foreach (ERR_MineDetour detour : m_Detours) detour.Clear(); m_Detours.Clear(); m_Mines.Clear(); m_Near.Clear(); } }