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:
+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");
|
||||
}
|
||||
}
|
||||
+1693
File diff suppressed because it is too large
Load Diff
+433
@@ -0,0 +1,433 @@
|
||||
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
bool RadioRequested;
|
||||
ref ERR_EnemyEquipmentCallback RadioOperation;
|
||||
string Source;
|
||||
float Deadline;
|
||||
float NextOrder;
|
||||
ref ERR_HQRoute Downlink;
|
||||
vector Destination;
|
||||
int SourceMessage;
|
||||
float LastContact = -1;
|
||||
float NextContactOrder;
|
||||
vector ContactPosition;
|
||||
vector DeliveredContactPosition;
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
bool CombatHeld;
|
||||
float CombatQuietUntil;
|
||||
float NextMotionLog;
|
||||
float ProgressAt;
|
||||
vector ProgressPosition;
|
||||
int Repaths;
|
||||
override bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
return Group && Entity && ERR_Actors.Alive(Entity) && Group.GetFaction() == mode.EnemyFaction()
|
||||
&& Members.Contains(Driver) && ERR_Actors.CanFight(Driver) && !ERR_Actors.Player(Driver)
|
||||
&& ERR_Actors.Group(Driver) == Group && Seats.Count() > 0 && Seats[0].GetOccupant() == Driver;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
if (Recovery) Recovery.Stop();
|
||||
if (PlayerCaptureObserved || !mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { Downlink = null; super.Release(mode); return; }
|
||||
Released = true; Downlink = null; ClearOrder();
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
if (Group)
|
||||
{
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq) hq.ReleaseRecovery(Group);
|
||||
}
|
||||
// Surviving original crew remain registered for skill, reports, intel and
|
||||
// local combat. Only the driving task ends; on-foot survivors can serve HQ.
|
||||
}
|
||||
}
|
||||
|
||||
// An escalation allowance of fifteen physical hunter UAZs, separate from supply
|
||||
// convoys. Losses remain physical; aggression never replenishes killed crews.
|
||||
class ERR_HunterService : ERR_ConvoyService
|
||||
{
|
||||
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
||||
protected float m_NextHunter;
|
||||
protected float m_NextSpawn;
|
||||
protected int m_Target = 3;
|
||||
protected ref ERR_HunterOpeningPlan m_Opening;
|
||||
bool Contains(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||
return false;
|
||||
}
|
||||
// A finite physical radio in the vehicle avoids replacing a medic's backpack
|
||||
// or depending on a modded vest accepting a vanilla manpack. Only the original
|
||||
// living crew currently inside their uncaptured vehicle can use this link.
|
||||
int VehicleRadioCapability(IEntity actor)
|
||||
{
|
||||
if (!actor || !m_Mode || !m_Mode.Active() || m_Stopped) return 0;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released || !unit.Members.Contains(actor) || !unit.Entity || !ERR_Actors.Alive(unit.Entity)) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != unit.Entity || !unit.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) continue;
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage) continue;
|
||||
array<IEntity> items = {}; storage.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Entry is an ownership event, not a later occupancy sample. Keep the fact
|
||||
// even if the player exits/disconnects before the one-second hunter tick.
|
||||
void PlayerEntered(IEntity actor, IEntity entered)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get()) return;
|
||||
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||
if (m_Hunters.Count() > 15) return;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
// Released survivors already have independent HQ ownership. Do not revoke
|
||||
// their infantry role just because the abandoned original vehicle is entered.
|
||||
if (!unit || unit.Released || !unit.Entity || !ERR_ConvoyRules.PartOf(entered, unit.Entity)) continue;
|
||||
unit.PlayerCaptureObserved = true;
|
||||
unit.Release(m_Mode);
|
||||
}
|
||||
}
|
||||
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||
{
|
||||
if (!unit.RadioRequested)
|
||||
{
|
||||
unit.RadioRequested = true;
|
||||
unit.RadioOperation = new ERR_EnemyEquipmentCallback();
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage || !storage.TrySpawnPrefabToStorage(ERR_EnemyRadio.MANPACK, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, unit.RadioOperation))
|
||||
{
|
||||
ERR_HQ.Event("HUNTER RADIO FAILED", unit.Source + ": vehicle refused its one physical HQ radio; no crew backpack is removed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return unit.RadioOperation && unit.RadioOperation.Finished && unit.RadioOperation.Succeeded && VehicleRadioCapability(unit.Driver) == 2;
|
||||
}
|
||||
int Deployed() { return m_Hunters.Count(); }
|
||||
int ActiveHunters()
|
||||
{
|
||||
int count;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
||||
return count;
|
||||
}
|
||||
override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); }
|
||||
protected bool StartHunter(ERR_HQ hq, float now)
|
||||
{
|
||||
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (m_Hunters.Count() < 3)
|
||||
{
|
||||
if (!m_Opening || !m_Opening.Ready()) return false;
|
||||
int slot = m_Hunters.Count();
|
||||
vector openingPosition = m_Opening.Positions[slot];
|
||||
vector openingAngles = m_Opening.Angles[slot];
|
||||
if (!ERR_HunterOpeningPlan.AwayFromPlayers(openingPosition, players) || !placement.Clear(openingPosition, openingAngles[0]))
|
||||
{ ERR_HQ.Event("HUNTER WAIT", "reserved opening road occupied; no unsafe spawn"); return false; }
|
||||
return SpawnHunter(hq, now, openingPosition, openingAngles, "prison-opening");
|
||||
}
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
||||
if (!site || !site.Definition || (site.Definition.Kind != ERR_LocationKind.RADIO && site.Definition.Kind != ERR_LocationKind.DEPOT && site.Definition.Kind != ERR_LocationKind.OUTPOST)) continue;
|
||||
if (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction()) continue;
|
||||
vector position, angles;
|
||||
vector seed = site.Definition.Position + Vector(Math.RandomFloat(-200, 200), 0, Math.RandomFloat(-200, 200));
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles) || !placement.Clear(position, angles[0])) continue;
|
||||
bool nearPlayer;
|
||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
||||
if (nearPlayer) continue;
|
||||
return SpawnHunter(hq, now, position, angles, site.Definition.Id);
|
||||
}
|
||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||
return false;
|
||||
}
|
||||
// Hunter UAZs carry a full response team, unlike the two-person convoy escort.
|
||||
override protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{ ERR_HQ.Event("HUNTER FAILED", "selected faction has no usable soldier/medic profile"); return false; }
|
||||
array<BaseCompartmentSlot> all = {}, cargo = {}, turrets = {}, drivers = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
foreach (BaseCompartmentSlot seat : all)
|
||||
{
|
||||
if (!seat.IsCompartmentAccessible() || seat.GetOccupant()) continue;
|
||||
if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat);
|
||||
}
|
||||
if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||
int medicIndex = unit.Seats.Count();
|
||||
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
{
|
||||
ResourceName selected = soldier;
|
||||
if (i == medicIndex) selected = medic;
|
||||
IEntity actor = unit.Seats[i].SpawnCharacterInCompartment(selected, 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];
|
||||
ERR_HQ.Event("HUNTER TEAM", string.Format("crew=%1 including one medic; soldier=%2 medic=%3", unit.Members.Count(), soldier, medic));
|
||||
return true;
|
||||
}
|
||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||
{
|
||||
// Missing faction data is a deferred deployment, not a lost physical team.
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
ERR_HQ.Event("HUNTER WAIT", string.Format("faction=%1 missing response crew; soldier=%2 medic=%3; no vehicle spawned or allowance consumed; configure ResponseSoldier/ResponseMedic", m_Mode.EnemyFaction().GetFactionKey(), soldier, medic));
|
||||
return false;
|
||||
}
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||
unit.Recovery = new ERR_HunterCrewRecovery(unit);
|
||||
unit.Recovery.Update(m_Mode, now);
|
||||
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(45);
|
||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count()));
|
||||
return true;
|
||||
}
|
||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (!unit.CrewValid(m_Mode) || !PrepareVehicleRadio(unit)) 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;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++) if (unit.Seats[i].GetOccupant() != unit.Members[i]) return false;
|
||||
utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record) return false;
|
||||
record.Pinned = true; unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
||||
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
ERR_HQ.Event("HUNTER READY", unit.Source + " full response crew and medic seated; physical vehicle HQ radio stocked; fire at will; radio registration active");
|
||||
return true;
|
||||
}
|
||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||
ERR_HQBelief best;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||
}
|
||||
bool freshContact;
|
||||
if (best) freshContact = ERR_HunterOpeningRules.FreshOrder(best.ObservedAt, unit.LastContact, now, unit.NextContactOrder, unit.Downlink && unit.SourceMessage > 0);
|
||||
// A first/new delivered sighting preempts routine patrol, including an in-flight patrol order.
|
||||
if (freshContact && unit.Downlink && unit.SourceMessage == 0) unit.Downlink = null;
|
||||
if (unit.Downlink || (!freshContact && now < unit.NextOrder)) return;
|
||||
unit.NextOrder = now + 30000;
|
||||
vector destination, angles;
|
||||
string reason = "road patrol between friendly objectives";
|
||||
if (best)
|
||||
{
|
||||
// Retain the driver's route for repeated reports from the same area.
|
||||
// Re-projecting every sighting can alternate between nearby roads and
|
||||
// repeatedly restart native navigation even for a stationary target.
|
||||
if (unit.Waypoint && unit.DeliveredContact && vector.DistanceXZ(best.PredictedPosition, unit.DeliveredContactPosition) < 75)
|
||||
{
|
||||
unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000;
|
||||
return;
|
||||
}
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.SourceMessage = 0;
|
||||
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
||||
bool found;
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At(Math.RandomInt(0, m_Mode.Sites().Count()));
|
||||
if (!site || !site.Definition || !ERR_ObjectiveDefence.Objective(site.Definition.Kind) || (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction())) continue;
|
||||
if (ERR_SupplyConvoySystem.ApproachDirected(site.Definition.Position, destination, angles) && vector.DistanceXZ(destination, unit.Entity.GetOrigin()) > 200) { found = true; break; }
|
||||
}
|
||||
if (!found) return;
|
||||
}
|
||||
unit.Destination = destination;
|
||||
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
||||
if (best)
|
||||
{
|
||||
unit.NextContactOrder = now + 5000;
|
||||
if (unit.Downlink) { unit.LastContact = best.ObservedAt; unit.ContactPosition = best.PredictedPosition; }
|
||||
}
|
||||
}
|
||||
protected void UpdateCombat(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
if (unit.Registered && ERR_HunterCombat.Active(unit.Group))
|
||||
{
|
||||
unit.CombatQuietUntil = now + 15000;
|
||||
if (!unit.CombatHeld)
|
||||
{
|
||||
unit.CombatHeld = true; unit.ClearOrder(); unit.Downlink = null;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": local contact; travel and forced reboarding suspended, native combat takes priority");
|
||||
}
|
||||
}
|
||||
if (unit.CombatHeld && now >= unit.CombatQuietUntil)
|
||||
{
|
||||
unit.CombatHeld = false; unit.NextOrder = 0;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": contact quiet for 15 seconds; HQ travel may resume");
|
||||
}
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
||||
m_NextHunter = now + 1000;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
||||
m_Target = hq.Watch().HunterTarget();
|
||||
if (!m_Opening)
|
||||
{
|
||||
array<IEntity> openingPlayers = {}; ERR_Actors.Players(openingPlayers);
|
||||
vector center = m_Mode.PrisonPosition(); vector sum; int count;
|
||||
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
|
||||
if (count > 0) center = sum / count;
|
||||
m_Opening = new ERR_HunterOpeningPlan(center);
|
||||
}
|
||||
if (!m_Opening.Done())
|
||||
{
|
||||
array<IEntity> currentPlayers = {}; ERR_Actors.Players(currentPlayers);
|
||||
m_Opening.Step(currentPlayers);
|
||||
}
|
||||
if (now >= m_NextSpawn && Deployed() < m_Target)
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
if (Deployed() < 3) m_NextSpawn = now + 1000;
|
||||
StartHunter(hq, now);
|
||||
}
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released) continue;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
||||
if (unit.Recovery && unit.Recovery.Lost(m_Mode))
|
||||
{
|
||||
string loss = unit.Recovery.Reason;
|
||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||
}
|
||||
UpdateCombat(unit, now);
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
if (unit.CombatHeld) continue;
|
||||
if (!unit.Registered)
|
||||
{
|
||||
if (AdmitHunter(unit, hq, now)) continue;
|
||||
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
||||
continue;
|
||||
}
|
||||
// A temporary unconscious/dismounted driver suspends driving without
|
||||
// throwing away the delivered destination or the original physical crew.
|
||||
if (!unit.CrewValid(m_Mode)) continue;
|
||||
ObserveMovement(unit, now);
|
||||
QueueDestination(unit, hq, now);
|
||||
if (unit.Downlink)
|
||||
{
|
||||
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
||||
if (delivered != 0)
|
||||
{
|
||||
if (delivered > 0)
|
||||
{
|
||||
unit.DeliveredContact = unit.SourceMessage > 0;
|
||||
unit.DeliveredContactPosition = unit.ContactPosition;
|
||||
if (unit.Waypoint && vector.DistanceXZ(unit.Waypoint.GetOrigin(), unit.Destination) < 50)
|
||||
{
|
||||
ERR_HQ.Event("HUNTER ORDER RETAINED", unit.Source + ": updated report confirms current destination; native route preserved", unit.SourceMessage);
|
||||
unit.Downlink = null; continue;
|
||||
}
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
else { ERR_HQ.Event("HUNTER ORDER FAILED", unit.Source + ": move waypoint could not be created"); unit.Downlink = null; continue; }
|
||||
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
||||
}
|
||||
unit.Downlink = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void ObserveMovement(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
vector position = unit.Entity.GetOrigin();
|
||||
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||
// Old entries remain in native perception after contact ends. They must
|
||||
// not suppress stuck-route recovery indefinitely.
|
||||
bool combat = ERR_HunterCombat.Active(unit.Group);
|
||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||
if (!travelling || combat) unit.ProgressAt = now;
|
||||
if (now >= unit.NextMotionLog)
|
||||
{
|
||||
unit.NextMotionLog = now + 30000;
|
||||
float speed;
|
||||
if (unit.Entity.GetPhysics()) speed = unit.Entity.GetPhysics().GetVelocity().Length();
|
||||
vector deliveredDestination;
|
||||
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||
}
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||
unit.ProgressAt = now; unit.Repaths++;
|
||||
// Destination can already contain a newer order awaiting radio delivery.
|
||||
// Retry only the waypoint the crew has actually received.
|
||||
vector retryDestination = unit.Waypoint.GetOrigin();
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) { unit.Downlink = null; unit.Release(m_Mode); }
|
||||
}
|
||||
}
|
||||
|
||||
// Match the original hunter before native callbacks can erase current player
|
||||
// occupancy. Other vehicles and already-released crews are not adopted here.
|
||||
modded class SCR_CompartmentAccessComponent
|
||||
{
|
||||
override protected void OnCompartmentEntered(IEntity targetEntity, BaseCompartmentManagerComponent manager, int mgrID, int slotID, bool move)
|
||||
{
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
||||
}
|
||||
super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
||||
}
|
||||
}
|
||||
+522
@@ -0,0 +1,522 @@
|
||||
// Original convoy tactics adapted to native AI waypoints. No old framework hooks.
|
||||
class ERR_SupplyTeam
|
||||
{
|
||||
SCR_AIGroup Group;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
ref array<AIWaypoint> Orders = {};
|
||||
bool Registered;
|
||||
bool Released;
|
||||
vector OrderedContact;
|
||||
ref ERR_ConvoyBoardingProgress BoardingProgress;
|
||||
float BoardingRetryAt;
|
||||
void Clear()
|
||||
{
|
||||
foreach (AIWaypoint waypoint : Orders)
|
||||
{
|
||||
if (Group && waypoint) Group.RemoveWaypoint(waypoint);
|
||||
if (waypoint) SCR_EntityHelper.DeleteEntityAndChildren(waypoint);
|
||||
}
|
||||
Orders.Clear();
|
||||
}
|
||||
void Add(AIWaypoint waypoint)
|
||||
{
|
||||
if (!waypoint) return;
|
||||
if (!Group || Released) { SCR_EntityHelper.DeleteEntityAndChildren(waypoint); return; }
|
||||
Orders.Insert(waypoint); Group.AddWaypoint(waypoint);
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
Released = true; Clear();
|
||||
if (!Group) return;
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && hq) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
ERR_ForceRecord record;
|
||||
if (mode && mode.Forces()) record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
int BoardingCount(Vehicle truck, out float remainingDistance)
|
||||
{
|
||||
int seated; remainingDistance = 0;
|
||||
foreach (IEntity member : Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(member) == truck) seated++;
|
||||
else remainingDistance = Math.Max(remainingDistance, vector.DistanceXZ(member.GetOrigin(), truck.GetOrigin()));
|
||||
}
|
||||
return seated;
|
||||
}
|
||||
bool Living()
|
||||
{
|
||||
foreach (IEntity member : Members)
|
||||
if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Group) return true;
|
||||
return false;
|
||||
}
|
||||
bool Aboard(Vehicle truck, bool required)
|
||||
{
|
||||
foreach (IEntity member : Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Group(member) != Group) continue;
|
||||
if ((CompartmentAccessComponent.GetVehicleIn(member) == truck) != required) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the exact original vehicle crews are retained. Infantry never enters this map.
|
||||
class ERR_ConvoyMountedCrew
|
||||
{
|
||||
protected static ref map<IEntity, Vehicle> s_Crews = new map<IEntity, Vehicle>();
|
||||
protected static ref map<IEntity, int> s_Couriers = new map<IEntity, int>();
|
||||
static bool BeginCourier(IEntity actor, Vehicle originalVehicle, int token)
|
||||
{
|
||||
if (!actor || !originalVehicle || token <= 0 || !s_Crews.Contains(actor) || s_Crews.Get(actor) != originalVehicle || s_Couriers.Contains(actor)) return false;
|
||||
s_Couriers.Set(actor, token); return true;
|
||||
}
|
||||
static void EndCourier(IEntity actor, int token)
|
||||
{
|
||||
if (actor && s_Couriers.Contains(actor) && s_Couriers.Get(actor) == token) s_Couriers.Remove(actor);
|
||||
}
|
||||
static void Hold(IEntity actor, Vehicle vehicle) { if (actor && vehicle) s_Crews.Set(actor, vehicle); }
|
||||
static void Release(IEntity actor) { if (actor) { s_Crews.Remove(actor); s_Couriers.Remove(actor); } }
|
||||
protected static bool RetainOriginal(IEntity actor, IEntity vehicle)
|
||||
{
|
||||
if (!actor || !vehicle || !s_Crews.Contains(actor) || s_Crews.Get(actor) != vehicle || ERR_Actors.Player(actor) || !ERR_Actors.CanFight(actor)) return false;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != vehicle) return false;
|
||||
SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(vehicle.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
// Let native emergency abandonment take over on critical damage or drivetrain failure.
|
||||
return damage && damage.GetState() != EDamageState.DESTROYED && damage.GetHealthScaled() > 0.25 && damage.GetEngineFunctional() && damage.GetGearboxFunctional();
|
||||
}
|
||||
// Individual permission never authorizes a whole-group dismount.
|
||||
static bool Retain(IEntity actor, IEntity vehicle)
|
||||
{
|
||||
return !s_Couriers.Contains(actor) && RetainOriginal(actor, vehicle);
|
||||
}
|
||||
static bool RetainGroup(SCR_AIGroup group, IEntity vehicle)
|
||||
{
|
||||
if (!group || group.GetPlayerCount(true) > 0) return false;
|
||||
if (!vehicle)
|
||||
{
|
||||
IEntity leader = group.GetLeaderEntity();
|
||||
if (!leader || !s_Crews.Contains(leader)) return false;
|
||||
vehicle = s_Crews.Get(leader);
|
||||
}
|
||||
if (!vehicle) return false;
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
if (agents.IsEmpty() || agents.Count() > 32) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (actor && ERR_Actors.Group(actor) == group && RetainOriginal(actor, vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
modded class SCR_AIGetOutVehicle
|
||||
{
|
||||
override float CustomEvaluate()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
|
||||
return super.CustomEvaluate();
|
||||
}
|
||||
override float EvaluatePriorityLevel()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
|
||||
return super.EvaluatePriorityLevel();
|
||||
}
|
||||
}
|
||||
modded class SCR_AIGetOutActivity
|
||||
{
|
||||
override float CustomEvaluate()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
|
||||
return super.CustomEvaluate();
|
||||
}
|
||||
override float EvaluatePriorityLevel()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
|
||||
return super.EvaluatePriorityLevel();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
override 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_Actors.Group(Driver) != Group || !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;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
foreach (IEntity member : Members) ERR_ConvoyMountedCrew.Release(member);
|
||||
super.Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyTask : ERR_ConvoyTask
|
||||
{
|
||||
ref array<ref ERR_SupplyTeam> Teams = {};
|
||||
ref ERR_SupplyConvoyTactics Tactics;
|
||||
ref ERR_SupplyConvoyColumn Column;
|
||||
ref array<vector> Bases = {};
|
||||
ref array<vector> Starts = {};
|
||||
int LastBase = -1;
|
||||
int Legs;
|
||||
float Departure;
|
||||
float TravelDeadline;
|
||||
bool Waiting;
|
||||
bool MagazinesIssued;
|
||||
ref array<ref ERR_MountedMagazineSeed> MountedStock = {};
|
||||
ref array<IEntity> CargoBefore = {};
|
||||
ref ERR_ConvoyMineStock MineStock;
|
||||
override void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
if (Tactics) Tactics.Stop();
|
||||
if (MineStock) MineStock.Stop();
|
||||
foreach (ERR_SupplyTeam team : Teams) team.Release(mode);
|
||||
super.Stop(mode, reason);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyTactics
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_SupplyConvoyTask m_Task;
|
||||
protected ref array<ref ERR_SupplyTeam> m_Support = {};
|
||||
protected int m_State; // mounted, fighting, reboarding
|
||||
protected vector m_Contact;
|
||||
protected vector m_Forward;
|
||||
protected vector m_Left;
|
||||
protected float m_LastContact = -1;
|
||||
protected float m_NextOrders;
|
||||
protected vector m_LastOrderContact;
|
||||
protected bool m_OrdersIssued;
|
||||
protected float m_BoardDeadline;
|
||||
protected bool m_TruckWithdrawn;
|
||||
protected float m_NextDismount;
|
||||
protected int m_DismountAttempts;
|
||||
protected bool m_Stopped;
|
||||
void ERR_SupplyConvoyTactics(ERR_GameMode mode, ERR_SupplyConvoyTask task)
|
||||
{
|
||||
m_Mode = mode; m_Task = task;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
ERR_SupplyTeam order = new ERR_SupplyTeam(); order.Group = vehicle.Group;
|
||||
m_Support.Insert(order);
|
||||
foreach (IEntity member : vehicle.Members) ERR_ConvoyMountedCrew.Hold(member, vehicle.Entity);
|
||||
}
|
||||
}
|
||||
int State() { return m_State; }
|
||||
static AIWaypoint Move(vector position, float radius = 20)
|
||||
{
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), position));
|
||||
if (waypoint) waypoint.SetCompletionRadius(radius);
|
||||
return waypoint;
|
||||
}
|
||||
static AIWaypoint Board(Vehicle truck, bool enter)
|
||||
{
|
||||
if (!truck) return null;
|
||||
ResourceName prefab = "{C40316EE26846CAB}Prefabs/AI/Waypoints/AIWaypoint_GetOut.et";
|
||||
if (enter) prefab = "{712F4795CF8B91C7}Prefabs/AI/Waypoints/AIWaypoint_GetIn.et";
|
||||
SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(ERR_Actors.Spawn(prefab, truck.GetOrigin()));
|
||||
if (!waypoint) return null;
|
||||
waypoint.SetAllowance(false, false, true);
|
||||
// Native defaults may complete when only the leader has left/boarded.
|
||||
waypoint.SetCompletionType(EAIWaypointCompletionType.All);
|
||||
// The waypoint approach applies to the whole team; leave space around the truck.
|
||||
waypoint.SetCompletionRadius(100);
|
||||
SCR_BoardingEntityWaypoint specific = SCR_BoardingEntityWaypoint.Cast(waypoint);
|
||||
if (specific) specific.SetEntity(truck);
|
||||
return waypoint;
|
||||
}
|
||||
protected bool Usable(int index)
|
||||
{
|
||||
return index >= 0 && index < m_Task.Vehicles.Count() && !m_Task.Vehicles[index].Released && m_Task.Vehicles[index].CrewValid(m_Mode);
|
||||
}
|
||||
protected bool InfantryAboard(bool required)
|
||||
{
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
if (!team.Released && !team.Aboard(m_Task.Vehicles[2].Entity, required)) return false;
|
||||
return true;
|
||||
}
|
||||
protected float DistanceToConvoy(vector position)
|
||||
{
|
||||
float distance = 1e20;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
if (!vehicle.Released && vehicle.Entity) distance = Math.Min(distance, vector.DistanceXZ(vehicle.Entity.GetOrigin(), position));
|
||||
return distance;
|
||||
}
|
||||
protected bool Observe(SCR_AIGroup group, out vector position, inout float nearest)
|
||||
{
|
||||
if (!group || !group.GetGroupUtilityComponent()) return false;
|
||||
SCR_AIGroupPerception perception = group.GetGroupUtilityComponent().m_Perception;
|
||||
PerceptionManager manager = GetGame().GetPerceptionManager();
|
||||
if (!perception || !manager) return false;
|
||||
bool found;
|
||||
foreach (SCR_AITargetInfo target : perception.m_aTargets)
|
||||
{
|
||||
if (!target || !target.m_Entity || !target.m_Faction || !group.GetFaction().IsFactionEnemy(target.m_Faction)) continue;
|
||||
float age = manager.GetTime() - target.m_fTimestamp;
|
||||
if (target.m_eCategory != EAITargetInfoCategory.IDENTIFIED || age < 0 || age > 5) continue;
|
||||
float distance = DistanceToConvoy(target.m_vWorldPos);
|
||||
if (distance > nearest || !group.GetGroupUtilityComponent().IsPositionAllowed(target.m_vWorldPos)) continue;
|
||||
position = target.m_vWorldPos; nearest = distance; found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
// Only production native observations or a delivered HQ snapshot may call this.
|
||||
void Report(vector position, float observed, float now)
|
||||
{
|
||||
if (m_Stopped || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observed) || !ERR_ForceRules.FiniteVector(position) || observed < 0 || observed > now || now - observed > 60000 || observed < m_LastContact) return;
|
||||
m_LastContact = observed;
|
||||
if (m_State != 1) Start(position, now);
|
||||
else if (vector.DistanceXZ(position, m_Contact) > 40 && now >= m_NextOrders) { m_Contact = position; Orders(false, now); }
|
||||
}
|
||||
protected bool FootPosition(ERR_SupplyTeam team, vector desired, out vector position)
|
||||
{
|
||||
if (!team.Group || !team.Group.GetGroupUtilityComponent()) return false;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
vector point = desired;
|
||||
if (i > 0) point += Vector(Math.RandomFloat(-12, 12), 0, Math.RandomFloat(-12, 12));
|
||||
point = ERR_Actors.Ground(point);
|
||||
if (!ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) && team.Group.GetGroupUtilityComponent().IsPositionAllowed(point) && SCR_WorldTools.TraceCilinderUtil(point + "0 1 0", 1, 2)) { position = point; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Flank(int index, bool dismount)
|
||||
{
|
||||
ERR_SupplyTeam team = m_Task.Teams[index];
|
||||
if (team.Released || !team.Living()) return;
|
||||
// Preserve an in-progress native all-member dismount across contact refreshes.
|
||||
if (!team.Aboard(m_Task.Vehicles[2].Entity, false) && SCR_BoardingWaypoint.Cast(team.Group.GetCurrentWaypoint())) return;
|
||||
team.Clear(); team.OrderedContact = m_Contact;
|
||||
if (dismount && !team.Aboard(m_Task.Vehicles[2].Entity, false)) team.Add(Board(m_Task.Vehicles[2].Entity, false));
|
||||
float side = 1; if (index == 1) side = -1;
|
||||
vector approach, sector;
|
||||
if (!FootPosition(team, m_Contact - m_Forward * 80 + m_Left * (110 * side), approach) || !FootPosition(team, m_Contact + m_Left * (70 * side), sector)) return;
|
||||
team.Add(Move(approach, 15));
|
||||
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), sector));
|
||||
if (search) search.SetCompletionRadius(30);
|
||||
team.Add(search);
|
||||
}
|
||||
protected bool SupportPosition(int index, vector desired, out vector position)
|
||||
{
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
vector road;
|
||||
if (ERR_SupplyConvoySystem.Approach(desired, road, 70) && vector.Dot(road - m_Contact, m_Forward) <= -100 && utility.IsPositionAllowed(road)) { position = road; return true; }
|
||||
if (index == 1 || index == 2) return false;
|
||||
// Original armed escorts can seek a nearby open firing position off the road.
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
vector candidate = desired;
|
||||
if (attempt > 0) candidate += Vector(Math.RandomFloat(-15, 15), 0, Math.RandomFloat(-15, 15));
|
||||
candidate = ERR_Actors.Ground(candidate);
|
||||
if (vector.Dot(candidate - m_Contact, m_Forward) > -100 || !utility.IsPositionAllowed(candidate)) continue;
|
||||
if (placement.Clear(candidate, ERR_ForceRules.Heading(candidate, m_Contact, 0))) { position = candidate; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Support(int index, bool holdTruck)
|
||||
{
|
||||
ERR_SupplyTeam orders = m_Support[index]; orders.Clear();
|
||||
if (!Usable(index)) return;
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
// An idle mounted driver stops without a Defend order asking the crew to exit.
|
||||
if (index == 2 && holdTruck) return;
|
||||
float rear = 180, side;
|
||||
if (index == 0) side = 150;
|
||||
if (index == 3) side = -150;
|
||||
if (index == 5) rear = 250;
|
||||
if (index == 1) { rear = 350; side = 40; }
|
||||
if (index == 2) { rear = 310; side = -40; }
|
||||
vector support = vehicle.Entity.GetOrigin();
|
||||
vector desired = m_Contact - m_Forward * rear + m_Left * side;
|
||||
vector chosen;
|
||||
if (SupportPosition(index, desired, chosen)) support = chosen;
|
||||
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20) orders.Add(Move(support));
|
||||
if (index == 1 || index == 2) return;
|
||||
// Mounted escorts engage using native vehicle combat after reaching cover.
|
||||
// Defend is an infantry occupation task; deleting it during regroup could
|
||||
// leave ActivityDefend executing with an already deleted waypoint.
|
||||
// Do not assign it to a crew whose role requires remaining in the vehicle.
|
||||
}
|
||||
protected void Clear()
|
||||
{
|
||||
foreach (ERR_SupplyTeam orders : m_Support) orders.Clear();
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams) team.Clear();
|
||||
}
|
||||
protected void Orders(bool dismount, float now)
|
||||
{
|
||||
m_NextOrders = now + 30000;
|
||||
if (m_OrdersIssued && vector.DistanceXZ(m_LastOrderContact, m_Contact) < 40) return;
|
||||
m_LastOrderContact = m_Contact; m_OrdersIssued = true;
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
|
||||
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
|
||||
m_NextOrders = now + 30000;
|
||||
}
|
||||
protected void Start(vector contact, float now)
|
||||
{
|
||||
// A local threat takes precedence, but may release only this exact detour owner.
|
||||
foreach (ERR_ConvoyVehicle intercept : m_Task.Vehicles)
|
||||
{
|
||||
if (intercept.InterceptToken == 0) continue;
|
||||
intercept.ClearIntercept(intercept.InterceptOrder);
|
||||
if (intercept.InterceptToken != 0) return;
|
||||
}
|
||||
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false;
|
||||
if (m_Task.Column) m_Task.Column.Reset(now);
|
||||
m_Task.Waiting = false;
|
||||
vector origin;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
vehicle.ClearOrder();
|
||||
if (!vehicle.Released) vehicle.Combat.Held = true;
|
||||
if (!vehicle.Released) origin = vehicle.Entity.GetOrigin();
|
||||
}
|
||||
m_Forward = contact - origin; m_Forward[1] = 0;
|
||||
if (m_Forward.Length() < 1) m_Forward = "0 0 1";
|
||||
m_Forward.Normalize(); m_Left = Vector(-m_Forward[2], 0, m_Forward[0]);
|
||||
Orders(true, now);
|
||||
m_DismountAttempts = 0; m_NextDismount = now + 10000;
|
||||
ERR_HQ.Event("CONVOY COMBAT", m_Task.Receipt.SourceId + ": infantry flank left/right; escorts cover; troop truck holds for dismount");
|
||||
}
|
||||
protected void Finish(float now)
|
||||
{
|
||||
Clear(); m_State = 0;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
vehicle.Combat.Held = false; vehicle.Arrived = false;
|
||||
// Combat time does not consume the driving deadline.
|
||||
vehicle.LegDeadline = now + 600000;
|
||||
}
|
||||
m_Task.TravelDeadline = now + 600000;
|
||||
if (m_Task.Column) m_Task.Column.Reset(now, false, true);
|
||||
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
|
||||
}
|
||||
protected float InfantryDistance(IEntity vehicle)
|
||||
{
|
||||
float distance = 1e20;
|
||||
if (!vehicle) return distance;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released) continue;
|
||||
foreach (IEntity member : team.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || CompartmentAccessComponent.GetVehicleIn(member)) continue;
|
||||
distance = Math.Min(distance, vector.DistanceXZ(member.GetOrigin(), vehicle.GetOrigin()));
|
||||
}
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
protected void ProtectInfantry()
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
if (vehicle.Released || !vehicle.Entity) continue;
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||
if (!movement) continue;
|
||||
float speed = 35;
|
||||
if (InfantryDistance(vehicle.Entity) < 45) speed = 8;
|
||||
movement.SetCruiseSpeed(speed);
|
||||
}
|
||||
}
|
||||
protected void RefreshBoarding(float now)
|
||||
{
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released || team.Aboard(m_Task.Vehicles[2].Entity, true)) continue;
|
||||
if (team.BoardingRetryAt > 0)
|
||||
{
|
||||
if (now < team.BoardingRetryAt) continue;
|
||||
team.BoardingRetryAt = 0;
|
||||
team.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
continue;
|
||||
}
|
||||
if (!team.BoardingProgress) team.BoardingProgress = new ERR_ConvoyBoardingProgress();
|
||||
float distance; int seated = team.BoardingCount(m_Task.Vehicles[2].Entity, distance);
|
||||
if (!team.BoardingProgress.Retry(seated, distance, now)) continue;
|
||||
// Let the old native activity release reservations before asking for seats
|
||||
// again. Occupants remain seated; no direct compartment move is performed.
|
||||
team.Clear(); team.BoardingRetryAt = now + 1000;
|
||||
ERR_HQ.Event("CONVOY BOARDING RETRY", string.Format("%1: team has %2 aboard; farthest remaining soldier %3 m from truck. No approach/seat progress for 30 s; retry %4/3", m_Task.Receipt.SourceId, seated, Math.Round(distance), team.BoardingProgress.Retries));
|
||||
}
|
||||
}
|
||||
bool Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return false;
|
||||
for (int released = 0; released < m_Task.Vehicles.Count(); released++)
|
||||
if (m_Task.Vehicles[released].Released) m_Support[released].Clear();
|
||||
foreach (ERR_SupplyTeam infantryTeam : m_Task.Teams)
|
||||
{
|
||||
if (infantryTeam.Released) continue;
|
||||
bool foreign = !infantryTeam.Group || infantryTeam.Group.GetFaction() != m_Mode.EnemyFaction();
|
||||
foreach (IEntity member : infantryTeam.Members)
|
||||
if (ERR_Actors.Alive(member) && ERR_Actors.Group(member) != infantryTeam.Group) foreign = true;
|
||||
if (foreign || !infantryTeam.Living()) infantryTeam.Release(m_Mode);
|
||||
}
|
||||
vector observed; float nearest = 400; bool found;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
if (!vehicle.Released && Observe(vehicle.Group, observed, nearest)) found = true;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
if (!team.Released && Observe(team.Group, observed, nearest)) found = true;
|
||||
if (found) Report(observed, now, now);
|
||||
if (m_State == 1)
|
||||
{
|
||||
if (!m_TruckWithdrawn && InfantryAboard(false) && InfantryDistance(m_Task.Vehicles[2].Entity) >= 25) { m_TruckWithdrawn = true; Support(2, false); }
|
||||
// Native exit attempts can fail while the truck brakes or a door is busy.
|
||||
// Retry only still-mounted teams, never teleport actors or reset quiet time.
|
||||
if (!m_TruckWithdrawn && !InfantryAboard(false) && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
||||
{
|
||||
m_DismountAttempts++; m_NextDismount = now + 10000;
|
||||
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
|
||||
{
|
||||
ERR_SupplyTeam delayed = m_Task.Teams[retry];
|
||||
if (delayed.Released || delayed.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
|
||||
delayed.Clear(); Flank(retry, true);
|
||||
}
|
||||
}
|
||||
if (found && now >= m_NextOrders) Orders(!InfantryAboard(false), now);
|
||||
if (now - m_LastContact >= 60000)
|
||||
{
|
||||
Clear();
|
||||
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
|
||||
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
|
||||
if (!boarding.Released && boarding.Living())
|
||||
{
|
||||
boarding.BoardingProgress = new ERR_ConvoyBoardingProgress(); boarding.BoardingRetryAt = 0;
|
||||
boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
}
|
||||
m_State = 2; m_BoardDeadline = now + 300000;
|
||||
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
|
||||
}
|
||||
}
|
||||
if (m_State == 2 && Usable(2) && now < m_BoardDeadline) RefreshBoarding(now);
|
||||
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
|
||||
{
|
||||
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
|
||||
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
|
||||
Finish(now);
|
||||
}
|
||||
if (m_State != 0) ProtectInfantry();
|
||||
return m_State != 0;
|
||||
}
|
||||
void Stop() { if (m_Stopped) return; m_Stopped = true; Clear(); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
exit=0
|
||||
@@ -0,0 +1,22 @@
|
||||
/tmp/err-policy-uxq5tr7a/check.cpp:15:13: error: ‘bool ERR_ConvoyRules::PartOf(IEntity*, IEntity*)’ defined but not used [-Werror=unused-function]
|
||||
15 | static bool PartOf(IEntity* entity, IEntity* root)
|
||||
| ^~~~~~
|
||||
cc1plus: all warnings being treated as errors
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-hunter-capture-2237/tests/test_hunter_capture.py", line 26, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<62 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',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 '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-uxq5tr7a/check.cpp', '-o', '/tmp/err-policy-uxq5tr7a/check']' returned non-zero exit status 1.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c": {
|
||||
"working": "825a35f7afe485c1e5ddfc96dfff3498b39cce54dc19dce857b5d30fe5dc6557",
|
||||
"index": "e1d5dff5584f1bf12c146df510c860c3e0e046c989ff758def88ecf6b49c6392"
|
||||
},
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c": {
|
||||
"working": "0769d4e7327439ce91b8f9ed0b47c4d92d382e97a1bbf62da8e6002ae4041bf0",
|
||||
"index": "0769d4e7327439ce91b8f9ed0b47c4d92d382e97a1bbf62da8e6002ae4041bf0"
|
||||
},
|
||||
"tests/test_hunter_lifecycle.py": {
|
||||
"working": "c2e3bff077dcb4cc181cbae5526d38a8638be7b2187c72ed64b1e279b63cbafc",
|
||||
"index": "c2e3bff077dcb4cc181cbae5526d38a8638be7b2187c72ed64b1e279b63cbafc"
|
||||
},
|
||||
"tests/test_convoy_survivors.py": {
|
||||
"working": "6386fe89b747af7504de0a18de598eae273a4657ec399baa216d0f423ee73fe8",
|
||||
"index": null
|
||||
}
|
||||
}
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
// 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;
|
||||
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 && 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");
|
||||
}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
bool RadioRequested;
|
||||
ref ERR_EnemyEquipmentCallback RadioOperation;
|
||||
string Source;
|
||||
float Deadline;
|
||||
float NextOrder;
|
||||
ref ERR_HQRoute Downlink;
|
||||
vector Destination;
|
||||
int SourceMessage;
|
||||
float LastContact = -1;
|
||||
float NextContactOrder;
|
||||
vector ContactPosition;
|
||||
vector DeliveredContactPosition;
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
bool CombatHeld;
|
||||
float CombatQuietUntil;
|
||||
float NextMotionLog;
|
||||
float ProgressAt;
|
||||
vector ProgressPosition;
|
||||
int Repaths;
|
||||
override bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
return Group && Entity && ERR_Actors.Alive(Entity) && Group.GetFaction() == mode.EnemyFaction()
|
||||
&& Members.Contains(Driver) && ERR_Actors.CanFight(Driver) && !ERR_Actors.Player(Driver)
|
||||
&& ERR_Actors.Group(Driver) == Group && Seats.Count() > 0 && Seats[0].GetOccupant() == Driver;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
if (Recovery) Recovery.Stop();
|
||||
if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }
|
||||
Released = true; Downlink = null; ClearOrder();
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
if (Group)
|
||||
{
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq) hq.ReleaseRecovery(Group);
|
||||
}
|
||||
// Surviving original crew remain registered for skill, reports, intel and
|
||||
// local combat. Only the driving task ends; on-foot survivors can serve HQ.
|
||||
}
|
||||
}
|
||||
|
||||
// An escalation allowance of fifteen physical hunter UAZs, separate from supply
|
||||
// convoys. Losses remain physical; aggression never replenishes killed crews.
|
||||
class ERR_HunterService : ERR_ConvoyService
|
||||
{
|
||||
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
||||
protected float m_NextHunter;
|
||||
protected float m_NextSpawn;
|
||||
protected int m_Target = 3;
|
||||
protected ref ERR_HunterOpeningPlan m_Opening;
|
||||
bool Contains(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||
return false;
|
||||
}
|
||||
// A finite physical radio in the vehicle avoids replacing a medic's backpack
|
||||
// or depending on a modded vest accepting a vanilla manpack. Only the original
|
||||
// living crew currently inside their uncaptured vehicle can use this link.
|
||||
int VehicleRadioCapability(IEntity actor)
|
||||
{
|
||||
if (!actor || !m_Mode || !m_Mode.Active() || m_Stopped) return 0;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released || !unit.Members.Contains(actor) || !unit.Entity || !ERR_Actors.Alive(unit.Entity)) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != unit.Entity || !unit.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) continue;
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage) continue;
|
||||
array<IEntity> items = {}; storage.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||
{
|
||||
if (!unit.RadioRequested)
|
||||
{
|
||||
unit.RadioRequested = true;
|
||||
unit.RadioOperation = new ERR_EnemyEquipmentCallback();
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage || !storage.TrySpawnPrefabToStorage(ERR_EnemyRadio.MANPACK, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, unit.RadioOperation))
|
||||
{
|
||||
ERR_HQ.Event("HUNTER RADIO FAILED", unit.Source + ": vehicle refused its one physical HQ radio; no crew backpack is removed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return unit.RadioOperation && unit.RadioOperation.Finished && unit.RadioOperation.Succeeded && VehicleRadioCapability(unit.Driver) == 2;
|
||||
}
|
||||
int Deployed() { return m_Hunters.Count(); }
|
||||
int ActiveHunters()
|
||||
{
|
||||
int count;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
||||
return count;
|
||||
}
|
||||
override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); }
|
||||
protected bool StartHunter(ERR_HQ hq, float now)
|
||||
{
|
||||
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (m_Hunters.Count() < 3)
|
||||
{
|
||||
if (!m_Opening || !m_Opening.Ready()) return false;
|
||||
int slot = m_Hunters.Count();
|
||||
vector openingPosition = m_Opening.Positions[slot];
|
||||
vector openingAngles = m_Opening.Angles[slot];
|
||||
if (!ERR_HunterOpeningPlan.AwayFromPlayers(openingPosition, players) || !placement.Clear(openingPosition, openingAngles[0]))
|
||||
{ ERR_HQ.Event("HUNTER WAIT", "reserved opening road occupied; no unsafe spawn"); return false; }
|
||||
return SpawnHunter(hq, now, openingPosition, openingAngles, "prison-opening");
|
||||
}
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
||||
if (!site || !site.Definition || (site.Definition.Kind != ERR_LocationKind.RADIO && site.Definition.Kind != ERR_LocationKind.DEPOT && site.Definition.Kind != ERR_LocationKind.OUTPOST)) continue;
|
||||
if (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction()) continue;
|
||||
vector position, angles;
|
||||
vector seed = site.Definition.Position + Vector(Math.RandomFloat(-200, 200), 0, Math.RandomFloat(-200, 200));
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles) || !placement.Clear(position, angles[0])) continue;
|
||||
bool nearPlayer;
|
||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
||||
if (nearPlayer) continue;
|
||||
return SpawnHunter(hq, now, position, angles, site.Definition.Id);
|
||||
}
|
||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||
return false;
|
||||
}
|
||||
// Hunter UAZs carry a full response team, unlike the two-person convoy escort.
|
||||
override protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{ ERR_HQ.Event("HUNTER FAILED", "selected faction has no usable soldier/medic profile"); return false; }
|
||||
array<BaseCompartmentSlot> all = {}, cargo = {}, turrets = {}, drivers = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
foreach (BaseCompartmentSlot seat : all)
|
||||
{
|
||||
if (!seat.IsCompartmentAccessible() || seat.GetOccupant()) continue;
|
||||
if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat);
|
||||
}
|
||||
if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||
int medicIndex = unit.Seats.Count();
|
||||
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
{
|
||||
ResourceName selected = soldier;
|
||||
if (i == medicIndex) selected = medic;
|
||||
IEntity actor = unit.Seats[i].SpawnCharacterInCompartment(selected, 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];
|
||||
ERR_HQ.Event("HUNTER TEAM", string.Format("crew=%1 including one medic; soldier=%2 medic=%3", unit.Members.Count(), soldier, medic));
|
||||
return true;
|
||||
}
|
||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||
{
|
||||
// Missing faction data is a deferred deployment, not a lost physical team.
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
ERR_HQ.Event("HUNTER WAIT", string.Format("faction=%1 missing response crew; soldier=%2 medic=%3; no vehicle spawned or allowance consumed; configure ResponseSoldier/ResponseMedic", m_Mode.EnemyFaction().GetFactionKey(), soldier, medic));
|
||||
return false;
|
||||
}
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||
unit.Recovery = new ERR_HunterCrewRecovery(unit);
|
||||
unit.Recovery.Update(m_Mode, now);
|
||||
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(45);
|
||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count()));
|
||||
return true;
|
||||
}
|
||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (!unit.CrewValid(m_Mode) || !PrepareVehicleRadio(unit)) 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;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++) if (unit.Seats[i].GetOccupant() != unit.Members[i]) return false;
|
||||
utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record) return false;
|
||||
record.Pinned = true; unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
||||
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
ERR_HQ.Event("HUNTER READY", unit.Source + " full response crew and medic seated; physical vehicle HQ radio stocked; fire at will; radio registration active");
|
||||
return true;
|
||||
}
|
||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||
ERR_HQBelief best;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||
}
|
||||
bool freshContact;
|
||||
if (best) freshContact = ERR_HunterOpeningRules.FreshOrder(best.ObservedAt, unit.LastContact, now, unit.NextContactOrder, unit.Downlink && unit.SourceMessage > 0);
|
||||
// A first/new delivered sighting preempts routine patrol, including an in-flight patrol order.
|
||||
if (freshContact && unit.Downlink && unit.SourceMessage == 0) unit.Downlink = null;
|
||||
if (unit.Downlink || (!freshContact && now < unit.NextOrder)) return;
|
||||
unit.NextOrder = now + 30000;
|
||||
vector destination, angles;
|
||||
string reason = "road patrol between friendly objectives";
|
||||
if (best)
|
||||
{
|
||||
// Retain the driver's route for repeated reports from the same area.
|
||||
// Re-projecting every sighting can alternate between nearby roads and
|
||||
// repeatedly restart native navigation even for a stationary target.
|
||||
if (unit.Waypoint && unit.DeliveredContact && vector.DistanceXZ(best.PredictedPosition, unit.DeliveredContactPosition) < 75)
|
||||
{
|
||||
unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000;
|
||||
return;
|
||||
}
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.SourceMessage = 0;
|
||||
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
||||
bool found;
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At(Math.RandomInt(0, m_Mode.Sites().Count()));
|
||||
if (!site || !site.Definition || !ERR_ObjectiveDefence.Objective(site.Definition.Kind) || (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction())) continue;
|
||||
if (ERR_SupplyConvoySystem.ApproachDirected(site.Definition.Position, destination, angles) && vector.DistanceXZ(destination, unit.Entity.GetOrigin()) > 200) { found = true; break; }
|
||||
}
|
||||
if (!found) return;
|
||||
}
|
||||
unit.Destination = destination;
|
||||
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
||||
if (best)
|
||||
{
|
||||
unit.NextContactOrder = now + 5000;
|
||||
if (unit.Downlink) { unit.LastContact = best.ObservedAt; unit.ContactPosition = best.PredictedPosition; }
|
||||
}
|
||||
}
|
||||
protected void UpdateCombat(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
if (unit.Registered && ERR_HunterCombat.Active(unit.Group))
|
||||
{
|
||||
unit.CombatQuietUntil = now + 15000;
|
||||
if (!unit.CombatHeld)
|
||||
{
|
||||
unit.CombatHeld = true; unit.ClearOrder(); unit.Downlink = null;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": local contact; travel and forced reboarding suspended, native combat takes priority");
|
||||
}
|
||||
}
|
||||
if (unit.CombatHeld && now >= unit.CombatQuietUntil)
|
||||
{
|
||||
unit.CombatHeld = false; unit.NextOrder = 0;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": contact quiet for 15 seconds; HQ travel may resume");
|
||||
}
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
||||
m_NextHunter = now + 1000;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
||||
m_Target = hq.Watch().HunterTarget();
|
||||
if (!m_Opening)
|
||||
{
|
||||
array<IEntity> openingPlayers = {}; ERR_Actors.Players(openingPlayers);
|
||||
vector center = m_Mode.PrisonPosition(); vector sum; int count;
|
||||
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
|
||||
if (count > 0) center = sum / count;
|
||||
m_Opening = new ERR_HunterOpeningPlan(center);
|
||||
}
|
||||
if (!m_Opening.Done())
|
||||
{
|
||||
array<IEntity> currentPlayers = {}; ERR_Actors.Players(currentPlayers);
|
||||
m_Opening.Step(currentPlayers);
|
||||
}
|
||||
if (now >= m_NextSpawn && Deployed() < m_Target)
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
if (Deployed() < 3) m_NextSpawn = now + 1000;
|
||||
StartHunter(hq, now);
|
||||
}
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released) continue;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
||||
if (unit.Recovery && unit.Recovery.Lost(m_Mode))
|
||||
{
|
||||
string loss = unit.Recovery.Reason;
|
||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||
}
|
||||
UpdateCombat(unit, now);
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
if (unit.CombatHeld) continue;
|
||||
if (!unit.Registered)
|
||||
{
|
||||
if (AdmitHunter(unit, hq, now)) continue;
|
||||
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
||||
continue;
|
||||
}
|
||||
// A temporary unconscious/dismounted driver suspends driving without
|
||||
// throwing away the delivered destination or the original physical crew.
|
||||
if (!unit.CrewValid(m_Mode)) continue;
|
||||
ObserveMovement(unit, now);
|
||||
QueueDestination(unit, hq, now);
|
||||
if (unit.Downlink)
|
||||
{
|
||||
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
||||
if (delivered != 0)
|
||||
{
|
||||
if (delivered > 0)
|
||||
{
|
||||
unit.DeliveredContact = unit.SourceMessage > 0;
|
||||
unit.DeliveredContactPosition = unit.ContactPosition;
|
||||
if (unit.Waypoint && vector.DistanceXZ(unit.Waypoint.GetOrigin(), unit.Destination) < 50)
|
||||
{
|
||||
ERR_HQ.Event("HUNTER ORDER RETAINED", unit.Source + ": updated report confirms current destination; native route preserved", unit.SourceMessage);
|
||||
unit.Downlink = null; continue;
|
||||
}
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
else { ERR_HQ.Event("HUNTER ORDER FAILED", unit.Source + ": move waypoint could not be created"); unit.Downlink = null; continue; }
|
||||
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
||||
}
|
||||
unit.Downlink = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void ObserveMovement(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
vector position = unit.Entity.GetOrigin();
|
||||
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||
// Old entries remain in native perception after contact ends. They must
|
||||
// not suppress stuck-route recovery indefinitely.
|
||||
bool combat = ERR_HunterCombat.Active(unit.Group);
|
||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||
if (!travelling || combat) unit.ProgressAt = now;
|
||||
if (now >= unit.NextMotionLog)
|
||||
{
|
||||
unit.NextMotionLog = now + 30000;
|
||||
float speed;
|
||||
if (unit.Entity.GetPhysics()) speed = unit.Entity.GetPhysics().GetVelocity().Length();
|
||||
vector deliveredDestination;
|
||||
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||
}
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||
unit.ProgressAt = now; unit.Repaths++;
|
||||
// Destination can already contain a newer order awaiting radio delivery.
|
||||
// Retry only the waypoint the crew has actually received.
|
||||
vector retryDestination = unit.Waypoint.GetOrigin();
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) { unit.Downlink = null; unit.Release(m_Mode); }
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
"""Actual convoy/team release, HQ retention/eligibility and exact claim cleanup."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
base=Path(os.environ.get('ERR_CONVOY_SURVIVOR_ROOT',str(ROOT)))
|
||||
ai=base/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
def translate(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('override ','').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Cast(mode.Command())','mode.Command()')
|
||||
s=s.replace('array<AIAgent>','Array<AIAgent*>').replace('array<IEntity>','Array<IEntity*>')
|
||||
s=re.sub(r'foreach \(([^:]+):',r'for (\1:',s)
|
||||
for t in ('ERR_GameMode','SCR_AIGroup','ERR_ForceRecord','ERR_HQUnit','IEntity','AIAgent','ERR_HQ'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
for n in ('mode','m_Mode','group','Group','record','unit','actor','agent','member','hq'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Forces().','Forces()->').replace('Command().','Command()->')
|
||||
for n in ('Replication','ERR_GameMode','ERR_Actors','ERR_ForceKind','ERR_ConvoyRules'):
|
||||
s=s.replace(n+'.',n+'::')
|
||||
s=s.replace('Combat.Held','Combat.Held')
|
||||
s=re.sub(r'ERR_HQ\* hq;',r'ERR_HQ* hq=nullptr;',s)
|
||||
s=re.sub(r'ERR_ForceRecord\* record;',r'ERR_ForceRecord* record=nullptr;',s)
|
||||
return s
|
||||
hq=ai/'ERR_HQ.c'
|
||||
# On the baseline the new helper is absent and never called; execute the old
|
||||
# release methods unchanged so the red control fails a gameplay assertion.
|
||||
retain=translate(body(hq,'bool ReleaseConvoySurvivors(')) if 'bool ReleaseConvoySurvivors(' in hq.read_text() else 'bool ReleaseConvoySurvivors(SCR_AIGroup*){return false;}'
|
||||
claims=translate(body(hq,'void ReleaseLogistics('))
|
||||
eligible=translate(body(hq,'bool Eligible(ERR_GameMode mode)')).replace('bool survivor;','bool survivor=false;')
|
||||
vehicle=translate(body(ai/'ERR_ConvoyService.c','void Release(ERR_GameMode mode)'))
|
||||
team=translate(body(ai/'ERR_SupplyConvoyTactics.c','void Release(ERR_GameMode mode)'))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T x){this->push_back(x);}bool Contains(T x)const{return std::find(this->begin(),this->end(),x)!=this->end();}};
|
||||
struct SCR_AIGroup;struct ERR_HQ;struct ERR_GameMode;
|
||||
struct IEntity{bool alive=true,player=false,fighting=true,mounted=false;int faction=1;SCR_AIGroup*group=nullptr;};
|
||||
struct AIAgent{IEntity*actor=nullptr;int lod=0,writes=0;IEntity*GetControlledEntity(){return actor;}void SetPermanentLOD(int n){lod=n;writes++;}};
|
||||
struct SCR_AIGroup{int faction=1,players=0,lod=0;bool slave=false;Array<AIAgent*>agents;int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}bool IsSlave(){return slave;}void GetAgents(Array<AIAgent*>&a){a=agents;}void SetPermanentLOD(int n){lod=n;}};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_Actors{bool Alive(IEntity*a){return a&&a->alive;}bool Player(IEntity*a){return a&&a->player;}bool CanFight(IEntity*a){return Alive(a)&&a->fighting;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity*a){return a&&a->mounted;}}
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,SCR_AIGroup*){return captured;}}
|
||||
namespace ERR_ForceKind{enum{CONVOY,GARRISON};}
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false,Pinned=true;int Kind=ERR_ForceKind::CONVOY,InitialStrength=2;std::string SourceId="original";};
|
||||
struct Ledger{ERR_ForceRecord record;bool present=true;ERR_ForceRecord*FindGroup(SCR_AIGroup*){return present?&record:nullptr;}};
|
||||
struct ERR_GameMode{static ERR_GameMode*current;bool active=true;ERR_HQ*command=nullptr;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Ledger*Forces(){return &ledger;}int EnemyFaction(){return 1;}};ERR_GameMode*ERR_GameMode::current=nullptr;
|
||||
struct Sampler{bool bound=true;std::string source="original";SCR_AIGroup*group=nullptr;Array<IEntity*>members;bool released[2]={false,false};bool OwnsRoster(SCR_AIGroup*g){return bound&&g==group;}std::string InitialSourceId(){return source;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int slot,IEntity*a,bool observe){assert(!observe);return a&&!released[slot]&&!a->player&&a->group==group&&a->faction==1;}};
|
||||
struct ERR_HQUnit{SCR_AIGroup*Group=nullptr;void*Utility=this;std::string ForceSourceId="original";Sampler*StatusSampler=nullptr;bool ReservedForSupport=true;int LogisticsReservation=0;bool PendingConvoyOrder=false,PendingOrder=false,PendingCheckin=false,Downlink=false;int pendingClears=0;bool reports=true;
|
||||
void CancelPending(){PendingConvoyOrder=PendingOrder=PendingCheckin=Downlink=false;pendingClears++;}
|
||||
'''+eligible.replace('CompartmentAccessComponent.','CompartmentAccessComponent::')+r'''
|
||||
};
|
||||
struct ERR_HQ{bool m_Stopping=false;ERR_GameMode*m_Mode=nullptr;Array<ERR_HQUnit*>m_Units;int unregistrations=0,registrations=0,equipmentStops=0;
|
||||
void UnregisterGroup(SCR_AIGroup*g){unregistrations++;equipmentStops++;for(auto u:m_Units)if(u->Group==g){u->Group=nullptr;u->Utility=nullptr;u->reports=false;}}
|
||||
'''+claims+retain+r'''
|
||||
};
|
||||
struct VehicleOwner{bool Released=false,Registered=true;void*InterceptOrder=nullptr;SCR_AIGroup*Group=nullptr;void*Entity=this;struct{bool Held=true;}Combat;int SupplyToken=101;bool SupplyHeld=true;int clears=0,intercepts=0;
|
||||
void ClearIntercept(void*){intercepts++;}void ClearOrder(){clears++;}
|
||||
'''+vehicle+r'''
|
||||
};
|
||||
struct TeamOwner{bool Released=false,Registered=true;SCR_AIGroup*Group=nullptr;int clears=0;void Clear(){clears++;}
|
||||
'''+team+r'''
|
||||
};
|
||||
struct Case{ERR_GameMode mode;ERR_HQ hq;SCR_AIGroup group,foreign;IEntity one,two,extra;AIAgent a,b,c;Sampler sampler;ERR_HQUnit unit;VehicleOwner vehicle;TeamOwner team;
|
||||
Case(){mode.command=&hq;ERR_GameMode::current=&mode;hq.m_Mode=&mode;hq.m_Units={&unit};unit.Group=&group;unit.StatusSampler=&sampler;sampler.group=&group;one.group=two.group=extra.group=&group;a.actor=&one;b.actor=&two;c.actor=&extra;sampler.members={&one,&two};group.agents={&a,&b};vehicle.Group=team.Group=&group;}
|
||||
void Release(bool passenger){if(passenger)team.Release(&mode);else vehicle.Release(&mode);}
|
||||
};
|
||||
int main(){
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.unit.PendingConvoyOrder=true;c.unit.Downlink=true;c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&c.unit.Group==&c.group&&c.unit.Utility&&c.unit.reports); // old release silenced living original survivors
|
||||
assert(!c.unit.ReservedForSupport&&c.unit.Eligible(&c.mode)&&!c.unit.Downlink&&c.unit.pendingClears==1);
|
||||
assert(!c.mode.ledger.record.Pinned&&c.hq.registrations==0&&c.hq.equipmentStops==0);
|
||||
assert(c.group.lod==-1&&c.a.writes==1&&c.b.writes==1);
|
||||
c.Release(passenger);assert(c.unit.pendingClears==1&&c.a.writes==1);
|
||||
}
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.one.fighting=false;c.two.alive=false;c.group.agents={&c.a};c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode)); // unconscious original retains future recovery/report identity
|
||||
c.one.fighting=true;assert(c.unit.Eligible(&c.mode));
|
||||
}
|
||||
{Case c;c.one.mounted=true;c.Release(false);assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode));c.one.mounted=false;assert(c.unit.Eligible(&c.mode));}
|
||||
for(bool passenger:{false,true})for(int bad=0;bad<22;bad++){
|
||||
Case c;
|
||||
if(bad==0)c.mode.active=false;
|
||||
if(bad==1)c.hq.m_Stopping=true;
|
||||
if(bad==2)ERR_GameMode::current=nullptr;
|
||||
if(bad==3)c.mode.command=nullptr;
|
||||
if(bad==4)c.group.players=1;
|
||||
if(bad==5)c.group.faction=2;
|
||||
if(bad==6)c.group.slave=true;
|
||||
if(bad==7)c.mode.ledger.present=false;
|
||||
if(bad==8)c.mode.ledger.record.Physical=false;
|
||||
if(bad==9)c.mode.ledger.record.Eliminated=true;
|
||||
if(bad==10)c.mode.ledger.record.Kind=ERR_ForceKind::GARRISON;
|
||||
if(bad==11)c.unit.ForceSourceId="foreign";
|
||||
if(bad==12)c.sampler.source="foreign";
|
||||
if(bad==13)c.sampler.bound=false;
|
||||
if(bad==14)c.one.group=&c.foreign;
|
||||
if(bad==15)c.one.player=true;
|
||||
if(bad==16)c.one.faction=2;
|
||||
if(bad==17)c.group.agents.Insert(&c.c);
|
||||
if(bad==18)c.group.agents.Insert(&c.a);
|
||||
if(bad==19)c.group.agents={&c.b};
|
||||
if(bad==20)c.one.alive=c.two.alive=false;
|
||||
if(bad==21)c.sampler.released[0]=true;
|
||||
c.Release(passenger);
|
||||
assert(c.hq.unregistrations==(bad==3?0:1));assert(c.unit.ReservedForSupport);
|
||||
}
|
||||
{Case c;ERR_ConvoyRules::captured=true;c.Release(false);assert(c.hq.unregistrations==1);ERR_ConvoyRules::captured=false;}
|
||||
for(bool passenger:{false,true}){Case c;c.vehicle.Registered=c.team.Registered=false;c.Release(passenger);assert(c.hq.unregistrations==1);}
|
||||
// Real HQ claim release keeps an unsettled inventory transaction quarantined,
|
||||
// ignores somebody else's token and permits retasking only after exact settlement.
|
||||
for(bool passenger:{false,true}){Case c;c.unit.LogisticsReservation=200007;c.Release(passenger);
|
||||
assert(c.unit.LogisticsReservation==200007&&!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200008);assert(!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200007);assert(c.unit.Eligible(&c.mode)&&c.unit.reports);
|
||||
}
|
||||
{Case c;c.unit.Downlink=true;c.unit.PendingOrder=true;c.Release(false);assert(c.unit.Downlink&&c.unit.PendingOrder&&c.unit.pendingClears==0);} // no cancellation of another owner's order
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy/team Release retains original active survivors and HQ identity, rejects takeover/terminal/unknown roster, preserves exact logistics quarantine and normal eligibility')
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
"""Production hunter release and objective-role policy, without native spawning."""
|
||||
from policy_harness import ROOT, body, run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
||||
s=body(source,'override void Release(ERR_GameMode mode)')
|
||||
s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr')
|
||||
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
|
||||
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
|
||||
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
|
||||
for name in ('mode','record','Group','agent','hq','Recovery'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
|
||||
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
template<class T>using Array=std::vector<T>;
|
||||
struct AIAgent{int lod=0,writes=0;void SetPermanentLOD(int x){lod=x;writes++;}};
|
||||
struct GroupType{int lod=0;AIAgent agent;void SetPermanentLOD(int x){lod=x;}void GetAgents(Array<AIAgent*>& a){a={&agent};}};
|
||||
struct ERR_ForceRecord{bool Pinned=true;};
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(GroupType*){return &record;}};
|
||||
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
|
||||
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
|
||||
struct RecoveryTask{int stops=0;void Stop(){stops++;}};
|
||||
struct Native{RecoveryTask* Recovery=nullptr;bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
||||
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
|
||||
};
|
||||
struct Hunter:Native{
|
||||
'''+s+r'''
|
||||
};
|
||||
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR,HOSPITAL};}
|
||||
'''+objective+r'''
|
||||
int main(){
|
||||
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;RecoveryTask recovery;unit.Recovery=&recovery;
|
||||
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
|
||||
assert(recovery.stops==1);
|
||||
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
|
||||
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
|
||||
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
|
||||
Hunter captured;ERR_ConvoyRules::captured=true;captured.Release(&mode);assert(captured.delegated==1&&mode.hq.releases==1);
|
||||
ERR_ConvoyRules::captured=false;Hunter stopped;mode.active=false;stopped.Release(&mode);assert(stopped.delegated==1);
|
||||
Hunter missing;missing.Release(nullptr);assert(missing.delegated==1);
|
||||
assert(Objective(ERR_LocationKind::RADIO)&&Objective(ERR_LocationKind::DEPOT)&&Objective(ERR_LocationKind::OUTPOST)&&Objective(ERR_LocationKind::EXTRACTION));
|
||||
assert(!Objective(ERR_LocationKind::PRISON)&&!Objective(ERR_LocationKind::PATROL)&&!Objective(ERR_LocationKind::MORTAR));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual hunter release preserves survivors/HQ, releases driving ownership once, delegates capture/teardown, and excludes prison/patrol/mortar from generic objective defence')
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"problem": "Active original hunter entry capture was only sampled once per second. Current control/occupancy can disappear before next Tick; predecessor records entry event immediately.",
|
||||
"fix": "Server canonical current-player entry matches only active original tracked hunter roots with existing bounded32 ancestry and15 record cap. Latch precedes Release/native callbacks. Hunter clears Downlink/stops Recovery; base rejects survivor retention even if live occupancy absent. Physical entities, finite allowance and registered original identifiers preserved.",
|
||||
"scope": "Already Released hunter ignored: abandoned asset entry does not revoke independently retained former infantry. No helicopter, supply-convoy event dispatch, spawn/refill/deletion/refund, or replay of registration. Polling fallback unchanged.",
|
||||
"tests": {
|
||||
"test_hunter_capture.py": 0,
|
||||
"test_hunter_lifecycle.py": 0,
|
||||
"test_convoy_survivors.py": 0,
|
||||
"test_convoy_lifecycle.py": 0
|
||||
},
|
||||
"baseline": "red.log: actual baseline compiles then fails capture latch/released assertion; baseline-adapter-unused.log retained separate rejected compiler-only control.",
|
||||
"mutation": "mutation.log: removing base !PlayerCaptureObserved guard compiles then fails unregister-vs-retain assertion with live occupancy unavailable.",
|
||||
"limits": "Offline actual-method boundary doubles, not native multiplayer callback timing or ordinary input qualification. SDK callback is supported; root owns native validation.",
|
||||
"registration_needed": "tests/test_hunter_capture.py only; parent owns tools/check.py",
|
||||
"changed_files": [
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c",
|
||||
"tests/test_hunter_lifecycle.py",
|
||||
"tests/test_convoy_survivors.py",
|
||||
"tests/test_hunter_capture.py"
|
||||
],
|
||||
"source_unchanged": true
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c
|
||||
@@ -116,6 +116,8 @@
|
||||
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;
|
||||
@@ -244,7 +246,7 @@
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
- if (Registered && hq && !ERR_ConvoyRules.Captured(Entity, Group)) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
+ 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);
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c
|
||||
@@ -30,7 +30,7 @@
|
||||
{
|
||||
if (Released) return;
|
||||
if (Recovery) Recovery.Stop();
|
||||
- if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }
|
||||
+ if (PlayerCaptureObserved || !mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { Downlink = null; super.Release(mode); return; }
|
||||
Released = true; Downlink = null; ClearOrder();
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
@@ -82,6 +82,24 @@
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
+ }
|
||||
+ // Entry is an ownership event, not a later occupancy sample. Keep the fact
|
||||
+ // even if the player exits/disconnects before the one-second hunter tick.
|
||||
+ void PlayerEntered(IEntity actor, IEntity entered)
|
||||
+ {
|
||||
+ if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get()) return;
|
||||
+ if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||
+ ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
+ if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||
+ if (m_Hunters.Count() > 15) return;
|
||||
+ foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
+ {
|
||||
+ // Released survivors already have independent HQ ownership. Do not revoke
|
||||
+ // their infantry role just because the abandoned original vehicle is entered.
|
||||
+ if (!unit || unit.Released || !unit.Entity || !ERR_ConvoyRules.PartOf(entered, unit.Entity)) continue;
|
||||
+ unit.PlayerCaptureObserved = true;
|
||||
+ unit.Release(m_Mode);
|
||||
+ }
|
||||
}
|
||||
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||
{
|
||||
@@ -396,3 +414,20 @@
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) { unit.Downlink = null; unit.Release(m_Mode); }
|
||||
}
|
||||
}
|
||||
+
|
||||
+// Match the original hunter before native callbacks can erase current player
|
||||
+// occupancy. Other vehicles and already-released crews are not adopted here.
|
||||
+modded class SCR_CompartmentAccessComponent
|
||||
+{
|
||||
+ override protected void OnCompartmentEntered(IEntity targetEntity, BaseCompartmentManagerComponent manager, int mgrID, int slotID, bool move)
|
||||
+ {
|
||||
+ if (Replication.IsServer())
|
||||
+ {
|
||||
+ ERR_GameMode mode = ERR_GameMode.Get();
|
||||
+ ERR_HQ hq;
|
||||
+ if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
+ if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
||||
+ }
|
||||
+ super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
||||
+ }
|
||||
+}
|
||||
--- a/tests/test_hunter_lifecycle.py
|
||||
+++ b/tests/test_hunter_lifecycle.py
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Production hunter release and objective-role policy, without native spawning."""
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
-source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
||||
+source=Path(os.environ.get('ERR_HUNTER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'))
|
||||
s=body(source,'override void Release(ERR_GameMode mode)')
|
||||
s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr')
|
||||
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
|
||||
@@ -21,7 +23,7 @@
|
||||
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
|
||||
struct RecoveryTask{int stops=0;void Stop(){stops++;}};
|
||||
-struct Native{RecoveryTask* Recovery=nullptr;bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
||||
+struct Native{RecoveryTask* Recovery=nullptr;bool Released=false,PlayerCaptureObserved=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
||||
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
|
||||
};
|
||||
struct Hunter:Native{
|
||||
--- a/tests/test_convoy_survivors.py
|
||||
+++ b/tests/test_convoy_survivors.py
|
||||
@@ -55,11 +55,11 @@
|
||||
void UnregisterGroup(SCR_AIGroup*g){unregistrations++;equipmentStops++;for(auto u:m_Units)if(u->Group==g){u->Group=nullptr;u->Utility=nullptr;u->reports=false;}}
|
||||
'''+claims+retain+r'''
|
||||
};
|
||||
-struct VehicleOwner{bool Released=false,Registered=true;void*InterceptOrder=nullptr;SCR_AIGroup*Group=nullptr;void*Entity=this;struct{bool Held=true;}Combat;int SupplyToken=101;bool SupplyHeld=true;int clears=0,intercepts=0;
|
||||
+struct VehicleOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;void*InterceptOrder=nullptr;SCR_AIGroup*Group=nullptr;void*Entity=this;struct{bool Held=true;}Combat;int SupplyToken=101;bool SupplyHeld=true;int clears=0,intercepts=0;
|
||||
void ClearIntercept(void*){intercepts++;}void ClearOrder(){clears++;}
|
||||
'''+vehicle+r'''
|
||||
};
|
||||
-struct TeamOwner{bool Released=false,Registered=true;SCR_AIGroup*Group=nullptr;int clears=0;void Clear(){clears++;}
|
||||
+struct TeamOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;SCR_AIGroup*Group=nullptr;int clears=0;void Clear(){clears++;}
|
||||
'''+team+r'''
|
||||
};
|
||||
struct Case{ERR_GameMode mode;ERR_HQ hq;SCR_AIGroup group,foreign;IEntity one,two,extra;AIAgent a,b,c;Sampler sampler;ERR_HQUnit unit;VehicleOwner vehicle;TeamOwner team;
|
||||
--- /dev/null
|
||||
+++ b/tests/test_hunter_capture.py
|
||||
@@ -0,0 +1,93 @@
|
||||
+"""Execute actual event, tracked-hunter match, bounded ancestry and both release layers."""
|
||||
+import os,re
|
||||
+from pathlib import Path
|
||||
+from policy_harness import ROOT,body,run
|
||||
+base=Path(os.environ.get('ERR_HUNTER_CAPTURE_ROOT',ROOT))
|
||||
+p=base/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
+h=p/'ERR_HunterService.c';c=p/'ERR_ConvoyService.c'
|
||||
+def tr(s):
|
||||
+ s=re.sub(r'//[^\n]*','',s).replace('override ','').replace('protected ','').replace('null','nullptr')
|
||||
+ s=s.replace('ERR_HQ.Cast(m_Mode.Command())','m_Mode.Command()').replace('ERR_HQ.Cast(mode.Command())','mode.Command()')
|
||||
+ s=s.replace('array<AIAgent>','Array<AIAgent*>')
|
||||
+ s=re.sub(r'foreach \(([^:]+):',r'for (\1:',s)
|
||||
+ for t in ('IEntity','ERR_GameMode','ERR_HQ','ERR_HunterVehicle','BaseCompartmentManagerComponent','ERR_ForceRecord','AIAgent'):
|
||||
+ s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
+ for n in ('m_Mode','mode','hq','unit','Group','agent','record','Recovery','entity'):
|
||||
+ s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
+ s=s.replace('Forces().','Forces()->').replace('Players().','Players()->').replace('Command().','Command()->').replace('Hunters().','Hunters()->')
|
||||
+ for n in ('Replication','ERR_GameMode','ERR_ConvoyRules','SCR_EntityHelper'):s=s.replace(n+'.',n+'::')
|
||||
+ s=s.replace('ERR_HQ* hq;','ERR_HQ* hq=nullptr;').replace('int depth;','int depth=0;')
|
||||
+ return s
|
||||
+service=tr(body(h,'void PlayerEntered(')) if 'void PlayerEntered(' in h.read_text() else 'void PlayerEntered(IEntity*,IEntity*){}'
|
||||
+event=tr(body(h,'override protected void OnCompartmentEntered(')).replace('super.','NativeAccess::') if 'override protected void OnCompartmentEntered(' in h.read_text() else 'void OnCompartmentEntered(IEntity* t,BaseCompartmentManagerComponent*m,int i,int s,bool b){NativeAccess::OnCompartmentEntered(t,m,i,s,b);}'
|
||||
+base_release=tr(body(c,'void Release(ERR_GameMode mode)'))
|
||||
+hunter_release=tr(body(h,'override void Release(ERR_GameMode mode)')).replace('super.','VehicleOwner::')
|
||||
+part=tr(body(c,'static bool PartOf('))
|
||||
+run(r'''
|
||||
+#include <cassert>
|
||||
+#include <vector>
|
||||
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
+struct IEntity{IEntity*parent=nullptr;IEntity*GetParent(){return parent;}};
|
||||
+struct AIAgent{int lod=0;void SetPermanentLOD(int n){lod=n;}};
|
||||
+struct GroupType{int lod=0;AIAgent a;void SetPermanentLOD(int n){lod=n;}void GetAgents(Array<AIAgent*>&v){v={&a};}};
|
||||
+struct BaseCompartmentManagerComponent{};
|
||||
+struct Service;struct ERR_HQ;
|
||||
+struct Roster{IEntity*current=nullptr;void*CurrentActor(IEntity*a){return a&&a==current?this:nullptr;}};
|
||||
+struct ERR_ForceRecord{bool Pinned=true;};struct Ledger{ERR_ForceRecord r;ERR_ForceRecord*FindGroup(GroupType*){return &r;}};
|
||||
+struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;ERR_HQ*command=nullptr;Roster roster;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Roster*Players(){return &roster;}Ledger*Forces(){return &ledger;}};
|
||||
+namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
+namespace ERR_ConvoyRules{bool occupied=false;bool Captured(IEntity*,GroupType*){return occupied;}
|
||||
+'''+part+r'''
|
||||
+}
|
||||
+struct ERR_HQ{Service*service=nullptr;int retained=0,unregistered=0,recoveries=0;Service*Hunters(){return service;}bool ReleaseConvoySurvivors(GroupType*){retained++;return true;}void UnregisterGroup(GroupType*){unregistered++;}void ReleaseRecovery(GroupType*){recoveries++;}};
|
||||
+struct RecoveryTask{int stops=0;bool*captured=nullptr;void Stop(){stops++;if(captured)assert(*captured);}};
|
||||
+struct VehicleOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;IEntity*Entity=nullptr;GroupType*Group=nullptr;void*InterceptOrder=nullptr;struct{bool Held=true;}Combat;int SupplyToken=7;bool SupplyHeld=true;int orders=1,clears=0;void ClearIntercept(void*){}void ClearOrder(){orders=0;clears++;}
|
||||
+'''+base_release+r'''
|
||||
+};
|
||||
+struct ERR_HunterVehicle:VehicleOwner{RecoveryTask*Recovery=nullptr;void*Downlink=this;
|
||||
+'''+hunter_release+r'''
|
||||
+};
|
||||
+struct Service{bool m_Stopped=false;ERR_GameMode*m_Mode=nullptr;Array<ERR_HunterVehicle*>m_Hunters;
|
||||
+'''+service+r'''
|
||||
+};
|
||||
+struct NativeAccess{IEntity*owner=nullptr;int nativeCalls=0;IEntity*GetOwner(){return owner;}void OnCompartmentEntered(IEntity*,BaseCompartmentManagerComponent*,int,int,bool){nativeCalls++;if(ERR_GameMode::current)ERR_GameMode::current->roster.current=nullptr;ERR_ConvoyRules::occupied=false;}};
|
||||
+struct Access:NativeAccess{
|
||||
+'''+event+r'''
|
||||
+};
|
||||
+struct Case{ERR_GameMode mode,other;ERR_HQ hq,otherHQ;Service service,foreign;ERR_HunterVehicle unit;GroupType group;IEntity player,vehicle,turret,unrelated;RecoveryTask recovery;Access access;
|
||||
+ Case(){Replication::server=true;ERR_GameMode::current=&mode;mode.command=&hq;mode.roster.current=&player;hq.service=&service;service.m_Mode=&mode;service.m_Hunters={&unit};unit.Entity=&vehicle;unit.Group=&group;unit.Recovery=&recovery;turret.parent=&vehicle;access.owner=&player;}
|
||||
+ void Enter(IEntity*t=nullptr){access.OnCompartmentEntered(t?t:&turret,nullptr,1,2,false);}
|
||||
+};
|
||||
+int main(){
|
||||
+ {IEntity root;assert(ERR_ConvoyRules::PartOf(&root,&root));}
|
||||
+ {Case c;c.recovery.captured=&c.unit.PlayerCaptureObserved;
|
||||
+ // Native occupancy is not visible yet; super removes current identity to model
|
||||
+ // disconnect/body loss before next one-second poll. Event must win that gap.
|
||||
+ c.Enter();assert(c.unit.PlayerCaptureObserved&&c.unit.Released);assert(c.access.nativeCalls==1);
|
||||
+ assert(c.hq.unregistered==1&&c.hq.retained==0&&c.hq.recoveries==0);
|
||||
+ assert(c.recovery.stops==1&&c.unit.Downlink==nullptr&&c.unit.orders==0&&c.unit.clears==1);
|
||||
+ assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
|
||||
+ c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1);
|
||||
+ }
|
||||
+ for(int bad=0;bad<9;bad++){Case c;
|
||||
+ if(bad==0)Replication::server=false;
|
||||
+ if(bad==1)c.service.m_Stopped=true;
|
||||
+ if(bad==2)c.mode.active=false;
|
||||
+ if(bad==3)c.service.m_Mode=&c.other;
|
||||
+ if(bad==4)c.mode.command=&c.otherHQ;
|
||||
+ if(bad==5)c.hq.service=&c.foreign;
|
||||
+ if(bad==6)c.mode.roster.current=&c.unrelated;
|
||||
+ if(bad==7)c.unit.Entity=nullptr;
|
||||
+ if(bad==8)c.service.m_Hunters.resize(16,&c.unit);
|
||||
+ c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1);
|
||||
+ }
|
||||
+ {Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);}
|
||||
+ {Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);}
|
||||
+ {Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);}
|
||||
+ {Case c;c.Enter(&c.vehicle);assert(c.unit.Released);} // root as well as turret child
|
||||
+ {Case c;c.unit.Release(&c.mode);assert(c.unit.Released&&c.hq.recoveries==1&&c.hq.unregistered==0);c.Enter();assert(!c.unit.PlayerCaptureObserved&&c.hq.unregistered==0&&c.hq.recoveries==1);} // abandoned asset does not revoke independent survivors
|
||||
+ {Case c;c.unit.PlayerCaptureObserved=true;c.unit.Release(&c.mode);assert(c.hq.unregistered==1&&c.hq.retained==0&&!c.unit.Downlink);} // latch reaches both release layers without current occupant
|
||||
+ {Case c;c.service.PlayerEntered(nullptr,&c.vehicle);c.service.PlayerEntered(&c.player,nullptr);assert(!c.unit.Released);}
|
||||
+}
|
||||
+''')
|
||||
+print('PASS: actual player-entry hook latches original hunter capture before native callbacks; both release layers cancel ownership without new entities/refund; bounded current-owner guards and prior-release policy')
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual player-entry hook latches original hunter capture before native callbacks; both release layers cancel ownership without new entities/refund; bounded current-owner guards and prior-release policy
|
||||
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-k5litrbi/check.cpp:117: int main(): Assertion `c.hq.unregistered==1&&c.hq.retained==0&&c.hq.recoveries==0' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-hunter-capture-2237/tests/test_hunter_capture.py", line 26, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<62 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
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-k5litrbi/check']' died with <Signals.SIGABRT: 6>.
|
||||
+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 && 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");
|
||||
}
|
||||
}
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
bool RadioRequested;
|
||||
ref ERR_EnemyEquipmentCallback RadioOperation;
|
||||
string Source;
|
||||
float Deadline;
|
||||
float NextOrder;
|
||||
ref ERR_HQRoute Downlink;
|
||||
vector Destination;
|
||||
int SourceMessage;
|
||||
float LastContact = -1;
|
||||
float NextContactOrder;
|
||||
vector ContactPosition;
|
||||
vector DeliveredContactPosition;
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
bool CombatHeld;
|
||||
float CombatQuietUntil;
|
||||
float NextMotionLog;
|
||||
float ProgressAt;
|
||||
vector ProgressPosition;
|
||||
int Repaths;
|
||||
override bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
return Group && Entity && ERR_Actors.Alive(Entity) && Group.GetFaction() == mode.EnemyFaction()
|
||||
&& Members.Contains(Driver) && ERR_Actors.CanFight(Driver) && !ERR_Actors.Player(Driver)
|
||||
&& ERR_Actors.Group(Driver) == Group && Seats.Count() > 0 && Seats[0].GetOccupant() == Driver;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
if (Recovery) Recovery.Stop();
|
||||
if (PlayerCaptureObserved || !mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { Downlink = null; super.Release(mode); return; }
|
||||
Released = true; Downlink = null; ClearOrder();
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
if (Group)
|
||||
{
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq) hq.ReleaseRecovery(Group);
|
||||
}
|
||||
// Surviving original crew remain registered for skill, reports, intel and
|
||||
// local combat. Only the driving task ends; on-foot survivors can serve HQ.
|
||||
}
|
||||
}
|
||||
|
||||
// An escalation allowance of fifteen physical hunter UAZs, separate from supply
|
||||
// convoys. Losses remain physical; aggression never replenishes killed crews.
|
||||
class ERR_HunterService : ERR_ConvoyService
|
||||
{
|
||||
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
||||
protected float m_NextHunter;
|
||||
protected float m_NextSpawn;
|
||||
protected int m_Target = 3;
|
||||
protected ref ERR_HunterOpeningPlan m_Opening;
|
||||
bool Contains(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||
return false;
|
||||
}
|
||||
// A finite physical radio in the vehicle avoids replacing a medic's backpack
|
||||
// or depending on a modded vest accepting a vanilla manpack. Only the original
|
||||
// living crew currently inside their uncaptured vehicle can use this link.
|
||||
int VehicleRadioCapability(IEntity actor)
|
||||
{
|
||||
if (!actor || !m_Mode || !m_Mode.Active() || m_Stopped) return 0;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released || !unit.Members.Contains(actor) || !unit.Entity || !ERR_Actors.Alive(unit.Entity)) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != unit.Entity || !unit.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) continue;
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage) continue;
|
||||
array<IEntity> items = {}; storage.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Entry is an ownership event, not a later occupancy sample. Keep the fact
|
||||
// even if the player exits/disconnects before the one-second hunter tick.
|
||||
void PlayerEntered(IEntity actor, IEntity entered)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get()) return;
|
||||
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||
if (m_Hunters.Count() > 15) return;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
// Released survivors already have independent HQ ownership. Do not revoke
|
||||
// their infantry role just because the abandoned original vehicle is entered.
|
||||
if (!unit || unit.Released || !unit.Entity || !ERR_ConvoyRules.PartOf(entered, unit.Entity)) continue;
|
||||
unit.PlayerCaptureObserved = true;
|
||||
unit.Release(m_Mode);
|
||||
}
|
||||
}
|
||||
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||
{
|
||||
if (!unit.RadioRequested)
|
||||
{
|
||||
unit.RadioRequested = true;
|
||||
unit.RadioOperation = new ERR_EnemyEquipmentCallback();
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage || !storage.TrySpawnPrefabToStorage(ERR_EnemyRadio.MANPACK, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, unit.RadioOperation))
|
||||
{
|
||||
ERR_HQ.Event("HUNTER RADIO FAILED", unit.Source + ": vehicle refused its one physical HQ radio; no crew backpack is removed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return unit.RadioOperation && unit.RadioOperation.Finished && unit.RadioOperation.Succeeded && VehicleRadioCapability(unit.Driver) == 2;
|
||||
}
|
||||
int Deployed() { return m_Hunters.Count(); }
|
||||
int ActiveHunters()
|
||||
{
|
||||
int count;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
||||
return count;
|
||||
}
|
||||
override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); }
|
||||
protected bool StartHunter(ERR_HQ hq, float now)
|
||||
{
|
||||
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (m_Hunters.Count() < 3)
|
||||
{
|
||||
if (!m_Opening || !m_Opening.Ready()) return false;
|
||||
int slot = m_Hunters.Count();
|
||||
vector openingPosition = m_Opening.Positions[slot];
|
||||
vector openingAngles = m_Opening.Angles[slot];
|
||||
if (!ERR_HunterOpeningPlan.AwayFromPlayers(openingPosition, players) || !placement.Clear(openingPosition, openingAngles[0]))
|
||||
{ ERR_HQ.Event("HUNTER WAIT", "reserved opening road occupied; no unsafe spawn"); return false; }
|
||||
return SpawnHunter(hq, now, openingPosition, openingAngles, "prison-opening");
|
||||
}
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
||||
if (!site || !site.Definition || (site.Definition.Kind != ERR_LocationKind.RADIO && site.Definition.Kind != ERR_LocationKind.DEPOT && site.Definition.Kind != ERR_LocationKind.OUTPOST)) continue;
|
||||
if (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction()) continue;
|
||||
vector position, angles;
|
||||
vector seed = site.Definition.Position + Vector(Math.RandomFloat(-200, 200), 0, Math.RandomFloat(-200, 200));
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles) || !placement.Clear(position, angles[0])) continue;
|
||||
bool nearPlayer;
|
||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
||||
if (nearPlayer) continue;
|
||||
return SpawnHunter(hq, now, position, angles, site.Definition.Id);
|
||||
}
|
||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||
return false;
|
||||
}
|
||||
// Hunter UAZs carry a full response team, unlike the two-person convoy escort.
|
||||
override protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{ ERR_HQ.Event("HUNTER FAILED", "selected faction has no usable soldier/medic profile"); return false; }
|
||||
array<BaseCompartmentSlot> all = {}, cargo = {}, turrets = {}, drivers = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
foreach (BaseCompartmentSlot seat : all)
|
||||
{
|
||||
if (!seat.IsCompartmentAccessible() || seat.GetOccupant()) continue;
|
||||
if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat);
|
||||
}
|
||||
if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||
int medicIndex = unit.Seats.Count();
|
||||
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
{
|
||||
ResourceName selected = soldier;
|
||||
if (i == medicIndex) selected = medic;
|
||||
IEntity actor = unit.Seats[i].SpawnCharacterInCompartment(selected, 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];
|
||||
ERR_HQ.Event("HUNTER TEAM", string.Format("crew=%1 including one medic; soldier=%2 medic=%3", unit.Members.Count(), soldier, medic));
|
||||
return true;
|
||||
}
|
||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||
{
|
||||
// Missing faction data is a deferred deployment, not a lost physical team.
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
ERR_HQ.Event("HUNTER WAIT", string.Format("faction=%1 missing response crew; soldier=%2 medic=%3; no vehicle spawned or allowance consumed; configure ResponseSoldier/ResponseMedic", m_Mode.EnemyFaction().GetFactionKey(), soldier, medic));
|
||||
return false;
|
||||
}
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||
unit.Recovery = new ERR_HunterCrewRecovery(unit);
|
||||
unit.Recovery.Update(m_Mode, now);
|
||||
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(45);
|
||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count()));
|
||||
return true;
|
||||
}
|
||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (!unit.CrewValid(m_Mode) || !PrepareVehicleRadio(unit)) 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;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++) if (unit.Seats[i].GetOccupant() != unit.Members[i]) return false;
|
||||
utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record) return false;
|
||||
record.Pinned = true; unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
||||
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
ERR_HQ.Event("HUNTER READY", unit.Source + " full response crew and medic seated; physical vehicle HQ radio stocked; fire at will; radio registration active");
|
||||
return true;
|
||||
}
|
||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||
ERR_HQBelief best;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||
}
|
||||
bool freshContact;
|
||||
if (best) freshContact = ERR_HunterOpeningRules.FreshOrder(best.ObservedAt, unit.LastContact, now, unit.NextContactOrder, unit.Downlink && unit.SourceMessage > 0);
|
||||
// A first/new delivered sighting preempts routine patrol, including an in-flight patrol order.
|
||||
if (freshContact && unit.Downlink && unit.SourceMessage == 0) unit.Downlink = null;
|
||||
if (unit.Downlink || (!freshContact && now < unit.NextOrder)) return;
|
||||
unit.NextOrder = now + 30000;
|
||||
vector destination, angles;
|
||||
string reason = "road patrol between friendly objectives";
|
||||
if (best)
|
||||
{
|
||||
// Retain the driver's route for repeated reports from the same area.
|
||||
// Re-projecting every sighting can alternate between nearby roads and
|
||||
// repeatedly restart native navigation even for a stationary target.
|
||||
if (unit.Waypoint && unit.DeliveredContact && vector.DistanceXZ(best.PredictedPosition, unit.DeliveredContactPosition) < 75)
|
||||
{
|
||||
unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000;
|
||||
return;
|
||||
}
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.SourceMessage = 0;
|
||||
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
||||
bool found;
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At(Math.RandomInt(0, m_Mode.Sites().Count()));
|
||||
if (!site || !site.Definition || !ERR_ObjectiveDefence.Objective(site.Definition.Kind) || (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction())) continue;
|
||||
if (ERR_SupplyConvoySystem.ApproachDirected(site.Definition.Position, destination, angles) && vector.DistanceXZ(destination, unit.Entity.GetOrigin()) > 200) { found = true; break; }
|
||||
}
|
||||
if (!found) return;
|
||||
}
|
||||
unit.Destination = destination;
|
||||
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
||||
if (best)
|
||||
{
|
||||
unit.NextContactOrder = now + 5000;
|
||||
if (unit.Downlink) { unit.LastContact = best.ObservedAt; unit.ContactPosition = best.PredictedPosition; }
|
||||
}
|
||||
}
|
||||
protected void UpdateCombat(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
if (unit.Registered && ERR_HunterCombat.Active(unit.Group))
|
||||
{
|
||||
unit.CombatQuietUntil = now + 15000;
|
||||
if (!unit.CombatHeld)
|
||||
{
|
||||
unit.CombatHeld = true; unit.ClearOrder(); unit.Downlink = null;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": local contact; travel and forced reboarding suspended, native combat takes priority");
|
||||
}
|
||||
}
|
||||
if (unit.CombatHeld && now >= unit.CombatQuietUntil)
|
||||
{
|
||||
unit.CombatHeld = false; unit.NextOrder = 0;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": contact quiet for 15 seconds; HQ travel may resume");
|
||||
}
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
||||
m_NextHunter = now + 1000;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
||||
m_Target = hq.Watch().HunterTarget();
|
||||
if (!m_Opening)
|
||||
{
|
||||
array<IEntity> openingPlayers = {}; ERR_Actors.Players(openingPlayers);
|
||||
vector center = m_Mode.PrisonPosition(); vector sum; int count;
|
||||
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
|
||||
if (count > 0) center = sum / count;
|
||||
m_Opening = new ERR_HunterOpeningPlan(center);
|
||||
}
|
||||
if (!m_Opening.Done())
|
||||
{
|
||||
array<IEntity> currentPlayers = {}; ERR_Actors.Players(currentPlayers);
|
||||
m_Opening.Step(currentPlayers);
|
||||
}
|
||||
if (now >= m_NextSpawn && Deployed() < m_Target)
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
if (Deployed() < 3) m_NextSpawn = now + 1000;
|
||||
StartHunter(hq, now);
|
||||
}
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released) continue;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
||||
if (unit.Recovery && unit.Recovery.Lost(m_Mode))
|
||||
{
|
||||
string loss = unit.Recovery.Reason;
|
||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||
}
|
||||
UpdateCombat(unit, now);
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
if (unit.CombatHeld) continue;
|
||||
if (!unit.Registered)
|
||||
{
|
||||
if (AdmitHunter(unit, hq, now)) continue;
|
||||
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
||||
continue;
|
||||
}
|
||||
// A temporary unconscious/dismounted driver suspends driving without
|
||||
// throwing away the delivered destination or the original physical crew.
|
||||
if (!unit.CrewValid(m_Mode)) continue;
|
||||
ObserveMovement(unit, now);
|
||||
QueueDestination(unit, hq, now);
|
||||
if (unit.Downlink)
|
||||
{
|
||||
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
||||
if (delivered != 0)
|
||||
{
|
||||
if (delivered > 0)
|
||||
{
|
||||
unit.DeliveredContact = unit.SourceMessage > 0;
|
||||
unit.DeliveredContactPosition = unit.ContactPosition;
|
||||
if (unit.Waypoint && vector.DistanceXZ(unit.Waypoint.GetOrigin(), unit.Destination) < 50)
|
||||
{
|
||||
ERR_HQ.Event("HUNTER ORDER RETAINED", unit.Source + ": updated report confirms current destination; native route preserved", unit.SourceMessage);
|
||||
unit.Downlink = null; continue;
|
||||
}
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
else { ERR_HQ.Event("HUNTER ORDER FAILED", unit.Source + ": move waypoint could not be created"); unit.Downlink = null; continue; }
|
||||
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
||||
}
|
||||
unit.Downlink = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void ObserveMovement(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
vector position = unit.Entity.GetOrigin();
|
||||
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||
// Old entries remain in native perception after contact ends. They must
|
||||
// not suppress stuck-route recovery indefinitely.
|
||||
bool combat = ERR_HunterCombat.Active(unit.Group);
|
||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||
if (!travelling || combat) unit.ProgressAt = now;
|
||||
if (now >= unit.NextMotionLog)
|
||||
{
|
||||
unit.NextMotionLog = now + 30000;
|
||||
float speed;
|
||||
if (unit.Entity.GetPhysics()) speed = unit.Entity.GetPhysics().GetVelocity().Length();
|
||||
vector deliveredDestination;
|
||||
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||
}
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||
unit.ProgressAt = now; unit.Repaths++;
|
||||
// Destination can already contain a newer order awaiting radio delivery.
|
||||
// Retry only the waypoint the crew has actually received.
|
||||
vector retryDestination = unit.Waypoint.GetOrigin();
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) { unit.Downlink = null; unit.Release(m_Mode); }
|
||||
}
|
||||
}
|
||||
|
||||
// Match the original hunter before native callbacks can erase current player
|
||||
// occupancy. Other vehicles and already-released crews are not adopted here.
|
||||
modded class SCR_CompartmentAccessComponent
|
||||
{
|
||||
override protected void OnCompartmentEntered(IEntity targetEntity, BaseCompartmentManagerComponent manager, int mgrID, int slotID, bool move)
|
||||
{
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
||||
}
|
||||
super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
import hashlib,json,difflib,subprocess
|
||||
p=Path('/tmp/erri-hunter-capture-2237');r=Path('/home/jakub/devel/escapistsrereintroduced')
|
||||
files=['addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c','addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c','tests/test_hunter_lifecycle.py','tests/test_convoy_survivors.py','tests/test_hunter_capture.py']
|
||||
patch=''
|
||||
for f in files:
|
||||
old=(p/'baseline'/f).read_text() if (p/'baseline'/f).exists() else ''
|
||||
patch+=''.join(difflib.unified_diff(old.splitlines(True),(p/f).read_text().splitlines(True),fromfile='a/'+f if old else '/dev/null',tofile='b/'+f))
|
||||
(p/'fix.patch').write_text(patch)
|
||||
q=subprocess.run(['git','apply','--check',str(p/'fix.patch')],cwd=r,capture_output=True,text=True);(p/'apply-check.log').write_text(q.stdout+q.stderr+'exit='+str(q.returncode)+'\n');assert q.returncode==0
|
||||
sha=lambda x:hashlib.sha256(x.read_bytes()).hexdigest()
|
||||
sdk=Path('/tmp/erri-vanilla-stock/scripts/Game/Vehicle/SCR_CompartmentAccessComponent.c')
|
||||
(p/'source-manifest.json').write_text(json.dumps({'files':{f:sha(p/f) for f in files},'patch_sha256':sha(p/'fix.patch'),'sdk':{'path':str(sdk),'sha256':sha(sdk),'signature_line':64,'ordering':'native general subscribers invoked first at66-67; GetOwner accessed at70; hook signature matches production override'}},indent=2)+'\n')
|
||||
(p/'finding.json').write_text(json.dumps({'problem':'Active original hunter entry capture was only sampled once per second. Current control/occupancy can disappear before next Tick; predecessor records entry event immediately.','fix':'Server canonical current-player entry matches only active original tracked hunter roots with existing bounded32 ancestry and15 record cap. Latch precedes Release/native callbacks. Hunter clears Downlink/stops Recovery; base rejects survivor retention even if live occupancy absent. Physical entities, finite allowance and registered original identifiers preserved.','scope':'Already Released hunter ignored: abandoned asset entry does not revoke independently retained former infantry. No helicopter, supply-convoy event dispatch, spawn/refill/deletion/refund, or replay of registration. Polling fallback unchanged.','tests':json.loads((p/'test-results.json').read_text()),'baseline':'red.log: actual baseline compiles then fails capture latch/released assertion; baseline-adapter-unused.log retained separate rejected compiler-only control.','mutation':'mutation.log: removing base !PlayerCaptureObserved guard compiles then fails unregister-vs-retain assertion with live occupancy unavailable.','limits':'Offline actual-method boundary doubles, not native multiplayer callback timing or ordinary input qualification. SDK callback is supported; root owns native validation.','registration_needed':'tests/test_hunter_capture.py only; parent owns tools/check.py','changed_files':files,'source_unchanged':all(sha(r/f)==v['working'] for f,v in json.loads((p/'baseline.json').read_text()).items())},indent=2)+'\n')
|
||||
print(sha(p/'fix.patch'))
|
||||
@@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
import hashlib,json,subprocess
|
||||
r=Path('/home/jakub/devel/escapistsrereintroduced');p=Path('/tmp/erri-hunter-capture-2237')
|
||||
files=['addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c','addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c','tests/test_hunter_lifecycle.py','tests/test_convoy_survivors.py']
|
||||
b={}
|
||||
for f in files:
|
||||
d=(r/f).read_bytes();t=p/f;t.parent.mkdir(parents=True,exist_ok=True);t.write_bytes(d);s=p/'baseline'/f;s.parent.mkdir(parents=True,exist_ok=True);s.write_bytes(d)
|
||||
q=subprocess.run(['git','show',':'+f],cwd=r,capture_output=True);b[f]={'working':hashlib.sha256(d).hexdigest(),'index':hashlib.sha256(q.stdout).hexdigest() if q.returncode==0 else None}
|
||||
(p/'baseline.json').write_text(json.dumps(b,indent=2)+'\n')
|
||||
f=p/files[0];s=f.read_text().replace(' bool Registered;',' bool Registered;\n // Latched only for an exact tracked hunter at authoritative player entry.\n bool PlayerCaptureObserved;',1).replace('if (Registered && hq && !ERR_ConvoyRules.Captured(Entity, Group))','if (Registered && !PlayerCaptureObserved && hq && !ERR_ConvoyRules.Captured(Entity, Group))',1);f.write_text(s)
|
||||
f=p/files[1];s=f.read_text().replace('if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }','if (PlayerCaptureObserved || !mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { Downlink = null; super.Release(mode); return; }',1)
|
||||
method=''' // Entry is an ownership event, not a later occupancy sample. Keep the fact
|
||||
// even if the player exits/disconnects before the one-second hunter tick.
|
||||
void PlayerEntered(IEntity actor, IEntity entered)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get()) return;
|
||||
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||
if (m_Hunters.Count() > 15) return;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
// Released survivors already have independent HQ ownership. Do not revoke
|
||||
// their infantry role just because the abandoned original vehicle is entered.
|
||||
if (!unit || unit.Released || !unit.Entity || !ERR_ConvoyRules.PartOf(entered, unit.Entity)) continue;
|
||||
unit.PlayerCaptureObserved = true;
|
||||
unit.Release(m_Mode);
|
||||
}
|
||||
}
|
||||
'''
|
||||
pos=s.index(' protected bool PrepareVehicleRadio(');s=s[:pos]+method+s[pos:]
|
||||
s+='''
|
||||
// Match the original hunter before native callbacks can erase current player
|
||||
// occupancy. Other vehicles and already-released crews are not adopted here.
|
||||
modded class SCR_CompartmentAccessComponent
|
||||
{
|
||||
override protected void OnCompartmentEntered(IEntity targetEntity, BaseCompartmentManagerComponent manager, int mgrID, int slotID, bool move)
|
||||
{
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
||||
}
|
||||
super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
||||
}
|
||||
}
|
||||
''';f.write_text(s)
|
||||
f=p/files[2];s=f.read_text().replace('from policy_harness import ROOT, body, run','import os\nfrom pathlib import Path\nfrom policy_harness import ROOT, body, run').replace("source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'","source=Path(os.environ.get('ERR_HUNTER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'))").replace('bool Released=false;','bool Released=false,PlayerCaptureObserved=false;');f.write_text(s)
|
||||
f=p/files[3];f.write_text(f.read_text().replace('bool Released=false,Registered=true;','bool Released=false,Registered=true,PlayerCaptureObserved=false;'))
|
||||
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-_14_mf12/check.cpp:92: int main(): Assertion `c.unit.PlayerCaptureObserved&&c.unit.Released' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-hunter-capture-2237/tests/test_hunter_capture.py", line 26, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<63 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
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-_14_mf12/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"files": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c": "e503e3a2eeaff638dee7fac6830cfbac6b8d87402e1191ff65aaa9961d9e0b86",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c": "79369d5cc30cf628f02df0023157ab423e40497325dfd6a73b218757f2b689d4",
|
||||
"tests/test_hunter_lifecycle.py": "4d228ec10c3d5f02ecf0eb755ca2773e3496a2c25a1e1c355e6ec79ee2ef53b6",
|
||||
"tests/test_convoy_survivors.py": "415cf9f5d23cce363e483d4bfd0292b58ab1fb3d9e727f9581271aa65c1c3109",
|
||||
"tests/test_hunter_capture.py": "d88b3a830c350e27a08fe5977cf85247dff140b3dc9135062eb5e7774b400790"
|
||||
},
|
||||
"patch_sha256": "60e3a89f09f3db61e6e6f89756031015523dee824a5f17f46f8538c51c0689c7",
|
||||
"sdk": {
|
||||
"path": "/tmp/erri-vanilla-stock/scripts/Game/Vehicle/SCR_CompartmentAccessComponent.c",
|
||||
"sha256": "55fc213f5136080dac12e916bb20b3a11913d3e2830b05f9669ba2447965d837",
|
||||
"signature_line": 64,
|
||||
"ordering": "native general subscribers invoked first at66-67; GetOwner accessed at70; hook signature matches production override"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"test_hunter_capture.py": 0,
|
||||
"test_hunter_lifecycle.py": 0,
|
||||
"test_convoy_survivors.py": 0,
|
||||
"test_convoy_lifecycle.py": 0
|
||||
}
|
||||
@@ -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 @@
|
||||
PASS: actual convoy/team Release retains original active survivors and HQ identity, rejects takeover/terminal/unknown roster, preserves exact logistics quarantine and normal eligibility
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual player-entry hook latches original hunter capture before native callbacks; both release layers cancel ownership without new entities/refund; bounded current-owner guards and prior-release policy
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual hunter release preserves survivors/HQ, releases driving ownership once, delegates capture/teardown, and excludes prison/patrol/mortar from generic objective defence
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Actual convoy/team release, HQ retention/eligibility and exact claim cleanup."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
base=Path(os.environ.get('ERR_CONVOY_SURVIVOR_ROOT',str(ROOT)))
|
||||
ai=base/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
def translate(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('override ','').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Cast(mode.Command())','mode.Command()')
|
||||
s=s.replace('array<AIAgent>','Array<AIAgent*>').replace('array<IEntity>','Array<IEntity*>')
|
||||
s=re.sub(r'foreach \(([^:]+):',r'for (\1:',s)
|
||||
for t in ('ERR_GameMode','SCR_AIGroup','ERR_ForceRecord','ERR_HQUnit','IEntity','AIAgent','ERR_HQ'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
for n in ('mode','m_Mode','group','Group','record','unit','actor','agent','member','hq'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Forces().','Forces()->').replace('Command().','Command()->')
|
||||
for n in ('Replication','ERR_GameMode','ERR_Actors','ERR_ForceKind','ERR_ConvoyRules'):
|
||||
s=s.replace(n+'.',n+'::')
|
||||
s=s.replace('Combat.Held','Combat.Held')
|
||||
s=re.sub(r'ERR_HQ\* hq;',r'ERR_HQ* hq=nullptr;',s)
|
||||
s=re.sub(r'ERR_ForceRecord\* record;',r'ERR_ForceRecord* record=nullptr;',s)
|
||||
return s
|
||||
hq=ai/'ERR_HQ.c'
|
||||
# On the baseline the new helper is absent and never called; execute the old
|
||||
# release methods unchanged so the red control fails a gameplay assertion.
|
||||
retain=translate(body(hq,'bool ReleaseConvoySurvivors(')) if 'bool ReleaseConvoySurvivors(' in hq.read_text() else 'bool ReleaseConvoySurvivors(SCR_AIGroup*){return false;}'
|
||||
claims=translate(body(hq,'void ReleaseLogistics('))
|
||||
eligible=translate(body(hq,'bool Eligible(ERR_GameMode mode)')).replace('bool survivor;','bool survivor=false;')
|
||||
vehicle=translate(body(ai/'ERR_ConvoyService.c','void Release(ERR_GameMode mode)'))
|
||||
team=translate(body(ai/'ERR_SupplyConvoyTactics.c','void Release(ERR_GameMode mode)'))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T x){this->push_back(x);}bool Contains(T x)const{return std::find(this->begin(),this->end(),x)!=this->end();}};
|
||||
struct SCR_AIGroup;struct ERR_HQ;struct ERR_GameMode;
|
||||
struct IEntity{bool alive=true,player=false,fighting=true,mounted=false;int faction=1;SCR_AIGroup*group=nullptr;};
|
||||
struct AIAgent{IEntity*actor=nullptr;int lod=0,writes=0;IEntity*GetControlledEntity(){return actor;}void SetPermanentLOD(int n){lod=n;writes++;}};
|
||||
struct SCR_AIGroup{int faction=1,players=0,lod=0;bool slave=false;Array<AIAgent*>agents;int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}bool IsSlave(){return slave;}void GetAgents(Array<AIAgent*>&a){a=agents;}void SetPermanentLOD(int n){lod=n;}};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_Actors{bool Alive(IEntity*a){return a&&a->alive;}bool Player(IEntity*a){return a&&a->player;}bool CanFight(IEntity*a){return Alive(a)&&a->fighting;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity*a){return a&&a->mounted;}}
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,SCR_AIGroup*){return captured;}}
|
||||
namespace ERR_ForceKind{enum{CONVOY,GARRISON};}
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false,Pinned=true;int Kind=ERR_ForceKind::CONVOY,InitialStrength=2;std::string SourceId="original";};
|
||||
struct Ledger{ERR_ForceRecord record;bool present=true;ERR_ForceRecord*FindGroup(SCR_AIGroup*){return present?&record:nullptr;}};
|
||||
struct ERR_GameMode{static ERR_GameMode*current;bool active=true;ERR_HQ*command=nullptr;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Ledger*Forces(){return &ledger;}int EnemyFaction(){return 1;}};ERR_GameMode*ERR_GameMode::current=nullptr;
|
||||
struct Sampler{bool bound=true;std::string source="original";SCR_AIGroup*group=nullptr;Array<IEntity*>members;bool released[2]={false,false};bool OwnsRoster(SCR_AIGroup*g){return bound&&g==group;}std::string InitialSourceId(){return source;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int slot,IEntity*a,bool observe){assert(!observe);return a&&!released[slot]&&!a->player&&a->group==group&&a->faction==1;}};
|
||||
struct ERR_HQUnit{SCR_AIGroup*Group=nullptr;void*Utility=this;std::string ForceSourceId="original";Sampler*StatusSampler=nullptr;bool ReservedForSupport=true;int LogisticsReservation=0;bool PendingConvoyOrder=false,PendingOrder=false,PendingCheckin=false,Downlink=false;int pendingClears=0;bool reports=true;
|
||||
void CancelPending(){PendingConvoyOrder=PendingOrder=PendingCheckin=Downlink=false;pendingClears++;}
|
||||
'''+eligible.replace('CompartmentAccessComponent.','CompartmentAccessComponent::')+r'''
|
||||
};
|
||||
struct ERR_HQ{bool m_Stopping=false;ERR_GameMode*m_Mode=nullptr;Array<ERR_HQUnit*>m_Units;int unregistrations=0,registrations=0,equipmentStops=0;
|
||||
void UnregisterGroup(SCR_AIGroup*g){unregistrations++;equipmentStops++;for(auto u:m_Units)if(u->Group==g){u->Group=nullptr;u->Utility=nullptr;u->reports=false;}}
|
||||
'''+claims+retain+r'''
|
||||
};
|
||||
struct VehicleOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;void*InterceptOrder=nullptr;SCR_AIGroup*Group=nullptr;void*Entity=this;struct{bool Held=true;}Combat;int SupplyToken=101;bool SupplyHeld=true;int clears=0,intercepts=0;
|
||||
void ClearIntercept(void*){intercepts++;}void ClearOrder(){clears++;}
|
||||
'''+vehicle+r'''
|
||||
};
|
||||
struct TeamOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;SCR_AIGroup*Group=nullptr;int clears=0;void Clear(){clears++;}
|
||||
'''+team+r'''
|
||||
};
|
||||
struct Case{ERR_GameMode mode;ERR_HQ hq;SCR_AIGroup group,foreign;IEntity one,two,extra;AIAgent a,b,c;Sampler sampler;ERR_HQUnit unit;VehicleOwner vehicle;TeamOwner team;
|
||||
Case(){mode.command=&hq;ERR_GameMode::current=&mode;hq.m_Mode=&mode;hq.m_Units={&unit};unit.Group=&group;unit.StatusSampler=&sampler;sampler.group=&group;one.group=two.group=extra.group=&group;a.actor=&one;b.actor=&two;c.actor=&extra;sampler.members={&one,&two};group.agents={&a,&b};vehicle.Group=team.Group=&group;}
|
||||
void Release(bool passenger){if(passenger)team.Release(&mode);else vehicle.Release(&mode);}
|
||||
};
|
||||
int main(){
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.unit.PendingConvoyOrder=true;c.unit.Downlink=true;c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&c.unit.Group==&c.group&&c.unit.Utility&&c.unit.reports); // old release silenced living original survivors
|
||||
assert(!c.unit.ReservedForSupport&&c.unit.Eligible(&c.mode)&&!c.unit.Downlink&&c.unit.pendingClears==1);
|
||||
assert(!c.mode.ledger.record.Pinned&&c.hq.registrations==0&&c.hq.equipmentStops==0);
|
||||
assert(c.group.lod==-1&&c.a.writes==1&&c.b.writes==1);
|
||||
c.Release(passenger);assert(c.unit.pendingClears==1&&c.a.writes==1);
|
||||
}
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.one.fighting=false;c.two.alive=false;c.group.agents={&c.a};c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode)); // unconscious original retains future recovery/report identity
|
||||
c.one.fighting=true;assert(c.unit.Eligible(&c.mode));
|
||||
}
|
||||
{Case c;c.one.mounted=true;c.Release(false);assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode));c.one.mounted=false;assert(c.unit.Eligible(&c.mode));}
|
||||
for(bool passenger:{false,true})for(int bad=0;bad<22;bad++){
|
||||
Case c;
|
||||
if(bad==0)c.mode.active=false;
|
||||
if(bad==1)c.hq.m_Stopping=true;
|
||||
if(bad==2)ERR_GameMode::current=nullptr;
|
||||
if(bad==3)c.mode.command=nullptr;
|
||||
if(bad==4)c.group.players=1;
|
||||
if(bad==5)c.group.faction=2;
|
||||
if(bad==6)c.group.slave=true;
|
||||
if(bad==7)c.mode.ledger.present=false;
|
||||
if(bad==8)c.mode.ledger.record.Physical=false;
|
||||
if(bad==9)c.mode.ledger.record.Eliminated=true;
|
||||
if(bad==10)c.mode.ledger.record.Kind=ERR_ForceKind::GARRISON;
|
||||
if(bad==11)c.unit.ForceSourceId="foreign";
|
||||
if(bad==12)c.sampler.source="foreign";
|
||||
if(bad==13)c.sampler.bound=false;
|
||||
if(bad==14)c.one.group=&c.foreign;
|
||||
if(bad==15)c.one.player=true;
|
||||
if(bad==16)c.one.faction=2;
|
||||
if(bad==17)c.group.agents.Insert(&c.c);
|
||||
if(bad==18)c.group.agents.Insert(&c.a);
|
||||
if(bad==19)c.group.agents={&c.b};
|
||||
if(bad==20)c.one.alive=c.two.alive=false;
|
||||
if(bad==21)c.sampler.released[0]=true;
|
||||
c.Release(passenger);
|
||||
assert(c.hq.unregistrations==(bad==3?0:1));assert(c.unit.ReservedForSupport);
|
||||
}
|
||||
{Case c;ERR_ConvoyRules::captured=true;c.Release(false);assert(c.hq.unregistrations==1);ERR_ConvoyRules::captured=false;}
|
||||
for(bool passenger:{false,true}){Case c;c.vehicle.Registered=c.team.Registered=false;c.Release(passenger);assert(c.hq.unregistrations==1);}
|
||||
// Real HQ claim release keeps an unsettled inventory transaction quarantined,
|
||||
// ignores somebody else's token and permits retasking only after exact settlement.
|
||||
for(bool passenger:{false,true}){Case c;c.unit.LogisticsReservation=200007;c.Release(passenger);
|
||||
assert(c.unit.LogisticsReservation==200007&&!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200008);assert(!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200007);assert(c.unit.Eligible(&c.mode)&&c.unit.reports);
|
||||
}
|
||||
{Case c;c.unit.Downlink=true;c.unit.PendingOrder=true;c.Release(false);assert(c.unit.Downlink&&c.unit.PendingOrder&&c.unit.pendingClears==0);} // no cancellation of another owner's order
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy/team Release retains original active survivors and HQ identity, rejects takeover/terminal/unknown roster, preserves exact logistics quarantine and normal eligibility')
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Execute actual event, tracked-hunter match, bounded ancestry and both release layers."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
base=Path(os.environ.get('ERR_HUNTER_CAPTURE_ROOT',ROOT))
|
||||
p=base/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
h=p/'ERR_HunterService.c';c=p/'ERR_ConvoyService.c'
|
||||
def tr(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('override ','').replace('protected ','').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Cast(m_Mode.Command())','m_Mode.Command()').replace('ERR_HQ.Cast(mode.Command())','mode.Command()')
|
||||
s=s.replace('array<AIAgent>','Array<AIAgent*>')
|
||||
s=re.sub(r'foreach \(([^:]+):',r'for (\1:',s)
|
||||
for t in ('IEntity','ERR_GameMode','ERR_HQ','ERR_HunterVehicle','BaseCompartmentManagerComponent','ERR_ForceRecord','AIAgent'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
for n in ('m_Mode','mode','hq','unit','Group','agent','record','Recovery','entity'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('Forces().','Forces()->').replace('Players().','Players()->').replace('Command().','Command()->').replace('Hunters().','Hunters()->')
|
||||
for n in ('Replication','ERR_GameMode','ERR_ConvoyRules','SCR_EntityHelper'):s=s.replace(n+'.',n+'::')
|
||||
s=s.replace('ERR_HQ* hq;','ERR_HQ* hq=nullptr;').replace('int depth;','int depth=0;')
|
||||
return s
|
||||
service=tr(body(h,'void PlayerEntered(')) if 'void PlayerEntered(' in h.read_text() else 'void PlayerEntered(IEntity*,IEntity*){}'
|
||||
event=tr(body(h,'override protected void OnCompartmentEntered(')).replace('super.','NativeAccess::') if 'override protected void OnCompartmentEntered(' in h.read_text() else 'void OnCompartmentEntered(IEntity* t,BaseCompartmentManagerComponent*m,int i,int s,bool b){NativeAccess::OnCompartmentEntered(t,m,i,s,b);}'
|
||||
base_release=tr(body(c,'void Release(ERR_GameMode mode)'))
|
||||
hunter_release=tr(body(h,'override void Release(ERR_GameMode mode)')).replace('super.','VehicleOwner::')
|
||||
part=tr(body(c,'static bool PartOf('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
struct IEntity{IEntity*parent=nullptr;IEntity*GetParent(){return parent;}};
|
||||
struct AIAgent{int lod=0;void SetPermanentLOD(int n){lod=n;}};
|
||||
struct GroupType{int lod=0;AIAgent a;void SetPermanentLOD(int n){lod=n;}void GetAgents(Array<AIAgent*>&v){v={&a};}};
|
||||
struct BaseCompartmentManagerComponent{};
|
||||
struct Service;struct ERR_HQ;
|
||||
struct Roster{IEntity*current=nullptr;void*CurrentActor(IEntity*a){return a&&a==current?this:nullptr;}};
|
||||
struct ERR_ForceRecord{bool Pinned=true;};struct Ledger{ERR_ForceRecord r;ERR_ForceRecord*FindGroup(GroupType*){return &r;}};
|
||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;ERR_HQ*command=nullptr;Roster roster;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Roster*Players(){return &roster;}Ledger*Forces(){return &ledger;}};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_ConvoyRules{bool occupied=false;bool Captured(IEntity*,GroupType*){return occupied;}
|
||||
'''+part+r'''
|
||||
}
|
||||
struct ERR_HQ{Service*service=nullptr;int retained=0,unregistered=0,recoveries=0;Service*Hunters(){return service;}bool ReleaseConvoySurvivors(GroupType*){retained++;return true;}void UnregisterGroup(GroupType*){unregistered++;}void ReleaseRecovery(GroupType*){recoveries++;}};
|
||||
struct RecoveryTask{int stops=0;bool*captured=nullptr;void Stop(){stops++;if(captured)assert(*captured);}};
|
||||
struct VehicleOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;IEntity*Entity=nullptr;GroupType*Group=nullptr;void*InterceptOrder=nullptr;struct{bool Held=true;}Combat;int SupplyToken=7;bool SupplyHeld=true;int orders=1,clears=0;void ClearIntercept(void*){}void ClearOrder(){orders=0;clears++;}
|
||||
'''+base_release+r'''
|
||||
};
|
||||
struct ERR_HunterVehicle:VehicleOwner{RecoveryTask*Recovery=nullptr;void*Downlink=this;
|
||||
'''+hunter_release+r'''
|
||||
};
|
||||
struct Service{bool m_Stopped=false;ERR_GameMode*m_Mode=nullptr;Array<ERR_HunterVehicle*>m_Hunters;
|
||||
'''+service+r'''
|
||||
};
|
||||
struct NativeAccess{IEntity*owner=nullptr;int nativeCalls=0;IEntity*GetOwner(){return owner;}void OnCompartmentEntered(IEntity*,BaseCompartmentManagerComponent*,int,int,bool){nativeCalls++;if(ERR_GameMode::current)ERR_GameMode::current->roster.current=nullptr;ERR_ConvoyRules::occupied=false;}};
|
||||
struct Access:NativeAccess{
|
||||
'''+event+r'''
|
||||
};
|
||||
struct Case{ERR_GameMode mode,other;ERR_HQ hq,otherHQ;Service service,foreign;ERR_HunterVehicle unit;GroupType group;IEntity player,vehicle,turret,unrelated;RecoveryTask recovery;Access access;
|
||||
Case(){Replication::server=true;ERR_GameMode::current=&mode;mode.command=&hq;mode.roster.current=&player;hq.service=&service;service.m_Mode=&mode;service.m_Hunters={&unit};unit.Entity=&vehicle;unit.Group=&group;unit.Recovery=&recovery;turret.parent=&vehicle;access.owner=&player;}
|
||||
void Enter(IEntity*t=nullptr){access.OnCompartmentEntered(t?t:&turret,nullptr,1,2,false);}
|
||||
};
|
||||
int main(){
|
||||
{IEntity root;assert(ERR_ConvoyRules::PartOf(&root,&root));}
|
||||
{Case c;c.recovery.captured=&c.unit.PlayerCaptureObserved;
|
||||
// Native occupancy is not visible yet; super removes current identity to model
|
||||
// disconnect/body loss before next one-second poll. Event must win that gap.
|
||||
c.Enter();assert(c.unit.PlayerCaptureObserved&&c.unit.Released);assert(c.access.nativeCalls==1);
|
||||
assert(c.hq.unregistered==1&&c.hq.retained==0&&c.hq.recoveries==0);
|
||||
assert(c.recovery.stops==1&&c.unit.Downlink==nullptr&&c.unit.orders==0&&c.unit.clears==1);
|
||||
assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
|
||||
c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1);
|
||||
}
|
||||
for(int bad=0;bad<9;bad++){Case c;
|
||||
if(bad==0)Replication::server=false;
|
||||
if(bad==1)c.service.m_Stopped=true;
|
||||
if(bad==2)c.mode.active=false;
|
||||
if(bad==3)c.service.m_Mode=&c.other;
|
||||
if(bad==4)c.mode.command=&c.otherHQ;
|
||||
if(bad==5)c.hq.service=&c.foreign;
|
||||
if(bad==6)c.mode.roster.current=&c.unrelated;
|
||||
if(bad==7)c.unit.Entity=nullptr;
|
||||
if(bad==8)c.service.m_Hunters.resize(16,&c.unit);
|
||||
c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1);
|
||||
}
|
||||
{Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);}
|
||||
{Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);}
|
||||
{Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);}
|
||||
{Case c;c.Enter(&c.vehicle);assert(c.unit.Released);} // root as well as turret child
|
||||
{Case c;c.unit.Release(&c.mode);assert(c.unit.Released&&c.hq.recoveries==1&&c.hq.unregistered==0);c.Enter();assert(!c.unit.PlayerCaptureObserved&&c.hq.unregistered==0&&c.hq.recoveries==1);} // abandoned asset does not revoke independent survivors
|
||||
{Case c;c.unit.PlayerCaptureObserved=true;c.unit.Release(&c.mode);assert(c.hq.unregistered==1&&c.hq.retained==0&&!c.unit.Downlink);} // latch reaches both release layers without current occupant
|
||||
{Case c;c.service.PlayerEntered(nullptr,&c.vehicle);c.service.PlayerEntered(&c.player,nullptr);assert(!c.unit.Released);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual player-entry hook latches original hunter capture before native callbacks; both release layers cancel ownership without new entities/refund; bounded current-owner guards and prior-release policy')
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Production hunter release and objective-role policy, without native spawning."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
source=Path(os.environ.get('ERR_HUNTER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'))
|
||||
s=body(source,'override void Release(ERR_GameMode mode)')
|
||||
s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr')
|
||||
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
|
||||
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
|
||||
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
|
||||
for name in ('mode','record','Group','agent','hq','Recovery'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
|
||||
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
template<class T>using Array=std::vector<T>;
|
||||
struct AIAgent{int lod=0,writes=0;void SetPermanentLOD(int x){lod=x;writes++;}};
|
||||
struct GroupType{int lod=0;AIAgent agent;void SetPermanentLOD(int x){lod=x;}void GetAgents(Array<AIAgent*>& a){a={&agent};}};
|
||||
struct ERR_ForceRecord{bool Pinned=true;};
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(GroupType*){return &record;}};
|
||||
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
|
||||
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
|
||||
struct RecoveryTask{int stops=0;void Stop(){stops++;}};
|
||||
struct Native{RecoveryTask* Recovery=nullptr;bool Released=false,PlayerCaptureObserved=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
||||
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
|
||||
};
|
||||
struct Hunter:Native{
|
||||
'''+s+r'''
|
||||
};
|
||||
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR,HOSPITAL};}
|
||||
'''+objective+r'''
|
||||
int main(){
|
||||
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;RecoveryTask recovery;unit.Recovery=&recovery;
|
||||
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
|
||||
assert(recovery.stops==1);
|
||||
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
|
||||
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
|
||||
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
|
||||
Hunter captured;ERR_ConvoyRules::captured=true;captured.Release(&mode);assert(captured.delegated==1&&mode.hq.releases==1);
|
||||
ERR_ConvoyRules::captured=false;Hunter stopped;mode.active=false;stopped.Release(&mode);assert(stopped.delegated==1);
|
||||
Hunter missing;missing.Release(nullptr);assert(missing.delegated==1);
|
||||
assert(Objective(ERR_LocationKind::RADIO)&&Objective(ERR_LocationKind::DEPOT)&&Objective(ERR_LocationKind::OUTPOST)&&Objective(ERR_LocationKind::EXTRACTION));
|
||||
assert(!Objective(ERR_LocationKind::PRISON)&&!Objective(ERR_LocationKind::PATROL)&&!Objective(ERR_LocationKind::MORTAR));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual hunter release preserves survivors/HQ, releases driving ownership once, delegates capture/teardown, and excludes prison/patrol/mortar from generic objective defence')
|
||||
Reference in New Issue
Block a user