// 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() { 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; ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState(); bool Arrived; float LegDeadline; float NextReadinessNote; void ClearOrder() { if (Waypoint && Group) Group.RemoveWaypoint(Waypoint); if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint); Waypoint = null; } void Release(ERR_GameMode mode) { if (Released) return; Released = true; Combat.Held = false; ClearOrder(); if (mode && mode.Forces()) { ERR_ForceRecord record = mode.Forces().FindGroup(Group); if (record) record.Pinned = false; } if (Group) { if (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 || Combat.Held || !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 (Released || Arrived) return; if (!CrewValid(mode)) { Release(mode); return; } if (Combat.Held) 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 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 selected stock crew and group are USSR. Other factions require a new // reviewed catalog; never silently rewrite affiliations on incompatible kit. if (!m_Mode.EnemyFaction() || m_Mode.EnemyFaction().GetFactionKey() != "USSR" || !m_Pool.Consume(receipt)) return null; ERR_ConvoyTask task = new ERR_ConvoyTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000; m_Tasks.Insert(task); for (int index = 0; index < 6; index++) { ERR_ConvoyVehicle unit = new ERR_ConvoyVehicle(); 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 and 19 finite crew; 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); // 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"); } } 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; 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.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"); } }