Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
+717
@@ -0,0 +1,717 @@
|
||||
// Independent physical adaptation of the owner's supply-convoy extension.
|
||||
// HQ must dispatch an authored route using a finite canonical pool receipt.
|
||||
class ERR_ConvoyResources
|
||||
{
|
||||
static ResourceName VehiclePrefab(int index)
|
||||
{
|
||||
if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et";
|
||||
if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et";
|
||||
if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et";
|
||||
if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et";
|
||||
return "";
|
||||
}
|
||||
static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; }
|
||||
static ResourceName Soldier()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier();
|
||||
return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; }
|
||||
static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; }
|
||||
static bool Available()
|
||||
{
|
||||
array<ResourceName> required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()};
|
||||
for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i));
|
||||
foreach (ResourceName prefab : required)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyRules
|
||||
{
|
||||
static bool EmptyCrewTemplate(SCR_AIGroup group)
|
||||
{
|
||||
// The vanilla faction base authors six queued soldiers. Only our memberless
|
||||
// template may receive finite seat occupants; never retire another spawn job.
|
||||
return group && !group.GetSpawnImmediately() && group.GetAgentsCount() == 0
|
||||
&& (!group.m_aUnitPrefabSlots || group.m_aUnitPrefabSlots.IsEmpty());
|
||||
}
|
||||
static bool Plan(array<vector> positions, array<vector> angles, ERR_ForceRoute route)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6 || !route || route.Looped() || route.Count() < 2 || !route.Seal()) return false;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (!ERR_ForceRules.FiniteVector(positions[i]) || !ERR_ForceRules.FiniteVector(angles[i])) return false;
|
||||
for (int j = 0; j < i; j++) if (vector.DistanceXZ(positions[i], positions[j]) < 18) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool GroundClear(array<vector> positions, array<vector> angles)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false;
|
||||
return true;
|
||||
}
|
||||
static bool PartOf(IEntity entity, IEntity root)
|
||||
{
|
||||
int depth;
|
||||
while (entity && depth++ < 32)
|
||||
{
|
||||
if (entity == root) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Captured(Vehicle vehicle, SCR_AIGroup group)
|
||||
{
|
||||
if (group && group.GetPlayerCount(true) > 0) return true;
|
||||
if (!vehicle) return false;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity player = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (player && PartOf(CompartmentAccessComponent.GetVehicleIn(player), vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool CollectSeats(IEntity root, array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
array<IEntity> pending = {root};
|
||||
int cursor;
|
||||
while (cursor < pending.Count() && cursor < 128)
|
||||
{
|
||||
IEntity entity = pending[cursor++];
|
||||
if (!entity) return false;
|
||||
SCR_BaseCompartmentManagerComponent manager = SCR_BaseCompartmentManagerComponent.Cast(entity.FindComponent(SCR_BaseCompartmentManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> local = {}; manager.GetCompartments(local);
|
||||
foreach (BaseCompartmentSlot seat : local) if (seat && !seats.Contains(seat)) seats.Insert(seat);
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 128 || pending.Contains(child)) return false;
|
||||
pending.Insert(child); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return cursor == pending.Count();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyVehicle
|
||||
{
|
||||
int Index;
|
||||
Vehicle Entity;
|
||||
SCR_AIGroup Group;
|
||||
IEntity Driver;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
AIWaypoint Waypoint;
|
||||
int RouteIndex;
|
||||
bool Released;
|
||||
bool Registered;
|
||||
// Latched only for an exact tracked hunter at authoritative player entry.
|
||||
bool PlayerCaptureObserved;
|
||||
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
|
||||
int SupplyToken;
|
||||
bool SupplyHeld;
|
||||
int InterceptToken;
|
||||
bool InterceptActive;
|
||||
AIWaypoint InterceptWaypoint;
|
||||
ref ERR_ConvoyInterceptOrder InterceptOrder;
|
||||
ERR_ConvoyReceipt InterceptReceipt;
|
||||
ERR_ForceRecord InterceptRecord;
|
||||
ERR_ForceRoute InterceptRoute;
|
||||
protected AIWaypoint m_InterceptWaypoint;
|
||||
protected Vehicle m_InterceptEntity;
|
||||
protected SCR_AIGroup m_InterceptGroup;
|
||||
protected IEntity m_InterceptDriver;
|
||||
protected ref array<IEntity> m_InterceptMembers = {};
|
||||
protected ref array<BaseCompartmentSlot> m_InterceptSeats = {};
|
||||
protected int m_InterceptToken;
|
||||
protected int m_LastInterceptToken;
|
||||
protected vector m_InterceptPosition;
|
||||
protected float m_InterceptExpiresAt;
|
||||
protected int m_InterceptRouteIndex;
|
||||
|
||||
bool Arrived;
|
||||
float LegDeadline;
|
||||
float NextReadinessNote;
|
||||
|
||||
// Pending downlink owns a claim only; normal authored travel continues.
|
||||
bool InterceptCrew(ERR_GameMode mode)
|
||||
{
|
||||
if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false;
|
||||
if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false;
|
||||
if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false;
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
if (agents.Count() != Members.Count()) return false;
|
||||
for (int i = 0; i < Members.Count(); i++)
|
||||
{
|
||||
if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false;
|
||||
int attributed;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !Members.Contains(agent.GetControlledEntity())) return false;
|
||||
if (agent.GetControlledEntity() == Members[i]) attributed++;
|
||||
}
|
||||
if (attributed != 1) return false;
|
||||
if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false;
|
||||
}
|
||||
if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptMatches(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0;
|
||||
}
|
||||
bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false;
|
||||
m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
foreach (IEntity member : Members) m_InterceptMembers.Insert(member);
|
||||
foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat);
|
||||
m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order;
|
||||
m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt;
|
||||
return true;
|
||||
}
|
||||
void ClearIntercept(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!InterceptMatches(order)) return;
|
||||
// Delete only the exact waypoint this owner created, never an adopted handle.
|
||||
if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint);
|
||||
if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null;
|
||||
m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0;
|
||||
InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
}
|
||||
// Pair preparation owns no route state. Recheck after both native allocations.
|
||||
bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
return !(order.Position != m_InterceptPosition) && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt;
|
||||
}
|
||||
// Caller transfers ownership of the prepared handle before AddWaypoint, which
|
||||
// is a native boundary rather than a transactional pair operation.
|
||||
void InstallIntercept(AIWaypoint detour)
|
||||
{
|
||||
m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
}
|
||||
bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false;
|
||||
AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition));
|
||||
if (!detour) return false;
|
||||
detour.SetCompletionRadius(15);
|
||||
ClearOrder(); m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
return true;
|
||||
}
|
||||
// True suppresses ordinary travel only after actual radio delivery. No route
|
||||
// index is consumed by arrival at the detour; removal leaves the seated group held.
|
||||
bool TickIntercept(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (!InterceptOrder) return false;
|
||||
if (!InterceptMatches(InterceptOrder)) return InterceptActive;
|
||||
if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0)
|
||||
{ bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; }
|
||||
if (!InterceptActive) return false;
|
||||
if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint)
|
||||
{ ClearIntercept(InterceptOrder); return false; }
|
||||
if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15)
|
||||
{
|
||||
Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
m_InterceptWaypoint = null; InterceptWaypoint = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ClearOrder()
|
||||
{
|
||||
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
|
||||
if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
Waypoint = null;
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
ClearIntercept(InterceptOrder);
|
||||
Released = true;
|
||||
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
|
||||
ClearOrder();
|
||||
if (mode && mode.Forces())
|
||||
{
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
}
|
||||
if (Group)
|
||||
{
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
// Vehicle, cargo, casualties and surviving crew remain physical and lootable.
|
||||
}
|
||||
bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
|
||||
if (ERR_Actors.Player(Driver) || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
|
||||
foreach (IEntity member : Members)
|
||||
if (!ERR_Actors.Alive(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) return false;
|
||||
return true;
|
||||
}
|
||||
bool Order(ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
|
||||
if (Waypoint) return true;
|
||||
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
|
||||
if (!Waypoint) return false;
|
||||
Waypoint.SetCompletionRadius(35);
|
||||
Group.AddWaypoint(Waypoint);
|
||||
LegDeadline = now + 600000;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (TickIntercept(mode, now)) return;
|
||||
if (Released || Arrived) return;
|
||||
if (!CrewValid(mode)) { Release(mode); return; }
|
||||
if (Combat.Held || SupplyHeld) return;
|
||||
// Native completion measures the seated group, not the vehicle origin.
|
||||
// Keep the same predicate after native removal of the completed waypoint.
|
||||
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
|
||||
{
|
||||
ClearOrder(); RouteIndex++;
|
||||
if (RouteIndex >= route.Count()) { Arrived = true; return; }
|
||||
}
|
||||
if (Waypoint && now >= LegDeadline) { Release(mode); return; }
|
||||
if (!Order(route, now)) Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyCargoCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ResourceName Prefab;
|
||||
bool Finished;
|
||||
bool Succeeded;
|
||||
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
|
||||
protected override void OnComplete() { Finish(true); }
|
||||
protected override void OnFailed() { Finish(false); }
|
||||
protected override void OnAllComplete() { Finish(true); }
|
||||
protected override void OnAllFailed() { Finish(false); }
|
||||
}
|
||||
|
||||
class ERR_ConvoyTask
|
||||
{
|
||||
ref ERR_ConvoyReceipt Receipt;
|
||||
ref ERR_ForceRoute Route;
|
||||
ref array<ref ERR_ConvoyVehicle> Vehicles = {};
|
||||
ref array<ref ERR_ConvoyCargoCallback> Cargo = {};
|
||||
bool Stopped;
|
||||
bool Ready;
|
||||
bool CargoIssued;
|
||||
float Deadline;
|
||||
float NextCheck;
|
||||
string Reason;
|
||||
void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
Stopped = true; Reason = reason;
|
||||
foreach (ERR_ConvoyVehicle vehicle : Vehicles) vehicle.Release(mode);
|
||||
ERR_HQ.Event("CONVOY STOP", reason + "; surviving entities and cargo preserved");
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyPool m_Pool;
|
||||
protected ref array<ref ERR_ConvoyTask> m_Tasks = {};
|
||||
protected bool m_Stopped;
|
||||
void ERR_ConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) { m_Mode = mode; m_Pool = pool; }
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool;
|
||||
}
|
||||
protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); }
|
||||
protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); }
|
||||
ERR_ConvoyTask At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Tasks.Count()) return null;
|
||||
return m_Tasks[index];
|
||||
}
|
||||
ERR_ConvoyTask Find(string source)
|
||||
{
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task;
|
||||
return null;
|
||||
}
|
||||
protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
array<BaseCompartmentSlot> drivers = {}, turrets = {}, cargo = {};
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot.IsCompartmentAccessible() || slot.IsOccupied()) continue;
|
||||
if (slot.GetType() == ECompartmentType.PILOT) drivers.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.TURRET) turrets.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot);
|
||||
}
|
||||
int requiredTurrets = 1;
|
||||
if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0;
|
||||
if (unit.Index == 4) requiredTurrets = 2;
|
||||
if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]);
|
||||
if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]);
|
||||
// Exact planned population is known before any character is created.
|
||||
if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false;
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
{
|
||||
IEntity actor = seat.SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||
if (!actor || nativeGroup != unit.Group) return false;
|
||||
}
|
||||
unit.Driver = unit.Members[0];
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
|
||||
// The server role selection owns the memberless group affiliation.
|
||||
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
m_Tasks.Insert(task);
|
||||
for (int index = 0; index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(35);
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending");
|
||||
return task;
|
||||
}
|
||||
protected string ReadinessReason(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
if (unit.Registered) return "already admitted";
|
||||
if (!unit.Group) return "missing group";
|
||||
if (!unit.Entity) return "missing vehicle";
|
||||
if (!ERR_Actors.Alive(unit.Entity)) return "vehicle destroyed or lacks damage state";
|
||||
if (unit.Group.GetFaction() != m_Mode.EnemyFaction()) return "group faction mismatch";
|
||||
if (!ERR_Actors.CanFight(unit.Driver)) return "driver absent/dead/unconscious or controller unavailable";
|
||||
if (ERR_Actors.Player(unit.Driver)) return "driver controlled by player";
|
||||
if (!ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(unit.Driver), unit.Entity)) return "driver outside intended vehicle";
|
||||
foreach (IEntity member : unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(member)) return "member absent/dead or damage state unavailable";
|
||||
if (ERR_Actors.Player(member)) return "member controlled by player";
|
||||
if (ERR_Actors.Group(member) != unit.Group) return "member native agent/parent group mismatch";
|
||||
}
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return "native agent count differs from original crew";
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) return "null native agent";
|
||||
if (!unit.Members.Contains(agent.GetControlledEntity())) return "native agent controls foreign entity";
|
||||
if (!ERR_Actors.CanFight(agent.GetControlledEntity())) return "native agent actor cannot fight";
|
||||
}
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return "group utility unavailable";
|
||||
if (!utility.m_VehicleMgr) return "group vehicle manager unavailable";
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage) return "vehicle usage component unavailable";
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
if (i >= unit.Members.Count() || !unit.Seats[i] || unit.Seats[i].GetOccupant() != unit.Members[i]) return "intended seat occupant mismatch";
|
||||
return "readiness guards satisfied; awaiting admission";
|
||||
}
|
||||
protected void InspectReadiness(ERR_ConvoyVehicle unit, float now)
|
||||
{
|
||||
if (!unit || now < unit.NextReadinessNote) return;
|
||||
unit.NextReadinessNote = now + 5000;
|
||||
int agents, activeAgents;
|
||||
bool activeGroup;
|
||||
if (unit.Group)
|
||||
{
|
||||
array<AIAgent> current = {}; unit.Group.GetAgents(current);
|
||||
agents = current.Count(); activeGroup = unit.Group.IsAIActivated();
|
||||
foreach (AIAgent agent : current) if (agent && agent.IsAIActivated()) activeAgents++;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY READINESS", string.Format("vehicle=%1 reason=%2 members=%3 agents=%4 activeAgents=%5 activeGroup=%6 seats=%7 registered=%8", unit.Index, ReadinessReason(unit), unit.Members.Count(), agents, activeAgents, activeGroup, unit.Seats.Count(), unit.Registered));
|
||||
}
|
||||
protected bool Admit(ERR_ConvoyTask task, float now)
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now);
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
{
|
||||
if (unit.Registered) continue;
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString(), ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record || !m_Pool.BindRecord(task.Receipt, unit.Index, record)) { task.Stop(m_Mode, "convoy ledger admission rejected"); return false; }
|
||||
record.Pinned = true;
|
||||
unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group);
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (hq)
|
||||
{
|
||||
hq.ReserveGroup(unit.Group);
|
||||
if (!hq.BindVehicle(unit.Group, unit.Entity))
|
||||
ERR_HQ.Event("MOUNTED AMMO", "original convoy vehicle binding unavailable; no replacement lookup");
|
||||
}
|
||||
// Native helper idempotently registers deletion/damage callbacks and mounted
|
||||
// combat activity; raw vehicle-manager admission alone omits those hooks.
|
||||
utility.AddUsableVehicle(usage);
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected void IssueCargo(ERR_ConvoyTask task)
|
||||
{
|
||||
if (task.CargoIssued || task.Stopped || task.Vehicles.Count() != 6) return;
|
||||
task.CargoIssued = true;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { ERR_HQ.Event("CONVOY CARGO", "ammo truck inventory unavailable; no retry or free replacement"); return; }
|
||||
for (int i = 0; i < 56; i++)
|
||||
{
|
||||
ResourceName prefab = ERR_ConvoyResources.Magazine();
|
||||
if (i < 8) prefab = ERR_ConvoyResources.Rifle();
|
||||
ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; task.Cargo.Insert(operation);
|
||||
if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CARGO", "finite target submitted once: eight AK74 and 48 magazines; native capacity/completion may reduce actual stock");
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Stopped || now < task.NextCheck) continue;
|
||||
task.NextCheck = now + 1000;
|
||||
bool captured;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) captured = true;
|
||||
if (captured) { task.Stop(m_Mode, "player captured convoy vehicle or joined crew"); continue; }
|
||||
if (!task.Ready)
|
||||
{
|
||||
if (now >= task.Deadline) { task.Stop(m_Mode, "native crew readiness expired; no replacement"); continue; }
|
||||
if (!Admit(task, now)) continue;
|
||||
task.Ready = true; IssueCargo(task);
|
||||
}
|
||||
bool pending;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
vehicle.Tick(m_Mode, task.Route, now);
|
||||
if (!vehicle.Released && !vehicle.Arrived) pending = true;
|
||||
}
|
||||
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
|
||||
}
|
||||
}
|
||||
protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
if (!m_Mode.Forces()) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false;
|
||||
if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false;
|
||||
if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false;
|
||||
if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false;
|
||||
if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false;
|
||||
foreach (ERR_ConvoyVehicle other : task.Vehicles)
|
||||
if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token
|
||||
&& task.Vehicles[order.VehicleIndex].InterceptMatches(order);
|
||||
}
|
||||
bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false;
|
||||
if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit.ClaimIntercept(m_Mode, order)) return false;
|
||||
unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false;
|
||||
if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false;
|
||||
if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false;
|
||||
if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false;
|
||||
if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false;
|
||||
if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false;
|
||||
if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false;
|
||||
if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false;
|
||||
return vector.DistanceXZ(first.Position, second.Position) >= 40;
|
||||
}
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now)) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.ClaimIntercept(m_Mode, first)) return false;
|
||||
lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route;
|
||||
if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second))
|
||||
{ lead.ClearIntercept(first); return false; }
|
||||
partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false;
|
||||
AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position));
|
||||
if (!firstWaypoint) return false;
|
||||
AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position));
|
||||
if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; }
|
||||
firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15);
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{ SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; }
|
||||
// Route removal is also a native boundary; both prepared handles remain
|
||||
// caller-owned until both exact claims have survived it.
|
||||
lead.ClearOrder();
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder();
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
lead.InstallIntercept(firstWaypoint);
|
||||
// AddWaypoint can invoke native listeners. Do not install the partner after
|
||||
// cancellation/ownership loss; release only these exact owned claims.
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive
|
||||
|| !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
partner.InstallIntercept(secondWaypoint);
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)
|
||||
&& task.Vehicles[first.VehicleIndex] == lead && task.Vehicles[second.VehicleIndex] == partner
|
||||
&& lead.InterceptActive && partner.InterceptActive) return true;
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now);
|
||||
}
|
||||
void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
if (unit && unit.InterceptMatches(order))
|
||||
{ bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; }
|
||||
}
|
||||
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false;
|
||||
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false;
|
||||
if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false;
|
||||
// No caller can replay even a command whose native waypoint recreation fails.
|
||||
unit.Combat.LastCommandId = order.CommandId;
|
||||
if (order.Hold)
|
||||
{
|
||||
if (!unit.Combat.Held) unit.ClearOrder();
|
||||
unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt;
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.Combat.Held = false;
|
||||
if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
|
||||
}
|
||||
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
|
||||
return true;
|
||||
}
|
||||
int Count() { return m_Tasks.Count(); }
|
||||
string Status()
|
||||
{
|
||||
int ready, stopped, vehicles, completed, failed, pending;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Ready) ready++;
|
||||
if (task.Stopped) stopped++;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity) vehicles++;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo)
|
||||
{
|
||||
if (!operation.Finished) pending++;
|
||||
else if (operation.Succeeded) completed++;
|
||||
else failed++;
|
||||
}
|
||||
}
|
||||
string reason;
|
||||
if (m_Tasks.Count() == 1 && m_Tasks[0].Stopped) reason = m_Tasks[0].Reason;
|
||||
if (reason.Length() > 160) reason = reason.Substring(0, 160);
|
||||
return string.Format("Convoys: %1 tasks, %2 reached readiness, %3 stopped, %4 retained physical vehicles; cargo native insertion successes=%5 failed=%6 pending=%7; current inventory may differ after use/looting", m_Tasks.Count(), ready, stopped, vehicles, completed, failed, pending) + "; reason=" + reason;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) task.Stop(m_Mode, "HQ ended convoy task service");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-pair-service-0054/tests/test_convoy_intercept_pair.py", line 9, in <module>
|
||||
assert 'bool ReserveInterceptPair(' in source and 'bool ApplyInterceptPair(' in source, 'paired service API absent (baseline feature gap)'
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AssertionError: paired service API absent (baseline feature gap)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"path": "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c",
|
||||
"working_sha256": "e503e3a2eeaff638dee7fac6830cfbac6b8d87402e1191ff65aaa9961d9e0b86",
|
||||
"index_sha256": "e1d5dff5584f1bf12c146df510c860c3e0e046c989ff758def88ecf6b49c6392"
|
||||
}
|
||||
+634
@@ -0,0 +1,634 @@
|
||||
// Independent physical adaptation of the owner's supply-convoy extension.
|
||||
// HQ must dispatch an authored route using a finite canonical pool receipt.
|
||||
class ERR_ConvoyResources
|
||||
{
|
||||
static ResourceName VehiclePrefab(int index)
|
||||
{
|
||||
if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et";
|
||||
if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et";
|
||||
if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et";
|
||||
if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et";
|
||||
return "";
|
||||
}
|
||||
static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; }
|
||||
static ResourceName Soldier()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier();
|
||||
return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; }
|
||||
static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; }
|
||||
static bool Available()
|
||||
{
|
||||
array<ResourceName> required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()};
|
||||
for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i));
|
||||
foreach (ResourceName prefab : required)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyRules
|
||||
{
|
||||
static bool EmptyCrewTemplate(SCR_AIGroup group)
|
||||
{
|
||||
// The vanilla faction base authors six queued soldiers. Only our memberless
|
||||
// template may receive finite seat occupants; never retire another spawn job.
|
||||
return group && !group.GetSpawnImmediately() && group.GetAgentsCount() == 0
|
||||
&& (!group.m_aUnitPrefabSlots || group.m_aUnitPrefabSlots.IsEmpty());
|
||||
}
|
||||
static bool Plan(array<vector> positions, array<vector> angles, ERR_ForceRoute route)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6 || !route || route.Looped() || route.Count() < 2 || !route.Seal()) return false;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (!ERR_ForceRules.FiniteVector(positions[i]) || !ERR_ForceRules.FiniteVector(angles[i])) return false;
|
||||
for (int j = 0; j < i; j++) if (vector.DistanceXZ(positions[i], positions[j]) < 18) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool GroundClear(array<vector> positions, array<vector> angles)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false;
|
||||
return true;
|
||||
}
|
||||
static bool PartOf(IEntity entity, IEntity root)
|
||||
{
|
||||
int depth;
|
||||
while (entity && depth++ < 32)
|
||||
{
|
||||
if (entity == root) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Captured(Vehicle vehicle, SCR_AIGroup group)
|
||||
{
|
||||
if (group && group.GetPlayerCount(true) > 0) return true;
|
||||
if (!vehicle) return false;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity player = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (player && PartOf(CompartmentAccessComponent.GetVehicleIn(player), vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool CollectSeats(IEntity root, array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
array<IEntity> pending = {root};
|
||||
int cursor;
|
||||
while (cursor < pending.Count() && cursor < 128)
|
||||
{
|
||||
IEntity entity = pending[cursor++];
|
||||
if (!entity) return false;
|
||||
SCR_BaseCompartmentManagerComponent manager = SCR_BaseCompartmentManagerComponent.Cast(entity.FindComponent(SCR_BaseCompartmentManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> local = {}; manager.GetCompartments(local);
|
||||
foreach (BaseCompartmentSlot seat : local) if (seat && !seats.Contains(seat)) seats.Insert(seat);
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 128 || pending.Contains(child)) return false;
|
||||
pending.Insert(child); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return cursor == pending.Count();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyVehicle
|
||||
{
|
||||
int Index;
|
||||
Vehicle Entity;
|
||||
SCR_AIGroup Group;
|
||||
IEntity Driver;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
AIWaypoint Waypoint;
|
||||
int RouteIndex;
|
||||
bool Released;
|
||||
bool Registered;
|
||||
// Latched only for an exact tracked hunter at authoritative player entry.
|
||||
bool PlayerCaptureObserved;
|
||||
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
|
||||
int SupplyToken;
|
||||
bool SupplyHeld;
|
||||
int InterceptToken;
|
||||
bool InterceptActive;
|
||||
AIWaypoint InterceptWaypoint;
|
||||
ref ERR_ConvoyInterceptOrder InterceptOrder;
|
||||
ERR_ConvoyReceipt InterceptReceipt;
|
||||
ERR_ForceRecord InterceptRecord;
|
||||
ERR_ForceRoute InterceptRoute;
|
||||
protected AIWaypoint m_InterceptWaypoint;
|
||||
protected Vehicle m_InterceptEntity;
|
||||
protected SCR_AIGroup m_InterceptGroup;
|
||||
protected IEntity m_InterceptDriver;
|
||||
protected ref array<IEntity> m_InterceptMembers = {};
|
||||
protected ref array<BaseCompartmentSlot> m_InterceptSeats = {};
|
||||
protected int m_InterceptToken;
|
||||
protected int m_LastInterceptToken;
|
||||
protected vector m_InterceptPosition;
|
||||
protected float m_InterceptExpiresAt;
|
||||
protected int m_InterceptRouteIndex;
|
||||
|
||||
bool Arrived;
|
||||
float LegDeadline;
|
||||
float NextReadinessNote;
|
||||
|
||||
// Pending downlink owns a claim only; normal authored travel continues.
|
||||
bool InterceptCrew(ERR_GameMode mode)
|
||||
{
|
||||
if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false;
|
||||
if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false;
|
||||
if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false;
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
if (agents.Count() != Members.Count()) return false;
|
||||
for (int i = 0; i < Members.Count(); i++)
|
||||
{
|
||||
if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false;
|
||||
int attributed;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !Members.Contains(agent.GetControlledEntity())) return false;
|
||||
if (agent.GetControlledEntity() == Members[i]) attributed++;
|
||||
}
|
||||
if (attributed != 1) return false;
|
||||
if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false;
|
||||
}
|
||||
if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptMatches(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0;
|
||||
}
|
||||
bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false;
|
||||
m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
foreach (IEntity member : Members) m_InterceptMembers.Insert(member);
|
||||
foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat);
|
||||
m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order;
|
||||
m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt;
|
||||
return true;
|
||||
}
|
||||
void ClearIntercept(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!InterceptMatches(order)) return;
|
||||
// Delete only the exact waypoint this owner created, never an adopted handle.
|
||||
if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint);
|
||||
if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null;
|
||||
m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0;
|
||||
InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
}
|
||||
bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false;
|
||||
AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition));
|
||||
if (!detour) return false;
|
||||
detour.SetCompletionRadius(15);
|
||||
ClearOrder(); m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
return true;
|
||||
}
|
||||
// True suppresses ordinary travel only after actual radio delivery. No route
|
||||
// index is consumed by arrival at the detour; removal leaves the seated group held.
|
||||
bool TickIntercept(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (!InterceptOrder) return false;
|
||||
if (!InterceptMatches(InterceptOrder)) return InterceptActive;
|
||||
if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0)
|
||||
{ bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; }
|
||||
if (!InterceptActive) return false;
|
||||
if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint)
|
||||
{ ClearIntercept(InterceptOrder); return false; }
|
||||
if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15)
|
||||
{
|
||||
Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
m_InterceptWaypoint = null; InterceptWaypoint = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ClearOrder()
|
||||
{
|
||||
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
|
||||
if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
Waypoint = null;
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
ClearIntercept(InterceptOrder);
|
||||
Released = true;
|
||||
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
|
||||
ClearOrder();
|
||||
if (mode && mode.Forces())
|
||||
{
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
}
|
||||
if (Group)
|
||||
{
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
// Vehicle, cargo, casualties and surviving crew remain physical and lootable.
|
||||
}
|
||||
bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
|
||||
if (ERR_Actors.Player(Driver) || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
|
||||
foreach (IEntity member : Members)
|
||||
if (!ERR_Actors.Alive(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) return false;
|
||||
return true;
|
||||
}
|
||||
bool Order(ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
|
||||
if (Waypoint) return true;
|
||||
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
|
||||
if (!Waypoint) return false;
|
||||
Waypoint.SetCompletionRadius(35);
|
||||
Group.AddWaypoint(Waypoint);
|
||||
LegDeadline = now + 600000;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (TickIntercept(mode, now)) return;
|
||||
if (Released || Arrived) return;
|
||||
if (!CrewValid(mode)) { Release(mode); return; }
|
||||
if (Combat.Held || SupplyHeld) return;
|
||||
// Native completion measures the seated group, not the vehicle origin.
|
||||
// Keep the same predicate after native removal of the completed waypoint.
|
||||
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
|
||||
{
|
||||
ClearOrder(); RouteIndex++;
|
||||
if (RouteIndex >= route.Count()) { Arrived = true; return; }
|
||||
}
|
||||
if (Waypoint && now >= LegDeadline) { Release(mode); return; }
|
||||
if (!Order(route, now)) Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyCargoCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ResourceName Prefab;
|
||||
bool Finished;
|
||||
bool Succeeded;
|
||||
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
|
||||
protected override void OnComplete() { Finish(true); }
|
||||
protected override void OnFailed() { Finish(false); }
|
||||
protected override void OnAllComplete() { Finish(true); }
|
||||
protected override void OnAllFailed() { Finish(false); }
|
||||
}
|
||||
|
||||
class ERR_ConvoyTask
|
||||
{
|
||||
ref ERR_ConvoyReceipt Receipt;
|
||||
ref ERR_ForceRoute Route;
|
||||
ref array<ref ERR_ConvoyVehicle> Vehicles = {};
|
||||
ref array<ref ERR_ConvoyCargoCallback> Cargo = {};
|
||||
bool Stopped;
|
||||
bool Ready;
|
||||
bool CargoIssued;
|
||||
float Deadline;
|
||||
float NextCheck;
|
||||
string Reason;
|
||||
void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
Stopped = true; Reason = reason;
|
||||
foreach (ERR_ConvoyVehicle vehicle : Vehicles) vehicle.Release(mode);
|
||||
ERR_HQ.Event("CONVOY STOP", reason + "; surviving entities and cargo preserved");
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyPool m_Pool;
|
||||
protected ref array<ref ERR_ConvoyTask> m_Tasks = {};
|
||||
protected bool m_Stopped;
|
||||
void ERR_ConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) { m_Mode = mode; m_Pool = pool; }
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool;
|
||||
}
|
||||
protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); }
|
||||
protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); }
|
||||
ERR_ConvoyTask At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Tasks.Count()) return null;
|
||||
return m_Tasks[index];
|
||||
}
|
||||
ERR_ConvoyTask Find(string source)
|
||||
{
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task;
|
||||
return null;
|
||||
}
|
||||
protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
array<BaseCompartmentSlot> drivers = {}, turrets = {}, cargo = {};
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot.IsCompartmentAccessible() || slot.IsOccupied()) continue;
|
||||
if (slot.GetType() == ECompartmentType.PILOT) drivers.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.TURRET) turrets.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot);
|
||||
}
|
||||
int requiredTurrets = 1;
|
||||
if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0;
|
||||
if (unit.Index == 4) requiredTurrets = 2;
|
||||
if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]);
|
||||
if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]);
|
||||
// Exact planned population is known before any character is created.
|
||||
if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false;
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
{
|
||||
IEntity actor = seat.SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||
if (!actor || nativeGroup != unit.Group) return false;
|
||||
}
|
||||
unit.Driver = unit.Members[0];
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
|
||||
// The server role selection owns the memberless group affiliation.
|
||||
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
m_Tasks.Insert(task);
|
||||
for (int index = 0; index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(35);
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending");
|
||||
return task;
|
||||
}
|
||||
protected string ReadinessReason(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
if (unit.Registered) return "already admitted";
|
||||
if (!unit.Group) return "missing group";
|
||||
if (!unit.Entity) return "missing vehicle";
|
||||
if (!ERR_Actors.Alive(unit.Entity)) return "vehicle destroyed or lacks damage state";
|
||||
if (unit.Group.GetFaction() != m_Mode.EnemyFaction()) return "group faction mismatch";
|
||||
if (!ERR_Actors.CanFight(unit.Driver)) return "driver absent/dead/unconscious or controller unavailable";
|
||||
if (ERR_Actors.Player(unit.Driver)) return "driver controlled by player";
|
||||
if (!ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(unit.Driver), unit.Entity)) return "driver outside intended vehicle";
|
||||
foreach (IEntity member : unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(member)) return "member absent/dead or damage state unavailable";
|
||||
if (ERR_Actors.Player(member)) return "member controlled by player";
|
||||
if (ERR_Actors.Group(member) != unit.Group) return "member native agent/parent group mismatch";
|
||||
}
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return "native agent count differs from original crew";
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) return "null native agent";
|
||||
if (!unit.Members.Contains(agent.GetControlledEntity())) return "native agent controls foreign entity";
|
||||
if (!ERR_Actors.CanFight(agent.GetControlledEntity())) return "native agent actor cannot fight";
|
||||
}
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return "group utility unavailable";
|
||||
if (!utility.m_VehicleMgr) return "group vehicle manager unavailable";
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage) return "vehicle usage component unavailable";
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
if (i >= unit.Members.Count() || !unit.Seats[i] || unit.Seats[i].GetOccupant() != unit.Members[i]) return "intended seat occupant mismatch";
|
||||
return "readiness guards satisfied; awaiting admission";
|
||||
}
|
||||
protected void InspectReadiness(ERR_ConvoyVehicle unit, float now)
|
||||
{
|
||||
if (!unit || now < unit.NextReadinessNote) return;
|
||||
unit.NextReadinessNote = now + 5000;
|
||||
int agents, activeAgents;
|
||||
bool activeGroup;
|
||||
if (unit.Group)
|
||||
{
|
||||
array<AIAgent> current = {}; unit.Group.GetAgents(current);
|
||||
agents = current.Count(); activeGroup = unit.Group.IsAIActivated();
|
||||
foreach (AIAgent agent : current) if (agent && agent.IsAIActivated()) activeAgents++;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY READINESS", string.Format("vehicle=%1 reason=%2 members=%3 agents=%4 activeAgents=%5 activeGroup=%6 seats=%7 registered=%8", unit.Index, ReadinessReason(unit), unit.Members.Count(), agents, activeAgents, activeGroup, unit.Seats.Count(), unit.Registered));
|
||||
}
|
||||
protected bool Admit(ERR_ConvoyTask task, float now)
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now);
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
{
|
||||
if (unit.Registered) continue;
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString(), ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record || !m_Pool.BindRecord(task.Receipt, unit.Index, record)) { task.Stop(m_Mode, "convoy ledger admission rejected"); return false; }
|
||||
record.Pinned = true;
|
||||
unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group);
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (hq)
|
||||
{
|
||||
hq.ReserveGroup(unit.Group);
|
||||
if (!hq.BindVehicle(unit.Group, unit.Entity))
|
||||
ERR_HQ.Event("MOUNTED AMMO", "original convoy vehicle binding unavailable; no replacement lookup");
|
||||
}
|
||||
// Native helper idempotently registers deletion/damage callbacks and mounted
|
||||
// combat activity; raw vehicle-manager admission alone omits those hooks.
|
||||
utility.AddUsableVehicle(usage);
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected void IssueCargo(ERR_ConvoyTask task)
|
||||
{
|
||||
if (task.CargoIssued || task.Stopped || task.Vehicles.Count() != 6) return;
|
||||
task.CargoIssued = true;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { ERR_HQ.Event("CONVOY CARGO", "ammo truck inventory unavailable; no retry or free replacement"); return; }
|
||||
for (int i = 0; i < 56; i++)
|
||||
{
|
||||
ResourceName prefab = ERR_ConvoyResources.Magazine();
|
||||
if (i < 8) prefab = ERR_ConvoyResources.Rifle();
|
||||
ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; task.Cargo.Insert(operation);
|
||||
if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CARGO", "finite target submitted once: eight AK74 and 48 magazines; native capacity/completion may reduce actual stock");
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Stopped || now < task.NextCheck) continue;
|
||||
task.NextCheck = now + 1000;
|
||||
bool captured;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) captured = true;
|
||||
if (captured) { task.Stop(m_Mode, "player captured convoy vehicle or joined crew"); continue; }
|
||||
if (!task.Ready)
|
||||
{
|
||||
if (now >= task.Deadline) { task.Stop(m_Mode, "native crew readiness expired; no replacement"); continue; }
|
||||
if (!Admit(task, now)) continue;
|
||||
task.Ready = true; IssueCargo(task);
|
||||
}
|
||||
bool pending;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
vehicle.Tick(m_Mode, task.Route, now);
|
||||
if (!vehicle.Released && !vehicle.Arrived) pending = true;
|
||||
}
|
||||
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
|
||||
}
|
||||
}
|
||||
protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
if (!m_Mode.Forces()) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false;
|
||||
if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false;
|
||||
if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false;
|
||||
if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false;
|
||||
if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false;
|
||||
foreach (ERR_ConvoyVehicle other : task.Vehicles)
|
||||
if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token
|
||||
&& task.Vehicles[order.VehicleIndex].InterceptMatches(order);
|
||||
}
|
||||
bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false;
|
||||
if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit.ClaimIntercept(m_Mode, order)) return false;
|
||||
unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now);
|
||||
}
|
||||
void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
if (unit && unit.InterceptMatches(order))
|
||||
{ bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; }
|
||||
}
|
||||
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false;
|
||||
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false;
|
||||
if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false;
|
||||
// No caller can replay even a command whose native waypoint recreation fails.
|
||||
unit.Combat.LastCommandId = order.CommandId;
|
||||
if (order.Hold)
|
||||
{
|
||||
if (!unit.Combat.Held) unit.ClearOrder();
|
||||
unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt;
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.Combat.Held = false;
|
||||
if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
|
||||
}
|
||||
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
|
||||
return true;
|
||||
}
|
||||
int Count() { return m_Tasks.Count(); }
|
||||
string Status()
|
||||
{
|
||||
int ready, stopped, vehicles, completed, failed, pending;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Ready) ready++;
|
||||
if (task.Stopped) stopped++;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity) vehicles++;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo)
|
||||
{
|
||||
if (!operation.Finished) pending++;
|
||||
else if (operation.Succeeded) completed++;
|
||||
else failed++;
|
||||
}
|
||||
}
|
||||
string reason;
|
||||
if (m_Tasks.Count() == 1 && m_Tasks[0].Stopped) reason = m_Tasks[0].Reason;
|
||||
if (reason.Length() > 160) reason = reason.Substring(0, 160);
|
||||
return string.Format("Convoys: %1 tasks, %2 reached readiness, %3 stopped, %4 retained physical vehicles; cargo native insertion successes=%5 failed=%6 pending=%7; current inventory may differ after use/looting", m_Tasks.Count(), ready, stopped, vehicles, completed, failed, pending) + "; reason=" + reason;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) task.Stop(m_Mode, "HQ ended convoy task service");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
from pathlib import Path
|
||||
import subprocess,hashlib,json
|
||||
root=Path('/home/jakub/devel/escapistsrereintroduced'); out=Path('/tmp/erri-pair-service-0054'); rel='addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c'
|
||||
s=(root/rel).read_text(); p=out/rel;p.parent.mkdir(parents=True,exist_ok=True)
|
||||
b=out/'baseline'/rel;b.parent.mkdir(parents=True,exist_ok=True);b.write_text(s)
|
||||
idx=subprocess.run(['git','show',':'+rel],cwd=root,capture_output=True,check=True).stdout
|
||||
(out/'baseline.json').write_text(json.dumps({'path':rel,'working_sha256':hashlib.sha256(s.encode()).hexdigest(),'index_sha256':hashlib.sha256(idx).hexdigest()},indent=2)+'\n')
|
||||
needle=' bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)'
|
||||
insert=''' // Pair preparation owns no route state. Recheck after both native allocations.
|
||||
bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
return order.Position == m_InterceptPosition && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt;
|
||||
}
|
||||
// Caller transfers ownership of the prepared handle before AddWaypoint, which
|
||||
// is a native boundary rather than a transactional pair operation.
|
||||
void InstallIntercept(AIWaypoint detour)
|
||||
{
|
||||
ClearOrder(); m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
}
|
||||
'''
|
||||
assert needle in s;s=s.replace(needle,insert+needle,1)
|
||||
needle=' bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)'
|
||||
insert=''' protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false;
|
||||
if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false;
|
||||
if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false;
|
||||
if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false;
|
||||
if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false;
|
||||
if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false;
|
||||
if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false;
|
||||
if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false;
|
||||
return vector.DistanceXZ(first.Position, second.Position) >= 40;
|
||||
}
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now)) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.ClaimIntercept(m_Mode, first)) return false;
|
||||
lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route;
|
||||
if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second))
|
||||
{ lead.ClearIntercept(first); return false; }
|
||||
partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false;
|
||||
AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position));
|
||||
if (!firstWaypoint) return false;
|
||||
AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position));
|
||||
if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; }
|
||||
firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15);
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{ SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; }
|
||||
lead.InstallIntercept(firstWaypoint);
|
||||
// AddWaypoint can invoke native listeners. Do not install the partner after
|
||||
// cancellation/ownership loss; release only these exact owned claims.
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive
|
||||
|| !InterceptCurrent(task, second) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
partner.InstallIntercept(secondWaypoint);
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)
|
||||
&& lead.InterceptActive && partner.InterceptActive) return true;
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
'''
|
||||
assert needle in s;s=s.replace(needle,insert+needle,1);p.write_text(s)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"scope": "One original convoy pair, distinct original escorts and HQ tokens, same delivered memory/timing, minimum 40m horizontal separation. No actors/items/ammo spawned. Only two native waypoint entities allocated.",
|
||||
"files": [
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c",
|
||||
"tests/test_convoy_intercept_pair.py"
|
||||
],
|
||||
"tests": {
|
||||
"pair": "green.log",
|
||||
"singleton_lifecycle": "lifecycle.log",
|
||||
"geometry_existing": "rules.log",
|
||||
"baseline": "baseline-red.log is explicit absent pair API assertion, not a claimed runtime behavior failure",
|
||||
"mutations": [
|
||||
"mutation-rollback-red.log",
|
||||
"mutation-spacing-red.log"
|
||||
]
|
||||
},
|
||||
"limits": [
|
||||
"Offline engine API doubles; physical driving/native callback timing unqualified.",
|
||||
"Preallocation and precommit failures leave both original routes unchanged; native ClearOrder/AddWaypoint boundary failures cancel exact claims, without atomic rollback or route restoration claim.",
|
||||
"Existing singleton behavior and legacy direct recursive ClearIntercept callback handling unchanged; no concrete native recursive callback demonstrated."
|
||||
],
|
||||
"adapters": "No existing harness changes needed; test_hq_mounted_intercept extracts geometry only. New pair test reuses unchanged lifecycle native doubles, extracts complete actual vehicle and actual service methods."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
from pathlib import Path
|
||||
p=Path('/tmp/erri-pair-service-0054/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c');s=p.read_text().replace('return order.Position == m_InterceptPosition &&','return !(order.Position != m_InterceptPosition) &&')
|
||||
s=s.replace(' ClearOrder(); m_InterceptRouteIndex = RouteIndex;\n m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;\n Group.AddWaypoint(detour);\n }\n bool DeliverIntercept',' m_InterceptRouteIndex = RouteIndex;\n m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;\n Group.AddWaypoint(detour);\n }\n bool DeliverIntercept',1)
|
||||
needle=' lead.InstallIntercept(firstWaypoint);'
|
||||
s=s.replace(needle,''' // Route removal is also a native boundary; both prepared handles remain
|
||||
// caller-owned until both exact claims have survived it.
|
||||
lead.ClearOrder();
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder();
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
'''+needle,1);p.write_text(s)
|
||||
@@ -0,0 +1,223 @@
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
|
||||
@@ -193,6 +193,20 @@
|
||||
InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
}
|
||||
+ // Pair preparation owns no route state. Recheck after both native allocations.
|
||||
+ bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
+ {
|
||||
+ if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
+ return !(order.Position != m_InterceptPosition) && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt;
|
||||
+ }
|
||||
+ // Caller transfers ownership of the prepared handle before AddWaypoint, which
|
||||
+ // is a native boundary rather than a transactional pair operation.
|
||||
+ void InstallIntercept(AIWaypoint detour)
|
||||
+ {
|
||||
+ m_InterceptRouteIndex = RouteIndex;
|
||||
+ m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
+ Group.AddWaypoint(detour);
|
||||
+ }
|
||||
bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
@@ -562,6 +576,75 @@
|
||||
unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
+ protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
+ {
|
||||
+ if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false;
|
||||
+ if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
+ if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false;
|
||||
+ if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false;
|
||||
+ if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false;
|
||||
+ if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false;
|
||||
+ if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false;
|
||||
+ if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false;
|
||||
+ if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false;
|
||||
+ return vector.DistanceXZ(first.Position, second.Position) >= 40;
|
||||
+ }
|
||||
+ bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
+ {
|
||||
+ if (!InterceptPairEligible(task, first, second, now)) return false;
|
||||
+ foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
+ foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
+ ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
+ ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
+ if (!lead.ClaimIntercept(m_Mode, first)) return false;
|
||||
+ lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route;
|
||||
+ if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second))
|
||||
+ { lead.ClearIntercept(first); return false; }
|
||||
+ partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route;
|
||||
+ return true;
|
||||
+ }
|
||||
+ bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
+ {
|
||||
+ if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false;
|
||||
+ ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
+ ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
+ if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false;
|
||||
+ AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position));
|
||||
+ if (!firstWaypoint) return false;
|
||||
+ AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position));
|
||||
+ if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; }
|
||||
+ firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15);
|
||||
+ if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
+ || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
+ || !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
+ { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; }
|
||||
+ // Route removal is also a native boundary; both prepared handles remain
|
||||
+ // caller-owned until both exact claims have survived it.
|
||||
+ lead.ClearOrder();
|
||||
+ if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder();
|
||||
+ if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
+ || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
+ || !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
+ {
|
||||
+ SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
+ ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
+ }
|
||||
+ lead.InstallIntercept(firstWaypoint);
|
||||
+ // AddWaypoint can invoke native listeners. Do not install the partner after
|
||||
+ // cancellation/ownership loss; release only these exact owned claims.
|
||||
+ if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive
|
||||
+ || !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
+ || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
+ {
|
||||
+ SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
+ ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
+ }
|
||||
+ partner.InstallIntercept(secondWaypoint);
|
||||
+ if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)
|
||||
+ && task.Vehicles[first.VehicleIndex] == lead && task.Vehicles[second.VehicleIndex] == partner
|
||||
+ && lead.InterceptActive && partner.InterceptActive) return true;
|
||||
+ ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
+ }
|
||||
bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
--- /dev/null
|
||||
+++ b/tests/test_convoy_intercept_pair.py
|
||||
@@ -0,0 +1,121 @@
|
||||
+"""Execute actual pair admission/apply plus the complete existing vehicle policy.
|
||||
+Native doubles inject allocation, AddWaypoint and RemoveWaypoint boundaries.
|
||||
+"""
|
||||
+import contextlib,io,os,re,runpy
|
||||
+from pathlib import Path
|
||||
+import policy_harness as h
|
||||
+path=Path(os.environ.get('ERR_CONVOY_SERVICE',h.ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c'))
|
||||
+source=path.read_text()
|
||||
+assert 'bool ReserveInterceptPair(' in source and 'bool ApplyInterceptPair(' in source, 'paired service API absent (baseline feature gap)'
|
||||
+# Reuse existing full vehicle/native doubles without running or changing its assertions.
|
||||
+captured=[]; original_run=h.run
|
||||
+try:
|
||||
+ h.run=captured.append
|
||||
+ with contextlib.redirect_stdout(io.StringIO()): runpy.run_path(str(h.ROOT/'tests/test_convoy_lifecycle.py'))
|
||||
+finally: h.run=original_run
|
||||
+assert len(captured)==1
|
||||
+cpp=captured[0]
|
||||
+pair='\n'.join(h.body(path,s) for s in ('protected bool InterceptPairEligible(', 'bool ReserveInterceptPair(', 'bool ApplyInterceptPair('))
|
||||
+pair=pair.replace('protected ','').replace('foreach (','for (')
|
||||
+for typ in ['ERR_ConvoyTask','ERR_ConvoyInterceptOrder','ERR_ConvoyVehicle','AIWaypoint']:
|
||||
+ pair=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',pair)
|
||||
+for obj in ['task','first','second','lead','partner','existing','claimed','m_Mode','firstWaypoint','secondWaypoint','Receipt']:
|
||||
+ pair=pair.replace(obj+'.',obj+'->')
|
||||
+pair=re.sub(r'(Vehicles\[[^\]]+\])\.',r'\1->',pair).replace('Forces().','Forces()->')
|
||||
+for ns in ['ERR_ForceRules','ERR_Actors','ERR_Resources','SCR_EntityHelper','vector']:
|
||||
+ pair=pair.replace(ns+'.',ns+'::')
|
||||
+pair=pair.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(')
|
||||
+cpp=cpp.replace('bool ApplyIntercept(ERR_ConvoyTask*',pair+'\nbool ApplyIntercept(ERR_ConvoyTask*',1)
|
||||
+cpp=cpp[:cpp.index('int main(){')]
|
||||
+cpp=cpp.replace('#include <cassert>','#include <cassert>\n#include <functional>\n#include <map>')
|
||||
+cpp=cpp.replace('void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}', 'std::function<void()> onRemove,onAdd;void RemoveWaypoint(AIWaypoint*){removes++;if(onRemove)onRemove();}void AddWaypoint(AIWaypoint*){adds++;if(onAdd)onAdd();}')
|
||||
+cpp=cpp.replace('ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}', 'std::map<SCR_AIGroup*,ERR_ForceRecord*> records;ERR_ForceRecord* FindGroup(SCR_AIGroup* g){return records.count(g)?records[g]:&record;}')
|
||||
+cpp=cpp.replace('int LogisticsToken(SCR_AIGroup*){return token;}', 'std::map<SCR_AIGroup*,int> tokens;int LogisticsToken(SCR_AIGroup* g){return tokens.count(g)?tokens[g]:token;}')
|
||||
+cpp=cpp.replace('int spawns=0;bool fail=false;', 'int spawns=0,failAt=0;bool fail=false;std::function<void()> onSpawn;')
|
||||
+cpp=cpp.replace('spawns++;return fail?nullptr:new AIWaypoint();','spawns++;if(onSpawn)onSpawn();return (fail||spawns==failAt)?nullptr:new AIWaypoint();')
|
||||
+cpp+=r'''
|
||||
+struct PairCase:InterceptCase{
|
||||
+ Case b; ERR_ConvoyInterceptOrder second;ERR_ConvoyVehicle fillers[4];
|
||||
+ PairCase(){
|
||||
+ b.unit.Index=3;b.mode.ledger.record.Id=10;b.mode.ledger.record.SourceId="original-vehicle-3";
|
||||
+ mode.ledger.records[&group]=&mode.ledger.record;mode.ledger.records[&b.group]=&b.mode.ledger.record;
|
||||
+ mode.command.tokens[&group]=400001;mode.command.tokens[&b.group]=400002;
|
||||
+ second=order;second.Token=400002;second.Group=&b.group;second.VehicleIndex=3;second.Position={740,0,0};
|
||||
+ task.Vehicles={&unit,&fillers[0],&fillers[1],&b.unit,&fillers[2],&fillers[3]};
|
||||
+ unit.Tick(&mode,&route,0);b.unit.Tick(&mode,&route,0);
|
||||
+ }
|
||||
+ bool ReservePair(){return service.ReserveInterceptPair(&task,&order,&second,1000);}
|
||||
+ bool ApplyPair(){return service.ApplyInterceptPair(&task,&order,&second,2000);}
|
||||
+};
|
||||
+int main(){
|
||||
+ {PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float da=f.unit.LegDeadline,db=f.b.unit.LegDeadline;
|
||||
+ assert(f.ReservePair());assert(f.service.InterceptCurrent(&f.task,&f.order)&&f.service.InterceptCurrent(&f.task,&f.second));
|
||||
+ assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.LegDeadline==da&&f.b.unit.LegDeadline==db);
|
||||
+ assert(f.ApplyPair());assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&f.unit.InterceptWaypoint!=f.b.unit.InterceptWaypoint);
|
||||
+ assert(f.unit.InterceptWaypoint->radius==15&&f.b.unit.InterceptWaypoint->radius==15&&f.unit.RouteIndex==0&&f.b.unit.RouteIndex==0);
|
||||
+ assert(!f.ApplyPair());f.vehicle.position=f.order.Position;f.b.vehicle.position=f.second.Position;
|
||||
+ f.unit.Tick(&f.mode,&f.route,3000);f.b.unit.Tick(&f.mode,&f.route,3000);
|
||||
+ assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&!f.unit.InterceptWaypoint&&!f.b.unit.InterceptWaypoint);
|
||||
+ f.unit.Tick(&f.mode,&f.route,121000);f.b.unit.Tick(&f.mode,&f.route,121000);
|
||||
+ assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint&&f.b.unit.Waypoint);
|
||||
+ }
|
||||
+ for(int bad=0;bad<19;bad++){PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;
|
||||
+ if(bad==0)f.second.Token=f.order.Token;
|
||||
+ if(bad==1)f.second.Group=f.order.Group;
|
||||
+ if(bad==2)f.second.VehicleIndex=0;
|
||||
+ if(bad==3)f.second.Position.x=739;
|
||||
+ if(bad==4)f.second.Position.x=NAN;
|
||||
+ if(bad==5)f.second.BeliefId++;
|
||||
+ if(bad==6)f.second.BeliefRevision++;
|
||||
+ if(bad==7)f.second.ReportId++;
|
||||
+ if(bad==8)f.second.ObservedAt++;
|
||||
+ if(bad==9)f.second.IssuedAt++;
|
||||
+ if(bad==10)f.second.ExpiresAt++;
|
||||
+ if(bad==11)f.second.SourceId="foreign";
|
||||
+ if(bad==12)f.b.driver.player=true;
|
||||
+ if(bad==13)f.b.seat.occupant=nullptr;
|
||||
+ if(bad==14)f.fillers[0].SupplyToken=9;
|
||||
+ if(bad==15)f.fillers[0].Combat->Held=true;
|
||||
+ if(bad==16)f.mode.command.tokens[&f.b.group]=0;
|
||||
+ if(bad==17)f.b.unit.Entity=&f.vehicle;
|
||||
+ if(bad==18){f.order.ExpiresAt=121001;f.second.ExpiresAt=121001;}
|
||||
+ assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
+ }
|
||||
+ // Claim two fails after claim one: spent token cannot be reused, route untouched.
|
||||
+ {PairCase f;assert(f.b.unit.ClaimIntercept(&f.mode,&f.second));f.b.unit.ClearIntercept(&f.second);
|
||||
+ auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float deadline=f.unit.LegDeadline;
|
||||
+ assert(!f.ReservePair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&f.unit.LegDeadline==deadline);
|
||||
+ }
|
||||
+ // Global exclusion includes a different original task; never steal its claim.
|
||||
+ {PairCase f;ERR_ConvoyTask other;ERR_ConvoyVehicle owned;owned.InterceptToken=400099;other.Vehicles={&owned};f.service.m_Tasks.Insert(&other);
|
||||
+ assert(!f.ReservePair()&&owned.InterceptToken==400099&&!f.unit.InterceptToken);}
|
||||
+ for(int allocation=1;allocation<=2;allocation++){PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes,adds=f.group.adds+f.b.group.adds,removes=f.group.removes+f.b.group.removes;
|
||||
+ ERR_Actors::failAt=ERR_Actors::spawns+allocation;assert(!f.ApplyPair());ERR_Actors::failAt=0;
|
||||
+ assert(!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
+ assert(SCR_EntityHelper::deletes==deletes+allocation-1);
|
||||
+ assert(f.group.adds+f.b.group.adds==adds&&f.group.removes+f.b.group.removes==removes);
|
||||
+ assert(f.unit.Entity==&f.vehicle&&f.b.unit.Entity==&f.b.vehicle&&f.unit.Driver==&f.driver&&f.b.unit.Driver==&f.b.driver);
|
||||
+ }
|
||||
+ // Allocation may revoke original crew. Both prepared handles are deleted before either route changes.
|
||||
+ {PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes;
|
||||
+ ERR_Actors::onSpawn=[&](){f.b.driver.alive=false;};assert(!f.ApplyPair());ERR_Actors::onSpawn={};
|
||||
+ assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&SCR_EntityHelper::deletes==deletes+2);}
|
||||
+ // Immutable claimed destinations: changing both orders together is still rejected.
|
||||
+ {PairCase f;assert(f.ReservePair());f.order.Position.x+=100;f.second.Position.x+=100;assert(!f.ApplyPair()&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);}
|
||||
+ // Native removal and AddWaypoint may synchronously cancel the pair.
|
||||
+ for(int boundary=0;boundary<3;boundary++){PairCase f;assert(f.ReservePair());int a=f.group.adds,b=f.b.group.adds;
|
||||
+ auto cancel=[&](){f.service.ReleaseIntercept(&f.task,&f.order,2000);f.service.ReleaseIntercept(&f.task,&f.second,2000);};
|
||||
+ if(boundary==0)f.group.onRemove=[&](){f.mode.command.tokens[&f.b.group]=0;};
|
||||
+ if(boundary==1)f.group.onAdd=cancel;
|
||||
+ if(boundary==2)f.b.group.onAdd=cancel;
|
||||
+ assert(!f.ApplyPair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);
|
||||
+ assert(f.b.group.adds==b+(boundary==2));assert(f.group.adds==a+(boundary!=0));
|
||||
+ }
|
||||
+ // Per-member release remains exact and never steals a foreign public waypoint.
|
||||
+ {PairCase f;assert(f.ReservePair());assert(f.ApplyPair());AIWaypoint foreign;f.b.unit.InterceptWaypoint=&foreign;
|
||||
+ f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken&&f.b.unit.InterceptToken==400002);
|
||||
+ f.service.ReleaseIntercept(&f.task,&f.second,3000);assert(f.b.unit.InterceptWaypoint==&foreign&&!f.b.unit.InterceptToken);}
|
||||
+}
|
||||
+'''
|
||||
+h.run(cpp)
|
||||
+print('PASS: actual pair admission, rollback, native allocation/revocation boundaries, per-member ownership, hold and expiry')
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual pair admission, rollback, native allocation/revocation boundaries, per-member ownership, hold and expiry
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual native seated-group arrival outside35m, native waypoint removal, idempotent advance, and convoy waypoint idempotence/arrival/deadline, missing/dead/captured/reparented crew refusal, failed native waypoint release, original vehicle/member preservation and no replacement
|
||||
@@ -0,0 +1,12 @@
|
||||
check: /tmp/err-policy-_v2ln7x4/check.cpp:413: int main(): Assertion `!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&f.unit.LegDeadline==deadline' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-pair-service-0054/tests/test_convoy_intercept_pair.py", line 120, in <module>
|
||||
h.run(cpp)
|
||||
~~~~~^^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-_v2ln7x4/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,717 @@
|
||||
// Independent physical adaptation of the owner's supply-convoy extension.
|
||||
// HQ must dispatch an authored route using a finite canonical pool receipt.
|
||||
class ERR_ConvoyResources
|
||||
{
|
||||
static ResourceName VehiclePrefab(int index)
|
||||
{
|
||||
if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et";
|
||||
if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et";
|
||||
if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et";
|
||||
if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et";
|
||||
return "";
|
||||
}
|
||||
static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; }
|
||||
static ResourceName Soldier()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier();
|
||||
return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; }
|
||||
static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; }
|
||||
static bool Available()
|
||||
{
|
||||
array<ResourceName> required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()};
|
||||
for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i));
|
||||
foreach (ResourceName prefab : required)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyRules
|
||||
{
|
||||
static bool EmptyCrewTemplate(SCR_AIGroup group)
|
||||
{
|
||||
// The vanilla faction base authors six queued soldiers. Only our memberless
|
||||
// template may receive finite seat occupants; never retire another spawn job.
|
||||
return group && !group.GetSpawnImmediately() && group.GetAgentsCount() == 0
|
||||
&& (!group.m_aUnitPrefabSlots || group.m_aUnitPrefabSlots.IsEmpty());
|
||||
}
|
||||
static bool Plan(array<vector> positions, array<vector> angles, ERR_ForceRoute route)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6 || !route || route.Looped() || route.Count() < 2 || !route.Seal()) return false;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (!ERR_ForceRules.FiniteVector(positions[i]) || !ERR_ForceRules.FiniteVector(angles[i])) return false;
|
||||
for (int j = 0; j < i; j++) if (vector.DistanceXZ(positions[i], positions[j]) < 18) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool GroundClear(array<vector> positions, array<vector> angles)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false;
|
||||
return true;
|
||||
}
|
||||
static bool PartOf(IEntity entity, IEntity root)
|
||||
{
|
||||
int depth;
|
||||
while (entity && depth++ < 32)
|
||||
{
|
||||
if (entity == root) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Captured(Vehicle vehicle, SCR_AIGroup group)
|
||||
{
|
||||
if (group && group.GetPlayerCount(true) > 0) return true;
|
||||
if (!vehicle) return false;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity player = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (player && PartOf(CompartmentAccessComponent.GetVehicleIn(player), vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool CollectSeats(IEntity root, array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
array<IEntity> pending = {root};
|
||||
int cursor;
|
||||
while (cursor < pending.Count() && cursor < 128)
|
||||
{
|
||||
IEntity entity = pending[cursor++];
|
||||
if (!entity) return false;
|
||||
SCR_BaseCompartmentManagerComponent manager = SCR_BaseCompartmentManagerComponent.Cast(entity.FindComponent(SCR_BaseCompartmentManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> local = {}; manager.GetCompartments(local);
|
||||
foreach (BaseCompartmentSlot seat : local) if (seat && !seats.Contains(seat)) seats.Insert(seat);
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 128 || pending.Contains(child)) return false;
|
||||
pending.Insert(child); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return cursor == pending.Count();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyVehicle
|
||||
{
|
||||
int Index;
|
||||
Vehicle Entity;
|
||||
SCR_AIGroup Group;
|
||||
IEntity Driver;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
AIWaypoint Waypoint;
|
||||
int RouteIndex;
|
||||
bool Released;
|
||||
bool Registered;
|
||||
// Latched only for an exact tracked hunter at authoritative player entry.
|
||||
bool PlayerCaptureObserved;
|
||||
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
|
||||
int SupplyToken;
|
||||
bool SupplyHeld;
|
||||
int InterceptToken;
|
||||
bool InterceptActive;
|
||||
AIWaypoint InterceptWaypoint;
|
||||
ref ERR_ConvoyInterceptOrder InterceptOrder;
|
||||
ERR_ConvoyReceipt InterceptReceipt;
|
||||
ERR_ForceRecord InterceptRecord;
|
||||
ERR_ForceRoute InterceptRoute;
|
||||
protected AIWaypoint m_InterceptWaypoint;
|
||||
protected Vehicle m_InterceptEntity;
|
||||
protected SCR_AIGroup m_InterceptGroup;
|
||||
protected IEntity m_InterceptDriver;
|
||||
protected ref array<IEntity> m_InterceptMembers = {};
|
||||
protected ref array<BaseCompartmentSlot> m_InterceptSeats = {};
|
||||
protected int m_InterceptToken;
|
||||
protected int m_LastInterceptToken;
|
||||
protected vector m_InterceptPosition;
|
||||
protected float m_InterceptExpiresAt;
|
||||
protected int m_InterceptRouteIndex;
|
||||
|
||||
bool Arrived;
|
||||
float LegDeadline;
|
||||
float NextReadinessNote;
|
||||
|
||||
// Pending downlink owns a claim only; normal authored travel continues.
|
||||
bool InterceptCrew(ERR_GameMode mode)
|
||||
{
|
||||
if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false;
|
||||
if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false;
|
||||
if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false;
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
if (agents.Count() != Members.Count()) return false;
|
||||
for (int i = 0; i < Members.Count(); i++)
|
||||
{
|
||||
if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false;
|
||||
int attributed;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !Members.Contains(agent.GetControlledEntity())) return false;
|
||||
if (agent.GetControlledEntity() == Members[i]) attributed++;
|
||||
}
|
||||
if (attributed != 1) return false;
|
||||
if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false;
|
||||
}
|
||||
if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptMatches(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0;
|
||||
}
|
||||
bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false;
|
||||
m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
foreach (IEntity member : Members) m_InterceptMembers.Insert(member);
|
||||
foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat);
|
||||
m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order;
|
||||
m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt;
|
||||
return true;
|
||||
}
|
||||
void ClearIntercept(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!InterceptMatches(order)) return;
|
||||
// Delete only the exact waypoint this owner created, never an adopted handle.
|
||||
if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint);
|
||||
if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null;
|
||||
m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0;
|
||||
InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
}
|
||||
// Pair preparation owns no route state. Recheck after both native allocations.
|
||||
bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
return !(order.Position != m_InterceptPosition) && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt;
|
||||
}
|
||||
// Caller transfers ownership of the prepared handle before AddWaypoint, which
|
||||
// is a native boundary rather than a transactional pair operation.
|
||||
void InstallIntercept(AIWaypoint detour)
|
||||
{
|
||||
m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
}
|
||||
bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false;
|
||||
AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition));
|
||||
if (!detour) return false;
|
||||
detour.SetCompletionRadius(15);
|
||||
ClearOrder(); m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
return true;
|
||||
}
|
||||
// True suppresses ordinary travel only after actual radio delivery. No route
|
||||
// index is consumed by arrival at the detour; removal leaves the seated group held.
|
||||
bool TickIntercept(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (!InterceptOrder) return false;
|
||||
if (!InterceptMatches(InterceptOrder)) return InterceptActive;
|
||||
if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0)
|
||||
{ bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; }
|
||||
if (!InterceptActive) return false;
|
||||
if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint)
|
||||
{ ClearIntercept(InterceptOrder); return false; }
|
||||
if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15)
|
||||
{
|
||||
Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
m_InterceptWaypoint = null; InterceptWaypoint = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ClearOrder()
|
||||
{
|
||||
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
|
||||
if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
Waypoint = null;
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
ClearIntercept(InterceptOrder);
|
||||
Released = true;
|
||||
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
|
||||
ClearOrder();
|
||||
if (mode && mode.Forces())
|
||||
{
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
}
|
||||
if (Group)
|
||||
{
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
// Vehicle, cargo, casualties and surviving crew remain physical and lootable.
|
||||
}
|
||||
bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
|
||||
if (ERR_Actors.Player(Driver) || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
|
||||
foreach (IEntity member : Members)
|
||||
if (!ERR_Actors.Alive(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) return false;
|
||||
return true;
|
||||
}
|
||||
bool Order(ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
|
||||
if (Waypoint) return true;
|
||||
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
|
||||
if (!Waypoint) return false;
|
||||
Waypoint.SetCompletionRadius(35);
|
||||
Group.AddWaypoint(Waypoint);
|
||||
LegDeadline = now + 600000;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (TickIntercept(mode, now)) return;
|
||||
if (Released || Arrived) return;
|
||||
if (!CrewValid(mode)) { Release(mode); return; }
|
||||
if (Combat.Held || SupplyHeld) return;
|
||||
// Native completion measures the seated group, not the vehicle origin.
|
||||
// Keep the same predicate after native removal of the completed waypoint.
|
||||
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
|
||||
{
|
||||
ClearOrder(); RouteIndex++;
|
||||
if (RouteIndex >= route.Count()) { Arrived = true; return; }
|
||||
}
|
||||
if (Waypoint && now >= LegDeadline) { Release(mode); return; }
|
||||
if (!Order(route, now)) Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyCargoCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ResourceName Prefab;
|
||||
bool Finished;
|
||||
bool Succeeded;
|
||||
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
|
||||
protected override void OnComplete() { Finish(true); }
|
||||
protected override void OnFailed() { Finish(false); }
|
||||
protected override void OnAllComplete() { Finish(true); }
|
||||
protected override void OnAllFailed() { Finish(false); }
|
||||
}
|
||||
|
||||
class ERR_ConvoyTask
|
||||
{
|
||||
ref ERR_ConvoyReceipt Receipt;
|
||||
ref ERR_ForceRoute Route;
|
||||
ref array<ref ERR_ConvoyVehicle> Vehicles = {};
|
||||
ref array<ref ERR_ConvoyCargoCallback> Cargo = {};
|
||||
bool Stopped;
|
||||
bool Ready;
|
||||
bool CargoIssued;
|
||||
float Deadline;
|
||||
float NextCheck;
|
||||
string Reason;
|
||||
void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
Stopped = true; Reason = reason;
|
||||
foreach (ERR_ConvoyVehicle vehicle : Vehicles) vehicle.Release(mode);
|
||||
ERR_HQ.Event("CONVOY STOP", reason + "; surviving entities and cargo preserved");
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyPool m_Pool;
|
||||
protected ref array<ref ERR_ConvoyTask> m_Tasks = {};
|
||||
protected bool m_Stopped;
|
||||
void ERR_ConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) { m_Mode = mode; m_Pool = pool; }
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool;
|
||||
}
|
||||
protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); }
|
||||
protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); }
|
||||
ERR_ConvoyTask At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Tasks.Count()) return null;
|
||||
return m_Tasks[index];
|
||||
}
|
||||
ERR_ConvoyTask Find(string source)
|
||||
{
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task;
|
||||
return null;
|
||||
}
|
||||
protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
array<BaseCompartmentSlot> drivers = {}, turrets = {}, cargo = {};
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot.IsCompartmentAccessible() || slot.IsOccupied()) continue;
|
||||
if (slot.GetType() == ECompartmentType.PILOT) drivers.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.TURRET) turrets.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot);
|
||||
}
|
||||
int requiredTurrets = 1;
|
||||
if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0;
|
||||
if (unit.Index == 4) requiredTurrets = 2;
|
||||
if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]);
|
||||
if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]);
|
||||
// Exact planned population is known before any character is created.
|
||||
if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false;
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
{
|
||||
IEntity actor = seat.SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||
if (!actor || nativeGroup != unit.Group) return false;
|
||||
}
|
||||
unit.Driver = unit.Members[0];
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
|
||||
// The server role selection owns the memberless group affiliation.
|
||||
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
m_Tasks.Insert(task);
|
||||
for (int index = 0; index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(35);
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending");
|
||||
return task;
|
||||
}
|
||||
protected string ReadinessReason(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
if (unit.Registered) return "already admitted";
|
||||
if (!unit.Group) return "missing group";
|
||||
if (!unit.Entity) return "missing vehicle";
|
||||
if (!ERR_Actors.Alive(unit.Entity)) return "vehicle destroyed or lacks damage state";
|
||||
if (unit.Group.GetFaction() != m_Mode.EnemyFaction()) return "group faction mismatch";
|
||||
if (!ERR_Actors.CanFight(unit.Driver)) return "driver absent/dead/unconscious or controller unavailable";
|
||||
if (ERR_Actors.Player(unit.Driver)) return "driver controlled by player";
|
||||
if (!ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(unit.Driver), unit.Entity)) return "driver outside intended vehicle";
|
||||
foreach (IEntity member : unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(member)) return "member absent/dead or damage state unavailable";
|
||||
if (ERR_Actors.Player(member)) return "member controlled by player";
|
||||
if (ERR_Actors.Group(member) != unit.Group) return "member native agent/parent group mismatch";
|
||||
}
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return "native agent count differs from original crew";
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) return "null native agent";
|
||||
if (!unit.Members.Contains(agent.GetControlledEntity())) return "native agent controls foreign entity";
|
||||
if (!ERR_Actors.CanFight(agent.GetControlledEntity())) return "native agent actor cannot fight";
|
||||
}
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return "group utility unavailable";
|
||||
if (!utility.m_VehicleMgr) return "group vehicle manager unavailable";
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage) return "vehicle usage component unavailable";
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
if (i >= unit.Members.Count() || !unit.Seats[i] || unit.Seats[i].GetOccupant() != unit.Members[i]) return "intended seat occupant mismatch";
|
||||
return "readiness guards satisfied; awaiting admission";
|
||||
}
|
||||
protected void InspectReadiness(ERR_ConvoyVehicle unit, float now)
|
||||
{
|
||||
if (!unit || now < unit.NextReadinessNote) return;
|
||||
unit.NextReadinessNote = now + 5000;
|
||||
int agents, activeAgents;
|
||||
bool activeGroup;
|
||||
if (unit.Group)
|
||||
{
|
||||
array<AIAgent> current = {}; unit.Group.GetAgents(current);
|
||||
agents = current.Count(); activeGroup = unit.Group.IsAIActivated();
|
||||
foreach (AIAgent agent : current) if (agent && agent.IsAIActivated()) activeAgents++;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY READINESS", string.Format("vehicle=%1 reason=%2 members=%3 agents=%4 activeAgents=%5 activeGroup=%6 seats=%7 registered=%8", unit.Index, ReadinessReason(unit), unit.Members.Count(), agents, activeAgents, activeGroup, unit.Seats.Count(), unit.Registered));
|
||||
}
|
||||
protected bool Admit(ERR_ConvoyTask task, float now)
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now);
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
{
|
||||
if (unit.Registered) continue;
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString(), ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record || !m_Pool.BindRecord(task.Receipt, unit.Index, record)) { task.Stop(m_Mode, "convoy ledger admission rejected"); return false; }
|
||||
record.Pinned = true;
|
||||
unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group);
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (hq)
|
||||
{
|
||||
hq.ReserveGroup(unit.Group);
|
||||
if (!hq.BindVehicle(unit.Group, unit.Entity))
|
||||
ERR_HQ.Event("MOUNTED AMMO", "original convoy vehicle binding unavailable; no replacement lookup");
|
||||
}
|
||||
// Native helper idempotently registers deletion/damage callbacks and mounted
|
||||
// combat activity; raw vehicle-manager admission alone omits those hooks.
|
||||
utility.AddUsableVehicle(usage);
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected void IssueCargo(ERR_ConvoyTask task)
|
||||
{
|
||||
if (task.CargoIssued || task.Stopped || task.Vehicles.Count() != 6) return;
|
||||
task.CargoIssued = true;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { ERR_HQ.Event("CONVOY CARGO", "ammo truck inventory unavailable; no retry or free replacement"); return; }
|
||||
for (int i = 0; i < 56; i++)
|
||||
{
|
||||
ResourceName prefab = ERR_ConvoyResources.Magazine();
|
||||
if (i < 8) prefab = ERR_ConvoyResources.Rifle();
|
||||
ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; task.Cargo.Insert(operation);
|
||||
if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CARGO", "finite target submitted once: eight AK74 and 48 magazines; native capacity/completion may reduce actual stock");
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Stopped || now < task.NextCheck) continue;
|
||||
task.NextCheck = now + 1000;
|
||||
bool captured;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) captured = true;
|
||||
if (captured) { task.Stop(m_Mode, "player captured convoy vehicle or joined crew"); continue; }
|
||||
if (!task.Ready)
|
||||
{
|
||||
if (now >= task.Deadline) { task.Stop(m_Mode, "native crew readiness expired; no replacement"); continue; }
|
||||
if (!Admit(task, now)) continue;
|
||||
task.Ready = true; IssueCargo(task);
|
||||
}
|
||||
bool pending;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
vehicle.Tick(m_Mode, task.Route, now);
|
||||
if (!vehicle.Released && !vehicle.Arrived) pending = true;
|
||||
}
|
||||
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
|
||||
}
|
||||
}
|
||||
protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
if (!m_Mode.Forces()) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false;
|
||||
if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false;
|
||||
if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false;
|
||||
if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false;
|
||||
if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false;
|
||||
foreach (ERR_ConvoyVehicle other : task.Vehicles)
|
||||
if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token
|
||||
&& task.Vehicles[order.VehicleIndex].InterceptMatches(order);
|
||||
}
|
||||
bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false;
|
||||
if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit.ClaimIntercept(m_Mode, order)) return false;
|
||||
unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false;
|
||||
if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false;
|
||||
if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false;
|
||||
if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false;
|
||||
if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false;
|
||||
if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false;
|
||||
if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false;
|
||||
if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false;
|
||||
return vector.DistanceXZ(first.Position, second.Position) >= 40;
|
||||
}
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now)) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.ClaimIntercept(m_Mode, first)) return false;
|
||||
lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route;
|
||||
if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second))
|
||||
{ return false; }
|
||||
partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false;
|
||||
AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position));
|
||||
if (!firstWaypoint) return false;
|
||||
AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position));
|
||||
if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; }
|
||||
firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15);
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{ SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; }
|
||||
// Route removal is also a native boundary; both prepared handles remain
|
||||
// caller-owned until both exact claims have survived it.
|
||||
lead.ClearOrder();
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder();
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
lead.InstallIntercept(firstWaypoint);
|
||||
// AddWaypoint can invoke native listeners. Do not install the partner after
|
||||
// cancellation/ownership loss; release only these exact owned claims.
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive
|
||||
|| !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
partner.InstallIntercept(secondWaypoint);
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)
|
||||
&& task.Vehicles[first.VehicleIndex] == lead && task.Vehicles[second.VehicleIndex] == partner
|
||||
&& lead.InterceptActive && partner.InterceptActive) return true;
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now);
|
||||
}
|
||||
void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
if (unit && unit.InterceptMatches(order))
|
||||
{ bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; }
|
||||
}
|
||||
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false;
|
||||
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false;
|
||||
if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false;
|
||||
// No caller can replay even a command whose native waypoint recreation fails.
|
||||
unit.Combat.LastCommandId = order.CommandId;
|
||||
if (order.Hold)
|
||||
{
|
||||
if (!unit.Combat.Held) unit.ClearOrder();
|
||||
unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt;
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.Combat.Held = false;
|
||||
if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
|
||||
}
|
||||
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
|
||||
return true;
|
||||
}
|
||||
int Count() { return m_Tasks.Count(); }
|
||||
string Status()
|
||||
{
|
||||
int ready, stopped, vehicles, completed, failed, pending;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Ready) ready++;
|
||||
if (task.Stopped) stopped++;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity) vehicles++;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo)
|
||||
{
|
||||
if (!operation.Finished) pending++;
|
||||
else if (operation.Succeeded) completed++;
|
||||
else failed++;
|
||||
}
|
||||
}
|
||||
string reason;
|
||||
if (m_Tasks.Count() == 1 && m_Tasks[0].Stopped) reason = m_Tasks[0].Reason;
|
||||
if (reason.Length() > 160) reason = reason.Substring(0, 160);
|
||||
return string.Format("Convoys: %1 tasks, %2 reached readiness, %3 stopped, %4 retained physical vehicles; cargo native insertion successes=%5 failed=%6 pending=%7; current inventory may differ after use/looting", m_Tasks.Count(), ready, stopped, vehicles, completed, failed, pending) + "; reason=" + reason;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) task.Stop(m_Mode, "HQ ended convoy task service");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
check: /tmp/err-policy-gxipern3/check.cpp:408: int main(): Assertion `!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-pair-service-0054/tests/test_convoy_intercept_pair.py", line 120, in <module>
|
||||
h.run(cpp)
|
||||
~~~~~^^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-gxipern3/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,717 @@
|
||||
// Independent physical adaptation of the owner's supply-convoy extension.
|
||||
// HQ must dispatch an authored route using a finite canonical pool receipt.
|
||||
class ERR_ConvoyResources
|
||||
{
|
||||
static ResourceName VehiclePrefab(int index)
|
||||
{
|
||||
if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et";
|
||||
if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et";
|
||||
if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et";
|
||||
if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et";
|
||||
return "";
|
||||
}
|
||||
static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; }
|
||||
static ResourceName Soldier()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier();
|
||||
return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; }
|
||||
static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; }
|
||||
static bool Available()
|
||||
{
|
||||
array<ResourceName> required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()};
|
||||
for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i));
|
||||
foreach (ResourceName prefab : required)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyRules
|
||||
{
|
||||
static bool EmptyCrewTemplate(SCR_AIGroup group)
|
||||
{
|
||||
// The vanilla faction base authors six queued soldiers. Only our memberless
|
||||
// template may receive finite seat occupants; never retire another spawn job.
|
||||
return group && !group.GetSpawnImmediately() && group.GetAgentsCount() == 0
|
||||
&& (!group.m_aUnitPrefabSlots || group.m_aUnitPrefabSlots.IsEmpty());
|
||||
}
|
||||
static bool Plan(array<vector> positions, array<vector> angles, ERR_ForceRoute route)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6 || !route || route.Looped() || route.Count() < 2 || !route.Seal()) return false;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (!ERR_ForceRules.FiniteVector(positions[i]) || !ERR_ForceRules.FiniteVector(angles[i])) return false;
|
||||
for (int j = 0; j < i; j++) if (vector.DistanceXZ(positions[i], positions[j]) < 18) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool GroundClear(array<vector> positions, array<vector> angles)
|
||||
{
|
||||
if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false;
|
||||
return true;
|
||||
}
|
||||
static bool PartOf(IEntity entity, IEntity root)
|
||||
{
|
||||
int depth;
|
||||
while (entity && depth++ < 32)
|
||||
{
|
||||
if (entity == root) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Captured(Vehicle vehicle, SCR_AIGroup group)
|
||||
{
|
||||
if (group && group.GetPlayerCount(true) > 0) return true;
|
||||
if (!vehicle) return false;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity player = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (player && PartOf(CompartmentAccessComponent.GetVehicleIn(player), vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool CollectSeats(IEntity root, array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
array<IEntity> pending = {root};
|
||||
int cursor;
|
||||
while (cursor < pending.Count() && cursor < 128)
|
||||
{
|
||||
IEntity entity = pending[cursor++];
|
||||
if (!entity) return false;
|
||||
SCR_BaseCompartmentManagerComponent manager = SCR_BaseCompartmentManagerComponent.Cast(entity.FindComponent(SCR_BaseCompartmentManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> local = {}; manager.GetCompartments(local);
|
||||
foreach (BaseCompartmentSlot seat : local) if (seat && !seats.Contains(seat)) seats.Insert(seat);
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 128 || pending.Contains(child)) return false;
|
||||
pending.Insert(child); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return cursor == pending.Count();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyVehicle
|
||||
{
|
||||
int Index;
|
||||
Vehicle Entity;
|
||||
SCR_AIGroup Group;
|
||||
IEntity Driver;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
AIWaypoint Waypoint;
|
||||
int RouteIndex;
|
||||
bool Released;
|
||||
bool Registered;
|
||||
// Latched only for an exact tracked hunter at authoritative player entry.
|
||||
bool PlayerCaptureObserved;
|
||||
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
|
||||
int SupplyToken;
|
||||
bool SupplyHeld;
|
||||
int InterceptToken;
|
||||
bool InterceptActive;
|
||||
AIWaypoint InterceptWaypoint;
|
||||
ref ERR_ConvoyInterceptOrder InterceptOrder;
|
||||
ERR_ConvoyReceipt InterceptReceipt;
|
||||
ERR_ForceRecord InterceptRecord;
|
||||
ERR_ForceRoute InterceptRoute;
|
||||
protected AIWaypoint m_InterceptWaypoint;
|
||||
protected Vehicle m_InterceptEntity;
|
||||
protected SCR_AIGroup m_InterceptGroup;
|
||||
protected IEntity m_InterceptDriver;
|
||||
protected ref array<IEntity> m_InterceptMembers = {};
|
||||
protected ref array<BaseCompartmentSlot> m_InterceptSeats = {};
|
||||
protected int m_InterceptToken;
|
||||
protected int m_LastInterceptToken;
|
||||
protected vector m_InterceptPosition;
|
||||
protected float m_InterceptExpiresAt;
|
||||
protected int m_InterceptRouteIndex;
|
||||
|
||||
bool Arrived;
|
||||
float LegDeadline;
|
||||
float NextReadinessNote;
|
||||
|
||||
// Pending downlink owns a claim only; normal authored travel continues.
|
||||
bool InterceptCrew(ERR_GameMode mode)
|
||||
{
|
||||
if (!mode || !CrewValid(mode) || !Entity || !Group || ERR_ConvoyRules.Captured(Entity, Group)) return false;
|
||||
if (Members.IsEmpty() || Members.Count() > 8 || Seats.Count() != Members.Count()) return false;
|
||||
if (InterceptOrder && (Members.Count() != m_InterceptMembers.Count() || Seats.Count() != m_InterceptSeats.Count())) return false;
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
if (agents.Count() != Members.Count()) return false;
|
||||
for (int i = 0; i < Members.Count(); i++)
|
||||
{
|
||||
if (!Members[i] || !ERR_Actors.CanFight(Members[i]) || !Seats[i] || Seats[i].GetOccupant() != Members[i]) return false;
|
||||
int attributed;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !Members.Contains(agent.GetControlledEntity())) return false;
|
||||
if (agent.GetControlledEntity() == Members[i]) attributed++;
|
||||
}
|
||||
if (attributed != 1) return false;
|
||||
if (InterceptOrder && (Members[i] != m_InterceptMembers[i] || Seats[i] != m_InterceptSeats[i])) return false;
|
||||
}
|
||||
if (InterceptOrder && (Entity != m_InterceptEntity || Group != m_InterceptGroup || Driver != m_InterceptDriver)) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptMatches(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return order && InterceptOrder == order && InterceptToken == m_InterceptToken && m_InterceptToken != 0;
|
||||
}
|
||||
bool ClaimIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!order || order.Token <= m_LastInterceptToken || InterceptToken != 0 || InterceptOrder || InterceptWaypoint || !InterceptCrew(mode)) return false;
|
||||
m_InterceptEntity = Entity; m_InterceptGroup = Group; m_InterceptDriver = Driver;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
foreach (IEntity member : Members) m_InterceptMembers.Insert(member);
|
||||
foreach (BaseCompartmentSlot seat : Seats) m_InterceptSeats.Insert(seat);
|
||||
m_InterceptToken = order.Token; m_LastInterceptToken = order.Token; InterceptToken = order.Token; InterceptOrder = order;
|
||||
m_InterceptPosition = order.Position; m_InterceptExpiresAt = order.ExpiresAt;
|
||||
return true;
|
||||
}
|
||||
void ClearIntercept(ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!InterceptMatches(order)) return;
|
||||
// Delete only the exact waypoint this owner created, never an adopted handle.
|
||||
if (m_InterceptWaypoint && m_InterceptGroup) m_InterceptGroup.RemoveWaypoint(m_InterceptWaypoint);
|
||||
if (m_InterceptWaypoint) SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
if (InterceptWaypoint == m_InterceptWaypoint) InterceptWaypoint = null;
|
||||
m_InterceptWaypoint = null; InterceptToken = 0; m_InterceptToken = 0;
|
||||
InterceptActive = false; InterceptOrder = null; InterceptReceipt = null; InterceptRecord = null; InterceptRoute = null;
|
||||
m_InterceptMembers.Clear(); m_InterceptSeats.Clear();
|
||||
}
|
||||
// Pair preparation owns no route state. Recheck after both native allocations.
|
||||
bool CanInstallIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
return !(order.Position != m_InterceptPosition) && order.ExpiresAt == m_InterceptExpiresAt && now < m_InterceptExpiresAt;
|
||||
}
|
||||
// Caller transfers ownership of the prepared handle before AddWaypoint, which
|
||||
// is a native boundary rather than a transactional pair operation.
|
||||
void InstallIntercept(AIWaypoint detour)
|
||||
{
|
||||
m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
}
|
||||
bool DeliverIntercept(ERR_GameMode mode, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptMatches(order) || order.Token != m_InterceptToken || InterceptActive || !InterceptCrew(mode)) return false;
|
||||
if (order.Position != m_InterceptPosition || order.ExpiresAt != m_InterceptExpiresAt || now >= m_InterceptExpiresAt) return false;
|
||||
AIWaypoint detour = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), m_InterceptPosition));
|
||||
if (!detour) return false;
|
||||
detour.SetCompletionRadius(15);
|
||||
ClearOrder(); m_InterceptRouteIndex = RouteIndex;
|
||||
m_InterceptWaypoint = detour; InterceptWaypoint = detour; InterceptActive = true;
|
||||
Group.AddWaypoint(detour);
|
||||
return true;
|
||||
}
|
||||
// True suppresses ordinary travel only after actual radio delivery. No route
|
||||
// index is consumed by arrival at the detour; removal leaves the seated group held.
|
||||
bool TickIntercept(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (!InterceptOrder) return false;
|
||||
if (!InterceptMatches(InterceptOrder)) return InterceptActive;
|
||||
if (Released || Arrived || now >= m_InterceptExpiresAt || !InterceptCrew(mode) || Combat.Held || SupplyHeld || SupplyToken != 0)
|
||||
{ bool delivered = InterceptActive; ClearIntercept(InterceptOrder); if (delivered) LegDeadline = now + 600000; return false; }
|
||||
if (!InterceptActive) return false;
|
||||
if (RouteIndex != m_InterceptRouteIndex || InterceptWaypoint != m_InterceptWaypoint)
|
||||
{ ClearIntercept(InterceptOrder); return false; }
|
||||
if (m_InterceptWaypoint && vector.DistanceXZ(Entity.GetOrigin(), m_InterceptPosition) <= 15)
|
||||
{
|
||||
Group.RemoveWaypoint(m_InterceptWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(m_InterceptWaypoint);
|
||||
m_InterceptWaypoint = null; InterceptWaypoint = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ClearOrder()
|
||||
{
|
||||
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
|
||||
if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
Waypoint = null;
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
ClearIntercept(InterceptOrder);
|
||||
Released = true;
|
||||
Combat.Held = false; SupplyToken = 0; SupplyHeld = false;
|
||||
ClearOrder();
|
||||
if (mode && mode.Forces())
|
||||
{
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
}
|
||||
if (Group)
|
||||
{
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
// Vehicle, cargo, casualties and surviving crew remain physical and lootable.
|
||||
}
|
||||
bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
|
||||
if (ERR_Actors.Player(Driver) || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
|
||||
foreach (IEntity member : Members)
|
||||
if (!ERR_Actors.Alive(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) return false;
|
||||
return true;
|
||||
}
|
||||
bool Order(ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (Released || Arrived || InterceptActive || Combat.Held || SupplyHeld || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
|
||||
if (Waypoint) return true;
|
||||
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
|
||||
if (!Waypoint) return false;
|
||||
Waypoint.SetCompletionRadius(35);
|
||||
Group.AddWaypoint(Waypoint);
|
||||
LegDeadline = now + 600000;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_GameMode mode, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (TickIntercept(mode, now)) return;
|
||||
if (Released || Arrived) return;
|
||||
if (!CrewValid(mode)) { Release(mode); return; }
|
||||
if (Combat.Held || SupplyHeld) return;
|
||||
// Native completion measures the seated group, not the vehicle origin.
|
||||
// Keep the same predicate after native removal of the completed waypoint.
|
||||
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
|
||||
{
|
||||
ClearOrder(); RouteIndex++;
|
||||
if (RouteIndex >= route.Count()) { Arrived = true; return; }
|
||||
}
|
||||
if (Waypoint && now >= LegDeadline) { Release(mode); return; }
|
||||
if (!Order(route, now)) Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyCargoCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ResourceName Prefab;
|
||||
bool Finished;
|
||||
bool Succeeded;
|
||||
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
|
||||
protected override void OnComplete() { Finish(true); }
|
||||
protected override void OnFailed() { Finish(false); }
|
||||
protected override void OnAllComplete() { Finish(true); }
|
||||
protected override void OnAllFailed() { Finish(false); }
|
||||
}
|
||||
|
||||
class ERR_ConvoyTask
|
||||
{
|
||||
ref ERR_ConvoyReceipt Receipt;
|
||||
ref ERR_ForceRoute Route;
|
||||
ref array<ref ERR_ConvoyVehicle> Vehicles = {};
|
||||
ref array<ref ERR_ConvoyCargoCallback> Cargo = {};
|
||||
bool Stopped;
|
||||
bool Ready;
|
||||
bool CargoIssued;
|
||||
float Deadline;
|
||||
float NextCheck;
|
||||
string Reason;
|
||||
void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
Stopped = true; Reason = reason;
|
||||
foreach (ERR_ConvoyVehicle vehicle : Vehicles) vehicle.Release(mode);
|
||||
ERR_HQ.Event("CONVOY STOP", reason + "; surviving entities and cargo preserved");
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyPool m_Pool;
|
||||
protected ref array<ref ERR_ConvoyTask> m_Tasks = {};
|
||||
protected bool m_Stopped;
|
||||
void ERR_ConvoyService(ERR_GameMode mode, ERR_ConvoyPool pool) { m_Mode = mode; m_Pool = pool; }
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool;
|
||||
}
|
||||
protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); }
|
||||
protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); }
|
||||
ERR_ConvoyTask At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Tasks.Count()) return null;
|
||||
return m_Tasks[index];
|
||||
}
|
||||
ERR_ConvoyTask Find(string source)
|
||||
{
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task;
|
||||
return null;
|
||||
}
|
||||
protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
array<BaseCompartmentSlot> drivers = {}, turrets = {}, cargo = {};
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot.IsCompartmentAccessible() || slot.IsOccupied()) continue;
|
||||
if (slot.GetType() == ECompartmentType.PILOT) drivers.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.TURRET) turrets.Insert(slot);
|
||||
else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot);
|
||||
}
|
||||
int requiredTurrets = 1;
|
||||
if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0;
|
||||
if (unit.Index == 4) requiredTurrets = 2;
|
||||
if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]);
|
||||
if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]);
|
||||
// Exact planned population is known before any character is created.
|
||||
if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false;
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
{
|
||||
IEntity actor = seat.SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||
if (!actor || nativeGroup != unit.Group) return false;
|
||||
}
|
||||
unit.Driver = unit.Members[0];
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
|
||||
// The server role selection owns the memberless group affiliation.
|
||||
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
m_Tasks.Insert(task);
|
||||
for (int index = 0; index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(35);
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending");
|
||||
return task;
|
||||
}
|
||||
protected string ReadinessReason(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
if (unit.Registered) return "already admitted";
|
||||
if (!unit.Group) return "missing group";
|
||||
if (!unit.Entity) return "missing vehicle";
|
||||
if (!ERR_Actors.Alive(unit.Entity)) return "vehicle destroyed or lacks damage state";
|
||||
if (unit.Group.GetFaction() != m_Mode.EnemyFaction()) return "group faction mismatch";
|
||||
if (!ERR_Actors.CanFight(unit.Driver)) return "driver absent/dead/unconscious or controller unavailable";
|
||||
if (ERR_Actors.Player(unit.Driver)) return "driver controlled by player";
|
||||
if (!ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(unit.Driver), unit.Entity)) return "driver outside intended vehicle";
|
||||
foreach (IEntity member : unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(member)) return "member absent/dead or damage state unavailable";
|
||||
if (ERR_Actors.Player(member)) return "member controlled by player";
|
||||
if (ERR_Actors.Group(member) != unit.Group) return "member native agent/parent group mismatch";
|
||||
}
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return "native agent count differs from original crew";
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) return "null native agent";
|
||||
if (!unit.Members.Contains(agent.GetControlledEntity())) return "native agent controls foreign entity";
|
||||
if (!ERR_Actors.CanFight(agent.GetControlledEntity())) return "native agent actor cannot fight";
|
||||
}
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return "group utility unavailable";
|
||||
if (!utility.m_VehicleMgr) return "group vehicle manager unavailable";
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage) return "vehicle usage component unavailable";
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
if (i >= unit.Members.Count() || !unit.Seats[i] || unit.Seats[i].GetOccupant() != unit.Members[i]) return "intended seat occupant mismatch";
|
||||
return "readiness guards satisfied; awaiting admission";
|
||||
}
|
||||
protected void InspectReadiness(ERR_ConvoyVehicle unit, float now)
|
||||
{
|
||||
if (!unit || now < unit.NextReadinessNote) return;
|
||||
unit.NextReadinessNote = now + 5000;
|
||||
int agents, activeAgents;
|
||||
bool activeGroup;
|
||||
if (unit.Group)
|
||||
{
|
||||
array<AIAgent> current = {}; unit.Group.GetAgents(current);
|
||||
agents = current.Count(); activeGroup = unit.Group.IsAIActivated();
|
||||
foreach (AIAgent agent : current) if (agent && agent.IsAIActivated()) activeAgents++;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY READINESS", string.Format("vehicle=%1 reason=%2 members=%3 agents=%4 activeAgents=%5 activeGroup=%6 seats=%7 registered=%8", unit.Index, ReadinessReason(unit), unit.Members.Count(), agents, activeAgents, activeGroup, unit.Seats.Count(), unit.Registered));
|
||||
}
|
||||
protected bool Admit(ERR_ConvoyTask task, float now)
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now);
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
{
|
||||
if (unit.Registered) continue;
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString(), ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record || !m_Pool.BindRecord(task.Receipt, unit.Index, record)) { task.Stop(m_Mode, "convoy ledger admission rejected"); return false; }
|
||||
record.Pinned = true;
|
||||
unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group);
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (hq)
|
||||
{
|
||||
hq.ReserveGroup(unit.Group);
|
||||
if (!hq.BindVehicle(unit.Group, unit.Entity))
|
||||
ERR_HQ.Event("MOUNTED AMMO", "original convoy vehicle binding unavailable; no replacement lookup");
|
||||
}
|
||||
// Native helper idempotently registers deletion/damage callbacks and mounted
|
||||
// combat activity; raw vehicle-manager admission alone omits those hooks.
|
||||
utility.AddUsableVehicle(usage);
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected void IssueCargo(ERR_ConvoyTask task)
|
||||
{
|
||||
if (task.CargoIssued || task.Stopped || task.Vehicles.Count() != 6) return;
|
||||
task.CargoIssued = true;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { ERR_HQ.Event("CONVOY CARGO", "ammo truck inventory unavailable; no retry or free replacement"); return; }
|
||||
for (int i = 0; i < 56; i++)
|
||||
{
|
||||
ResourceName prefab = ERR_ConvoyResources.Magazine();
|
||||
if (i < 8) prefab = ERR_ConvoyResources.Rifle();
|
||||
ERR_ConvoyCargoCallback operation = new ERR_ConvoyCargoCallback(); operation.Prefab = prefab; task.Cargo.Insert(operation);
|
||||
if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, operation)) operation.Finished = true;
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CARGO", "finite target submitted once: eight AK74 and 48 magazines; native capacity/completion may reduce actual stock");
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Stopped || now < task.NextCheck) continue;
|
||||
task.NextCheck = now + 1000;
|
||||
bool captured;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles) if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) captured = true;
|
||||
if (captured) { task.Stop(m_Mode, "player captured convoy vehicle or joined crew"); continue; }
|
||||
if (!task.Ready)
|
||||
{
|
||||
if (now >= task.Deadline) { task.Stop(m_Mode, "native crew readiness expired; no replacement"); continue; }
|
||||
if (!Admit(task, now)) continue;
|
||||
task.Ready = true; IssueCargo(task);
|
||||
}
|
||||
bool pending;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
vehicle.Tick(m_Mode, task.Route, now);
|
||||
if (!vehicle.Released && !vehicle.Arrived) pending = true;
|
||||
}
|
||||
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
|
||||
}
|
||||
}
|
||||
protected bool InterceptEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
if (!m_Mode.Forces()) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.LogisticsToken(order.Group) != order.Token) return false;
|
||||
if (order.Token < 400001 || order.Token > 400256 || order.SourceId != task.Receipt.SourceId) return false;
|
||||
if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
if (order.VehicleIndex >= task.Vehicles.Count() || order.VehicleIndex >= task.Receipt.Records.Count()) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group) return false;
|
||||
if (!unit.InterceptCrew(m_Mode) || !task.Route || unit.RouteIndex < 0 || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
if (record.Group != unit.Group || record.SourceId != task.Receipt.SourceId + "-vehicle-" + unit.Index.ToString()) return false;
|
||||
if (unit.InterceptOrder && (unit.InterceptReceipt != task.Receipt || unit.InterceptRecord != record || unit.InterceptRoute != task.Route)) return false;
|
||||
foreach (ERR_ConvoyVehicle other : task.Vehicles)
|
||||
if (!other || other.SupplyToken != 0 || other.SupplyHeld || other.Combat.Held) return false;
|
||||
return true;
|
||||
}
|
||||
bool InterceptCurrent(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order)
|
||||
{
|
||||
return InterceptEligible(task, order) && task.Vehicles[order.VehicleIndex].InterceptToken == order.Token
|
||||
&& task.Vehicles[order.VehicleIndex].InterceptMatches(order);
|
||||
}
|
||||
bool ReserveIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptEligible(task, order) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (!ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
if (order.ExpiresAt > order.ObservedAt + 120000 || !ERR_ForceRules.FiniteVector(order.Position)) return false;
|
||||
if (order.BeliefId <= 0 || order.BeliefRevision <= 0 || order.ReportId <= 0) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit.ClaimIntercept(m_Mode, order)) return false;
|
||||
unit.InterceptReceipt = task.Receipt; unit.InterceptRecord = m_Mode.Forces().FindGroup(unit.Group); unit.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
protected bool InterceptPairEligible(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!first || !second || first == second || first.Group == second.Group || first.VehicleIndex == second.VehicleIndex || first.Token == second.Token) return false;
|
||||
if (!InterceptEligible(task, first) || !InterceptEligible(task, second) || !ERR_ForceRules.Finite(now) || now < 0) return false;
|
||||
if (task.Vehicles[first.VehicleIndex].Entity == task.Vehicles[second.VehicleIndex].Entity) return false;
|
||||
if (first.BeliefId <= 0 || first.BeliefRevision <= 0 || first.ReportId <= 0) return false;
|
||||
if (first.BeliefId != second.BeliefId || first.BeliefRevision != second.BeliefRevision || first.ReportId != second.ReportId) return false;
|
||||
if (first.ObservedAt != second.ObservedAt || first.IssuedAt != second.IssuedAt || first.ExpiresAt != second.ExpiresAt) return false;
|
||||
if (!ERR_ForceRules.Finite(first.ObservedAt) || !ERR_ForceRules.Finite(first.IssuedAt) || !ERR_ForceRules.Finite(first.ExpiresAt)) return false;
|
||||
if (first.ObservedAt < 0 || first.IssuedAt < first.ObservedAt || now < first.IssuedAt || now >= first.ExpiresAt || first.ExpiresAt > first.ObservedAt + 120000) return false;
|
||||
if (!ERR_ForceRules.FiniteVector(first.Position) || !ERR_ForceRules.FiniteVector(second.Position)) return false;
|
||||
return vector.DistanceXZ(first.Position, second.Position) >= 0;
|
||||
}
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now)) return false;
|
||||
foreach (ERR_ConvoyTask existing : m_Tasks)
|
||||
foreach (ERR_ConvoyVehicle claimed : existing.Vehicles) if (claimed && claimed.InterceptToken != 0) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.ClaimIntercept(m_Mode, first)) return false;
|
||||
lead.InterceptReceipt = task.Receipt; lead.InterceptRecord = m_Mode.Forces().FindGroup(lead.Group); lead.InterceptRoute = task.Route;
|
||||
if (!InterceptPairEligible(task, first, second, now) || !partner.ClaimIntercept(m_Mode, second))
|
||||
{ lead.ClearIntercept(first); return false; }
|
||||
partner.InterceptReceipt = task.Receipt; partner.InterceptRecord = m_Mode.Forces().FindGroup(partner.Group); partner.InterceptRoute = task.Route;
|
||||
return true;
|
||||
}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder first, ERR_ConvoyInterceptOrder second, float now)
|
||||
{
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)) return false;
|
||||
ERR_ConvoyVehicle lead = task.Vehicles[first.VehicleIndex];
|
||||
ERR_ConvoyVehicle partner = task.Vehicles[second.VehicleIndex];
|
||||
if (!lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now)) return false;
|
||||
AIWaypoint firstWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), first.Position));
|
||||
if (!firstWaypoint) return false;
|
||||
AIWaypoint secondWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), second.Position));
|
||||
if (!secondWaypoint) { SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); return false; }
|
||||
firstWaypoint.SetCompletionRadius(15); secondWaypoint.SetCompletionRadius(15);
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{ SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint); return false; }
|
||||
// Route removal is also a native boundary; both prepared handles remain
|
||||
// caller-owned until both exact claims have survived it.
|
||||
lead.ClearOrder();
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)) partner.ClearOrder();
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !InterceptCurrent(task, second)
|
||||
|| task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !lead.CanInstallIntercept(m_Mode, first, now) || !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(firstWaypoint); SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
lead.InstallIntercept(firstWaypoint);
|
||||
// AddWaypoint can invoke native listeners. Do not install the partner after
|
||||
// cancellation/ownership loss; release only these exact owned claims.
|
||||
if (!InterceptPairEligible(task, first, second, now) || !InterceptCurrent(task, first) || !lead.InterceptActive
|
||||
|| !InterceptCurrent(task, second) || task.Vehicles[first.VehicleIndex] != lead || task.Vehicles[second.VehicleIndex] != partner
|
||||
|| !partner.CanInstallIntercept(m_Mode, second, now))
|
||||
{
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(secondWaypoint);
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
partner.InstallIntercept(secondWaypoint);
|
||||
if (InterceptPairEligible(task, first, second, now) && InterceptCurrent(task, first) && InterceptCurrent(task, second)
|
||||
&& task.Vehicles[first.VehicleIndex] == lead && task.Vehicles[second.VehicleIndex] == partner
|
||||
&& lead.InterceptActive && partner.InterceptActive) return true;
|
||||
ReleaseIntercept(task, first, now); ReleaseIntercept(task, second, now); return false;
|
||||
}
|
||||
bool ApplyIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!InterceptCurrent(task, order) || !ERR_ForceRules.Finite(now) || now < order.IssuedAt || now >= order.ExpiresAt) return false;
|
||||
return task.Vehicles[order.VehicleIndex].DeliverIntercept(m_Mode, order, now);
|
||||
}
|
||||
void ReleaseIntercept(ERR_ConvoyTask task, ERR_ConvoyInterceptOrder order, float now)
|
||||
{
|
||||
if (!task || !order || !m_Tasks.Contains(task) || !ERR_ForceRules.Finite(now)) return;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
if (unit && unit.InterceptMatches(order))
|
||||
{ bool delivered = unit.InterceptActive; unit.ClearIntercept(order); if (delivered) unit.LegDeadline = now + 600000; }
|
||||
}
|
||||
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
|
||||
{
|
||||
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
|
||||
foreach (ERR_ConvoyVehicle intercept : task.Vehicles) if (intercept && intercept.InterceptToken != 0) return false;
|
||||
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
|
||||
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
|
||||
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false;
|
||||
if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
|
||||
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false;
|
||||
// No caller can replay even a command whose native waypoint recreation fails.
|
||||
unit.Combat.LastCommandId = order.CommandId;
|
||||
if (order.Hold)
|
||||
{
|
||||
if (!unit.Combat.Held) unit.ClearOrder();
|
||||
unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt;
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.Combat.Held = false;
|
||||
if (!unit.SupplyHeld && !unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
|
||||
}
|
||||
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
|
||||
return true;
|
||||
}
|
||||
int Count() { return m_Tasks.Count(); }
|
||||
string Status()
|
||||
{
|
||||
int ready, stopped, vehicles, completed, failed, pending;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks)
|
||||
{
|
||||
if (task.Ready) ready++;
|
||||
if (task.Stopped) stopped++;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity) vehicles++;
|
||||
foreach (ERR_ConvoyCargoCallback operation : task.Cargo)
|
||||
{
|
||||
if (!operation.Finished) pending++;
|
||||
else if (operation.Succeeded) completed++;
|
||||
else failed++;
|
||||
}
|
||||
}
|
||||
string reason;
|
||||
if (m_Tasks.Count() == 1 && m_Tasks[0].Stopped) reason = m_Tasks[0].Reason;
|
||||
if (reason.Length() > 160) reason = reason.Substring(0, 160);
|
||||
return string.Format("Convoys: %1 tasks, %2 reached readiness, %3 stopped, %4 retained physical vehicles; cargo native insertion successes=%5 failed=%6 pending=%7; current inventory may differ after use/looting", m_Tasks.Count(), ready, stopped, vehicles, completed, failed, pending) + "; reason=" + reason;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) task.Stop(m_Mode, "HQ ended convoy task service");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim
|
||||
@@ -0,0 +1,22 @@
|
||||
from pathlib import Path
|
||||
import os,subprocess,json,hashlib,difflib
|
||||
root=Path('/home/jakub/devel/escapistsrereintroduced');out=Path('/tmp/erri-pair-service-0054');rel='addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c';test='tests/test_convoy_intercept_pair.py'
|
||||
env=os.environ.copy();env.update(PYTHONPATH=str(root/'tests'),PYTHONDONTWRITEBYTECODE='1')
|
||||
def check(name,source,expected):
|
||||
env['ERR_CONVOY_SERVICE']=str(source)
|
||||
p=subprocess.run(['python3',str(out/test)],cwd=root,env=env,capture_output=True,text=True)
|
||||
(out/name).write_text(p.stdout+p.stderr);assert (p.returncode==0)==expected,(name,p.returncode)
|
||||
return p.returncode
|
||||
check('baseline-red.log',out/'baseline'/rel,False)
|
||||
s=(out/rel).read_text()
|
||||
mut=out/'mutation-rollback.c';mut.write_text(s.replace('{ lead.ClearIntercept(first); return false; }','{ return false; }',1));check('mutation-rollback-red.log',mut,False)
|
||||
mut=out/'mutation-spacing.c';mut.write_text(s.replace('return vector.DistanceXZ(first.Position, second.Position) >= 40;','return vector.DistanceXZ(first.Position, second.Position) >= 0;',1));check('mutation-spacing-red.log',mut,False)
|
||||
check('green.log',out/rel,True)
|
||||
patch=''.join(difflib.unified_diff((out/'baseline'/rel).read_text().splitlines(True),s.splitlines(True),fromfile='a/'+rel,tofile='b/'+rel))
|
||||
patch+=''.join(difflib.unified_diff([], (out/test).read_text().splitlines(True),fromfile='/dev/null',tofile='b/'+test))
|
||||
(out/'fix.patch').write_text(patch)
|
||||
p=subprocess.run(['git','apply','--check',str(out/'fix.patch')],cwd=root,capture_output=True,text=True);(out/'apply-check.log').write_text(p.stdout+p.stderr);assert p.returncode==0
|
||||
manifest={f:hashlib.sha256((out/f).read_bytes()).hexdigest() for f in [rel,test,'fix.patch']}
|
||||
(out/'source-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
(out/'finding.json').write_text(json.dumps({'scope':'One original convoy pair, distinct original escorts and HQ tokens, same delivered memory/timing, minimum 40m horizontal separation. No actors/items/ammo spawned. Only two native waypoint entities allocated.','files':[rel,test],'tests':{'pair':'green.log','singleton_lifecycle':'lifecycle.log','geometry_existing':'rules.log','baseline':'baseline-red.log is explicit absent pair API assertion, not a claimed runtime behavior failure','mutations':['mutation-rollback-red.log','mutation-spacing-red.log']},'limits':['Offline engine API doubles; physical driving/native callback timing unqualified.','Preallocation and precommit failures leave both original routes unchanged; native ClearOrder/AddWaypoint boundary failures cancel exact claims, without atomic rollback or route restoration claim.','Existing singleton behavior and legacy direct recursive ClearIntercept callback handling unchanged; no concrete native recursive callback demonstrated.'],'adapters':'No existing harness changes needed; test_hq_mounted_intercept extracts geometry only. New pair test reuses unchanged lifecycle native doubles, extracts complete actual vehicle and actual service methods.'},indent=2)+'\n')
|
||||
print(json.dumps(manifest,indent=2))
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c": "b59016d83cb9d40136b98b4e0067ea490bc553574fc53603593efd5522dbed20",
|
||||
"tests/test_convoy_intercept_pair.py": "52b55727b7c2441d43530efa701045b9439b9175c71fbd2ae7577f851d2c98ec",
|
||||
"fix.patch": "dd034f9837703ba3cbf157490999152ef1d00fae7878c5fe8073d808e3abe1c4"
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Execute actual pair admission/apply plus the complete existing vehicle policy.
|
||||
Native doubles inject allocation, AddWaypoint and RemoveWaypoint boundaries.
|
||||
"""
|
||||
import contextlib,io,os,re,runpy
|
||||
from pathlib import Path
|
||||
import policy_harness as h
|
||||
path=Path(os.environ.get('ERR_CONVOY_SERVICE',h.ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c'))
|
||||
source=path.read_text()
|
||||
assert 'bool ReserveInterceptPair(' in source and 'bool ApplyInterceptPair(' in source, 'paired service API absent (baseline feature gap)'
|
||||
# Reuse existing full vehicle/native doubles without running or changing its assertions.
|
||||
captured=[]; original_run=h.run
|
||||
try:
|
||||
h.run=captured.append
|
||||
with contextlib.redirect_stdout(io.StringIO()): runpy.run_path(str(h.ROOT/'tests/test_convoy_lifecycle.py'))
|
||||
finally: h.run=original_run
|
||||
assert len(captured)==1
|
||||
cpp=captured[0]
|
||||
pair='\n'.join(h.body(path,s) for s in ('protected bool InterceptPairEligible(', 'bool ReserveInterceptPair(', 'bool ApplyInterceptPair('))
|
||||
pair=pair.replace('protected ','').replace('foreach (','for (')
|
||||
for typ in ['ERR_ConvoyTask','ERR_ConvoyInterceptOrder','ERR_ConvoyVehicle','AIWaypoint']:
|
||||
pair=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',pair)
|
||||
for obj in ['task','first','second','lead','partner','existing','claimed','m_Mode','firstWaypoint','secondWaypoint','Receipt']:
|
||||
pair=pair.replace(obj+'.',obj+'->')
|
||||
pair=re.sub(r'(Vehicles\[[^\]]+\])\.',r'\1->',pair).replace('Forces().','Forces()->')
|
||||
for ns in ['ERR_ForceRules','ERR_Actors','ERR_Resources','SCR_EntityHelper','vector']:
|
||||
pair=pair.replace(ns+'.',ns+'::')
|
||||
pair=pair.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(')
|
||||
cpp=cpp.replace('bool ApplyIntercept(ERR_ConvoyTask*',pair+'\nbool ApplyIntercept(ERR_ConvoyTask*',1)
|
||||
cpp=cpp[:cpp.index('int main(){')]
|
||||
cpp=cpp.replace('#include <cassert>','#include <cassert>\n#include <functional>\n#include <map>')
|
||||
cpp=cpp.replace('void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}', 'std::function<void()> onRemove,onAdd;void RemoveWaypoint(AIWaypoint*){removes++;if(onRemove)onRemove();}void AddWaypoint(AIWaypoint*){adds++;if(onAdd)onAdd();}')
|
||||
cpp=cpp.replace('ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}', 'std::map<SCR_AIGroup*,ERR_ForceRecord*> records;ERR_ForceRecord* FindGroup(SCR_AIGroup* g){return records.count(g)?records[g]:&record;}')
|
||||
cpp=cpp.replace('int LogisticsToken(SCR_AIGroup*){return token;}', 'std::map<SCR_AIGroup*,int> tokens;int LogisticsToken(SCR_AIGroup* g){return tokens.count(g)?tokens[g]:token;}')
|
||||
cpp=cpp.replace('int spawns=0;bool fail=false;', 'int spawns=0,failAt=0;bool fail=false;std::function<void()> onSpawn;')
|
||||
cpp=cpp.replace('spawns++;return fail?nullptr:new AIWaypoint();','spawns++;if(onSpawn)onSpawn();return (fail||spawns==failAt)?nullptr:new AIWaypoint();')
|
||||
cpp+=r'''
|
||||
struct PairCase:InterceptCase{
|
||||
Case b; ERR_ConvoyInterceptOrder second;ERR_ConvoyVehicle fillers[4];
|
||||
PairCase(){
|
||||
b.unit.Index=3;b.mode.ledger.record.Id=10;b.mode.ledger.record.SourceId="original-vehicle-3";
|
||||
mode.ledger.records[&group]=&mode.ledger.record;mode.ledger.records[&b.group]=&b.mode.ledger.record;
|
||||
mode.command.tokens[&group]=400001;mode.command.tokens[&b.group]=400002;
|
||||
second=order;second.Token=400002;second.Group=&b.group;second.VehicleIndex=3;second.Position={740,0,0};
|
||||
task.Vehicles={&unit,&fillers[0],&fillers[1],&b.unit,&fillers[2],&fillers[3]};
|
||||
unit.Tick(&mode,&route,0);b.unit.Tick(&mode,&route,0);
|
||||
}
|
||||
bool ReservePair(){return service.ReserveInterceptPair(&task,&order,&second,1000);}
|
||||
bool ApplyPair(){return service.ApplyInterceptPair(&task,&order,&second,2000);}
|
||||
};
|
||||
int main(){
|
||||
{PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float da=f.unit.LegDeadline,db=f.b.unit.LegDeadline;
|
||||
assert(f.ReservePair());assert(f.service.InterceptCurrent(&f.task,&f.order)&&f.service.InterceptCurrent(&f.task,&f.second));
|
||||
assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.LegDeadline==da&&f.b.unit.LegDeadline==db);
|
||||
assert(f.ApplyPair());assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&f.unit.InterceptWaypoint!=f.b.unit.InterceptWaypoint);
|
||||
assert(f.unit.InterceptWaypoint->radius==15&&f.b.unit.InterceptWaypoint->radius==15&&f.unit.RouteIndex==0&&f.b.unit.RouteIndex==0);
|
||||
assert(!f.ApplyPair());f.vehicle.position=f.order.Position;f.b.vehicle.position=f.second.Position;
|
||||
f.unit.Tick(&f.mode,&f.route,3000);f.b.unit.Tick(&f.mode,&f.route,3000);
|
||||
assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&!f.unit.InterceptWaypoint&&!f.b.unit.InterceptWaypoint);
|
||||
f.unit.Tick(&f.mode,&f.route,121000);f.b.unit.Tick(&f.mode,&f.route,121000);
|
||||
assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint&&f.b.unit.Waypoint);
|
||||
}
|
||||
for(int bad=0;bad<19;bad++){PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;
|
||||
if(bad==0)f.second.Token=f.order.Token;
|
||||
if(bad==1)f.second.Group=f.order.Group;
|
||||
if(bad==2)f.second.VehicleIndex=0;
|
||||
if(bad==3)f.second.Position.x=739;
|
||||
if(bad==4)f.second.Position.x=NAN;
|
||||
if(bad==5)f.second.BeliefId++;
|
||||
if(bad==6)f.second.BeliefRevision++;
|
||||
if(bad==7)f.second.ReportId++;
|
||||
if(bad==8)f.second.ObservedAt++;
|
||||
if(bad==9)f.second.IssuedAt++;
|
||||
if(bad==10)f.second.ExpiresAt++;
|
||||
if(bad==11)f.second.SourceId="foreign";
|
||||
if(bad==12)f.b.driver.player=true;
|
||||
if(bad==13)f.b.seat.occupant=nullptr;
|
||||
if(bad==14)f.fillers[0].SupplyToken=9;
|
||||
if(bad==15)f.fillers[0].Combat->Held=true;
|
||||
if(bad==16)f.mode.command.tokens[&f.b.group]=0;
|
||||
if(bad==17)f.b.unit.Entity=&f.vehicle;
|
||||
if(bad==18){f.order.ExpiresAt=121001;f.second.ExpiresAt=121001;}
|
||||
assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
}
|
||||
// Claim two fails after claim one: spent token cannot be reused, route untouched.
|
||||
{PairCase f;assert(f.b.unit.ClaimIntercept(&f.mode,&f.second));f.b.unit.ClearIntercept(&f.second);
|
||||
auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float deadline=f.unit.LegDeadline;
|
||||
assert(!f.ReservePair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&f.unit.LegDeadline==deadline);
|
||||
}
|
||||
// Global exclusion includes a different original task; never steal its claim.
|
||||
{PairCase f;ERR_ConvoyTask other;ERR_ConvoyVehicle owned;owned.InterceptToken=400099;other.Vehicles={&owned};f.service.m_Tasks.Insert(&other);
|
||||
assert(!f.ReservePair()&&owned.InterceptToken==400099&&!f.unit.InterceptToken);}
|
||||
for(int allocation=1;allocation<=2;allocation++){PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes,adds=f.group.adds+f.b.group.adds,removes=f.group.removes+f.b.group.removes;
|
||||
ERR_Actors::failAt=ERR_Actors::spawns+allocation;assert(!f.ApplyPair());ERR_Actors::failAt=0;
|
||||
assert(!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
assert(SCR_EntityHelper::deletes==deletes+allocation-1);
|
||||
assert(f.group.adds+f.b.group.adds==adds&&f.group.removes+f.b.group.removes==removes);
|
||||
assert(f.unit.Entity==&f.vehicle&&f.b.unit.Entity==&f.b.vehicle&&f.unit.Driver==&f.driver&&f.b.unit.Driver==&f.b.driver);
|
||||
}
|
||||
// Allocation may revoke original crew. Both prepared handles are deleted before either route changes.
|
||||
{PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes;
|
||||
ERR_Actors::onSpawn=[&](){f.b.driver.alive=false;};assert(!f.ApplyPair());ERR_Actors::onSpawn={};
|
||||
assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&SCR_EntityHelper::deletes==deletes+2);}
|
||||
// Immutable claimed destinations: changing both orders together is still rejected.
|
||||
{PairCase f;assert(f.ReservePair());f.order.Position.x+=100;f.second.Position.x+=100;assert(!f.ApplyPair()&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);}
|
||||
// Native removal and AddWaypoint may synchronously cancel the pair.
|
||||
for(int boundary=0;boundary<3;boundary++){PairCase f;assert(f.ReservePair());int a=f.group.adds,b=f.b.group.adds;
|
||||
auto cancel=[&](){f.service.ReleaseIntercept(&f.task,&f.order,2000);f.service.ReleaseIntercept(&f.task,&f.second,2000);};
|
||||
if(boundary==0)f.group.onRemove=[&](){f.mode.command.tokens[&f.b.group]=0;};
|
||||
if(boundary==1)f.group.onAdd=cancel;
|
||||
if(boundary==2)f.b.group.onAdd=cancel;
|
||||
assert(!f.ApplyPair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);
|
||||
assert(f.b.group.adds==b+(boundary==2));assert(f.group.adds==a+(boundary!=0));
|
||||
}
|
||||
// Per-member release remains exact and never steals a foreign public waypoint.
|
||||
{PairCase f;assert(f.ReservePair());assert(f.ApplyPair());AIWaypoint foreign;f.b.unit.InterceptWaypoint=&foreign;
|
||||
f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken&&f.b.unit.InterceptToken==400002);
|
||||
f.service.ReleaseIntercept(&f.task,&f.second,3000);assert(f.b.unit.InterceptWaypoint==&foreign&&!f.b.unit.InterceptToken);}
|
||||
}
|
||||
'''
|
||||
h.run(cpp)
|
||||
print('PASS: actual pair admission, rollback, native allocation/revocation boundaries, per-member ownership, hold and expiry')
|
||||
Reference in New Issue
Block a user