diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Performance.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Performance.c index 4b44443..2f107ab 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Performance.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Performance.c @@ -6,7 +6,7 @@ enum ERR_PerfMetric HQ_PREP, HQ_HUNTERS, HQ_GARRISONS, HQ_CONVOYS, HQ_BODIES, HQ_RADIO_INDEX, HQ_REPORTS, HQ_PLANNER, HQ_SUPPORT, HQ_JOURNAL, UPDATE_INTERVAL, ENGINE_SIM_SAMPLE, ENGINE_FRAME_SAMPLE, ENGINE_FPS_SAMPLE, - ACTIVE_AI_SAMPLE, PLAYERS_SAMPLE, HQ_GROUPS_SAMPLE, GARRISON_WORK, PERF_FLUSH, COUNT + ACTIVE_AI_SAMPLE, PLAYERS_SAMPLE, HQ_GROUPS_SAMPLE, GARRISON_WORK, PERF_FLUSH, CONVOY_INTERCEPT, CONVOY_RUNTIME, CONVOY_STARTUP, CONVOY_SURVEY, CONVOY_CREATE, CONVOY_CARGO, COUNT } class ERR_PerfStat @@ -40,7 +40,8 @@ class ERR_Performance "hq_prep_ms", "hq_hunters_ms", "hq_garrisons_ms", "hq_convoys_ms", "hq_bodies_ms", "hq_radio_index_ms", "hq_reports_ms", "hq_planner_ms", "hq_support_ms", "hq_journal_ms", "update_interval_ms", "engine_sim_sample_ms", "engine_frame_sample_ms", "engine_fps_sample", - "active_ai_sample", "players_sample", "hq_groups_sample", "garrison_work_ms", "performance_flush_ms" + "active_ai_sample", "players_sample", "hq_groups_sample", "garrison_work_ms", "performance_flush_ms", + "convoy_intercept_ms", "convoy_runtime_ms", "convoy_startup_ms", "convoy_survey_ms", "convoy_create_ms", "convoy_cargo_ms" }; protected string m_Run; protected int m_Window; diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c index 88e42e6..9216619 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c @@ -126,42 +126,94 @@ class ERR_ConvoyRoadPlan } static bool Build(vector seed, out array positions, out array angles, out ERR_ForceRoute route, array excludedStarts = null, float separation = 0) { - positions = {}; angles = {}; route = null; - if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(seed)) return false; - SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld()); - if (!world || !world.GetRoadNetworkManager()) return false; - array roads = {}; - world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-500 -1000 -500", seed + "500 1000 500", roads); - if (roads.Count() > 128) return false; - float bestDistance = 1e20; - foreach (BaseRoad road : roads) + ERR_ConvoyRoadSurvey survey = new ERR_ConvoyRoadSurvey(); + survey.Begin(seed, excludedStarts, separation); + while (!survey.Step()) {} + return survey.Result(positions, angles, route); + } +} + +// Cooperative survey: each Step performs one native road lookup/setup OR one +// formation candidate. Native calls themselves are not a hard time budget. +// The complete road/candidate order and final tie-breaking match Build. +class ERR_ConvoyRoadSurvey +{ + protected vector m_Seed; + protected float m_Separation; + protected float m_BestDistance; + protected ref array m_Excluded = {}; + protected ref array m_Roads = {}; + protected ref array m_Points = {}; + protected ref array m_Positions = {}; + protected ref array m_Angles = {}; + protected ref ERR_ForceRoute m_Route; + protected int m_RoadIndex; + protected int m_CandidateIndex; + protected bool m_Queried; + protected bool m_Loaded; + protected bool m_Done = true; + + void Begin(vector seed, array excludedStarts = null, float separation = 0) + { + m_Seed = seed; m_Separation = separation; m_BestDistance = 1e20; + m_Excluded.Clear(); + if (excludedStarts) foreach (vector excluded : excludedStarts) m_Excluded.Insert(excluded); + m_Roads.Clear(); m_Points.Clear(); m_Positions = {}; m_Angles = {}; m_Route = null; + m_RoadIndex = 0; m_CandidateIndex = 0; + m_Queried = false; m_Loaded = false; m_Done = false; + } + bool Step() + { + if (m_Done) return true; + if (!m_Queried) { - if (!road || road.GetWidth() < 4) continue; - array points = {}; road.GetPoints(points); - for (int candidateIndex = 0; candidateIndex < 33; candidateIndex++) - { - array candidatePositions, candidateAngles; - ERR_ForceRoute polyline; - float distance; - if (!FromPolyline(points, seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) continue; - // Exclude occupied deployment areas before selecting the best formation. - // Returning the nearest clear-but-too-close road used to hide valid - // alternatives from startup's outer separation check. - if (excludedStarts && !ERR_SupplyConvoyRules.Separated(candidatePositions[0], excludedStarts, separation)) continue; - for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]); - ERR_ForceRoute grounded = new ERR_ForceRoute(); - bool valid = true; - for (int i = 0; i < polyline.Count(); i++) - { - vector point = ERR_Actors.Ground(polyline.At(i)); - if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !grounded.Add(point)) { valid = false; break; } - } - if (!valid || !grounded.Seal() || !ERR_ConvoyRules.Plan(candidatePositions, candidateAngles, grounded)) continue; - if (!Prefer(distance, candidatePositions, grounded, bestDistance, positions, route)) continue; - if (!ERR_ConvoyRules.GroundClear(candidatePositions, candidateAngles)) continue; - positions = candidatePositions; angles = candidateAngles; route = grounded; bestDistance = distance; - } + m_Queried = true; + if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(m_Seed)) { m_Done = true; return true; } + SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld()); + if (!world || !world.GetRoadNetworkManager()) { m_Done = true; return true; } + world.GetRoadNetworkManager().GetRoadsInAABB(m_Seed + "-500 -1000 -500", m_Seed + "500 1000 500", m_Roads); + if (m_Roads.Count() > 128 || m_Roads.IsEmpty()) m_Done = true; + return m_Done; + } + if (!m_Loaded) + { + if (m_RoadIndex >= m_Roads.Count()) { m_Done = true; return true; } + BaseRoad road = m_Roads[m_RoadIndex++]; + if (!road || road.GetWidth() < 4) return false; + m_Points.Clear(); road.GetPoints(m_Points); + m_CandidateIndex = 0; m_Loaded = true; + return false; + } + int candidateIndex = m_CandidateIndex++; + if (m_CandidateIndex >= 33) m_Loaded = false; + Consider(candidateIndex); + return false; + } + protected void Consider(int candidateIndex) + { + array candidatePositions, candidateAngles; + ERR_ForceRoute polyline; + float distance; + if (!ERR_ConvoyRoadPlan.FromPolyline(m_Points, m_Seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) return; + if (!ERR_SupplyConvoyRules.Separated(candidatePositions[0], m_Excluded, m_Separation)) return; + for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]); + ERR_ForceRoute grounded = new ERR_ForceRoute(); + bool valid = true; + for (int i = 0; i < polyline.Count(); i++) + { + vector point = ERR_Actors.Ground(polyline.At(i)); + if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !grounded.Add(point)) { valid = false; break; } } - return route != null; + if (!valid || !grounded.Seal() || !ERR_ConvoyRules.Plan(candidatePositions, candidateAngles, grounded)) return; + if (!ERR_ConvoyRoadPlan.Prefer(distance, candidatePositions, grounded, m_BestDistance, m_Positions, m_Route)) return; + if (!ERR_ConvoyRules.GroundClear(candidatePositions, candidateAngles)) return; + m_Positions = candidatePositions; m_Angles = candidateAngles; m_Route = grounded; m_BestDistance = distance; + } + bool Result(out array positions, out array angles, out ERR_ForceRoute route) + { + positions = {}; angles = {}; route = null; + if (!m_Done || !m_Route) return false; + positions = m_Positions; angles = m_Angles; route = m_Route; + return true; } } diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c index cc773b9..9459938 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c @@ -334,6 +334,7 @@ class ERR_ConvoyTask ref array Cargo = {}; bool Stopped; bool Ready; + bool Creating; // Supply service queues physical creation before readiness begins. bool CargoIssued; float Deadline; float NextCheck; @@ -370,9 +371,9 @@ class ERR_ConvoyService foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task; return null; } - protected bool FillCrew(ERR_ConvoyVehicle unit) + protected bool SelectCrewSeats(ERR_ConvoyVehicle unit) { - if (unit.Index == 2) return false; + if (!unit || !unit.Group || !unit.Entity || !unit.Seats.IsEmpty() || unit.Index == 2) return false; array all = {}; if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false; array drivers = {}, turrets = {}, cargo = {}; @@ -391,14 +392,25 @@ class ERR_ConvoyService for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]); // Exact planned population is known before any character is created. if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false; + return true; + } + protected bool SpawnCrewMember(ERR_ConvoyVehicle unit, int index) + { + if (!unit || !unit.Group || index < 0 || index >= unit.Seats.Count() || index != unit.Members.Count()) return false; + BaseCompartmentSlot seat = unit.Seats[index]; + if (!seat || !seat.IsCompartmentAccessible() || seat.IsOccupied()) 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]; + 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; + if (index == 0) unit.Driver = actor; + return true; + } + protected bool FillCrew(ERR_ConvoyVehicle unit) + { + if (!SelectCrewSeats(unit)) return false; + for (int index = 0; index < unit.Seats.Count(); index++) + if (!SpawnCrewMember(unit, index)) return false; return true; } ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array positions, array angles, ERR_ForceRoute route, float now) diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c index 5c38e20..f1da514 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c @@ -1813,10 +1813,17 @@ class ERR_HQ : ERR_CommandService m_ObjectiveDefence.Tick(now); m_Towns.Tick(now); if (perf) perfStamp = perf.Mark(ERR_PerfMetric.HQ_GARRISONS, perfStamp); + int convoyStamp = perfStamp; TickMountedIntercept(now); + if (perf) convoyStamp = perf.Mark(ERR_PerfMetric.CONVOY_INTERCEPT, convoyStamp); m_Convoys.Tick(now); + if (perf) convoyStamp = perf.Mark(ERR_PerfMetric.CONVOY_RUNTIME, convoyStamp); AutomaticConvoy(now); - if (perf) perfStamp = perf.Mark(ERR_PerfMetric.HQ_CONVOYS, perfStamp); + if (perf) + { + perf.Mark(ERR_PerfMetric.CONVOY_STARTUP, convoyStamp); + perfStamp = perf.Mark(ERR_PerfMetric.HQ_CONVOYS, perfStamp); + } m_Bodies.TickBudget(now); if (perf) perfStamp = perf.Mark(ERR_PerfMetric.HQ_BODIES, perfStamp); if (now >= m_NextNetwork) diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c index 9edda1c..5cf5e07 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c @@ -1,3 +1,26 @@ +// A consumed convoy receipt owns one finite physical creation transaction. +// Native spawn calls are spread across callbacks; successful partial entities +// stay mission-owned and lootable on refusal, capture or cancellation. +class ERR_SupplyConvoyCreation +{ + ref ERR_SupplyConvoyTask Task; + ref array Positions = {}; + ref array Angles = {}; + ref array Prefabs = {}; + float Expires; + int VehicleIndex; + int Phase; +} + +class ERR_SupplyConvoyLoad +{ + ref ERR_SupplyConvoyTask Task; + ERR_ConvoyVehicle OriginalVehicle; + IEntity Truck; + InventoryStorageManagerComponent Inventory; + ref ERR_ConvoyCargoCallback Operation; +} + // Full supply convoys reuse finite vehicle creation and cargo ownership. The // legacy finite-route service remains available for isolated migration fixtures. class ERR_SupplyConvoyService : ERR_ConvoyService @@ -5,6 +28,240 @@ class ERR_SupplyConvoyService : ERR_ConvoyService void ERR_SupplyConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) {} protected ref ERR_SupplyConvoyTask m_Creating; protected ref array m_Bases = {}; + protected ref array m_Creations = {}; + protected bool m_CreationQueued; + protected ref array m_Loads = {}; + protected bool m_CargoQueued; + override ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array positions, array angles, ERR_ForceRoute route, float now) + { + if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || m_Creations.Count() >= 20 + || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null; + array selected = {}; + for (int slot = 0; slot < 6; slot++) + { + ResourceName prefab = ERR_ConvoyResources.VehiclePrefab(slot); + if (slot == 1) prefab = ERR_VehicleSelection.ForRole(4, prefab); + if (slot == 0 || slot == 3 || slot == 5) prefab = ERR_VehicleSelection.ForRole(3, prefab); + if (slot != 2 && prefab.IsEmpty()) return null; + selected.Insert(prefab); + } + if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null; + ERR_SupplyConvoyTask task = ERR_SupplyConvoyTask.Cast(NewTask()); + task.Receipt = receipt; task.Route = route; task.Creating = true; + for (int index = 0; index < 6; index++) + { + ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; + if (index == 2) unit.Released = true; + task.Vehicles.Insert(unit); + } + m_Tasks.Insert(task); + ERR_SupplyConvoyCreation creation = new ERR_SupplyConvoyCreation(); + creation.Task = task; creation.Positions.Copy(positions); creation.Angles.Copy(angles); creation.Prefabs.Copy(selected); + creation.Expires = now + 60000; m_Creations.Insert(creation); + if (!m_CreationQueued) + { + m_CreationQueued = true; + GetGame().GetCallqueue().CallLater(CreationStep, 50, true); + } + return task; + } + protected void FinishCreation(ERR_SupplyConvoyCreation creation, bool success, string reason) + { + ERR_SupplyConvoyTask task = creation.Task; + task.Creating = false; + if (success) + { + task.Deadline = GetGame().GetWorld().GetWorldTime() + 30000; + task.NextCheck = 0; + ERR_HQ.Event("CONVOY CREATED", "five physical vehicles; finite crew created over queued callbacks; native readiness and ledger admission pending"); + } + else task.Stop(m_Mode, reason); + m_Creations.RemoveItem(creation); + if (m_Creations.IsEmpty()) + { + GetGame().GetCallqueue().Remove(CreationStep); + m_CreationQueued = false; + } + } + protected bool CreationOwned(ERR_SupplyConvoyCreation creation) + { + ERR_SupplyConvoyTask task = creation.Task; + if (!task || task.Stopped || !task.Creating || !task.Receipt || !task.Receipt.Consumed || task.Vehicles.Count() != 6) return false; + if (Captured(task)) return false; + foreach (ERR_ConvoyVehicle unit : task.Vehicles) + { + if (!unit || (unit.Index != 2 && unit.Released)) return false; + if (unit.Index == 2) continue; + bool groupCreated = unit.Index < creation.VehicleIndex || (unit.Index == creation.VehicleIndex && creation.Phase >= 1); + bool vehicleCreated = unit.Index < creation.VehicleIndex || (unit.Index == creation.VehicleIndex && creation.Phase >= 2); + if ((groupCreated && !unit.Group) || (vehicleCreated && !unit.Entity)) return false; + if (unit.Group && (unit.Group.GetFaction() != m_Mode.EnemyFaction() || unit.Group.GetPlayerCount(true) > 0)) return false; + if (unit.Entity && !ERR_Actors.Alive(unit.Entity)) return false; + for (int member = 0; member < unit.Members.Count(); member++) + { + IEntity actor = unit.Members[member]; + if (!actor || !ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || member >= unit.Seats.Count() + || !unit.Seats[member]) return false; + // Native compartment membership can settle only after the later Wake/Admit + // pass. An empty pending seat is not a foreign occupant or permission to refill. + IEntity occupant = unit.Seats[member].GetOccupant(); + if (occupant && occupant != actor) return false; + SCR_AIGroup parent = ERR_Actors.Group(actor); + if (parent && parent != unit.Group) return false; + } + if (unit.Group) + { + array agents = {}; unit.Group.GetAgents(agents); + foreach (AIAgent agent : agents) + if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false; + } + } + return true; + } + // One native group, vehicle or character spawn at most, globally for this + // service's queue. No catch-up loop after a long frame or empty-server pause. + protected void CreationStep() + { + int started = System.GetTickCount(); + CreationPiece(); + if (m_Mode && m_Mode.Performance()) m_Mode.Performance().Mark(ERR_PerfMetric.CONVOY_CREATE, started); + } + protected void CreationPiece() + { + if (!Current()) { Stop(); return; } + if (m_Creations.IsEmpty()) + { GetGame().GetCallqueue().Remove(CreationStep); m_CreationQueued = false; return; } + // Match the mission's existing no-player suspension. A resumed transaction + // past its finite deadline stops instead of creating replacement bodies. + if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return; + ERR_SupplyConvoyCreation creation = m_Creations[0]; + ERR_SupplyConvoyTask task = creation.Task; + float now = GetGame().GetWorld().GetWorldTime(); + if (!ERR_ForceRules.Finite(now) || now >= creation.Expires || !CreationOwned(creation)) + { FinishCreation(creation, false, "finite convoy creation expired, captured or changed; partial entities retained"); return; } + if (creation.VehicleIndex == 2) creation.VehicleIndex++; + if (creation.VehicleIndex >= 6) { FinishCreation(creation, true, ""); return; } + int index = creation.VehicleIndex; + ERR_ConvoyVehicle unit = task.Vehicles[index]; + if (creation.Phase == 0) + { + unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), creation.Positions[index])); + if (unit.Group) m_Mode.Own(unit.Group); + if (!unit.Group || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group)) + { FinishCreation(creation, false, "finite convoy group creation failed; no retry"); return; } + creation.Phase = 1; return; + } + if (creation.Phase == 1) + { + // A player or unrelated entity may have entered this footprint since dispatch. + ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement(); + if (!placement.Clear(creation.Positions[index], creation.Angles[index][0]) || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group)) + { FinishCreation(creation, false, "convoy placement or original empty crew changed; no retry"); return; } + unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(creation.Prefabs[index], creation.Positions[index], creation.Angles[index])); + if (unit.Entity) m_Mode.Own(unit.Entity); + bool assigned = creation.Prefabs[index] != ERR_ConvoyResources.VehiclePrefab(index); + bool compatible = !assigned || (ERR_VehicleSelection.RoadFit(unit.Entity) && (index != 1 || unit.Entity.FindComponent(InventoryStorageManagerComponent))); + if (!unit.Entity || !compatible) + { + int role = 3; if (index == 1) role = 4; + if (assigned) GetGame().ERR_ServerVehicles().Reject(creation.Prefabs[index], role); + FinishCreation(creation, false, "finite convoy vehicle creation failed; no retry"); return; + } + task.Starts.Insert(unit.Entity.GetOrigin()); + 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); + creation.Phase = 2; return; + } + if (creation.Phase == 2) + { + if (!ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !SelectCrewSeats(unit)) + { FinishCreation(creation, false, "finite convoy seats unavailable; no retry"); return; } + creation.Phase = 3; return; + } + if (!SpawnCrewMember(unit, unit.Members.Count())) + { FinishCreation(creation, false, "finite convoy crew creation failed; no retry"); return; } + if (unit.Members.Count() == unit.Seats.Count()) + { creation.VehicleIndex++; creation.Phase = 0; } + } + protected void QueueCargo(ERR_SupplyConvoyTask task, InventoryStorageManagerComponent inventory, ERR_ConvoyCargoCallback operation) + { + ERR_SupplyConvoyLoad load = new ERR_SupplyConvoyLoad(); + load.Task = task; load.OriginalVehicle = task.Vehicles[1]; load.Truck = load.OriginalVehicle.Entity; + load.Inventory = inventory; load.Operation = operation; m_Loads.Insert(load); + if (!m_CargoQueued) + { + m_CargoQueued = true; + GetGame().GetCallqueue().CallLater(CargoStep, 50, true); + } + } + protected bool CargoPending(ERR_SupplyConvoyTask task) + { + foreach (ERR_SupplyConvoyLoad load : m_Loads) if (load.Task == task) return true; + return false; + } + protected void CancelCargo(ERR_SupplyConvoyTask task) + { + // Only requests still in this queue have not crossed the native boundary. + // Accepted operations were removed before submission and are never rewritten. + for (int index = m_Loads.Count() - 1; index >= 0; index--) + { + ERR_SupplyConvoyLoad load = m_Loads[index]; + if (task && load.Task != task) continue; + load.Operation.Finished = true; load.Operation.Succeeded = false; + m_Loads.Remove(index); + } + } + protected bool CargoOwner(ERR_SupplyConvoyLoad load) + { + ERR_SupplyConvoyTask task = load.Task; + if (!task || task.Stopped || !task.Ready || task.Creating || task.Vehicles.Count() != 6 || task.Vehicles[1] != load.OriginalVehicle + || !load.OriginalVehicle || load.OriginalVehicle.Released || load.OriginalVehicle.Entity != load.Truck || !load.Truck + || !load.Inventory || load.Inventory.GetOwner() != load.Truck || !load.OriginalVehicle.CrewValid(m_Mode) || Captured(task)) return false; + return load.Truck.FindComponent(InventoryStorageManagerComponent) == load.Inventory; + } + protected void CargoStep() + { + int started = System.GetTickCount(); + CargoPiece(); + if (m_Mode && m_Mode.Performance()) m_Mode.Performance().Mark(ERR_PerfMetric.CONVOY_CARGO, started); + } + protected void CargoPiece() + { + if (!Current()) { Stop(); return; } + if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return; + // One globally bounded pair of native insertion requests, never a catch-up + // loop. Original prefabs/callbacks and random choices were frozen by planning. + for (int issued = 0; issued < 2 && !m_Loads.IsEmpty(); issued++) + { + ERR_SupplyConvoyLoad load = m_Loads[0]; + if (!CargoOwner(load)) + { + CancelCargo(load.Task); + // The ordinary convoy tick owns capture and per-vehicle release policy. + // Losing this truck's cargo authority must not stop surviving escorts. + continue; + } + m_Loads.Remove(0); + if (!load.Inventory.TrySpawnPrefabToStorage(load.Operation.Prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, load.Operation)) load.Operation.Finished = true; + } + if (m_Loads.IsEmpty()) + { + GetGame().GetCallqueue().Remove(CargoStep); + m_CargoQueued = false; + } + } + override void Stop() + { + if (GetGame()) GetGame().GetCallqueue().Remove(CreationStep); + m_CreationQueued = false; + foreach (ERR_SupplyConvoyCreation creation : m_Creations) creation.Task.Creating = false; + m_Creations.Clear(); + if (GetGame()) GetGame().GetCallqueue().Remove(CargoStep); + m_CargoQueued = false; CancelCargo(null); + super.Stop(); + } void SetBases(array bases) { m_Bases.Copy(bases); } protected override ERR_ConvoyTask NewTask() { @@ -81,7 +338,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService { ERR_ConvoyCargoCallback load = new ERR_ConvoyCargoCallback(); load.Prefab = seed.Prefab; task.Cargo.Insert(load); issued++; - if (!inventory.TrySpawnPrefabToStorage(seed.Prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, load)) load.Finished = true; + QueueCargo(task, inventory, load); } } return issued; @@ -108,12 +365,22 @@ class ERR_SupplyConvoyService : ERR_ConvoyService if (choice) prefab = choice.GetItemResourceName(); } ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; supply.Cargo.Insert(operation); - if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true; + QueueCargo(supply, inventory, operation); } } protected void IssueMagazines(ERR_SupplyConvoyTask task, float now) { - if (task.MagazinesIssued || !task.CargoIssued || task.Stopped) return; + if (!task || task.MagazinesIssued || !task.CargoIssued || task.Stopped || CargoPending(task)) return; + ERR_ConvoyVehicle vehicle = null; + if (task.Vehicles.Count() == 6) vehicle = task.Vehicles[1]; + // A queued weapon batch may have been cancelled after its truck disappeared + // or lost its original crew. Finish only cargo planning; escorts keep running. + if (!vehicle || vehicle.Released || !vehicle.Entity || !vehicle.CrewValid(m_Mode)) + { + task.MagazinesIssued = true; + task.MountedStock.Clear(); task.CargoBefore.Clear(); + return; + } if (task.Cargo.Count() > 128) { task.MagazinesIssued = true; return; } bool cargoSettled = true; foreach (ERR_ConvoyCargoCallback operation : task.Cargo) @@ -152,7 +419,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService for (int mag = 0; mag < 6 && issued < 96 && task.Cargo.Count() < 128; mag++) { ERR_ConvoyCargoCallback load = new ERR_ConvoyCargoCallback(); load.Prefab = magazine; task.Cargo.Insert(load); issued++; - if (!inventory.TrySpawnPrefabToStorage(magazine, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, load)) load.Finished = true; + QueueCargo(task, inventory, load); } } } @@ -204,7 +471,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService foreach (ERR_ConvoyTask baseTask : m_Tasks) { ERR_SupplyConvoyTask task = ERR_SupplyConvoyTask.Cast(baseTask); - if (!task || task.Stopped || now < task.NextCheck) continue; + if (!task || task.Stopped || task.Creating || now < task.NextCheck) continue; task.NextCheck = now + 1000; if (Captured(task)) { task.Stop(m_Mode, "player captured convoy vehicle or crew"); continue; } if (!task.Ready) diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c index 1c44973..f387997 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c @@ -13,8 +13,8 @@ class ERR_SupplyConvoyRules } } -// Original finite startup target, eight bounded road candidates per attempt, -// separated initial positions and base-to-base circulation; never replacement spawns. +// Original finite startup target and eight seeds per attempt. Survey and +// physical startup yield between bounded pieces; never replacement spawns. class ERR_SupplyConvoySystem { protected ERR_GameMode m_Mode; @@ -30,6 +30,12 @@ class ERR_SupplyConvoySystem protected float m_Next; protected string m_Reason; protected string m_LastFailure; + protected bool m_Searching, m_FindingBases; + protected int m_BaseCursor, m_SeedAttempt, m_StartingCount; + protected string m_Source; + protected vector m_Low, m_High; + protected float m_Separation; + protected ref ERR_ConvoyRoadSurvey m_Survey; void ERR_SupplyConvoySystem(ERR_GameMode mode, ERR_ConvoyPool pool, ERR_SupplyConvoyService service, int target, bool automatic) { m_Mode = mode; m_Pool = pool; m_Service = service; @@ -61,63 +67,116 @@ class ERR_SupplyConvoySystem if (length < 0.01) continue; vector offset = center - points[i - 1]; offset[1] = 0; float fraction = Math.Clamp(vector.Dot(offset, delta) / length, 0, 1); - vector candidate = ERR_Actors.Ground(vector.Lerp(points[i - 1], points[i], fraction)); + vector candidate = vector.Lerp(points[i - 1], points[i], fraction); float distance = vector.DistanceXZ(center, candidate); - if (distance >= nearest || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), candidate)) continue; + // Height cannot change XZ ranking. Avoid terrain/native water calls for + // segments that cannot improve the nearest road approach. + if (distance >= nearest) continue; + candidate = ERR_Actors.Ground(candidate); + if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), candidate)) continue; nearest = distance; position = candidate; angles = Vector(Math.Atan2(delta[0], delta[2]) * Math.RAD2DEG, 0, 0); found = true; } } return found; } - protected void FindBases() + // One site's road approach per callback, instead of all sites in one HQ tick. + protected bool FindBaseStep() { - m_Bases.Clear(); - if (!m_Mode.Sites()) return; - for (int i = 0; i < m_Mode.Sites().Count(); i++) - { - ERR_SiteRuntime site = m_Mode.Sites().At(i); - if (!site || !site.Definition) continue; - ERR_LocationKind kind = site.Definition.Kind; - if (kind != ERR_LocationKind.OUTPOST && kind != ERR_LocationKind.DEPOT && kind != ERR_LocationKind.RADIO) continue; - if (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction()) continue; - vector approach; - if (Approach(site.Definition.Position, approach) && ERR_SupplyConvoyRules.Separated(approach, m_Bases, 300)) m_Bases.Insert(approach); - } - m_Service.SetBases(m_Bases); + if (!m_Mode.Sites() || m_BaseCursor >= m_Mode.Sites().Count()) + { m_Service.SetBases(m_Bases); return true; } + ERR_SiteRuntime site = m_Mode.Sites().At(m_BaseCursor++); + if (!site || !site.Definition) return false; + ERR_LocationKind kind = site.Definition.Kind; + if (kind != ERR_LocationKind.OUTPOST && kind != ERR_LocationKind.DEPOT && kind != ERR_LocationKind.RADIO) return false; + if (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction()) return false; + vector approach; + if (Approach(site.Definition.Position, approach) && ERR_SupplyConvoyRules.Separated(approach, m_Bases, 300)) m_Bases.Insert(approach); + return false; } bool StartOne() { - if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || m_Service.Count() >= m_Target) return false; - string source = "supply-convoy-" + (m_Service.Count() + 1).ToString(); - // Existing source receipts already reserve their future crew. Only an - // impossible new body reservation can be refused before terrain surveying. - if (!m_Pool || !m_Pool.BodyBudgetAvailable(source)) + if (m_Stopped || m_Searching || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || m_Service.Count() >= m_Target) return false; + m_StartingCount = m_Service.Count(); + m_Source = "supply-convoy-" + (m_StartingCount + 1).ToString(); + if (!m_Pool || !m_Pool.BodyBudgetAvailable(m_Source)) { m_Reason = RefusalReason(null); return false; } - if (m_Bases.Count() < 2) FindBases(); - if (m_Bases.Count() < 2) { m_Reason = "waiting for two friendly military road approaches"; return false; } if (!GetGame().GetWorldEntity()) { m_Reason = "world bounds unavailable for road survey"; return false; } - vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high); - float separation = Math.Min(1000, Math.Max(high[0] - low[0], high[2] - low[2]) * 0.1); - for (int attempt = 0; attempt < 8; attempt++) + GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High); + m_Separation = Math.Min(1000, Math.Max(m_High[0] - m_Low[0], m_High[2] - m_Low[2]) * 0.1); + m_FindingBases = m_Bases.Count() < 2; + if (m_FindingBases) { m_Bases.Clear(); m_BaseCursor = 0; } + m_SeedAttempt = 0; m_Survey = null; m_Searching = true; + m_Reason = "road survey queued; no convoy slot consumed yet"; + GetGame().GetCallqueue().CallLater(SearchStep, 20, true); + return true; + } + protected void FinishSearch(string reason, bool failed = true) + { + if (GetGame()) GetGame().GetCallqueue().Remove(SearchStep); + m_Survey = null; m_Searching = false; m_FindingBases = false; m_Reason = reason; + m_Next = GetGame().GetWorld().GetWorldTime() + 5000; + if (!failed) { m_LastFailure = string.Empty; return; } + NoteFailure(); + if (m_Attempts >= 120 && m_Service.Count() < m_Target) + ERR_HQ.Event("CONVOY STARTUP", "bounded startup attempts exhausted; existing crews retained, no replacements. " + Status()); + } + protected void SearchStep() + { + if (!m_Searching) return; + if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return; } + // Match ordinary HQ's empty-server pause; never survey or spawn unseen rounds. + if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return; + ERR_Performance perf = m_Mode.Performance(); + int started = System.GetTickCount(); + SearchPiece(); + if (perf) perf.Mark(ERR_PerfMetric.CONVOY_SURVEY, started); + } + protected void SearchPiece() + { + // A queued plan grants no reservation. Other admissions may spend capacity + // while it yields, so validate again before any native mutation. + if (m_Service.Count() != m_StartingCount || m_Service.Count() >= m_Target || !m_Pool || !m_Pool.BodyBudgetAvailable(m_Source)) + { FinishSearch("convoy capacity changed while surveying; no entities created"); return; } + if (m_FindingBases) { - vector seed = Vector(Math.RandomFloat(low[0], high[0]), 0, Math.RandomFloat(low[2], high[2])); - // A nearby base is the first candidate; remaining attempts cover the island. - if (attempt == 0) seed = m_Bases[(m_Attempts + m_Service.Count()) % m_Bases.Count()]; - seed = ERR_Actors.Ground(seed); - array positions, angles; ERR_ForceRoute route; - if (!ERR_ConvoyRoadPlan.Build(seed, positions, angles, route, m_Starts, separation) || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, separation)) continue; - ERR_ConvoyReceipt receipt = m_Pool.Reserve(source); - if (!receipt || receipt.Consumed) { m_Reason = RefusalReason(receipt); return false; } - ERR_ConvoyTask task = m_Service.Dispatch(receipt, positions, angles, route, GetGame().GetWorld().GetWorldTime()); - if (!task) { m_Reason = "road preflight changed; no entities created"; return false; } - m_Starts.Insert(positions[0]); - m_Reason = "finite convoy slot deployed"; - if (task.Stopped) m_Reason = "partial startup retained; this slot cannot respawn"; - ERR_HQ.Event("CONVOY STARTUP", string.Format("%1/%2 at %3: %4", m_Service.Count(), m_Target, positions[0], m_Reason)); - return !task.Stopped; + if (!FindBaseStep()) return; + m_FindingBases = false; + if (m_Bases.Count() < 2) FinishSearch("waiting for two friendly military road approaches"); + return; } - m_Reason = "no clear separated road formation in this bounded survey"; - return false; + if (!m_Survey) + { + if (m_SeedAttempt >= 8) { FinishSearch("no clear separated road formation in this bounded survey"); return; } + vector seed = Vector(Math.RandomFloat(m_Low[0], m_High[0]), 0, Math.RandomFloat(m_Low[2], m_High[2])); + if (m_SeedAttempt == 0) seed = m_Bases[(m_Attempts + m_StartingCount) % m_Bases.Count()]; + m_SeedAttempt++; + m_Survey = new ERR_ConvoyRoadSurvey(); + m_Survey.Begin(ERR_Actors.Ground(seed), m_Starts, m_Separation); + return; + } + // Cooperative 2ms target and hard eight-piece cap. A single native query or + // terrain trace cannot be preempted, and uptime can coarsen the native timer. + int started = System.GetTickCount(); + bool complete; + for (int piece = 0; piece < 8; piece++) + { + complete = m_Survey.Step(); + if (complete || System.GetTickCount() - started >= 2) break; + } + if (!complete) return; + array positions, angles; ERR_ForceRoute route; + bool found = m_Survey.Result(positions, angles, route); + m_Survey = null; + if (!found || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, m_Separation)) return; + ERR_ConvoyReceipt receipt = m_Pool.Reserve(m_Source); + if (!receipt || receipt.Consumed) { FinishSearch(RefusalReason(receipt)); return; } + ERR_ConvoyTask task = m_Service.Dispatch(receipt, positions, angles, route, GetGame().GetWorld().GetWorldTime()); + if (!task) { FinishSearch("road preflight changed; no entities created"); return; } + m_Starts.Insert(positions[0]); + string reason = "finite convoy slot reserved; staged physical startup pending"; + if (task.Stopped) reason = "partial startup retained; this slot cannot respawn"; + FinishSearch(reason, false); + ERR_HQ.Event("CONVOY STARTUP", string.Format("%1/%2 at %3: %4", m_Service.Count(), m_Target, positions[0], m_Reason)); } protected string RefusalReason(ERR_ConvoyReceipt receipt) { @@ -139,7 +198,7 @@ class ERR_SupplyConvoySystem } void Tick(float now, bool admitted) { - if (m_Stopped || !m_Automatic || m_Target == 0 || m_Attempts >= 120 || m_Service.Count() >= m_Target || !ERR_ForceRules.Finite(now) || now < 0) return; + if (m_Stopped || m_Searching || !m_Automatic || m_Target == 0 || m_Attempts >= 120 || m_Service.Count() >= m_Target || !ERR_ForceRules.Finite(now) || now < 0) return; if (m_Began < 0) { m_Began = now; m_Next = now + 30000; } if (now < m_Next) return; m_Next = now + 5000; @@ -147,11 +206,15 @@ class ERR_SupplyConvoySystem m_Attempts++; if (StartOne()) m_LastFailure = string.Empty; else NoteFailure(); - if (m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", "bounded startup attempts exhausted; existing crews retained, no replacements. " + Status()); + if (!m_Searching && m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", "bounded startup attempts exhausted; existing crews retained, no replacements. " + Status()); } string Status() { return string.Format("This round: %1/%2 convoy slots; automatic=%3, startup attempts=%4/120; %5", m_Service.Count(), m_Target, m_Automatic, m_Attempts, m_Reason) + "; " + BudgetStatus(); } - void Stop() { m_Stopped = true; } + void Stop() + { + m_Stopped = true; m_Searching = false; m_FindingBases = false; m_Survey = null; + if (GetGame()) GetGame().GetCallqueue().Remove(SearchStep); + } }