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:
@@ -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(); }
|
||||
}
|
||||
Reference in New Issue
Block a user