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:
+476
@@ -0,0 +1,476 @@
|
||||
// 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;
|
||||
bool Arrived;
|
||||
float LegDeadline;
|
||||
float NextReadinessNote;
|
||||
|
||||
void ClearOrder()
|
||||
{
|
||||
if (Waypoint && Group) Group.RemoveWaypoint(Waypoint);
|
||||
if (Waypoint) SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
|
||||
Waypoint = null;
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
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)
|
||||
{
|
||||
if (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 || 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 (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");
|
||||
}
|
||||
}
|
||||
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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
"""Actual native convoy ownership/waypoint policy with small engine API doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c')))
|
||||
source=path.read_text();code=source[source.index('class ERR_ConvoyVehicle'):source.index('class ERR_ConvoyCargoCallback')]
|
||||
code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','')
|
||||
code=code.replace('array<IEntity>','Array<IEntity*>').replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>').replace('array<AIAgent>','Array<AIAgent*>')
|
||||
for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState']:
|
||||
code=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',code)
|
||||
for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat']:
|
||||
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
|
||||
for cls in ['SCR_EntityHelper','ERR_Actors','ERR_ConvoyRules','ERR_Resources','CompartmentAccessComponent','vector']:
|
||||
code=code.replace(cls+'.',cls+'::')
|
||||
code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
|
||||
code=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct SCR_AIGroup;struct Vehicle;struct ERR_ConvoyCombatState{bool Held=false;};
|
||||
struct IEntity{bool alive=true,player=false,awake=true;Vehicle* vehicle=nullptr;SCR_AIGroup* group=nullptr;IEntity* parent=nullptr;IEntity* GetParent(){return parent;}};
|
||||
struct AIAgent{int lod=0;void SetPermanentLOD(int value){lod=value;}};
|
||||
struct AIWaypoint:IEntity{float radius=0;void SetCompletionRadius(float value){radius=value;}bool IsWithinCompletionRadius(SCR_AIGroup* group);};
|
||||
struct SCR_AIGroup:IEntity{bool within=false;AIWaypoint* current=nullptr;int lod=0,faction=1,removes=0,adds=0;Array<AIAgent*> agents;int GetFaction(){return faction;}void SetPermanentLOD(int value){lod=value;}void GetAgents(Array<AIAgent*>& out){out=agents;}void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}};
|
||||
bool AIWaypoint::IsWithinCompletionRadius(SCR_AIGroup* group){return group->within;}
|
||||
struct Vehicle:IEntity{vector position;vector GetOrigin(){return position;}};
|
||||
struct BaseCompartmentSlot{};
|
||||
struct ERR_ForceRecord{bool Pinned=true;};
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
|
||||
struct Command{int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
|
||||
struct ERR_GameMode{struct Ledger ledger;struct Command command;int EnemyFaction(){return 1;}struct Ledger* Forces(){return &ledger;}struct Command* Command(){return &command;}};
|
||||
struct ERR_ForceRoute{Array<vector> points;int Count(){return points.size();}vector At(int i){assert(i>=0&&i<Count());return points[i];}};
|
||||
namespace ERR_Actors{int spawns=0;bool fail=false;bool Alive(IEntity* a){return a&&a->alive;}bool CanFight(IEntity* a){return Alive(a)&&a->awake;}bool Player(IEntity* a){return a&&a->player;}SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}IEntity* Spawn(int,vector){spawns++;return fail?nullptr:new AIWaypoint();}}
|
||||
namespace ERR_Resources{int MoveWaypoint(){return 1;}}
|
||||
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* a){return a?a->vehicle:nullptr;}}
|
||||
namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
|
||||
namespace SCR_EntityHelper{int deletes=0;void DeleteEntityAndChildren(AIWaypoint* p){deletes++;delete p;}}
|
||||
''' + code + r'''
|
||||
struct Case{
|
||||
ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
|
||||
Case(){unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
|
||||
};
|
||||
int main(){
|
||||
Case travel;travel.unit.Tick(&travel.mode,&travel.route,1000);assert(travel.group.adds==1&&travel.unit.Waypoint&&travel.unit.LegDeadline==601000);
|
||||
travel.unit.Tick(&travel.mode,&travel.route,2000);assert(travel.group.adds==1&&travel.unit.LegDeadline==601000);
|
||||
travel.vehicle.position={100,0,0};travel.group.within=true;travel.unit.Tick(&travel.mode,&travel.route,3000);assert(travel.unit.RouteIndex==1&&travel.group.adds==2&&travel.group.removes==1);
|
||||
travel.vehicle.position={300,0,0};travel.unit.Tick(&travel.mode,&travel.route,4000);assert(travel.unit.Arrived&&!travel.unit.Waypoint&&travel.group.removes==2);
|
||||
travel.unit.Tick(&travel.mode,&travel.route,5000);assert(travel.group.adds==2);
|
||||
// Native completion may use seated actors ahead of the vehicle origin; native
|
||||
// current WP can already be removed while the controller still owns its handle.
|
||||
Case offset;offset.unit.Tick(&offset.mode,&offset.route,1000);offset.vehicle.position={64.5,0,0};offset.group.within=true;offset.group.current=nullptr;
|
||||
assert(vector::DistanceXZ(offset.vehicle.position,offset.route.At(0))>35);
|
||||
offset.unit.Tick(&offset.mode,&offset.route,2000);assert(offset.unit.RouteIndex==1&&offset.group.adds==2&&offset.group.removes==1&&offset.unit.Waypoint);
|
||||
offset.group.within=false;offset.unit.Tick(&offset.mode,&offset.route,3000);offset.unit.Tick(&offset.mode,&offset.route,4000);assert(offset.unit.RouteIndex==1&&offset.group.adds==2&&offset.group.removes==1);
|
||||
// A close vehicle origin alone is not the native seated-group arrival predicate.
|
||||
Case notNative;notNative.vehicle.position={99,0,0};notNative.unit.Tick(¬Native.mode,¬Native.route,1000);notNative.unit.Tick(¬Native.mode,¬Native.route,2000);assert(notNative.unit.RouteIndex==0&¬Native.group.adds==1);
|
||||
Case supply;supply.unit.SupplyToken=100001;supply.unit.Tick(&supply.mode,&supply.route,1000);assert(supply.group.adds==1); // reservation alone does not deliver hold
|
||||
supply.unit.SupplyHeld=true;supply.unit.ClearOrder();supply.group.within=true;supply.unit.Tick(&supply.mode,&supply.route,700000);assert(supply.unit.RouteIndex==0&&!supply.unit.Released&&supply.group.adds==1);
|
||||
supply.unit.SupplyToken=0;supply.unit.SupplyHeld=false;supply.group.within=false;supply.unit.Tick(&supply.mode,&supply.route,701000);assert(supply.group.adds==2&&supply.unit.RouteIndex==0);
|
||||
supply.unit.Release(&supply.mode);assert(supply.unit.SupplyToken==0&&!supply.unit.SupplyHeld);
|
||||
Case held;held.unit.Tick(&held.mode,&held.route,1000);held.unit.Combat->Held=true;held.unit.ClearOrder();held.group.within=true;assert(!held.unit.Order(&held.route,2000)&&held.group.adds==1);
|
||||
held.unit.Tick(&held.mode,&held.route,700000);assert(held.unit.RouteIndex==0&&held.group.adds==1&&!held.unit.Released&&!held.unit.Waypoint);
|
||||
held.driver.alive=false;held.unit.Tick(&held.mode,&held.route,700001);assert(held.unit.Released&&!held.unit.Combat->Held);
|
||||
Case timeout;timeout.unit.Tick(&timeout.mode,&timeout.route,1000);timeout.unit.Tick(&timeout.mode,&timeout.route,601000);assert(timeout.unit.Released&&!timeout.unit.Waypoint&&timeout.mode.command.unregistrations==1&&!timeout.mode.ledger.record.Pinned);
|
||||
timeout.unit.Release(&timeout.mode);timeout.unit.Tick(&timeout.mode,&timeout.route,999999);assert(timeout.mode.command.unregistrations==1&&timeout.group.adds==1&&timeout.group.lod==-1&&timeout.agent.lod==-1);
|
||||
for(int reason=0;reason<6;reason++){
|
||||
Case lost;lost.unit.Tick(&lost.mode,&lost.route,1000);
|
||||
if(reason==0)lost.driver.alive=false;
|
||||
if(reason==1)lost.driver.player=true;
|
||||
if(reason==2)lost.driver.vehicle=nullptr;
|
||||
if(reason==3)lost.group.faction=2;
|
||||
if(reason==4)lost.vehicle.alive=false;
|
||||
if(reason==5)lost.driver.group=nullptr;
|
||||
lost.unit.Tick(&lost.mode,&lost.route,2000);assert(lost.unit.Released&&!lost.unit.Waypoint&&lost.mode.command.unregistrations==1);
|
||||
assert(lost.unit.Entity==&lost.vehicle&&lost.unit.Group==&lost.group&&lost.unit.Members[0]==&lost.driver);
|
||||
}
|
||||
Case failed;ERR_Actors::fail=true;failed.unit.Tick(&failed.mode,&failed.route,1000);assert(failed.unit.Released&&failed.group.adds==0);ERR_Actors::fail=false;
|
||||
}
|
||||
''')
|
||||
print('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')
|
||||
Reference in New Issue
Block a user