// Original convoy tactics adapted to native AI waypoints. No old framework hooks. class ERR_SupplyTeam { SCR_AIGroup Group; ref array Members = {}; ref array Seats = {}; ref array 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 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 s_Crews = new map(); protected static ref map s_Couriers = new map(); 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 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 Teams = {}; ref ERR_SupplyConvoyTactics Tactics; ref ERR_SupplyConvoyColumn Column; ref array Bases = {}; ref array Starts = {}; int LastBase = -1; int Legs; float Departure; float TravelDeadline; bool Waiting; bool MagazinesIssued; ref array MountedStock = {}; ref array 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 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); } }