Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_NativeMortarCourierProbe.c
T

214 lines
12 KiB
C

#ifdef WORKBENCH
class ERR_NativeCourierMoveCallback : ScriptedInventoryOperationCallback
{
int Result;
protected override void OnComplete() { if (Result == 0) Result = 1; }
protected override void OnFailed() { if (Result == 0) Result = -1; }
}
modded class ERR_HQ
{
override bool QueueSupplyStatus(ERR_MortarBattery battery, float now)
{
bool queued = super.QueueSupplyStatus(battery, now);
ERR_NativeMortarCourierProbe probe = ERR_NativeMortarCourierProbe.Current();
if (queued && probe)
foreach (ERR_HQUnit unit : m_Units)
if (unit.Group == battery.Group && unit.PendingSupplyStatus) probe.Uplink(unit.Uplink, unit.PendingSupplyStatus);
return queued;
}
}
class ERR_NativeMortarCourierProbe
{
protected static ERR_NativeMortarCourierProbe s_Current;
protected ERR_GameMode m_Mode;
protected ERR_MortarLogistics m_Service;
protected ERR_MortarBattery m_Battery;
protected ref array<IEntity> m_Original = {};
protected ref array<IEntity> m_Crew = {};
protected ref array<IEntity> m_Depot = {};
protected ref array<IEntity> m_Forces = {};
protected ref array<IEntity> m_Boxes = {};
protected ref array<BaseInventoryStorageComponent> m_Stores = {};
protected ref ERR_NativeCourierMoveCallback m_Callback;
protected IEntity m_Moving;
protected IEntity m_Source;
protected BaseInventoryStorageComponent m_Destination;
protected ref ERR_HQRoute m_Uplink;
protected ref ERR_HQRoute m_Downlink;
protected ref ERR_MortarSupplyStatus m_Status;
protected float m_ObservedAt;
protected int m_Message;
protected int m_Index;
protected int m_Stage;
protected float m_Deadline;
protected float m_MoveDeadline;
protected bool m_Complete;
protected bool m_Stopped;
protected vector m_Departure;
protected IEntity m_Bearer;
protected AIWaypointCycle m_OriginalPatrolCycle;
protected ref array<AIWaypoint> m_OriginalPatrolMoves = {};
static const int STORAGE_CANDIDATES = 64;
static const int STORAGE_COUNT = 5;
// The compact 8x8 grid keeps 3m between centers; blocked points are skipped.
static vector StorageOffset(int index)
{
if (index < 0 || index >= STORAGE_CANDIDATES) return vector.Zero;
int column = Math.Mod(index, 8);
int row = index / 8;
return Vector(20 + column * 3, 0, 15 + row * 3);
}
static const ResourceName BOX = "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et";
static ERR_NativeMortarCourierProbe Current() { return s_Current; }
static bool Enabled()
{
string smoke, outcome;
return Replication.IsServer() && System.GetCLIParam("errSmoke", smoke) && smoke == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mortar-courier";
}
void ERR_NativeMortarCourierProbe(ERR_GameMode mode) { m_Mode = mode; if (Enabled()) s_Current = this; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: mortar courier " + detail); return true; }
Print("ERR SMOKE FAIL: mortar courier " + detail, LogLevel.ERROR); Stop(); return false;
}
protected bool Has(IEntity owner, IEntity shell)
{
if (!owner || !shell) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(shell);
}
void Uplink(ERR_HQRoute route, ERR_MortarSupplyStatus report)
{
if (m_Stopped || !report || !m_Battery || report.Group != m_Battery.Group || m_Status) return;
m_Uplink = route; m_Status = report; m_ObservedAt = report.ObservedAt; m_Message = report.MessageId;
}
protected bool OriginalStored()
{
foreach (IEntity shell : m_Original)
{
if (!shell) return false;
int owners;
foreach (BaseInventoryStorageComponent storage : m_Stores) if (storage && storage.Contains(shell)) owners++;
foreach (IEntity actor : m_Crew) if (Has(actor, shell)) owners++;
if (owners != 1) return false;
}
return true;
}
protected bool PreservedForces(ERR_HQ hq)
{
array<IEntity> forces = {}; hq.PhysicalForces(forces);
if (forces.Count() != m_Forces.Count()) return false;
foreach (IEntity original : m_Forces) if (!original || !forces.Contains(original)) return false;
return true;
}
int Tick(float now)
{
if (m_Complete) return 1;
if (m_Stopped || !Enabled()) return -1;
if (m_Deadline == 0) m_Deadline = now + 360000;
if (now > m_Deadline) { Check(false, "bounded native courier deadline"); return -1; }
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return 0;
m_Service = hq.Logistics(); if (!m_Service) return 0;
m_Battery = m_Service.Battery();
if (m_Service.Stopped()) { Check(false, "production finite stock setup remains available"); return -1; }
if (!m_Battery || !m_Battery.StartupActivated || m_Service.State() == 0) return 0;
// No fire-controller hold, synthetic contact or routing bypass: this fixture
// requires the actual local prisoner to remain inside the gun's minimum range.
PlayerController controller = GetGame().GetPlayerController();
IEntity player; if (controller) player = controller.GetControlledEntity();
if (!player || !m_Mode.Players().CurrentActor(player) || vector.DistanceXZ(player.GetOrigin(), m_Battery.Gun.GetOrigin()) >= 300) { Check(false, "actual local prisoner remains below artillery minimum engagement range"); return -1; }
if (m_Stage == 0)
{
m_Service.InitialStock(m_Depot); hq.PhysicalForces(m_Forces);
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(m_Mode.FindComponent(ERR_ScenarioComponent));
ERR_SiteRuntime site = scenario.Sites().Find("everon-supply-2");
if (!Check(site && site.Group == m_Service.Group() && site.PatrolCycle, "same original western cache patrol")) return -1;
m_OriginalPatrolCycle = site.PatrolCycle; m_OriginalPatrolMoves.Copy(site.PatrolWaypoints);
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity(); m_Crew.Insert(actor);
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items) if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL && !m_Original.Contains(item)) m_Original.Insert(item);
}
if (!Check(m_Original.Count() == 27 && m_Depot.Count() == 12 && m_Battery.TotalShots == 0, "27 original crew rounds and 12 distinct finite depot rounds")) return -1;
foreach (IEntity depotShell : m_Depot) if (!Check(!m_Original.Contains(depotShell) && Has(m_Service.Cache(), depotShell), "depot round is a distinct original cache item")) return -1;
int tested, lowGround, obstructed;
for (int i = 0; i < STORAGE_CANDIDATES && m_Stores.Count() < STORAGE_COUNT; i++)
{
tested++;
vector position = ERR_Actors.Ground(m_Battery.Gun.GetOrigin() + StorageOffset(i));
if (position[1] <= 1) { lowGround++; continue; }
if (!SCR_WorldTools.TraceCilinderUtil(position + "0 1 0", 1.3, 2, TraceFlags.ENTS | TraceFlags.WORLD)) { obstructed++; continue; }
IEntity box = ERR_Actors.Spawn(BOX, position);
if (!Check(box != null, "fixture-only empty box created; no new ammunition")) return -1;
m_Mode.Own(box); m_Boxes.Insert(box);
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(box.FindComponent(BaseInventoryStorageComponent));
if (!Check(storage != null, "fixture box has real native storage")) return -1;
m_Stores.Insert(storage);
}
Print(string.Format("ERR COURIER STORAGE: tested=%1/64 ready=%2/5 lowGround=%3 obstructed=%4", tested, m_Stores.Count(), lowGround, obstructed));
if (!Check(m_Boxes.Count() == STORAGE_COUNT && m_Stores.Count() == STORAGE_COUNT, "five owned empty native stores ready before moving any original shell")) return -1;
Print("ERR COURIER FIXTURE: original 27 shells will be physically sequestered in five fixture boxes, not fired/destroyed; no report insertion, teleport, AI hold or replacement ammunition");
m_Stage = 1;
}
if (m_Stage == 1)
{
if (m_Callback)
{
if (m_Callback.Result < 0 || now >= m_MoveDeadline || !m_Moving) { Check(false, "native depletion move neither failed nor timed out/item lost"); return -1; }
InventoryItemComponent item = InventoryItemComponent.Cast(m_Moving.FindComponent(InventoryItemComponent));
if (m_Callback.Result == 0 || !item || !item.GetParentSlot() || item.GetParentSlot().GetStorage() != m_Destination || Has(m_Source, m_Moving) || !m_Destination.Contains(m_Moving)) return 0;
m_Callback = null; m_Moving = null; m_Source = null; m_Destination = null; m_Index++;
}
if (m_Index < m_Original.Count())
{
m_Moving = m_Original[m_Index];
foreach (IEntity owner : m_Crew) if (Has(owner, m_Moving)) m_Source = owner;
if (!Check(m_Source != null, "original shell still belongs to original crew before depletion")) return -1;
foreach (BaseInventoryStorageComponent candidate : m_Stores) if (candidate.PerformVolumeValidation(m_Moving)) { m_Destination = candidate; break; }
if (!Check(m_Destination != null, "five native boxes have capacity for original shells")) return -1;
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(m_Source.FindComponent(InventoryStorageManagerComponent));
m_Callback = new ERR_NativeCourierMoveCallback(); m_MoveDeadline = now + 8000;
if (!Check(source.TryMoveItemToStorage(m_Moving, m_Destination, -1, m_Callback), "native depletion move accepted, awaiting exact ownership proof")) return -1;
return 0;
}
if (!Check(OriginalStored() && m_Battery.CountShells() == 0, "all 27 original shells remain physical while real battery inventory is exhausted")) return -1;
m_Stage = 2;
}
if (!OriginalStored() || !PreservedForces(hq)) { Check(false, "all original shells and all original physical forces preserved"); return -1; }
if (!m_Downlink && m_Service.ProbeOrder()) m_Downlink = m_Service.ProbeOrder();
if (!m_Bearer && m_Service.Courier()) { m_Bearer = m_Service.Courier(); m_Departure = m_Bearer.GetOrigin(); }
if (m_Service.Delivered() < 1 || m_Service.CompletedTrips() < 1 || m_Service.State() != 1) return 0;
AIWaypoint current = m_Service.Group().GetCurrentWaypoint();
if (!current || (current != m_OriginalPatrolCycle && !m_OriginalPatrolMoves.Contains(current))) return 0;
if (!Check(m_Service.CompletedTrips() == 1 && hq.LogisticsToken(m_Service.Group()) == 0, "complete physical trip released HQ reservation and restored original native patrol")) return -1;
if (!Check(m_Status && m_Service.ProbeSourceMessage() == m_Message && m_Status.MessageId == m_Message && m_Status.ObservedAt == m_ObservedAt && ERR_MortarLogisticsRules.Fresh(m_ObservedAt, m_Service.ProbeLastDelivery()), "same immutable originally delivered supply status authorizes delivery")) return -1;
if (!Check(m_Uplink && !m_Uplink.Failed && !m_Uplink.Sending && m_Uplink.Cursor >= m_Uplink.Nodes.Count(), "actual low-ammo radio uplink reached HQ")) return -1;
if (!Check(m_Downlink && !m_Downlink.Failed && !m_Downlink.Sending && m_Downlink.Cursor == m_Downlink.Nodes.Count() - 1, "actual courier downlink completed before item delivery")) return -1;
if (!Check(m_Bearer && ERR_Actors.Group(m_Bearer) == m_Service.Group() && vector.DistanceXZ(m_Departure, m_Bearer.GetOrigin()) > 100, "same original bearer physically walked more than 100m")) return -1;
bool found; foreach (IEntity deliveredShell : m_Depot) foreach (IEntity recipient : m_Crew) if (Has(recipient, deliveredShell)) found = true;
if (!Check(found && m_Battery.CountShells() > 0 && m_Battery.TotalShots == 0, "previously empty battery now owns an exact original depot shell; no synthetic rounds or shots")) return -1;
foreach (IEntity retained : m_Depot)
{
int owners;
if (Has(m_Service.Cache(), retained)) owners++;
if (Has(m_Bearer, retained)) owners++;
foreach (IEntity crew : m_Crew) if (Has(crew, retained)) owners++;
if (!Check(retained && owners == 1, "each of 12 initial depot shells has exactly one physical owner")) return -1;
}
m_Complete = true; Stop(); return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true; if (s_Current == this) s_Current = null;
// Boxes with borrowed originals remain owned by normal mission cleanup.
// No manual restoration/deletion can race the production/native moves.
}
}
#endif