Files
EscapistsReintroduced/docs/evidence/roadblock-mining-2026-10-07/source/ERR_KnownMines.c
T
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

230 lines
9.9 KiB
C

// 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<ref AIActionBase> actions = {}; Utility.GetActions(actions);
if (actions.Contains(Action)) Action.Complete();
}
Action = null;
}
}
class ERR_KnownMines
{
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_KnownMine> m_Mines = {};
protected ref array<ref ERR_MineDetour> m_Detours = {};
protected ref array<IEntity> 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();
}
}