Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,353 @@
// 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();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,229 @@
// 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();
}
}
@@ -0,0 +1,157 @@
// Original checkpoint duty, not an invented HQ sighting/order. One carried mine
// per job; common engineer custody/callback/withdrawal rules own all mutations.
class ERR_RoadblockMine : ERR_EngineerMine
{
protected AIWaypoint m_Hold;
protected ERR_KnownMines m_Known;
protected AIAgent m_WorkAgent;
protected int m_PreviousLOD;
protected bool m_LODLease;
protected override bool OrderValid()
{
if (!m_Hold || !m_Known || m_Unit.Task || m_Unit.Assignment || m_Unit.PendingTask || m_Unit.PendingOrder || m_Unit.Downlink) return false;
if (m_Group.GetCurrentWaypoint() != m_Hold || vector.Distance(m_Hold.GetOrigin(), m_Rally) > 0.01) return false;
// The mine must stay outside the entire ordinary guard/cover-search area.
return vector.DistanceXZ(m_Rally, m_Position) >= m_Hold.GetCompletionRadius() + 20;
}
override void Stop()
{
super.Stop();
// Release only this job's temporary simulation lease; preserve GM changes.
if (m_LODLease && m_WorkAgent && m_WorkAgent.GetPermanentLOD() == 0) m_WorkAgent.SetPermanentLOD(m_PreviousLOD);
m_LODLease = false;
}
protected override void OnArmed() { m_Known.Add(m_Mine, m_Position); }
bool BeginPost(ERR_GameMode mode, ERR_HQUnit unit, AIWaypoint hold, vector position, float now, array<vector> priorSites, ERR_KnownMines known)
{
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 || !hold || !known || !mode.Forces()) return Refuse();
if (!ERR_ForceRules.Finite(now) || now < 0 || !ERR_ForceRules.FiniteVector(position)) return Refuse();
if (!m_Attempted || m_Attempted.Count() >= 256 || !priorSites || priorSites.Count() > 32) return Refuse();
m_Mode = mode; m_Unit = unit; m_Group = unit.Group; m_Hold = hold; m_Known = known;
m_SourceId = unit.ForceSourceId; m_Strength = unit.StatusSampler.InitialStrength(); m_Record = mode.Forces().FindGroup(m_Group);
m_Position = position; m_Rally = hold.GetOrigin();
float distance = vector.DistanceXZ(m_Position, m_Rally);
if (!ERR_ForceRules.FiniteVector(m_Rally) || distance < 45 || distance > 60) 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();
m_Attempted.Insert(m_Mine);
if (!Carried()) return Refuse();
if (m_Mode.MineIntel()) m_Mode.MineIntel().Candidates(m_Actor, m_IntelFolders);
m_WorkAgent = SCR_AIUtils.GetAIAgent(m_Actor);
if (!m_WorkAgent) return Refuse();
m_PreviousLOD = m_WorkAgent.GetPermanentLOD(); m_LODLease = true;
m_WorkAgent.SetPermanentLOD(0);
if (!m_WorkAgent.ActivateAI()) return Refuse();
m_Started = now; m_LastTick = now; m_State = 0;
Move(m_Position);
ERR_HQ.Event("ROADBLOCK MINE", "guard carrying original PMN4 to off-road site; original hold order retained");
return true;
}
}
class ERR_RoadblockMinePost
{
SCR_AIGroup Group;
AIWaypoint Hold;
vector Right;
int Attempts;
float NextAttempt;
ref array<vector> Sites = {};
ref array<ref ERR_RoadblockMine> Jobs = {};
}
class ERR_RoadblockMining
{
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_RoadblockMinePost> m_Posts = {};
protected ref ERR_KnownMines m_Known;
protected bool m_Stopped;
void ERR_RoadblockMining(ERR_GameMode mode) { m_Mode = mode; m_Known = new ERR_KnownMines(mode); }
void Register(SCR_AIGroup group, AIWaypoint hold, vector right)
{
if (m_Stopped || !group || !hold || m_Posts.Count() >= 12) return;
ERR_RoadblockMinePost post = new ERR_RoadblockMinePost();
post.Group = group; post.Hold = hold; post.Right = right;
// Equipment insertion and original roster sampling must settle first.
post.NextAttempt = GetGame().GetWorld().GetWorldTime() + 30000;
m_Posts.Insert(post);
}
// Four separated flanks, with open road and a clear inner guard area. Every
// nearby road's *edge* is checked, including intersections and narrow tracks.
static bool Site(vector post, vector right, int slot, out vector position)
{
if (slot < 0 || slot >= 4 || !ERR_ForceRules.FiniteVector(post) || !ERR_ForceRules.FiniteVector(right)) return false;
vector forward = Vector(-right[2], 0, right[0]);
int side = 1, row = 1;
if (slot % 2 == 1) side = -1;
if (slot >= 2) row = -1;
position = ERR_Actors.Ground(post + right * (side * 47) + forward * (row * 20));
if (!ERR_EngineerMineSite.InBand(post, position) || !ERR_HQRoadIntercept.Standable(position)) return false;
ERR_SettlementSafety settlements = new ERR_SettlementSafety();
if (!settlements.OutsideSettlements(position)) return false;
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!world || !world.GetRoadNetworkManager()) return false;
array<BaseRoad> roads = {};
world.GetRoadNetworkManager().GetRoadsInAABB(position - "100 1000 100", position + "100 1000 100", roads);
if (roads.IsEmpty() || roads.Count() > 128) return false;
int segments;
foreach (BaseRoad road : roads)
{
if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 0 || road.GetWidth() > 160) return false;
array<vector> points = {}; road.GetPoints(points);
if (points.Count() < 2 || points.Count() > 1024) return false;
segments += points.Count() - 1;
if (segments > 4096) return false;
for (int i = 1; i < points.Count(); i++)
if (!ERR_MineAvoidanceRules.Clear(points[i - 1], points[i], position, road.GetWidth() * 0.5 + 15)) return false;
}
return true;
}
void Tick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now)
{
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
int active;
foreach (ERR_RoadblockMinePost post : m_Posts)
foreach (ERR_RoadblockMine job : post.Jobs)
{ job.Tick(now); if (!job.Finished()) active++; }
if (active >= 2) return;
foreach (ERR_RoadblockMinePost post : m_Posts)
{
if (!post.Group || !post.Hold || post.Attempts >= 4 || now < post.NextAttempt) continue;
if (post.Group.GetCurrentWaypoint() != post.Hold) { post.Attempts = 4; continue; }
ERR_HQUnit owner;
foreach (ERR_HQUnit unit : units) if (unit.Group == post.Group) { owner = unit; break; }
if (!owner || !owner.Eligible(m_Mode) || !owner.StatusSampler || !owner.StatusSampler.OwnsRoster(post.Group)) continue;
if (owner.Task || owner.Assignment || owner.PendingTask || owner.PendingOrder || owner.Downlink || ERR_HunterCombat.Active(post.Group)) continue;
bool blocked;
ERR_ForceRecord record = m_Mode.Forces().FindGroup(post.Group);
foreach (ERR_RoadblockMine previous : post.Jobs) if (previous.BlocksNextPost(record)) { blocked = true; break; }
if (blocked) continue;
int slot = post.Attempts++;
post.NextAttempt = now + 30000;
vector position;
if (!Site(post.Hold.GetOrigin(), post.Right, slot, position))
{ ERR_HQ.Event("ROADBLOCK MINE", "unsafe off-road site skipped; finite attempt consumed"); return; }
ERR_RoadblockMine next = new ERR_RoadblockMine(attempted);
post.Jobs.Insert(next); owner.EngineerMine = next;
if (next.BeginPost(m_Mode, owner, post.Hold, position, now, post.Sites, m_Known)) post.Sites.Insert(position);
else { next.Stop(); ERR_HQ.Event("ROADBLOCK MINE", "placement refused: no eligible carried mine, quiet guard or clear withdrawal route"); }
return; // One bounded site scan/admission per HQ tick.
}
}
void Stop()
{
m_Stopped = true;
foreach (ERR_RoadblockMinePost post : m_Posts) foreach (ERR_RoadblockMine job : post.Jobs) job.Stop();
m_Known.Stop();
}
}
@@ -0,0 +1,228 @@
// Optional finite roadside checkpoints. Planned before player admission, using
// this terrain's road network and navmesh; never replenish casualties in-round.
class ERR_RoadblockRules
{
static bool Road(float width, float length, float heightDifference)
{
return ERR_ForceRules.Finite(width) && ERR_ForceRules.Finite(length) && ERR_ForceRules.Finite(heightDifference)
&& width >= 4 && width <= 16 && length >= 40 && Math.AbsFloat(heightDifference) <= 2;
}
static bool Spaced(vector point, array<vector> used, vector prison, float escapeRadius)
{
if (!ERR_ForceRules.FiniteVector(point) || !ERR_ForceRules.FiniteVector(prison) || !used || !ERR_ForceRules.Finite(escapeRadius)) return false;
float distance = vector.DistanceXZ(point, prison);
if (distance < 700 || distance + 200 >= escapeRadius) return false;
foreach (vector other : used)
if (!ERR_ForceRules.FiniteVector(other) || vector.DistanceXZ(point, other) < 600) return false;
return true;
}
}
class ERR_Roadblocks
{
protected ERR_GameMode m_Mode;
protected ref ERR_RoadblockMining m_Mining;
protected ref array<vector> m_Positions = {};
protected ref array<SCR_AIGroup> m_Groups = {};
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety();
protected bool m_Begun, m_Ready, m_Stopped, m_HaveCandidate;
protected int m_Target, m_Attempts, m_Started, m_CandidateStarted, m_Candidates, m_Planned;
protected vector m_Low, m_High, m_Centre, m_Forward, m_Right;
protected float m_Shoulder;
protected ref array<ResourceName> m_Roster = {};
protected ref array<ref Resource> m_Loaded = {};
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et";
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); }
string Status()
{
if (!m_Begun) return "Roadblocks: awaiting terrain preparation.";
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements.", m_Groups.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"));
}
protected ERR_PrepareResult Finish(string reason)
{
m_Ready = true; m_Loaded.Clear();
PrintFormat("Reintroduced [roadblocks]: placed=%1 requested=%2 attempts=%3 roadCandidates=%4 clearPlans=%5; %6", m_Groups.Count(), m_Target, m_Attempts, m_Candidates, m_Planned, reason);
ERR_HQ.Event("ROADBLOCKS", Status() + " " + reason);
return ERR_PrepareResult.READY;
}
protected bool LoadRoster()
{
Resource profile = Resource.Load(m_Mode.FactionSelection().Choice("enemy").Group);
IEntitySource source;
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
// Preload before physical creation. A failed prefab never leaves half a post.
array<ResourceName> resources = {COVER, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
foreach (ResourceName prefab : resources)
{
Resource loaded = Resource.Load(prefab);
if (!loaded || !loaded.IsValid()) return false;
m_Loaded.Insert(loaded);
}
return true;
}
protected bool Candidate(ERR_ScenarioDefinition definition)
{
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!world || !world.GetRoadNetworkManager()) return false;
vector seed = Vector(Math.RandomFloat(m_Low[0], m_High[0]), 0, Math.RandomFloat(m_Low[2], m_High[2]));
array<BaseRoad> roads = {};
world.GetRoadNetworkManager().GetRoadsInAABB(seed - "600 5000 600", seed + "600 5000 600", roads);
if (roads.IsEmpty() || roads.Count() > 512) return false;
BaseRoad road = roads.GetRandomElement();
if (!road) return false;
array<vector> points = {}; road.GetPoints(points);
if (points.Count() < 2 || points.Count() > 2048) return false;
float length;
for (int i = 0; i < points.Count(); i++)
{
if (!ERR_ForceRules.FiniteVector(points[i])) return false;
if (i > 0) length += vector.DistanceXZ(points[i - 1], points[i]);
}
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, 0)) return false;
float distance = Math.RandomFloat(20, length - 20);
vector centre, before, after, beforeDirection, afterDirection;
if (!ERR_ConvoyRoadPlan.Sample(points, distance, centre, m_Forward)
|| !ERR_ConvoyRoadPlan.Sample(points, distance - 15, before, beforeDirection)
|| !ERR_ConvoyRoadPlan.Sample(points, distance + 15, after, afterDirection)) return false;
if (vector.Dot(m_Forward, beforeDirection) < 0.95 || vector.Dot(m_Forward, afterDirection) < 0.95) return false;
m_Centre = ERR_Actors.Ground(centre);
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, m_Centre[1] - centre[1])) return false;
ERR_SiteDefinition prison = definition.Find("everon-prison-1");
if (!prison || !ERR_RoadblockRules.Spaced(m_Centre, m_Positions, prison.Position, m_Mode.EscapeRadius())) return false;
foreach (ERR_SiteDefinition site : definition.Sites)
if (vector.DistanceXZ(site.Position, m_Centre) < 150) return false;
if (!m_Settlements.OutsideSettlements(m_Centre) || !ERR_HQRoadIntercept.Standable(m_Centre)) return false;
// Flatten so cover/guard offsets keep their metre distances on sloped roads.
m_Forward[1] = 0;
if (m_Forward.Length() < 0.1) return false;
m_Forward.Normalize();
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
// Keep all physical cover outside the travelled road. Even a narrow road has
// a >=6m centre gap between these compact sandbag positions.
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 1.5, 4);
m_Candidates++; m_CandidateStarted = System.GetTickCount();
return true;
}
protected bool PlayersClear()
{
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
foreach (int id : ids)
{
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
if (actor && vector.DistanceXZ(actor.GetOrigin(), m_Centre) < 700) return false;
}
return true;
}
protected bool CoverClear(vector point)
{
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++)
{
vector sample = ERR_Actors.Ground(point + Vector(x * 1.5, 0, z * 1.5));
if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false;
}
TraceBox trace = new TraceBox(); trace.Start = point + "0 0.15 0"; trace.End = trace.Start + "0 0.01 0";
trace.Mins = "-1.5 0 -1.5"; trace.Maxs = "1.5 2 1.5"; trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
return GetGame().GetWorld().TraceMove(trace, null) == 1;
}
protected void Rollback(array<IEntity> spawned, SCR_AIGroup group)
{
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
ERR_Actors.DeleteSpawned(group);
}
protected bool SpawnPost(array<vector> positions, array<vector> covers)
{
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) return false;
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
if (!group) return false;
group.SetFaction(m_Mode.EnemyFaction()); group.SetSpawnImmediately(false); group.SetDeleteWhenEmpty(false); group.SetPermanentLOD(0);
array<IEntity> spawned = {};
for (int i = 0; i < positions.Count(); i++)
{
if (!PlayersClear()) { Rollback(spawned, group); return false; }
IEntity actor = ERR_Actors.Spawn(m_Roster[i % m_Roster.Count()], positions[i]);
if (!actor) { Rollback(spawned, group); return false; }
spawned.Insert(actor);
if (!ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !group.AddAIEntityToGroup(actor)) { Rollback(spawned, group); return false; }
}
foreach (vector coverPosition : covers)
{
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
if (!cover) { Rollback(spawned, group); return false; }
spawned.Insert(cover);
}
AIWaypoint hold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), positions[0]));
if (!hold) { Rollback(spawned, group); return false; }
spawned.Insert(hold); hold.SetCompletionRadius(25); group.AddWaypoint(hold);
string id = "roadblock-" + m_Groups.Count().ToString();
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, group, GetGame().GetWorld().GetWorldTime()))
{ Rollback(spawned, group); return false; }
foreach (IEntity ownedEntity : spawned) m_Mode.Own(ownedEntity);
m_Mode.Own(group); m_Groups.Insert(group); m_Positions.Insert(m_Centre);
m_Mode.RegisterGroup(group); group.ActivateAI();
m_Mining.Register(group, hold, m_Right);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
return true;
}
// 0: reject, 1: awaiting native tile, 2: ready.
protected int Plan(out array<vector> positions, out array<vector> covers)
{
positions.Clear(); covers.Clear();
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
if (!mesh || !PlayersClear()) return 0;
map<int, vector> reserved = new map<int, vector>();
for (int side = -1; side <= 1; side += 2)
{
vector cover = ERR_Actors.Ground(m_Centre + m_Right * (side * m_Shoulder));
if (!CoverClear(cover)) return 0;
covers.Insert(cover);
for (int row = -1; row <= 1; row += 2)
{
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 3)) + m_Forward * (row * 3));
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
vector reachable;
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
reserved.Set(positions.Count(), point); positions.Insert(point);
}
}
return 2;
}
ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
{
if (!Replication.IsServer() || m_Stopped || !definition) return ERR_PrepareResult.FAILED;
if (m_Ready) return ERR_PrepareResult.READY;
if (!m_Begun)
{
m_Begun = true; m_Started = System.GetTickCount(); m_Target = m_Mode.Settings().Get("roadblockCount");
if (m_Target == 0) return Finish("disabled");
#ifdef WORKBENCH
// The legacy convoy/hq-memory fixtures use the original 60-body allowance and six-squad layout.
string fixtureEnabled, fixtureOutcome;
if (System.GetCLIParam("errSmoke", fixtureEnabled) && fixtureEnabled == "1" && System.GetCLIParam("errSmokeOutcome", fixtureOutcome) && (fixtureOutcome == "convoy" || fixtureOutcome == "hq-memory"))
return Finish("disabled for the legacy body-budget fixture");
#endif
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
if (!ERR_TerrainSites.Bounds(m_Low, m_High) || !LoadRoster()) return Finish("terrain or faction resources unavailable; optional roadblocks skipped");
}
if (m_Groups.Count() >= m_Target) return Finish("requested count reached");
if (System.GetTickCount() - m_Started >= 12000 || m_Attempts >= 300) return Finish("bounded search complete; unsuitable locations skipped");
// Amortize geometry work: at most eight cheap road samples and one detailed
// placement per preparation call. Never depend on finding every requested post.
for (int sample = 0; sample < 8 && !m_HaveCandidate && m_Attempts < 300; sample++)
{ m_Attempts++; m_HaveCandidate = Candidate(definition); }
if (!m_HaveCandidate) return ERR_PrepareResult.PENDING;
array<vector> positions = {}, covers = {};
int plan = Plan(positions, covers);
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 1500) return ERR_PrepareResult.PENDING;
if (plan == 2) { m_Planned++; SpawnPost(positions, covers); }
m_HaveCandidate = false;
return ERR_PrepareResult.PENDING;
}
void Stop() { m_Stopped = true; m_Mining.Stop(); m_Loaded.Clear(); }
}