// Independent physical adaptation of the owner's supply-convoy extension. // HQ must dispatch an authored route using a finite canonical pool receipt. class ERR_ConvoyResources { static ResourceName VehiclePrefab(int index) { if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et"; if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et"; if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et"; if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et"; return ""; } static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; } static ResourceName Soldier() { ERR_GameMode mode = ERR_GameMode.Get(); if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier(); return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; } static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; } static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; } static bool Available() { array required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()}; for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i)); foreach (ResourceName prefab : required) { Resource resource = Resource.Load(prefab); if (!resource || !resource.IsValid()) return false; } return true; } } class ERR_ConvoyRules { static bool EmptyCrewTemplate(SCR_AIGroup group) { // The vanilla faction base authors six queued soldiers. Only our memberless // template may receive finite seat occupants; never retire another spawn job. return group && !group.GetSpawnImmediately() && group.GetAgentsCount() == 0 && (!group.m_aUnitPrefabSlots || group.m_aUnitPrefabSlots.IsEmpty()); } static bool Plan(array positions, array angles, ERR_ForceRoute route) { if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6 || !route || route.Looped() || route.Count() < 2 || !route.Seal()) return false; for (int i = 0; i < 6; i++) { if (!ERR_ForceRules.FiniteVector(positions[i]) || !ERR_ForceRules.FiniteVector(angles[i])) return false; for (int j = 0; j < i; j++) if (vector.DistanceXZ(positions[i], positions[j]) < 18) return false; } return true; } static bool GroundClear(array positions, array angles) { if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false; ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement(); for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false; return true; } static bool PartOf(IEntity entity, IEntity root) { int depth; while (entity && depth++ < 32) { if (entity == root) return true; entity = entity.GetParent(); } return false; } static bool Captured(Vehicle vehicle, SCR_AIGroup group) { if (group && group.GetPlayerCount(true) > 0) return true; if (!vehicle) return false; array ids = {}; GetGame().GetPlayerManager().GetPlayers(ids); foreach (int id : ids) { IEntity player = GetGame().GetPlayerManager().GetPlayerControlledEntity(id); if (player && PartOf(CompartmentAccessComponent.GetVehicleIn(player), vehicle)) return true; } return false; } static bool CollectSeats(IEntity root, array seats) { array pending = {root}; int cursor; while (cursor < pending.Count() && cursor < 128) { IEntity entity = pending[cursor++]; if (!entity) return false; SCR_BaseCompartmentManagerComponent manager = SCR_BaseCompartmentManagerComponent.Cast(entity.FindComponent(SCR_BaseCompartmentManagerComponent)); if (manager) { array local = {}; manager.GetCompartments(local); foreach (BaseCompartmentSlot seat : local) if (seat && !seats.Contains(seat)) seats.Insert(seat); } IEntity child = entity.GetChildren(); while (child) { if (pending.Count() >= 128 || pending.Contains(child)) return false; pending.Insert(child); child = child.GetSibling(); } } return cursor == pending.Count(); } } class ERR_ConvoyVehicle { int Index; Vehicle Entity; SCR_AIGroup Group; IEntity Driver; ref array Members = {}; ref array Seats = {}; AIWaypoint Waypoint; int RouteIndex; bool Released; bool Registered; // Latched only for an exact tracked hunter at authoritative player entry. bool PlayerCaptureObserved; ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState(); int SupplyToken; bool SupplyHeld; int InterceptToken; bool InterceptActive; AIWaypoint InterceptWaypoint; ref ERR_ConvoyInterceptOrder InterceptOrder; ERR_ConvoyReceipt InterceptReceipt; ERR_ForceRecord InterceptRecord; ERR_ForceRoute InterceptRoute; protected AIWaypoint m_InterceptWaypoint; protected Vehicle m_InterceptEntity; protected SCR_AIGroup m_InterceptGroup; protected IEntity m_InterceptDriver; protected ref array m_InterceptMembers = {}; protected ref array m_InterceptSeats = {}; protected int m_InterceptToken; protected int m_LastInterceptToken; protected vector m_InterceptPosition; protected float m_InterceptExpiresAt; protected int m_InterceptRouteIndex; bool Arrived; float LegDeadline; float NextReadinessNote; // Pending downlink owns a claim only; normal authored travel continues. bool InterceptCrew(ERR_GameMode mode) { if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false; if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false; if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false; array agents = {}; Group.GetAgents(agents); if (agents.Count() != Members.Count()) return false; for (int i = 0; i < Members.Count(); i++) { if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false; int attributed; foreach (AIAgent agent : agents) { if (!agent || !Members.Contains(agent.GetControlledEntity())) return false; if (agent.GetControlledEntity() == Members[i]) attributed++; } if (attributed != 1) return false; if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false; } if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false; return true; } bool InterceptMatches(ERR_ConvoyInterceptOrder order) { return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0; } bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order) { if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false; m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver; m_InterceptMembers.Clear(); m_InterceptSeats.Clear(); foreach (IEntity member : Members) m_InterceptMembers.Insert(member); foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat); m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order; m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt; return true; } void ClearIntercept(ERR_ConvoyInterceptOrder order) { if (!InterceptMatches(order)) return; // Delete only the exact waypoint this owner created, never an adopted handle. if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint); if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint); if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null; m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0; InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null; m_InterceptMembers.Clear(); m_InterceptSeats.Clear(); } // Pair preparation owns no route state. Recheck after both native allocations. bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now) { if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false; return !(order.Position != m_InterceptPosition) && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt; } // Caller transfers ownership of the prepared handle before AddWaypoint, which // is a native boundary rather than a transactional pair operation. void InstallIntercept(AIWaypoint detour) { m_InterceptRouteIndex = RouteIndex; m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true; Group.AddWaypoint(detour); } bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now) { if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false; if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false; AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition)); if (!detour) return false; detour.SetCompletionRadius(15); ClearOrder(); m_InterceptRouteIndex = RouteIndex; m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true; Group.AddWaypoint(detour); return true; } // True suppresses ordinary travel only after actual radio delivery. No route // index is consumed by arrival at the detour; removal leaves the seated group held. bool TickIntercept(ERR_GameMode mode, float now) { if (!InterceptOrder) return false; if (!InterceptMatches(InterceptOrder)) return InterceptActive; if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0) { bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; } if (!InterceptActive) return false; if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint) { ClearIntercept(InterceptOrder); return false; } if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15) { Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint); m_InterceptWaypoint = null; InterceptWaypoint = null; } return true; } void ClearOrder() { if (Waypoint && Group) Group.RemoveWaypoint(Waypoint); if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint); Waypoint = null; } void Release(ERR_GameMode mode) { if (Released) return; ClearIntercept(InterceptOrder); Released = true; Combat.Held = false; SupplyToken = 0; SupplyHeld = false; ClearOrder(); if (mode && mode.Forces()) { ERR_ForceRecord record = mode.Forces().FindGroup(Group); if (record) record.Pinned = false; } if (Group) { ERR_HQ hq; if (mode) hq = ERR_HQ.Cast(mode.Command()); bool retained = false; if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group); if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group); Group.SetPermanentLOD(-1); array agents = {}; Group.GetAgents(agents); foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1); } // Vehicle, cargo, casualties and surviving crew remain physical and lootable. } 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_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; } bool Order(ERR_ForceRoute route, float now) { if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false; if (Waypoint) return true; Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex))); if (!Waypoint) return false; Waypoint.SetCompletionRadius(35); Group.AddWaypoint(Waypoint); LegDeadline = now + 600000; return true; } void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now) { if (TickIntercept(mode, now)) return; if (Released || Arrived) return; if (!CrewValid(mode)) { Release(mode); return; } if (Combat.Held || SupplyHeld) return; // Native completion measures the seated group, not the vehicle origin. // Keep the same predicate after native removal of the completed waypoint. if (Waypoint && Waypoint.IsWithinCompletionRadius(Group)) { ClearOrder(); RouteIndex++; if (RouteIndex >= route.Count()) { Arrived = true; return; } } if (Waypoint && now >= LegDeadline) { Release(mode); return; } if (!Order(route, now)) Release(mode); } } class ERR_ConvoyCargoCallback : ScriptedInventoryOperationCallback { ResourceName Prefab; bool Finished; bool Succeeded; protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } } protected override void OnComplete() { Finish(true); } protected override void OnFailed() { Finish(false); } protected override void OnAllComplete() { Finish(true); } protected override void OnAllFailed() { Finish(false); } } class ERR_ConvoyTask { ref ERR_ConvoyReceipt Receipt; ref ERR_ForceRoute Route; ref array Vehicles = {}; ref array Cargo = {}; bool Stopped; bool Ready; bool CargoIssued; float Deadline; float NextCheck; string Reason; void Stop(ERR_GameMode mode, string reason) { if (Stopped) return; Stopped = true; Reason = reason; foreach (ERR_ConvoyVehicle vehicle : Vehicles) vehicle.Release(mode); ERR_HQ.Event("CONVOY STOP", reason + "; surviving entities and cargo preserved"); } } class ERR_ConvoyService { protected ERR_GameMode m_Mode; protected ERR_ConvoyPool m_Pool; protected ref array m_Tasks = {}; protected bool m_Stopped; void ERR_ConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) { m_Mode = mode; m_Pool = pool; } protected bool Current() { return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool; } protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); } protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); } ERR_ConvoyTask At(int index) { if (index < 0 || index >= m_Tasks.Count()) return null; return m_Tasks[index]; } ERR_ConvoyTask Find(string source) { foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task; return null; } protected bool FillCrew(ERR_ConvoyVehicle unit) { array all = {}; if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false; array drivers = {}, turrets = {}, cargo = {}; foreach (BaseCompartmentSlot slot : all) { if (!slot.IsCompartmentAccessible() || slot.IsOccupied()) continue; if (slot.GetType() == ECompartmentType.PILOT) drivers.Insert(slot); else if (slot.GetType() == ECompartmentType.TURRET) turrets.Insert(slot); else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot); } int requiredTurrets = 1; if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0; if (unit.Index == 4) requiredTurrets = 2; if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false; unit.Seats.Insert(drivers[0]); for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]); if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]); // Exact planned population is known before any character is created. if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false; AIGroup nativeGroup = unit.Group; foreach (BaseCompartmentSlot seat : unit.Seats) { IEntity actor = seat.SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab()); if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); } if (!actor || nativeGroup != unit.Group) return false; } unit.Driver = unit.Members[0]; return true; } ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array positions, array angles, ERR_ForceRoute route, float now) { if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null; // The server role selection owns the memberless group affiliation. if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null; ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000; m_Tasks.Insert(task); for (int index = 0; index < 6; index++) { ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit); unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index])); if (unit.Group) m_Mode.Own(unit.Group); unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index])); if (unit.Entity) m_Mode.Own(unit.Entity); if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit)) { task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; } FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent)); if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction()); AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent)); if (movement) movement.SetCruiseSpeed(35); } ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending"); return task; } protected string ReadinessReason(ERR_ConvoyVehicle unit) { if (unit.Registered) return "already admitted"; if (!unit.Group) return "missing group"; if (!unit.Entity) return "missing vehicle"; if (!ERR_Actors.Alive(unit.Entity)) return "vehicle destroyed or lacks damage state"; if (unit.Group.GetFaction() != m_Mode.EnemyFaction()) return "group faction mismatch"; if (!ERR_Actors.CanFight(unit.Driver)) return "driver absent/dead/unconscious or controller unavailable"; if (ERR_Actors.Player(unit.Driver)) return "driver controlled by player"; if (!ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(unit.Driver), unit.Entity)) return "driver outside intended vehicle"; foreach (IEntity member : unit.Members) { if (!ERR_Actors.Alive(member)) return "member absent/dead or damage state unavailable"; if (ERR_Actors.Player(member)) return "member controlled by player"; if (ERR_Actors.Group(member) != unit.Group) return "member native agent/parent group mismatch"; } array agents = {}; unit.Group.GetAgents(agents); if (agents.Count() != unit.Members.Count()) return "native agent count differs from original crew"; foreach (AIAgent agent : agents) { if (!agent) return "null native agent"; if (!unit.Members.Contains(agent.GetControlledEntity())) return "native agent controls foreign entity"; if (!ERR_Actors.CanFight(agent.GetControlledEntity())) return "native agent actor cannot fight"; } SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent(); if (!utility) return "group utility unavailable"; if (!utility.m_VehicleMgr) return "group vehicle manager unavailable"; SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent)); if (!usage) return "vehicle usage component unavailable"; for (int i = 0; i < unit.Seats.Count(); i++) if (i >= unit.Members.Count() || !unit.Seats[i] || unit.Seats[i].GetOccupant() != unit.Members[i]) return "intended seat occupant mismatch"; return "readiness guards satisfied; awaiting admission"; } protected void InspectReadiness(ERR_ConvoyVehicle unit, float now) { if (!unit || now < unit.NextReadinessNote) return; unit.NextReadinessNote = now + 5000; int agents, activeAgents; bool activeGroup; if (unit.Group) { array current = {}; unit.Group.GetAgents(current); agents = current.Count(); activeGroup = unit.Group.IsAIActivated(); foreach (AIAgent agent : current) if (agent && agent.IsAIActivated()) activeAgents++; } ERR_HQ.Event("CONVOY READINESS", string.Format("vehicle=%1 reason=%2 members=%3 agents=%4 activeAgents=%5 activeGroup=%6 seats=%7 registered=%8", unit.Index, ReadinessReason(unit), unit.Members.Count(), agents, activeAgents, activeGroup, unit.Seats.Count(), unit.Registered)); } protected bool Admit(ERR_ConvoyTask task, float now) { foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now); foreach (ERR_ConvoyVehicle unit : task.Vehicles) { if (unit.Registered) continue; if (!unit.CrewValid(m_Mode)) return false; array agents = {}; unit.Group.GetAgents(agents); if (agents.Count() != unit.Members.Count()) return false; foreach (AIAgent agent : agents) if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false; SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent(); SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent)); if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false; ERR_ForceRecord record = m_Mode.Forces().Register(task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString(), ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now); if (!record || !m_Pool.BindRecord(task.Receipt, unit.Index, record)) { task.Stop(m_Mode, "convoy ledger admission rejected"); return false; } record.Pinned = true; unit.Registered = true; m_Mode.RegisterGroup(unit.Group); ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (hq) { hq.ReserveGroup(unit.Group); if (!hq.BindVehicle(unit.Group, unit.Entity)) ERR_HQ.Event("MOUNTED AMMO", "original convoy vehicle binding unavailable; no replacement lookup"); } // Native helper idempotently registers deletion/damage callbacks and mounted // combat activity; raw vehicle-manager admission alone omits those hooks. utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI(); foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); } } return true; } protected void IssueCargo(ERR_ConvoyTask task) { if (task.CargoIssued || task.Stopped || task.Vehicles.Count() != 6) return; task.CargoIssued = true; InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent)); if (!inventory) { ERR_HQ.Event("CONVOY CARGO", "ammo truck inventory unavailable; no retry or free replacement"); return; } for (int i = 0; i < 56; i++) { ResourceName prefab = ERR_ConvoyResources.Magazine(); if (i < 8) prefab = ERR_ConvoyResources.Rifle(); ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; task.Cargo.Insert(operation); if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true; } ERR_HQ.Event("CONVOY CARGO", "finite target submitted once: eight AK74 and 48 magazines; native capacity/completion may reduce actual stock"); } void Tick(float now) { if (!Current() || !ERR_ForceRules.Finite(now)) return; foreach (ERR_ConvoyTask task : m_Tasks) { if (task.Stopped || now < task.NextCheck) continue; task.NextCheck = now + 1000; bool captured; foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) captured = true; if (captured) { task.Stop(m_Mode, "player captured convoy vehicle or joined crew"); continue; } if (!task.Ready) { if (now >= task.Deadline) { task.Stop(m_Mode, "native crew readiness expired; no replacement"); continue; } if (!Admit(task, now)) continue; task.Ready = true; IssueCargo(task); } bool pending; foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) { vehicle.Tick(m_Mode, task.Route, now); if (!vehicle.Released && !vehicle.Arrived) pending = true; } if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released"); } } protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order) { if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false; if (!m_Mode.Forces()) return false; ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false; if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false; if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false; if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false; ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex]; if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false; if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false; ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group); if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false; if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false; if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false; foreach (ERR_ConvoyVehicle other : task.Vehicles) if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false; return true; } bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order) { return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token && task.Vehicles[order.VehicleIndex].InterceptMatches(order); } bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now) { if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false; if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false; if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false; if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false; if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false; foreach (ERR_ConvoyTask existing : m_Tasks) foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false; ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex]; if (!unit.ClaimIntercept(m_Mode, order)) return false; unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route; return true; } protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now) { if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false; if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false; if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false; if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false; if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false; if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false; if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false; if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false; if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false; return vector.DistanceXZ(first.Position, second.Position) >= 40; } bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now) { if (!InterceptPairEligible(task, first, second, now)) return false; foreach (ERR_ConvoyTask existing : m_Tasks) foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false; ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex]; ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex]; if (!lead.ClaimIntercept(m_Mode, first)) return false; lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route; if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second)) { return false; } partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route; return true; } bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now) { if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false; ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex]; ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex]; if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false; AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position)); if (!firstWaypoint) return false; AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position)); if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; } firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15); if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner || !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; } // Route removal is also a native boundary; both prepared handles remain // caller-owned until both exact claims have survived it. lead.ClearOrder(); if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder(); if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner || !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false; } lead.InstallIntercept(firstWaypoint); // AddWaypoint can invoke native listeners. Do not install the partner after // cancellation/ownership loss; release only these exact owned claims. if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive || !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner || !partner.CanInstallIntercept(m_Mode, second, now)) { SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false; } partner.InstallIntercept(secondWaypoint); if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second) && task.Vehicles[first.VehicleIndex] == lead && task.Vehicles[second.VehicleIndex] == partner && lead.InterceptActive && partner.InterceptActive) return true; ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false; } bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now) { if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false; return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now); } void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now) { if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return; foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (unit && unit.InterceptMatches(order)) { bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; } } bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now) { if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false; foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false; if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false; ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex]; if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false; if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false; if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false; ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group); if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false; array agents = {}; unit.Group.GetAgents(agents); if (agents.Count() != unit.Members.Count()) return false; foreach (AIAgent agent : agents) if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false; // No caller can replay even a command whose native waypoint recreation fails. unit.Combat.LastCommandId = order.CommandId; if (order.Hold) { if (!unit.Combat.Held) unit.ClearOrder(); unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt; } else { unit.Combat.Held = false; if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; } } ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex)); return true; } int Count() { return m_Tasks.Count(); } string Status() { int ready, stopped, vehicles, completed, failed, pending; foreach (ERR_ConvoyTask task : m_Tasks) { if (task.Ready) ready++; if (task.Stopped) stopped++; foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity) vehicles++; foreach (ERR_ConvoyCargoCallback operation : task.Cargo) { if (!operation.Finished) pending++; else if (operation.Succeeded) completed++; else failed++; } } string reason; if (m_Tasks.Count() == 1 && m_Tasks[0].Stopped) reason = m_Tasks[0].Reason; if (reason.Length() > 160) reason = reason.Substring(0, 160); return string.Format("Convoys: %1 tasks, %2 reached readiness, %3 stopped, %4 retained physical vehicles; cargo native insertion successes=%5 failed=%6 pending=%7; current inventory may differ after use/looting", m_Tasks.Count(), ready, stopped, vehicles, completed, failed, pending) + "; reason=" + reason; } void Stop() { if (m_Stopped) return; m_Stopped = true; foreach (ERR_ConvoyTask task : m_Tasks) task.Stop(m_Mode, "HQ ended convoy task service"); } }