Smooth convoy startup and record Workshop 0.0.153
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-10 15:34:24 +01:00
parent d8352136e8
commit 2390f77554
72 changed files with 15088 additions and 161 deletions
@@ -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;
@@ -126,42 +126,94 @@ class ERR_ConvoyRoadPlan
}
static bool Build(vector seed, out array<vector> positions, out array<vector> angles, out ERR_ForceRoute route, array<vector> 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<BaseRoad> 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<vector> m_Excluded = {};
protected ref array<BaseRoad> m_Roads = {};
protected ref array<vector> m_Points = {};
protected ref array<vector> m_Positions = {};
protected ref array<vector> 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<vector> 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)
{
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<vector> 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++)
{
if (!road || road.GetWidth() < 4) continue;
array<vector> points = {}; road.GetPoints(points);
for (int candidateIndex = 0; candidateIndex < 33; candidateIndex++)
{
array<vector> 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;
}
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<vector> positions, out array<vector> 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;
}
}
@@ -334,6 +334,7 @@ class ERR_ConvoyTask
ref array<ref ERR_ConvoyCargoCallback> 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<BaseCompartmentSlot> all = {};
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
array<BaseCompartmentSlot> 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<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
+8 -1
View File
@@ -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)
@@ -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<vector> Positions = {};
ref array<vector> Angles = {};
ref array<ResourceName> 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<vector> m_Bases = {};
protected ref array<ref ERR_SupplyConvoyCreation> m_Creations = {};
protected bool m_CreationQueued;
protected ref array<ref ERR_SupplyConvoyLoad> m_Loads = {};
protected bool m_CargoQueued;
override ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> 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<ResourceName> 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<AIAgent> 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<vector> 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)
@@ -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<vector> 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<vector> 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);
}
}