Files
EscapistsReintroduced/docs/evidence/village-standoff-2026-10-09/baseline/ERR_SupplyConvoyTactics.c
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

677 lines
30 KiB
C

// Original convoy tactics adapted to native AI waypoints. No old framework hooks.
class ERR_SupplyTeam
{
SCR_AIGroup Group;
ref array<IEntity> Members = {};
ref array<BaseCompartmentSlot> Seats = {};
ref array<AIWaypoint> Orders = {};
bool Registered;
bool Released;
vector OrderedContact;
vector SearchPosition;
int SearchStep;
bool SearchPlanned;
ref ERR_ConvoyBoardingProgress BoardingProgress;
float BoardingRetryAt;
protected Vehicle m_WalkVehicle;
void VehicleUse(ERR_GameMode mode, Vehicle truck, bool enabled)
{
if (!Group || !truck) return;
SCR_AIGroupUtilityComponent utility = Group.GetGroupUtilityComponent();
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(truck.FindComponent(SCR_AIVehicleUsageComponent));
if (!utility || !usage) return;
if (!enabled && !m_WalkVehicle) { utility.RemoveUsableVehicle(usage); m_WalkVehicle = truck; }
else if (enabled && m_WalkVehicle == truck)
{
if (mode && mode.Active() && Living() && Group.GetFaction() == mode.EnemyFaction() && !ERR_ConvoyRules.Captured(truck, Group)) utility.AddUsableVehicle(usage);
m_WalkVehicle = null;
}
}
void Clear()
{
foreach (AIWaypoint waypoint : Orders)
{
if (Group && waypoint) Group.RemoveWaypoint(waypoint);
if (waypoint) SCR_EntityHelper.DeleteEntityAndChildren(waypoint);
}
Orders.Clear();
}
void Add(AIWaypoint waypoint)
{
if (!waypoint) return;
if (!Group || Released) { SCR_EntityHelper.DeleteEntityAndChildren(waypoint); return; }
Orders.Insert(waypoint); Group.AddWaypoint(waypoint);
}
void Release(ERR_GameMode mode)
{
if (Released) return;
// Only a live regroup may restore transport. Released foot survivors must
// not reclaim a truck after its crew was lost or player capture was latched.
Released = true; Clear();
ERR_HQ coordination = ERR_HQ.Get(); if (coordination) coordination.AttackSpace().Release(Group);
if (!Group) return;
ERR_HQ hq;
if (mode) hq = ERR_HQ.Cast(mode.Command());
bool retained = false;
if (Registered && hq) retained = hq.ReleaseConvoySurvivors(Group);
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
ERR_ForceRecord record;
if (mode && mode.Forces()) record = mode.Forces().FindGroup(Group);
if (record) record.Pinned = false;
Group.SetPermanentLOD(-1);
array<AIAgent> agents = {}; Group.GetAgents(agents);
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
}
int BoardingCount(Vehicle truck, out float remainingDistance)
{
int seated; remainingDistance = 0;
foreach (IEntity member : Members)
{
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) continue;
if (CompartmentAccessComponent.GetVehicleIn(member) == truck) seated++;
else remainingDistance = Math.Max(remainingDistance, vector.DistanceXZ(member.GetOrigin(), truck.GetOrigin()));
}
return seated;
}
bool Living()
{
foreach (IEntity member : Members)
if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Group) return true;
return false;
}
bool Aboard(Vehicle truck, bool required)
{
foreach (IEntity member : Members)
{
if (!ERR_Actors.CanFight(member) || ERR_Actors.Group(member) != Group) continue;
if ((CompartmentAccessComponent.GetVehicleIn(member) == truck) != required) return false;
}
return true;
}
}
// Only the exact original vehicle crews are retained. Infantry never enters this map.
class ERR_ConvoyMountedCrew
{
protected static ref map<IEntity, Vehicle> s_Crews = new map<IEntity, Vehicle>();
protected static ref map<IEntity, int> s_Couriers = new map<IEntity, int>();
static bool BeginCourier(IEntity actor, Vehicle originalVehicle, int token)
{
if (!actor || !originalVehicle || token <= 0 || !s_Crews.Contains(actor) || s_Crews.Get(actor) != originalVehicle || s_Couriers.Contains(actor)) return false;
s_Couriers.Set(actor, token); return true;
}
static void EndCourier(IEntity actor, int token)
{
if (actor && s_Couriers.Contains(actor) && s_Couriers.Get(actor) == token) s_Couriers.Remove(actor);
}
static void Hold(IEntity actor, Vehicle vehicle) { if (actor && vehicle) s_Crews.Set(actor, vehicle); }
static void Release(IEntity actor) { if (actor) { s_Crews.Remove(actor); s_Couriers.Remove(actor); } }
protected static bool RetainOriginal(IEntity actor, IEntity vehicle)
{
if (!actor || !vehicle || !s_Crews.Contains(actor) || s_Crews.Get(actor) != vehicle || ERR_Actors.Player(actor) || !ERR_Actors.CanFight(actor)) return false;
if (CompartmentAccessComponent.GetVehicleIn(actor) != vehicle) return false;
SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(vehicle.FindComponent(SCR_VehicleDamageManagerComponent));
// Let native emergency abandonment take over on critical damage or drivetrain failure.
return damage && damage.GetState() != EDamageState.DESTROYED && damage.GetHealthScaled() > 0.25 && damage.GetEngineFunctional() && damage.GetGearboxFunctional();
}
// Individual permission never authorizes a whole-group dismount.
static bool Retain(IEntity actor, IEntity vehicle)
{
return !s_Couriers.Contains(actor) && RetainOriginal(actor, vehicle);
}
static bool RetainGroup(SCR_AIGroup group, IEntity vehicle)
{
if (!group || group.GetPlayerCount(true) > 0) return false;
if (!vehicle)
{
IEntity leader = group.GetLeaderEntity();
if (!leader || !s_Crews.Contains(leader)) return false;
vehicle = s_Crews.Get(leader);
}
if (!vehicle) return false;
array<AIAgent> agents = {}; group.GetAgents(agents);
if (agents.IsEmpty() || agents.Count() > 32) return false;
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity actor = agent.GetControlledEntity();
if (actor && ERR_Actors.Group(actor) == group && RetainOriginal(actor, vehicle)) return true;
}
return false;
}
}
modded class SCR_AIGetOutVehicle
{
override float CustomEvaluate()
{
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
return super.CustomEvaluate();
}
override float EvaluatePriorityLevel()
{
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
return super.EvaluatePriorityLevel();
}
}
modded class SCR_AIGetOutActivity
{
override float CustomEvaluate()
{
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
return super.CustomEvaluate();
}
override float EvaluatePriorityLevel()
{
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
return super.EvaluatePriorityLevel();
}
}
class ERR_SupplyConvoyVehicle : ERR_ConvoyVehicle
{
override bool CrewValid(ERR_GameMode mode)
{
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
if (ERR_Actors.Player(Driver) || ERR_Actors.Group(Driver) != Group || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
foreach (IEntity member : Members)
if (ERR_Actors.Alive(member) && (ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group)) return false;
return true;
}
override void Release(ERR_GameMode mode)
{
foreach (IEntity member : Members) ERR_ConvoyMountedCrew.Release(member);
super.Release(mode);
}
}
class ERR_SupplyConvoyTask : ERR_ConvoyTask
{
ref array<ref ERR_SupplyTeam> Teams = {};
ref ERR_SupplyConvoyTactics Tactics;
ref ERR_SupplyConvoyColumn Column;
ref array<vector> Bases = {};
ref array<vector> Starts = {};
int LastBase = -1;
int Legs;
float Departure;
float TravelDeadline;
bool Waiting;
bool MagazinesIssued;
ref array<ref ERR_MountedMagazineSeed> MountedStock = {};
ref array<IEntity> CargoBefore = {};
ref ERR_ConvoyMineStock MineStock;
override void Stop(ERR_GameMode mode, string reason)
{
if (Stopped) return;
if (Tactics) Tactics.Stop();
if (MineStock) MineStock.Stop();
foreach (ERR_SupplyTeam team : Teams) team.Release(mode);
super.Stop(mode, reason);
}
}
class ERR_SupplyConvoyTactics
{
protected ERR_GameMode m_Mode;
protected ERR_SupplyConvoyTask m_Task;
protected ref array<ref ERR_SupplyTeam> m_Support = {};
protected int m_State; // mounted, fighting, reboarding
protected vector m_Contact;
protected vector m_Forward;
protected vector m_Left;
protected float m_LastContact = -1;
protected float m_NextOrders;
protected float m_NextSearch;
protected vector m_LastOrderContact;
protected bool m_OrdersIssued;
protected float m_BoardDeadline;
protected bool m_TruckWithdrawn;
protected float m_NextDismount;
protected int m_DismountAttempts;
protected bool m_Stopped;
void ERR_SupplyConvoyTactics(ERR_GameMode mode, ERR_SupplyConvoyTask task)
{
m_Mode = mode; m_Task = task;
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
{
ERR_SupplyTeam order = new ERR_SupplyTeam(); order.Group = vehicle.Group;
m_Support.Insert(order);
foreach (IEntity member : vehicle.Members) ERR_ConvoyMountedCrew.Hold(member, vehicle.Entity);
}
}
int State() { return m_State; }
static AIWaypoint Move(vector position, float radius = 20)
{
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), position));
if (waypoint) waypoint.SetCompletionRadius(radius);
return waypoint;
}
static void Command(AIWaypoint order)
{
SCR_AIWaypoint commanded = SCR_AIWaypoint.Cast(order);
if (commanded) commanded.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
}
protected bool FreePosition(SCR_AIGroup group, vector position)
{
ERR_HQ hq = ERR_HQ.Get();
return !hq || hq.AttackSpace().Free(group, position, GetGame().GetWorld().GetWorldTime());
}
protected void Claim(SCR_AIGroup group, vector position, float now)
{
ERR_HQ hq = ERR_HQ.Get(); if (hq) hq.AttackSpace().Claim(group, position, now);
}
static AIWaypoint Board(Vehicle truck, bool enter)
{
if (!truck) return null;
ResourceName prefab = "{C40316EE26846CAB}Prefabs/AI/Waypoints/AIWaypoint_GetOut.et";
if (enter) prefab = "{712F4795CF8B91C7}Prefabs/AI/Waypoints/AIWaypoint_GetIn.et";
SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(ERR_Actors.Spawn(prefab, truck.GetOrigin()));
if (!waypoint) return null;
Command(waypoint);
waypoint.SetAllowance(false, false, true);
// Native defaults may complete when only the leader has left/boarded.
waypoint.SetCompletionType(EAIWaypointCompletionType.All);
// The waypoint approach applies to the whole team; leave space around the truck.
waypoint.SetCompletionRadius(100);
SCR_BoardingEntityWaypoint specific = SCR_BoardingEntityWaypoint.Cast(waypoint);
if (specific) specific.SetEntity(truck);
return waypoint;
}
protected bool Usable(int index)
{
return index >= 0 && index < m_Task.Vehicles.Count() && !m_Task.Vehicles[index].Released && m_Task.Vehicles[index].CrewValid(m_Mode);
}
protected bool InfantryAboard(bool required)
{
foreach (ERR_SupplyTeam team : m_Task.Teams)
if (!team.Released && !team.Aboard(m_Task.Vehicles[2].Entity, required)) return false;
return true;
}
protected float DistanceToConvoy(vector position)
{
float distance = 1e20;
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
if (!vehicle.Released && vehicle.Entity) distance = Math.Min(distance, vector.DistanceXZ(vehicle.Entity.GetOrigin(), position));
return distance;
}
protected bool Observe(SCR_AIGroup group, out vector position, inout float nearest)
{
if (!group || !group.GetGroupUtilityComponent()) return false;
SCR_AIGroupPerception perception = group.GetGroupUtilityComponent().m_Perception;
PerceptionManager manager = GetGame().GetPerceptionManager();
if (!perception || !manager) return false;
bool found;
foreach (SCR_AITargetInfo target : perception.m_aTargets)
{
if (!target || !target.m_Entity || !target.m_Faction || !group.GetFaction().IsFactionEnemy(target.m_Faction)) continue;
float age = manager.GetTime() - target.m_fTimestamp;
if (target.m_eCategory != EAITargetInfoCategory.IDENTIFIED || age < 0 || age > 5) continue;
float distance = DistanceToConvoy(target.m_vWorldPos);
if (distance > nearest || !group.GetGroupUtilityComponent().IsPositionAllowed(target.m_vWorldPos)) continue;
position = target.m_vWorldPos; nearest = distance; found = true;
}
return found;
}
// Only production native observations or a delivered HQ snapshot may call this.
void Report(vector position, float observed, float now)
{
if (m_Stopped || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observed) || !ERR_ForceRules.FiniteVector(position) || observed < 0 || observed > now || now - observed > 60000 || observed <= m_LastContact) return;
m_LastContact = observed;
if (m_State != 1) Start(position, now);
else if (vector.DistanceXZ(position, m_Contact) > 40 && now >= m_NextOrders) { m_Contact = position; Orders(false, now); }
}
protected bool FootPosition(ERR_SupplyTeam team, vector desired, out vector position)
{
if (!team.Group || !team.Group.GetGroupUtilityComponent()) return false;
for (int i = 0; i < 8; i++)
{
vector point = desired;
if (i > 0) point += Vector(Math.RandomFloat(-12, 12), 0, Math.RandomFloat(-12, 12));
point = ERR_Actors.Ground(point);
if (FreePosition(team.Group, point) && !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) && team.Group.GetGroupUtilityComponent().IsPositionAllowed(point) && SCR_WorldTools.TraceCilinderUtil(point + "0 1 0", 1, 2)) { position = point; return true; }
}
return false;
}
protected void Flank(int index, bool dismount)
{
PlanFlank(index, dismount, 0);
}
protected bool PlanFlank(int index, bool dismount, int step)
{
ERR_SupplyTeam team = m_Task.Teams[index];
if (team.Released || !team.Living() || step < 0 || step > 2) return false;
// Preserve an in-progress native all-member dismount across contact refreshes.
if (!team.Aboard(m_Task.Vehicles[2].Entity, false) && SCR_BoardingWaypoint.Cast(team.Group.GetCurrentWaypoint())) return false;
// Do not queue a vehicle-capable Move behind GetOut: native AI may board
// again before our next tick. Start foot movement only after the whole team
// has actually left and its shared truck is detached from this group's usage.
if (!team.Aboard(m_Task.Vehicles[2].Entity, false))
{
if (!dismount) return false;
AIWaypoint disembark = Board(m_Task.Vehicles[2].Entity, false);
if (!disembark) return false;
team.Clear(); team.SearchPlanned = false; team.Add(disembark); return true;
}
float side = 1; if (index == 1) side = -1;
vector desired = m_Contact + m_Left * (70 * side);
if (step == 1) desired = m_Contact + m_Forward * 100 + m_Left * (140 * side);
if (step == 2) desired = m_Contact + m_Forward * 160 + m_Left * (50 * side);
vector approach, sector;
if (!FootPosition(team, desired, sector)) return false;
approach = sector;
if (step == 0 && !FootPosition(team, m_Contact - m_Forward * 80 + m_Left * (110 * side), approach)) return false;
AIWaypoint move = Move(approach, 15);
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), sector));
// Failed placement/allocation must not erase the team's working orders.
if (!move || !search)
{
if (move) SCR_EntityHelper.DeleteEntityAndChildren(move);
if (search) SCR_EntityHelper.DeleteEntityAndChildren(search);
return false;
}
Command(move); Command(search);
team.Clear(); team.OrderedContact = m_Contact;
team.SearchPosition = sector; team.SearchStep = step; team.SearchPlanned = true;
team.Add(move);
search.SetCompletionRadius(30);
team.Add(search);
Claim(team.Group, sector, GetGame().GetWorld().GetWorldTime());
ERR_HQ.Event("CONVOY HUNT", string.Format("%1 team %2: search step %3/3 at %4 from last observed contact; formation suspended", m_Task.Receipt.SourceId, index + 1, step + 1, sector));
return true;
}
protected void ContinueSearch(float now)
{
if (now < m_NextSearch) return;
m_NextSearch = now + 45000;
for (int i = 0; i < m_Task.Teams.Count(); i++)
{
ERR_SupplyTeam team = m_Task.Teams[i];
if (team.Released || !team.Living() || !team.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
// Retry refused initial orders, but never replace a team still travelling.
if (!team.SearchPlanned || vector.DistanceXZ(team.OrderedContact, m_Contact) > 40) { PlanFlank(i, false, 0); continue; }
if (team.SearchStep >= 2) continue;
IEntity leader = team.Group.GetLeaderEntity();
if (!leader || vector.DistanceXZ(leader.GetOrigin(), team.SearchPosition) > 45) continue;
PlanFlank(i, false, team.SearchStep + 1);
}
}
protected bool SupportPosition(int index, vector desired, out vector position)
{
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
if (!utility) return false;
vector forward = ERR_MountedCover.Approach(vehicle.Entity.GetOrigin(), m_Contact, m_Forward);
bool armed = index != 1 && index != 2;
vector road;
if (ERR_SupplyConvoySystem.Approach(desired, road, 70) && FreePosition(vehicle.Group, road) && vector.Dot(road - m_Contact, forward) <= -100 && utility.IsPositionAllowed(road) && (!armed || ERR_MountedCover.ClearLane(vehicle.Entity, road, m_Contact))) { position = road; return true; }
if (index == 1 || index == 2) return false;
// Original armed escorts can seek a nearby open firing position off the road.
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
for (int attempt = 0; attempt < 8; attempt++)
{
vector candidate = desired;
if (attempt > 0) candidate += Vector(Math.RandomFloat(-80, 80), 0, Math.RandomFloat(-80, 80));
candidate = ERR_Actors.Ground(candidate);
if (!FreePosition(vehicle.Group, candidate) || vector.Dot(candidate - m_Contact, forward) > -100 || !utility.IsPositionAllowed(candidate)) continue;
if (!ERR_MountedCover.ClearLane(vehicle.Entity, candidate, m_Contact)) continue;
if (placement.Clear(candidate, ERR_ForceRules.Heading(candidate, m_Contact, 0), 2.5)) { position = candidate; return true; }
}
return false;
}
protected void Support(int index, bool holdTruck, bool retainApproach = false)
{
ERR_SupplyTeam orders = m_Support[index];
if (!Usable(index)) { orders.Clear(); return; }
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
// An idle mounted driver stops without a Defend order asking the crew to exit.
if (index == 2 && holdTruck) { orders.Clear(); return; }
bool armed = index != 1 && index != 2;
float range = vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Contact);
// Keep a gun that can already see the reported fight. Do not back it away
// just to satisfy the convoy formation's fixed rear/side offsets.
if (armed && range >= 100 && range <= 600 && ERR_MountedCover.SeesSector(vehicle.Group, m_Contact))
{
orders.Clear(); Claim(vehicle.Group, vehicle.Entity.GetOrigin(), GetGame().GetWorld().GetWorldTime());
ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": holding visual contact at " + vehicle.Entity.GetOrigin().ToString());
return;
}
// Periodic cover checks must not restart a native driver still approaching.
if (retainApproach && !orders.Orders.IsEmpty() && orders.Orders[0] && vector.DistanceXZ(vehicle.Entity.GetOrigin(), orders.Orders[0].GetOrigin()) > 30) return;
float rear = 180, side;
if (index == 0) side = 150;
if (index == 3) side = -150;
if (index == 5) rear = 250;
if (index == 1) { rear = 350; side = 40; }
if (index == 2) { rear = 310; side = -40; }
vector support = vehicle.Entity.GetOrigin();
vector forward = ERR_MountedCover.Approach(vehicle.Entity.GetOrigin(), m_Contact, m_Forward);
vector left = Vector(-forward[2], 0, forward[0]);
vector desired = m_Contact - forward * rear + left * side;
vector chosen;
if (SupportPosition(index, desired, chosen)) support = chosen;
else if (armed)
{
orders.Clear(); Claim(vehicle.Group, support, GetGame().GetWorld().GetWorldTime());
ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": no clear firing lane found; holding and retrying at " + support.ToString());
return;
}
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20)
{
AIWaypoint movement = Move(support);
if (!movement) return;
Command(movement); orders.Clear(); orders.Add(movement);
ERR_HQ.Event("CONVOY SUPPORT", string.Format("%1 vehicle %2: position=%3 destination=%4 reported contact=%5 armed=%6", m_Task.Receipt.SourceId, index, vehicle.Entity.GetOrigin(), support, m_Contact, armed));
}
else orders.Clear();
Claim(vehicle.Group, support, GetGame().GetWorld().GetWorldTime());
if (index == 1 || index == 2) return;
// Mounted escorts engage using native vehicle combat after reaching cover.
// Defend is an infantry occupation task; deleting it during regroup could
// leave ActivityDefend executing with an already deleted waypoint.
// Do not assign it to a crew whose role requires remaining in the vehicle.
}
protected void Clear()
{
foreach (ERR_SupplyTeam orders : m_Support) orders.Clear();
foreach (ERR_SupplyTeam team : m_Task.Teams) team.Clear();
}
protected void Orders(bool dismount, float now)
{
m_NextOrders = now + 30000;
if (m_OrdersIssued && vector.DistanceXZ(m_LastOrderContact, m_Contact) < 40)
{
// Recheck masked firing positions even when the reported target is stationary.
for (int support = 0; support < m_Task.Vehicles.Count(); support++) Support(support, !m_TruckWithdrawn, true);
return;
}
m_LastOrderContact = m_Contact; m_OrdersIssued = true;
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
m_NextOrders = now + 30000;
}
protected void Start(vector contact, float now)
{
// A local threat takes precedence, but may release only this exact detour owner.
foreach (ERR_ConvoyVehicle intercept : m_Task.Vehicles)
{
if (intercept.InterceptToken == 0) continue;
intercept.ClearIntercept(intercept.InterceptOrder);
if (intercept.InterceptToken != 0) return;
}
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = m_Task.Teams.IsEmpty(); m_OrdersIssued = false;
m_NextSearch = now + 45000;
foreach (ERR_SupplyTeam team : m_Task.Teams) { team.SearchPlanned = false; team.SearchStep = 0; }
if (m_Task.Column) m_Task.Column.Reset(now);
m_Task.Waiting = false;
vector origin = contact;
foreach (ERR_ConvoyVehicle stagingVehicle : m_Task.Vehicles)
if (!stagingVehicle.Released && stagingVehicle.Entity) { origin = stagingVehicle.Entity.GetOrigin(); break; }
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
{
vehicle.ClearOrder();
if (!vehicle.Released) vehicle.Combat.Held = true;
}
m_Forward = contact - origin; m_Forward[1] = 0;
if (m_Forward.Length() < 1) m_Forward = "0 0 1";
m_Forward.Normalize(); m_Left = Vector(-m_Forward[2], 0, m_Forward[0]);
Orders(true, now);
m_DismountAttempts = 0; m_NextDismount = now + 10000;
ERR_HQ.Event("CONVOY COMBAT", m_Task.Receipt.SourceId + ": armed escorts take covering positions; ammo truck withdraws");
}
protected void Finish(float now)
{
Clear(); m_State = 0;
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
{
vehicle.Combat.Held = false; vehicle.Arrived = false;
// Combat time does not consume the driving deadline.
vehicle.LegDeadline = now + 600000;
}
m_Task.TravelDeadline = now + 600000;
if (m_Task.Column) m_Task.Column.Reset(now, false, true);
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
}
protected float InfantryDistance(IEntity vehicle)
{
float distance = 1e20;
if (!vehicle) return distance;
foreach (ERR_SupplyTeam team : m_Task.Teams)
{
if (team.Released) continue;
foreach (IEntity member : team.Members)
{
if (!ERR_Actors.CanFight(member) || CompartmentAccessComponent.GetVehicleIn(member)) continue;
distance = Math.Min(distance, vector.DistanceXZ(member.GetOrigin(), vehicle.GetOrigin()));
}
}
return distance;
}
protected void ProtectInfantry()
{
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
{
if (vehicle.Released || !vehicle.Entity) continue;
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
if (!movement) continue;
float speed = 35;
if (InfantryDistance(vehicle.Entity) < 45) speed = 8;
movement.SetCruiseSpeed(speed);
}
}
protected void RefreshBoarding(float now)
{
foreach (ERR_SupplyTeam team : m_Task.Teams)
{
if (team.Released || team.Aboard(m_Task.Vehicles[2].Entity, true)) continue;
if (team.BoardingRetryAt > 0)
{
if (now < team.BoardingRetryAt) continue;
team.BoardingRetryAt = 0;
team.Add(Board(m_Task.Vehicles[2].Entity, true));
continue;
}
if (!team.BoardingProgress) team.BoardingProgress = new ERR_ConvoyBoardingProgress();
float distance; int seated = team.BoardingCount(m_Task.Vehicles[2].Entity, distance);
if (!team.BoardingProgress.Retry(seated, distance, now)) continue;
// Let the old native activity release reservations before asking for seats
// again. Occupants remain seated; no direct compartment move is performed.
team.Clear(); team.BoardingRetryAt = now + 1000;
ERR_HQ.Event("CONVOY BOARDING RETRY", string.Format("%1: team has %2 aboard; farthest remaining soldier %3 m from truck. No approach/seat progress for 30 s; retry %4/3", m_Task.Receipt.SourceId, seated, Math.Round(distance), team.BoardingProgress.Retries));
}
}
bool Tick(float now)
{
if (m_Stopped) return false;
for (int released = 0; released < m_Task.Vehicles.Count(); released++)
if (m_Task.Vehicles[released].Released) m_Support[released].Clear();
foreach (ERR_SupplyTeam infantryTeam : m_Task.Teams)
{
if (infantryTeam.Released) continue;
bool foreign = !infantryTeam.Group || infantryTeam.Group.GetFaction() != m_Mode.EnemyFaction();
foreach (IEntity member : infantryTeam.Members)
if (ERR_Actors.Alive(member) && ERR_Actors.Group(member) != infantryTeam.Group) foreign = true;
if (foreign || !infantryTeam.Living()) infantryTeam.Release(m_Mode);
}
vector observed; float nearest = 400; bool found;
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
if (!vehicle.Released && Observe(vehicle.Group, observed, nearest)) found = true;
foreach (ERR_SupplyTeam team : m_Task.Teams)
if (!team.Released && Observe(team.Group, observed, nearest)) found = true;
if (found) Report(observed, now, now);
if (m_State == 1)
{
for (int walking = 0; walking < m_Task.Teams.Count(); walking++)
{
ERR_SupplyTeam foot = m_Task.Teams[walking];
if (foot.Released || !foot.Living() || !foot.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
foot.VehicleUse(m_Mode, m_Task.Vehicles[2].Entity, false);
if (!foot.SearchPlanned) PlanFlank(walking, false, 0);
if (foot.SearchPlanned) Claim(foot.Group, foot.SearchPosition, now);
}
for (int supporting = 0; supporting < m_Support.Count(); supporting++)
{
ERR_ConvoyVehicle covering = m_Task.Vehicles[supporting];
if (covering.Released || !covering.Entity) continue;
vector coverPosition = covering.Entity.GetOrigin();
if (!m_Support[supporting].Orders.IsEmpty() && m_Support[supporting].Orders[0])
coverPosition = m_Support[supporting].Orders[0].GetOrigin();
Claim(covering.Group, coverPosition, now);
}
if (!m_TruckWithdrawn && InfantryAboard(false) && InfantryDistance(m_Task.Vehicles[2].Entity) >= 25) { m_TruckWithdrawn = true; Support(2, false); }
// Native exit attempts can fail while the truck brakes or a door is busy.
// Retry only still-mounted teams, never teleport actors or reset quiet time.
if (!m_TruckWithdrawn && !InfantryAboard(false) && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
{
m_DismountAttempts++; m_NextDismount = now + 10000;
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
{
ERR_SupplyTeam delayed = m_Task.Teams[retry];
if (delayed.Released || delayed.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
delayed.Clear(); Flank(retry, true);
}
}
if (now >= m_NextOrders && now - m_LastContact < 180000) Orders(!InfantryAboard(false), now);
// Search only remembered positions. Real new sightings can redirect the
// teams; silence gives them time to reach and sweep both flanks, not a
// one-minute dash followed by forced reboarding.
if (!found && now - m_LastContact < 180000) ContinueSearch(now);
if (now - m_LastContact >= 180000)
{
Clear();
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
if (!boarding.Released && boarding.Living())
{
boarding.VehicleUse(m_Mode, m_Task.Vehicles[2].Entity, true);
boarding.BoardingProgress = new ERR_ConvoyBoardingProgress(); boarding.BoardingRetryAt = 0;
boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
}
m_State = 2; m_BoardDeadline = now + 300000;
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
}
}
if (m_State == 2 && Usable(2) && now < m_BoardDeadline) RefreshBoarding(now);
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
{
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
Finish(now);
}
if (m_State != 0) ProtectInfantry();
return m_State != 0;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true; Clear();
ERR_HQ hq = ERR_HQ.Get();
foreach (ERR_SupplyTeam team : m_Task.Teams)
{
if (hq) hq.AttackSpace().Release(team.Group);
}
foreach (ERR_SupplyTeam support : m_Support) if (hq) hq.AttackSpace().Release(support.Group);
}
}