345 lines
17 KiB
C
345 lines
17 KiB
C
#ifdef WORKBENCH
|
|
// Failure diagnostics only. Every query is read-only and uses the production seed/volume rules.
|
|
class ERR_NativeConvoyRoadSurvey
|
|
{
|
|
void Run(vector seed)
|
|
{
|
|
if (!ERR_NativeConvoyProbe.Enabled()) return;
|
|
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
|
if (!world || !world.GetRoadNetworkManager())
|
|
{
|
|
Print("ERR CONVOY SURVEY: native AI world or road-network manager unavailable"); return;
|
|
}
|
|
array<BaseRoad> roads = {};
|
|
vector low = seed + "-500 -1000 -500", high = seed + "500 1000 500";
|
|
world.GetRoadNetworkManager().GetRoadsInAABB(low, high, roads);
|
|
PrintFormat("ERR CONVOY SURVEY: seed=%1 AABB=%2..%3 candidates=%4; inspecting at most16, production cap128",
|
|
seed, low, high, roads.Count());
|
|
for (int r = 0; r < roads.Count() && r < 16; r++)
|
|
{
|
|
BaseRoad road = roads[r];
|
|
if (!road) { Print("ERR CONVOY SURVEY: null candidate"); continue; }
|
|
array<vector> points = {}; road.GetPoints(points);
|
|
float length, nearest = 1e20;
|
|
for (int i = 1; i < points.Count() && i <= 1024; i++)
|
|
{
|
|
vector delta = points[i] - points[i - 1]; delta[1] = 0;
|
|
float segment = delta.Length(); length += segment;
|
|
if (segment < 0.01) continue;
|
|
vector offset = seed - points[i - 1]; offset[1] = 0;
|
|
float fraction = Math.Clamp(vector.Dot(offset, delta) / (segment * segment), 0, 1);
|
|
nearest = Math.Min(nearest, vector.DistanceXZ(seed, vector.Lerp(points[i - 1], points[i], fraction)));
|
|
}
|
|
vector first, last;
|
|
if (!points.IsEmpty()) { first = points[0]; last = points[points.Count() - 1]; }
|
|
PrintFormat("ERR CONVOY SURVEY: road=%1 width=%2 points=%3 XZlength=%4 nearest=%5 first=%6 last=%7",
|
|
r, road.GetWidth(), points.Count(), length, nearest, first, last);
|
|
array<vector> positions, angles;
|
|
ERR_ForceRoute route;
|
|
float proximity;
|
|
bool polyline = ERR_ConvoyRoadPlan.FromPolyline(points, seed, positions, angles, route, proximity);
|
|
PrintFormat("ERR CONVOY SURVEY: road=%1 widthAllowed=%2 FromPolyline=%3 proximity=%4",
|
|
r, road.GetWidth() >= 4, polyline, proximity);
|
|
if (!polyline) continue;
|
|
for (int i = 0; i < positions.Count(); i++) positions[i] = ERR_Actors.Ground(positions[i]);
|
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
|
for (int i = 0; i < positions.Count() && i < 6; i++)
|
|
{
|
|
bool clear = placement.Clear(positions[i], angles[i][0]);
|
|
PrintFormat("ERR CONVOY SURVEY: road=%1 vehicle=%2 ground=%3 heading=%4 GroundClear=%5",
|
|
r, i, positions[i], angles[i][0], clear);
|
|
Print("ERR CONVOY SURVEY: " + placement.Diagnostic());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Opt-in native fixture: production road survey, canonical HQ dispatch and native driving.
|
|
// The fixture does not create vehicles, seat crew, move entities or advance convoy service.
|
|
class ERR_NativeConvoyProbe
|
|
{
|
|
protected ERR_GameMode m_Mode;
|
|
protected ref ERR_ConvoyTask m_Task;
|
|
protected ref array<vector> m_Origins = {};
|
|
protected ref array<IEntity> m_OriginalVehicles = {};
|
|
protected ref array<IEntity> m_CargoBefore = {};
|
|
protected float m_Deadline;
|
|
protected float m_ReadyAt;
|
|
protected float m_NextLog;
|
|
protected float m_NextMovementDiagnostic;
|
|
protected int m_InitialLedger;
|
|
protected int m_Step;
|
|
protected bool m_Failed;
|
|
|
|
static bool Enabled()
|
|
{
|
|
string enabled, outcome;
|
|
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
|
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "convoy";
|
|
}
|
|
void ERR_NativeConvoyProbe(ERR_GameMode mode) { m_Mode = mode; }
|
|
protected bool Check(bool condition, string description)
|
|
{
|
|
if (condition) { Print("ERR SMOKE PASS: convoy " + description); return true; }
|
|
m_Failed = true; Print("ERR SMOKE FAIL: convoy " + description, LogLevel.ERROR); return false;
|
|
}
|
|
protected InventoryStorageManagerComponent TruckInventory()
|
|
{
|
|
if (!m_Task || m_Task.Vehicles.Count() != 6 || !m_Task.Vehicles[1].Entity) return null;
|
|
return InventoryStorageManagerComponent.Cast(m_Task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent));
|
|
}
|
|
protected void PopulationDiagnostic(ERR_ConvoyVehicle vehicle, array<AIAgent> agents, int expected)
|
|
{
|
|
PrintFormat("ERR CONVOY POPULATION: vehicle=%1 registered=%2 members=%3 seats=%4 agents=%5 expected=%6",
|
|
vehicle.Index, vehicle.Registered, vehicle.Members.Count(), vehicle.Seats.Count(), agents.Count(), expected);
|
|
if (vehicle.Index != 0) return;
|
|
for (int i = 0; i < agents.Count() && i < 19; i++)
|
|
{
|
|
AIAgent agent = agents[i];
|
|
IEntity controlled;
|
|
AIGroup parent;
|
|
if (agent) { controlled = agent.GetControlledEntity(); parent = agent.GetParentGroup(); }
|
|
string prefab = "none";
|
|
if (controlled && controlled.GetPrefabData()) prefab = controlled.GetPrefabData().GetPrefabName();
|
|
PrintFormat("ERR CONVOY POPULATION: returned agent=%1 controlled=%2 prefab=%3 parent=%4 expectedGroup=%5",
|
|
agent, controlled, prefab, parent, vehicle.Group);
|
|
}
|
|
for (int i = 0; i < vehicle.Members.Count() && i < 19; i++)
|
|
{
|
|
IEntity actor = vehicle.Members[i];
|
|
AIControlComponent control;
|
|
AIAgent agent;
|
|
AIGroup parent;
|
|
if (actor) control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
|
|
if (control) agent = control.GetControlAIAgent();
|
|
if (agent) parent = agent.GetParentGroup();
|
|
PrintFormat("ERR CONVOY POPULATION: intended member=%1 actor=%2 controlAgent=%3 parent=%4 expectedGroup=%5",
|
|
i, actor, agent, parent, vehicle.Group);
|
|
}
|
|
}
|
|
protected string MovementAction(AIActionBase action)
|
|
{
|
|
if (!action) return "none";
|
|
SCR_AIActionBase scripted = SCR_AIActionBase.Cast(action);
|
|
string detail = action.ToString();
|
|
if (scripted) detail = scripted.GetActionDebugInfo();
|
|
if (detail.Length() > 200) detail = detail.Substring(0, 200);
|
|
return string.Format("%1 state=%2 suspended=%3", detail, action.GetActionState(), action.GetActionIsSuspended());
|
|
}
|
|
protected void MovementDiagnostic(float now)
|
|
{
|
|
if (!m_Task || now < m_NextMovementDiagnostic || m_Origins.Count() != 6) return;
|
|
m_NextMovementDiagnostic = now + 30000;
|
|
for (int i = 0; i < m_Task.Vehicles.Count() && i < 6; i++)
|
|
{
|
|
ERR_ConvoyVehicle unit = m_Task.Vehicles[i];
|
|
if (!unit || !unit.Entity || !unit.Group) { PrintFormat("ERR CONVOY MOTION: vehicle=%1 missing original identity", i); continue; }
|
|
AIWaypoint current = unit.Group.GetCurrentWaypoint();
|
|
vector target, nativeTarget;
|
|
float targetDistance = -1;
|
|
bool nativeWithin;
|
|
if (unit.Waypoint)
|
|
{
|
|
target = unit.Waypoint.GetOrigin();
|
|
targetDistance = vector.DistanceXZ(unit.Entity.GetOrigin(), target);
|
|
nativeWithin = unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
|
}
|
|
if (current) nativeTarget = current.GetOrigin();
|
|
PrintFormat("ERR CONVOY MOTION: vehicle=%1 travel=%2 pos=%3 routeIndex=%4 released=%5 arrived=%6 groupOrigin=%7",
|
|
i, vector.DistanceXZ(m_Origins[i], unit.Entity.GetOrigin()), unit.Entity.GetOrigin(), unit.RouteIndex,
|
|
unit.Released, unit.Arrived, unit.Group.GetOrigin());
|
|
PrintFormat("ERR CONVOY MOTION: vehicle=%1 ownWaypoint=%2 currentWaypoint=%3 same=%4 target=%5 distance=%6 nativeWithin=%7 nativeTarget=%8",
|
|
i, unit.Waypoint != null, current != null, current == unit.Waypoint, target, targetDistance, nativeWithin, nativeTarget);
|
|
SCR_AIGroupUtilityComponent groupUtility = unit.Group.GetGroupUtilityComponent();
|
|
string groupAction = "no group utility", driverBehavior = "no driver utility";
|
|
if (groupUtility) groupAction = MovementAction(groupUtility.GetCurrentAction());
|
|
AIControlComponent control;
|
|
if (unit.Driver) control = AIControlComponent.Cast(unit.Driver.FindComponent(AIControlComponent));
|
|
AIAgent driverAgent;
|
|
if (control) driverAgent = control.GetControlAIAgent();
|
|
SCR_AIUtilityComponent driverUtility;
|
|
if (driverAgent) driverUtility = SCR_AIUtilityComponent.Cast(driverAgent.FindComponent(SCR_AIUtilityComponent));
|
|
if (driverUtility) driverBehavior = MovementAction(driverUtility.GetCurrentBehavior());
|
|
PrintFormat("ERR CONVOY MOTION: vehicle=%1 groupAction=%2 driverBehavior=%3", i, groupAction, driverBehavior);
|
|
}
|
|
}
|
|
protected bool Population(ERR_HQ hq)
|
|
{
|
|
if (!Check(m_Task.Vehicles.Count() == 6, "six original vehicles remain tracked")) return false;
|
|
array<IEntity> vehicles = {}, crew = {};
|
|
array<SCR_AIGroup> groups = {};
|
|
array<BaseCompartmentSlot> slots = {};
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i];
|
|
if (!Check(vehicle.Entity && vehicle.Entity == m_OriginalVehicles[i] && ERR_Actors.Alive(vehicle.Entity)
|
|
&& !vehicles.Contains(vehicle.Entity) && vehicle.Group && !groups.Contains(vehicle.Group),
|
|
"vehicle and group identity " + i.ToString())) return false;
|
|
vehicles.Insert(vehicle.Entity); groups.Insert(vehicle.Group);
|
|
int expected = ERR_ConvoyPool.CrewCount(i);
|
|
array<AIAgent> agents = {}; vehicle.Group.GetAgents(agents);
|
|
PopulationDiagnostic(vehicle, agents, expected);
|
|
if (!Check(vehicle.Registered && vehicle.Members.Count() == expected && vehicle.Seats.Count() == expected
|
|
&& agents.Count() == expected, "native group membership matches exact crew plan " + i.ToString())) return false;
|
|
ERR_ForceRecord record = m_Mode.Forces().FindGroup(vehicle.Group);
|
|
if (!Check(record && record.Kind == ERR_ForceKind.CONVOY && record.Physical && !record.Eliminated
|
|
&& record.Strength == expected && record.InitialStrength == expected
|
|
&& record.Id == m_Task.Receipt.Records[i], "Core ledger owns physical convoy group " + i.ToString())) return false;
|
|
for (int seatIndex = 0; seatIndex < expected; seatIndex++)
|
|
{
|
|
IEntity actor = vehicle.Members[seatIndex];
|
|
BaseCompartmentSlot seat = vehicle.Seats[seatIndex];
|
|
if (!Check(actor && ERR_Actors.CanFight(actor) && !crew.Contains(actor)
|
|
&& ERR_Actors.Group(actor) == vehicle.Group && seat && !slots.Contains(seat) && seat.GetOccupant() == actor
|
|
&& ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(actor), vehicle.Entity),
|
|
"original crew occupies intended native compartment " + crew.Count().ToString())) return false;
|
|
crew.Insert(actor); slots.Insert(seat);
|
|
bool nativeMember;
|
|
foreach (AIAgent agent : agents) if (agent && agent.GetControlledEntity() == actor) nativeMember = true;
|
|
if (!Check(nativeMember, "seated actor is a real native group member")) return false;
|
|
}
|
|
if (!Check(vehicle.Driver == vehicle.Members[0]
|
|
&& vehicle.Seats[0].GetType() == ECompartmentType.PILOT, "actual driver seat " + i.ToString())) return false;
|
|
}
|
|
return Check(crew.Count() == 19 && hq.ConvoyPool().CommittedBodies() <= 60
|
|
&& m_Task.Receipt.OutstandingBodies == 0 && m_Task.Receipt.OutstandingGroups == 0,
|
|
"nineteen unique crew admitted once within shared sixty-body cap");
|
|
}
|
|
protected int Cargo(float now)
|
|
{
|
|
if (!m_Task.CargoIssued) return 0;
|
|
if (m_Task.Cargo.Count() != 56) { Check(false, "cargo request count differs from56"); return -1; }
|
|
int rifles, magazines;
|
|
foreach (ERR_ConvoyCargoCallback operation : m_Task.Cargo)
|
|
{
|
|
if (!operation || !operation.Finished)
|
|
{
|
|
if (now - m_ReadyAt > 60000) { Check(false, "cargo callback deadline"); return -1; }
|
|
return 0;
|
|
}
|
|
if (!operation.Succeeded) { Check(false, "native cargo insertion failed"); return -1; }
|
|
if (operation.Prefab == ERR_ConvoyResources.Rifle()) rifles++;
|
|
if (operation.Prefab == ERR_ConvoyResources.Magazine()) magazines++;
|
|
}
|
|
if (!Check(rifles == 8 && magazines == 48, "cargo requests are eight rifles and forty-eight magazines")) return -1;
|
|
InventoryStorageManagerComponent inventory = TruckInventory();
|
|
if (!Check(inventory != null, "real ammunition truck inventory exists")) return -1;
|
|
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
|
rifles = 0; magazines = 0;
|
|
foreach (IEntity item : items)
|
|
{
|
|
if (!item || m_CargoBefore.Contains(item) || !item.GetPrefabData()) continue;
|
|
ResourceName prefab = item.GetPrefabData().GetPrefabName();
|
|
if (prefab == ERR_ConvoyResources.Rifle()) rifles++;
|
|
if (prefab == ERR_ConvoyResources.Magazine()) magazines++;
|
|
}
|
|
if (!Check(rifles == 8 && magazines >= 48,
|
|
"eight new physical rifles and at least forty-eight magazines found in truck")) return -1;
|
|
return 1;
|
|
}
|
|
static bool MovementComplete(float leaderMetres, float minimumMetres)
|
|
{
|
|
return ERR_ForceRules.Finite(leaderMetres) && ERR_ForceRules.Finite(minimumMetres)
|
|
&& leaderMetres >= 100 && minimumMetres >= 20;
|
|
}
|
|
int Tick(ERR_HQ hq, float now)
|
|
{
|
|
if (!Enabled() || !hq || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Failed) return -1;
|
|
if (m_Deadline == 0) m_Deadline = now + 480000;
|
|
if (now >= m_Deadline) { Check(false, "native fixture deadline"); return -1; }
|
|
if (m_Step == 0)
|
|
{
|
|
// Existing finite support must finish ordinary readiness before measuring the ledger baseline.
|
|
array<ref ERR_MortarBattery> batteries = {}; hq.Mortars().Batteries(batteries);
|
|
foreach (ERR_MortarBattery battery : batteries)
|
|
{
|
|
if (!battery || battery.Stopped()) { Check(false, "baseline mortar readiness failed"); return -1; }
|
|
if (!battery.StartupActivated) return 0;
|
|
}
|
|
// Canonical authored supply-site seed; a missing suitable real road is a test failure.
|
|
ERR_SiteRuntime site = m_Mode.Sites().Find("everon-supply-1");
|
|
if (!Check(site && site.Definition, "authored supply-site road survey seed exists")) return -1;
|
|
vector seed = site.Definition.Position;
|
|
m_InitialLedger = m_Mode.Forces().Count();
|
|
Print("ERR CONVOY NATIVE: road survey seed=" + seed.ToString());
|
|
if (!hq.PrepareConvoy(seed))
|
|
{
|
|
ERR_NativeConvoyRoadSurvey survey = new ERR_NativeConvoyRoadSurvey(); survey.Run(seed);
|
|
Check(false, "production road survey refused seed; read-only diagnostics above"); return -1;
|
|
}
|
|
Check(true, "production road survey accepted six real road positions");
|
|
if (!Check(hq.StartConvoy(), "canonical HQ dispatch accepted once")) return -1;
|
|
m_Task = hq.ConvoyTask();
|
|
if (!Check(m_Task && m_Task.Receipt && m_Task.Receipt.Consumed && m_Task.Vehicles.Count() == 6,
|
|
"canonical receipt owns six attempted physical vehicles")) return -1;
|
|
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
|
{
|
|
if (!Check(vehicle.Entity != null, "original physical vehicle created")) return -1;
|
|
m_OriginalVehicles.Insert(vehicle.Entity); m_Origins.Insert(vehicle.Entity.GetOrigin());
|
|
}
|
|
ERR_Inventory.Items(TruckInventory(), m_CargoBefore);
|
|
if (!Check(!hq.StartConvoy() && hq.ConvoyTask() == m_Task && hq.Convoys().Count() == 1,
|
|
"duplicate canonical dispatch creates no second native task")) return -1;
|
|
if (!Check(!hq.ConvoyPool().Reserve("native-over-cap"), "second convoy reservation rejected by finite cap")) return -1;
|
|
m_Step = 1;
|
|
}
|
|
bool completed = m_Task && m_Task.Vehicles.Count() == 6;
|
|
if (m_Task) foreach (ERR_ConvoyVehicle candidate : m_Task.Vehicles)
|
|
if (!candidate.Arrived) completed = false;
|
|
if (!m_Task || (m_Task.Stopped && !completed))
|
|
{
|
|
string reason = "missing task";
|
|
if (m_Task) reason = m_Task.Reason;
|
|
Check(false, "production convoy stopped before proof: " + reason); return -1;
|
|
}
|
|
if (m_Step == 1)
|
|
{
|
|
if (!m_Task.Ready) return 0;
|
|
m_ReadyAt = now;
|
|
if (!Population(hq)) return -1;
|
|
ERR_ConvoyPlacement occupied = new ERR_ConvoyPlacement();
|
|
Vehicle lead = m_Task.Vehicles[0].Entity;
|
|
bool empty = occupied.Clear(ERR_Actors.Ground(lead.GetOrigin()), lead.GetAngles()[0]);
|
|
float fraction = occupied.LastTraceFraction();
|
|
Print("ERR CONVOY NATIVE: existing lead vehicle blocker control; " + occupied.Diagnostic());
|
|
if (!Check(!empty && ERR_ForceRules.Finite(fraction) && fraction >= 0 && fraction < 1
|
|
&& occupied.Diagnostic().IndexOf("native filled volume collision") >= 0,
|
|
"native filled volume detects the actual lead vehicle as a positive blocker")) return -1;
|
|
if (!Check(m_Mode.Forces().Count() == m_InitialLedger + 6,
|
|
"exactly six new Core records after native readiness")) return -1;
|
|
m_Step = 2;
|
|
}
|
|
if (m_Step == 2)
|
|
{
|
|
int cargo = Cargo(now);
|
|
if (cargo <= 0) return cargo;
|
|
m_Step = 3;
|
|
}
|
|
float minimum = 1000000;
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i];
|
|
if (!vehicle.Entity || (vehicle.Released && !vehicle.Arrived)) { Check(false, "vehicle released before road movement proof"); return -1; }
|
|
minimum = Math.Min(minimum, vector.DistanceXZ(m_Origins[i], vehicle.Entity.GetOrigin()));
|
|
}
|
|
float leader = vector.DistanceXZ(m_Origins[0], m_Task.Vehicles[0].Entity.GetOrigin());
|
|
if (now >= m_NextLog)
|
|
{
|
|
m_NextLog = now + 10000;
|
|
PrintFormat("ERR CONVOY NATIVE: leader travel=%1m minimum vehicle travel=%2m; age=%3s",
|
|
leader, minimum, (now - m_ReadyAt) / 1000);
|
|
}
|
|
MovementDiagnostic(now);
|
|
if (!MovementComplete(leader, minimum)) return 0;
|
|
if (!Check(m_Task.Route && m_Task.Route.Count() >= 2 && !m_Task.Route.Looped(),
|
|
"driving uses finite road-polyline route")) return -1;
|
|
if (!Population(hq)) return -1;
|
|
if (!Check(!hq.StartConvoy() && hq.Convoys().Count() == 1 && hq.ConvoyTask() == m_Task
|
|
&& m_Mode.Forces().Count() == m_InitialLedger + 6 && m_Task.Cargo.Count() == 56,
|
|
"later repeated dispatch preserves exact task, force and cargo counts")) return -1;
|
|
Check(true, "all six native vehicles drove at least 20m and leader at least 100m without fixture movement");
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|