Harden base progression and cleanup; validate finite physical resupply

This commit is contained in:
2026-09-19 10:39:39 +02:00
parent 40a474934c
commit 7c53b4bb81
377 changed files with 51882 additions and 92 deletions
+58 -6
View File
@@ -12,10 +12,12 @@ class ERR_HQUnit
ref ERR_HQReport Assignment;
AIWaypoint Waypoint;
bool ReservedForSupport;
int LogisticsReservation;
ref ERR_MortarSupplyStatus PendingSupplyStatus;
bool Eligible(ERR_GameMode mode)
{
if (ReservedForSupport || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
if (ReservedForSupport || LogisticsReservation > 0 || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
array<AIAgent> agents = {};
Group.GetAgents(agents);
bool survivor;
@@ -46,6 +48,7 @@ class ERR_HQ : ERR_CommandService
{
protected ERR_GameMode m_Mode;
protected ref ERR_MortarSystem m_Mortars;
protected ref ERR_MortarLogistics m_Logistics;
protected ref array<ref ERR_HQUnit> m_Units = {};
protected ref array<ref ERR_HQReport> m_Contacts = {};
protected ref ERR_HQNetwork m_Network = new ERR_HQNetwork();
@@ -54,7 +57,7 @@ class ERR_HQ : ERR_CommandService
protected float m_NextNetwork;
protected float m_NextAssignment;
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); }
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); m_Logistics = new ERR_MortarLogistics(this, mode); }
static ERR_HQ Get()
{
ERR_GameMode mode = ERR_GameMode.Get();
@@ -72,7 +75,8 @@ class ERR_HQ : ERR_CommandService
if (hq) hq.Journal().Write(kind, message, messageId);
}
ERR_MortarSystem Mortars() { return m_Mortars; }
bool PrepareSupport() { return m_Mortars.Prepare(); }
bool PrepareSupport() { return m_Mortars.Prepare() && m_Logistics.Prepare(); }
ERR_MortarLogistics Logistics() { return m_Logistics; }
void ReserveGroup(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units)
@@ -82,6 +86,48 @@ class ERR_HQ : ERR_CommandService
unit.ReservedForSupport = true;
}
}
// Temporary reservations never alter the permanent support reservation.
int LogisticsToken(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.LogisticsReservation;
return 0;
}
bool ReserveLogistics(SCR_AIGroup group, int token)
{
if (!Replication.IsServer() || token <= 0 || !MissionActive()) return false;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group || !unit.Eligible(m_Mode) || !unit.Radio || unit.Assignment || unit.PendingOrder || unit.Downlink || unit.Waypoint) continue;
if (unit.Utility.m_Perception && !unit.Utility.m_Perception.m_aTargets.IsEmpty()) continue;
unit.LogisticsReservation = token;
return true;
}
return false;
}
void ReleaseLogistics(SCR_AIGroup group, int token)
{
foreach (ERR_HQUnit unit : m_Units)
if (unit.Group == group && unit.LogisticsReservation == token) unit.LogisticsReservation = 0;
}
bool QueueSupplyStatus(ERR_MortarBattery battery, float now)
{
if (!Replication.IsServer() || !MissionActive() || !battery || !battery.StartupActivated || !battery.Operational() || !m_Logistics.CanSample(battery) || !ERR_MortarLogisticsRules.Need(battery.CountShells())) return false;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != battery.Group || unit.Uplink || !unit.Radio || unit.Radio != battery.RadioOperator) continue;
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route) return false;
ERR_MortarSupplyStatus report = new ERR_MortarSupplyStatus();
report.Group = battery.Group; report.Gun = battery.Gun; report.Position = battery.Gun.GetOrigin();
report.Shells = battery.CountShells(); report.ObservedAt = now; report.MessageId = m_Journal.Message();
route.MessageId = report.MessageId;
route.Payload = string.Format("mortar supply status; usable HE=%1; position=%2; sampled=%3", report.Shells, report.Position, report.ObservedAt);
unit.PendingSupplyStatus = report; unit.Uplink = route;
m_Journal.Write("SUPPLY REPORT", route.Payload, route.MessageId);
return true;
}
return false;
}
void PhysicalForces(out array<IEntity> entities)
{
entities.Clear();
@@ -131,7 +177,7 @@ class ERR_HQ : ERR_CommandService
ERR_HQJournal Journal() { return m_Journal; }
string Status()
{
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + "; " + m_Mortars.Status();
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + "; " + m_Mortars.Status() + "; " + m_Logistics.Status();
}
override void RegisterGroup(SCR_AIGroup group)
@@ -304,8 +350,12 @@ class ERR_HQ : ERR_CommandService
if (unit.Uplink)
{
int delivered = unit.Uplink.Advance(now, m_Journal);
if (delivered == 1) Accept(unit.PendingReport, now);
if (delivered != 0) { unit.Uplink = null; unit.PendingReport = null; }
if (delivered == 1)
{
if (unit.PendingSupplyStatus) m_Logistics.Receive(unit.PendingSupplyStatus, now);
else Accept(unit.PendingReport, now);
}
if (delivered != 0) { unit.Uplink = null; unit.PendingReport = null; unit.PendingSupplyStatus = null; }
}
if (unit.Downlink)
{
@@ -317,12 +367,14 @@ class ERR_HQ : ERR_CommandService
Observe(unit, now);
}
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
m_Logistics.Tick(now);
m_Mortars.Tick(now);
m_Journal.Flush();
}
override void Stop()
{
m_Logistics.Stop();
m_Mortars.Stop();
foreach (ERR_HQUnit unit : m_Units) unit.Release();
m_Units.Clear();
@@ -0,0 +1,68 @@
class ERR_MortarCourierCallback : ScriptedInventoryOperationCallback
{
ERR_MortarCourierTransfer Transfer;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
}
// One exact native move belonging to one courier trip. Never retries ambiguous work.
class ERR_MortarCourierTransfer
{
protected ERR_MortarLogistics m_Service;
protected IEntity m_Shell;
protected IEntity m_From;
protected IEntity m_To;
protected InventoryStorageManagerComponent m_Source;
protected InventoryStorageManagerComponent m_Destination;
protected BaseInventoryStorageComponent m_Storage;
protected ref ERR_MortarCourierCallback m_Callback;
protected int m_Trip;
protected int m_Acknowledgement;
protected float m_Deadline;
protected bool m_Accepted;
protected bool m_Invalidated;
protected bool m_Resolved;
void ERR_MortarCourierTransfer(ERR_MortarLogistics service, int trip, IEntity shell, IEntity from, IEntity to, BaseInventoryStorageComponent storage, float now)
{
m_Service = service; m_Trip = trip; m_Shell = shell; m_From = from; m_To = to; m_Storage = storage; m_Deadline = now + 8000;
m_Source = InventoryStorageManagerComponent.Cast(from.FindComponent(InventoryStorageManagerComponent));
m_Destination = InventoryStorageManagerComponent.Cast(to.FindComponent(InventoryStorageManagerComponent));
}
static bool Proven(bool complete, bool source, bool destination, bool exact) { return complete && !source && destination && exact; }
protected bool Contains(InventoryStorageManagerComponent inventory)
{
if (!inventory || !m_Shell) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(m_Shell);
}
void Acknowledge(bool success)
{
if (m_Acknowledgement != 0) return;
if (success) m_Acknowledgement = 1;
else m_Acknowledgement = -1;
}
bool Begin(float now)
{
if (m_Callback || !Replication.IsServer() || !m_Service.TransferAllowed(m_Trip, m_Shell, m_From, m_To, now) || !Contains(m_Source) || Contains(m_Destination)) return false;
m_Callback = new ERR_MortarCourierCallback(); m_Callback.Transfer = this;
m_Accepted = m_Source.TryMoveItemToStorage(m_Shell, m_Storage, -1, m_Callback);
if (!m_Accepted) { Acknowledge(false); m_Resolved = true; }
return m_Accepted;
}
void Invalidate() { m_Invalidated = true; }
bool Unresolved() { return m_Accepted && !m_Resolved; }
IEntity Shell() { return m_Shell; }
int Poll(float now)
{
// A deleted exact entity cannot complete later; close as a permanent loss.
if (!m_Shell) { m_Resolved = true; m_Invalidated = true; return -1; }
bool exact;
InventoryItemComponent item;
if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));
if (item && item.GetParentSlot()) exact = item.GetParentSlot().GetStorage() == m_Storage;
bool proven = Proven(m_Acknowledgement == 1, Contains(m_Source), Contains(m_Destination), exact);
if (proven || m_Acknowledgement < 0) m_Resolved = true;
if (m_Invalidated || now >= m_Deadline || !m_Service.TransferAllowed(m_Trip, m_Shell, m_From, m_To, now)) { m_Invalidated = true; return -1; }
if (proven) return 1;
if (m_Acknowledgement < 0 || !m_Accepted) return -1;
return 0;
}
}
@@ -0,0 +1,347 @@
class ERR_MortarSupplyStatus
{
SCR_AIGroup Group;
IEntity Gun;
vector Position;
float ObservedAt;
int MessageId;
int Shells;
}
class ERR_MortarLogisticsRules
{
static bool Fresh(float observedAt, float now) { return observedAt >= 0 && now >= observedAt && now - observedAt <= 180000; }
static bool Need(int shells) { return shells >= 0 && shells <= 6; }
static bool NewRequest(int message, int consumed, bool fresh) { return fresh && message > 0 && message > consumed; }
static bool Near(float distance) { return distance >= 0 && distance <= 12; }
static bool ActiveTrip(bool server, bool stopped, int trip, int current, bool fresh, bool ownershipValid) { return server && !stopped && trip > 0 && trip == current && fresh && ownershipValid; }
}
// One finite western-cache courier, owned and ticked exclusively by HQ.
class ERR_MortarLogistics
{
protected ERR_HQ m_HQ;
protected ERR_GameMode m_Mode;
protected ERR_SiteRuntime m_Site;
protected IEntity m_Cache;
protected SCR_AIGroup m_Group;
protected ERR_MortarBattery m_Battery;
protected IEntity m_OriginalGun;
protected SCR_AIGroup m_OriginalBatteryGroup;
protected ref array<IEntity> m_BatteryMembers = {};
protected ref array<IEntity> m_OriginalMembers = {};
protected ref array<IEntity> m_Stock = {};
protected ref array<IEntity> m_Cargo = {};
protected ref array<IEntity> m_AttemptedPickup = {};
protected ref array<IEntity> m_AttemptedUnload = {};
protected ref array<ref ERR_MortarInitialItem> m_Initial = {};
protected ref ERR_MortarSupplyStatus m_Report;
protected ref ERR_HQRoute m_Order;
protected ref ERR_MortarCourierTransfer m_Move;
protected IEntity m_Courier;
protected AIWaypoint m_Waypoint;
protected AIWaypoint m_HoldWaypoint;
protected float m_InitialDeadline;
protected float m_NextReport;
protected float m_LastDeliveryAt = -1;
protected int m_ConsumedMessage;
protected int m_Trip;
protected int m_State; // 0 initial, 1 idle, 2 downlink, 3 pickup, 4 travel, 5 unload
protected int m_Delivered;
protected int m_CompletedTrips;
protected bool m_Started;
protected bool m_Stopped;
protected string m_Reason;
static const int INITIAL_SHELLS = 12;
static const int TRIP_SHELLS = 6;
void ERR_MortarLogistics(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
int State() { return m_State; }
int Trip() { return m_Trip; }
int Delivered() { return m_Delivered; }
int CompletedTrips() { return m_CompletedTrips; }
IEntity Cache() { return m_Cache; }
SCR_AIGroup Group() { return m_Group; }
IEntity Courier() { return m_Courier; }
ERR_MortarBattery Battery() { return m_Battery; }
ERR_MortarSupplyStatus Report() { return m_Report; }
bool Stopped() { return m_Stopped; }
void InitialStock(out array<IEntity> items) { items.Copy(m_Stock); }
protected void Audit(string kind, string reason)
{
int message; if (m_Report) message = m_Report.MessageId;
m_Reason = reason;
m_HQ.Journal().Write("COURIER " + kind, string.Format("trip=%1 state=%2 delivered=%3; %4", m_Trip, m_State, m_Delivered, reason), message);
}
protected InventoryStorageManagerComponent Inventory(IEntity entity)
{
if (!entity) return null;
return InventoryStorageManagerComponent.Cast(entity.FindComponent(InventoryStorageManagerComponent));
}
protected bool Contains(IEntity owner, IEntity shell)
{
InventoryStorageManagerComponent inventory = Inventory(owner);
if (!inventory || !shell) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items); return items.Contains(shell);
}
protected void Shells(IEntity owner, out array<IEntity> shells)
{
shells.Clear(); InventoryStorageManagerComponent inventory = Inventory(owner); if (!inventory) return;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) shells.Insert(item);
}
bool Prepare()
{
if (!Replication.IsServer() || m_Started || m_Stopped) return false;
m_Started = true;
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(m_Mode.FindComponent(ERR_ScenarioComponent));
if (!scenario || !scenario.Sites()) return false;
m_Site = scenario.Sites().Find("everon-supply-2");
if (!m_Site || !m_Site.Group || !m_Site.PatrolCycle || !scenario.Sites().Owns(m_Site.Container)) return false;
m_Cache = m_Site.Container; m_Group = m_Site.Group;
array<ref ERR_MortarBattery> batteries = {}; m_HQ.Mortars().Batteries(batteries);
if (batteries.Count() != 1) return false;
m_Battery = batteries[0]; m_OriginalGun = m_Battery.Gun; m_OriginalBatteryGroup = m_Battery.Group;
InventoryStorageManagerComponent inventory = Inventory(m_Cache);
if (!inventory || !inventory.CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT)) return false;
array<IEntity> existing = {}; Shells(m_Cache, existing); if (!existing.IsEmpty()) return false;
array<AIAgent> agents = {}; m_Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
m_OriginalMembers.Insert(agent.GetControlledEntity());
}
if (m_OriginalMembers.Count() != m_Site.InitialGuardCount) return false;
m_InitialDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
// SiteService owns this existing cache and its contents through rollback/world teardown.
// This bounded startup allocation is never reopened, including partial failure/theft.
for (int i = 0; i < INITIAL_SHELLS; i++)
{
IEntity shell = ERR_Actors.Spawn(ERR_MortarBattery.SHELL, m_Cache.GetOrigin());
if (!shell) return false;
// Keep the exact created identity; counts cannot prove an external item was
// not substituted while an asynchronous insertion was pending.
m_Mode.Own(shell); m_Stock.Insert(shell);
ERR_MortarInitialItem operation = new ERR_MortarInitialItem(); m_Initial.Insert(operation);
if (!inventory.TryInsertItem(shell, EStoragePurpose.PURPOSE_DEPOSIT, operation)) { operation.Rejected(); return false; }
}
return true;
}
protected bool OriginalGroup()
{
if (!m_Site || m_Site.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
array<AIAgent> agents = {}; m_Group.GetAgents(agents); bool living;
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity();
if (!m_OriginalMembers.Contains(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Group || SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction()) return false;
if (ERR_Actors.CanFight(actor)) living = true;
}
return living;
}
protected bool BindBatteryMembers()
{
if (!m_Battery || !m_Battery.StartupActivated || m_Battery.Group != m_OriginalBatteryGroup || m_Battery.Gun != m_OriginalGun) return false;
if (!m_BatteryMembers.IsEmpty()) return true;
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
if (agent && agent.GetControlledEntity() && !m_BatteryMembers.Contains(agent.GetControlledEntity())) m_BatteryMembers.Insert(agent.GetControlledEntity());
return !m_BatteryMembers.IsEmpty();
}
bool TargetAvailable()
{
if (!m_Battery || m_Battery.Gun != m_OriginalGun || m_Battery.Group != m_OriginalBatteryGroup || !m_OriginalGun || !m_Battery.StartupActivated || m_Battery.Stopped() || !m_Battery.Operational() || m_HQ.Mortars().FindGun(m_OriginalGun) != m_Battery || m_BatteryMembers.IsEmpty()) return false;
if (SCR_FactionManager.SGetFaction(m_OriginalGun) != m_Mode.EnemyFaction()) return false;
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_OriginalGun.FindComponent(SCR_AIVehicleUsageComponent));
if (!usage || !usage.GetTurretCompartmentSlot()) return false;
IEntity occupant = usage.GetTurretCompartmentSlot().GetOccupant();
if (!occupant) return true;
return m_BatteryMembers.Contains(occupant) && ERR_Actors.CanFight(occupant) && !ERR_Actors.Player(occupant) && ERR_Actors.Group(occupant) == m_OriginalBatteryGroup && SCR_FactionManager.SGetFaction(occupant) == m_Mode.EnemyFaction();
}
bool CanSample(ERR_MortarBattery battery) { return battery == m_Battery && TargetAvailable(); }
protected bool Bearer(IEntity actor)
{
return actor && m_OriginalMembers.Contains(actor) && ERR_Actors.Group(actor) == m_Group && ERR_Actors.CanFight(actor) && !ERR_Actors.Player(actor) && !CompartmentAccessComponent.GetVehicleIn(actor) && SCR_FactionManager.SGetFaction(actor) == m_Mode.EnemyFaction() && ERR_HQRadio.Capability(actor) != 2;
}
bool AcceptsStatus(ERR_MortarSupplyStatus report, float now)
{
return !m_Stopped && m_State == 1 && report && m_Battery && report.Group == m_Battery.Group && report.Gun == m_Battery.Gun && ERR_MortarLogisticsRules.Need(report.Shells) && ERR_MortarLogisticsRules.NewRequest(report.MessageId, m_ConsumedMessage, ERR_MortarLogisticsRules.Fresh(report.ObservedAt, now));
}
void Receive(ERR_MortarSupplyStatus report, float now)
{
if (!AcceptsStatus(report, now)) return;
m_Report = report;
Audit("RECEIVED", "immutable low-ammunition status delivered by native physical radio route");
}
protected bool Authorized(float now)
{
if (!m_Mode || !m_Mode.Active() || !m_Report || !m_Battery) return false;
return ERR_MortarLogisticsRules.ActiveTrip(Replication.IsServer(), m_Stopped, m_Trip, m_HQ.LogisticsToken(m_Group), ERR_MortarLogisticsRules.Fresh(m_Report.ObservedAt, now), OriginalGroup()) && TargetAvailable() && m_Report.Gun == m_Battery.Gun && m_Report.Group == m_Battery.Group && vector.Distance(m_Report.Position, m_Battery.Gun.GetOrigin()) <= 5;
}
bool TransferAllowed(int trip, IEntity shell, IEntity from, IEntity to, float now)
{
if (trip != m_Trip || !Authorized(now) || !shell || !m_Stock.Contains(shell) || !Bearer(m_Courier)) return false;
if (m_State == 3) return from == m_Cache && to == m_Courier && ERR_MortarLogisticsRules.Near(vector.Distance(from.GetOrigin(), to.GetOrigin()));
if (m_State != 5 || from != m_Courier || !m_Cargo.Contains(shell) || !ERR_Actors.CanFight(to) || ERR_Actors.Player(to) || to == m_Battery.RadioOperator || !m_BatteryMembers.Contains(to) || ERR_Actors.Group(to) != m_Battery.Group || SCR_FactionManager.SGetFaction(to) != m_Mode.EnemyFaction()) return false;
return !CompartmentAccessComponent.GetVehicleIn(to) && ERR_MortarLogisticsRules.Near(vector.Distance(from.GetOrigin(), to.GetOrigin()));
}
protected void ClearWaypoint()
{
if (m_Waypoint) { if (m_Group) m_Group.RemoveWaypoint(m_Waypoint); ERR_Actors.DeleteSpawned(m_Waypoint); }
m_Waypoint = null;
if (m_HoldWaypoint) { if (m_Group) m_Group.RemoveWaypoint(m_HoldWaypoint); ERR_Actors.DeleteSpawned(m_HoldWaypoint); }
m_HoldWaypoint = null;
}
protected bool MoveTo(vector position)
{
ClearWaypoint();
m_Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), ERR_Actors.Ground(position)));
if (!m_Waypoint) return false;
// A completed Move alone would let the original patrol resume during the
// asynchronous item operation. This owned, non-completing native Defend
// keeps that queue below us until release; its original entries are untouched.
m_HoldWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), ERR_Actors.Ground(position)));
if (!m_HoldWaypoint) { ClearWaypoint(); return false; }
m_HoldWaypoint.SetCompletionRadius(8);
m_Group.AddWaypointAt(m_HoldWaypoint, 0); m_Group.AddWaypointAt(m_Waypoint, 0); m_Group.ActivateAI(); return true;
}
protected void Finish(string reason)
{
Audit("END", reason);
if (m_Move) m_Move.Invalidate();
ClearWaypoint(); m_HQ.ReleaseLogistics(m_Group, m_Trip);
m_Order = null; m_Report = null; m_Courier = null; m_Cargo.Clear(); m_State = 1;
// Native requests cannot be canceled. A pending move remains held until its
// physical outcome resolves; callbacks cannot authorize a later trip.
m_NextReport = GetGame().GetWorld().GetWorldTime() + 60000;
}
protected bool BeginMove(IEntity shell, IEntity from, IEntity to, float now)
{
InventoryStorageManagerComponent destination = Inventory(to);
BaseInventoryStorageComponent storage; if (destination) storage = destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT);
if (!storage) return false;
m_Move = new ERR_MortarCourierTransfer(this, m_Trip, shell, from, to, storage, now);
return m_Move.Begin(now);
}
protected bool PollMove(float now)
{
if (!m_Move) return true;
int result = m_Move.Poll(now);
if (result == 0) return false;
if (result < 0) { Finish("native item operation rejected, expired or lost authority; physical items retained"); return false; }
IEntity shell = m_Move.Shell(); m_Move = null;
if (m_State == 3) { m_Cargo.Insert(shell); Audit("PICKUP", "exact depot shell confirmed in original bearer inventory"); }
else if (m_State == 5) { m_Cargo.RemoveItem(shell); m_Delivered++; m_LastDeliveryAt = now; Audit("DELIVERED", "exact original shell confirmed in living mortar crew inventory"); }
return true;
}
protected void Pickup(float now)
{
if (!m_Courier)
{
foreach (IEntity candidate : m_OriginalMembers)
if (Bearer(candidate) && Inventory(candidate) && Inventory(candidate).CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT) && ERR_MortarLogisticsRules.Near(vector.Distance(candidate.GetOrigin(), m_Cache.GetOrigin()))) { m_Courier = candidate; break; }
if (!m_Courier) return;
}
if (!Bearer(m_Courier)) { Finish("original bearer lost or captured"); return; }
if (!PollMove(now)) return;
if (m_Cargo.Count() < TRIP_SHELLS)
{
foreach (IEntity shell : m_Stock)
{
if (!shell || !Contains(m_Cache, shell) || m_AttemptedPickup.Contains(shell)) continue;
InventoryStorageManagerComponent destination = Inventory(m_Courier);
if (!destination || !destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT)) break;
m_AttemptedPickup.Insert(shell);
if (!BeginMove(shell, m_Cache, m_Courier, now)) Finish("native pickup rejected; no second request for that shell");
return;
}
}
if (m_Cargo.IsEmpty()) { Finish("finite cache empty or bearer native storage full"); return; }
m_State = 4;
if (!MoveTo(m_Report.Position)) { Finish("native delivery waypoint unavailable"); return; }
Audit("OUTBOUND", "original patrol walking with verified finite cargo to reported battery position");
}
protected void Unload(float now)
{
if (!PollMove(now)) return;
if (m_Cargo.IsEmpty()) { m_CompletedTrips++; Finish("finite delivery complete; original patrol restored"); return; }
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
foreach (IEntity shell : m_Cargo)
{
if (!shell || !Contains(m_Courier, shell)) { Finish("carried shell lost or stolen; no replacement"); return; }
if (m_AttemptedUnload.Contains(shell)) { Finish("previous unload unresolved or failed; no retry"); return; }
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity recipient = agent.GetControlledEntity();
if (!TransferAllowed(m_Trip, shell, m_Courier, recipient, now)) continue;
InventoryStorageManagerComponent destination = Inventory(recipient);
if (!destination || !destination.FindStorageForItem(shell, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
m_AttemptedUnload.Insert(shell);
if (!BeginMove(shell, m_Courier, recipient, now)) Finish("native recipient move rejected; cargo remains physical");
return;
}
}
}
void Tick(float now)
{
if (m_Stopped || !m_Started || !Replication.IsServer() || !m_Mode.Active()) return;
if (m_State == 0)
{
bool failed; foreach (ERR_MortarInitialItem operation : m_Initial) { if (!operation.Finished) { if (now < m_InitialDeadline) return; failed = true; } else if (!operation.Succeeded) failed = true; }
if (!OriginalGroup()) failed = true;
array<IEntity> shells = {}; Shells(m_Cache, shells);
if (shells.Count() != INITIAL_SHELLS) { if (now < m_InitialDeadline && !failed) return; failed = true; }
foreach (IEntity originalShell : m_Stock) if (!originalShell || !shells.Contains(originalShell)) failed = true;
if (m_Stock.Count() != INITIAL_SHELLS) failed = true;
if (failed) { Audit("STARTUP FAILED", "one-time native stock allocation incomplete; no refill"); Stop(); return; }
m_State = 1; Audit("READY", "12 distinct original HE shells physically confirmed in existing western cache");
}
if (m_State == 1)
{
if (m_Move) { m_Move.Poll(now); if (m_Move.Unresolved()) return; m_Move = null; }
if (!OriginalGroup() || !BindBatteryMembers() || !TargetAvailable()) return;
bool available; foreach (IEntity stocked : m_Stock) if (Contains(m_Cache, stocked) && !m_AttemptedPickup.Contains(stocked)) available = true;
if (!available) return;
if (now >= m_NextReport && ERR_MortarLogisticsRules.Need(m_Battery.CountShells())) { if (m_HQ.QueueSupplyStatus(m_Battery, now)) m_NextReport = now + 60000; }
if (!m_Report || !ERR_MortarLogisticsRules.NewRequest(m_Report.MessageId, m_ConsumedMessage, ERR_MortarLogisticsRules.Fresh(m_Report.ObservedAt, now))) return;
int nextTrip = m_Trip + 1;
if (!m_HQ.ReserveLogistics(m_Group, nextTrip)) return;
m_Trip = nextTrip; m_ConsumedMessage = m_Report.MessageId;
m_Order = m_HQ.Downlink(m_Group, "MORTAR SUPPLY", "collect finite western-cache HE; walk to reported battery", m_Report.MessageId);
m_State = 2;
if (!m_Order) { Finish("no native courier downlink"); return; }
Audit("QUEUED", "original patrol reserved pending physical radio command delivery");
}
if (!Authorized(now)) { Finish("supply report expired or original ownership/gun/group changed"); return; }
if (m_Courier && !Bearer(m_Courier)) { Finish("original bearer unavailable; surviving cargo retained"); return; }
if (m_State == 2)
{
int delivered = m_Order.Advance(now, m_HQ.Journal());
if (delivered == 0) return;
if (delivered < 0) { Finish("courier command interrupted"); return; }
m_Order = null; m_State = 3;
if (!MoveTo(m_Cache.GetOrigin())) { Finish("native pickup waypoint unavailable"); return; }
Audit("DISPATCH", "same original patrol receives delivered order and walks to finite cache");
}
if (m_State == 3) { Pickup(now); return; }
if (m_State == 4)
{
if (!ERR_MortarLogisticsRules.Near(vector.Distance(m_Courier.GetOrigin(), m_Report.Position))) return;
m_State = 5; Audit("ARRIVED", "physical bearer reached reported gun area; exact recipient checks still required");
}
if (m_State == 5) Unload(now);
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Move) m_Move.Invalidate();
ClearWaypoint(); m_HQ.ReleaseLogistics(m_Group, m_Trip); m_Order = null; m_Report = null;
}
#ifdef WORKBENCH
ERR_HQRoute ProbeOrder() { return m_Order; }
int ProbeSourceMessage() { return m_ConsumedMessage; }
float ProbeLastDelivery() { return m_LastDeliveryAt; }
#endif
string Status() { return string.Format("courier state=%1 finite initial=%2 delivered=%3 stopped=%4 (%5)", m_State, m_Stock.Count(), m_Delivered, m_Stopped, m_Reason); }
}
@@ -60,6 +60,11 @@ class ERR_MortarSystem
return true;
}
static ResourceName MortarGroup() { return "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"; }
void Batteries(out array<ref ERR_MortarBattery> batteries)
{
batteries.Clear();
foreach (ERR_MortarBattery battery : m_Batteries) batteries.Insert(battery);
}
ERR_MortarBattery FindGun(IEntity gun)
{
foreach (ERR_MortarBattery battery : m_Batteries)
@@ -0,0 +1,213 @@
#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
@@ -233,7 +233,11 @@ class ERR_NativeHQResponseProof
if (!m_Started || m_Stopped || hq != m_HQ || !unit.Assignment) return;
ERR_NativeResponseLineage proof = Report(unit.Assignment.MessageId); if (!proof || proof.Recipient) return;
ERR_NativeResponseUnit original = Original(unit);
if (!proof.Accepted || !original || unit == proof.Source || !unit.Assignment || unit.Assignment.Target != m_Player || unit.Assignment.Source != proof.Report.Source || unit.Assignment.ObservedAt != proof.Report.ObservedAt || vector.Distance(unit.Assignment.Position, proof.Report.Position) > 0.01 || unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || !route.Downlink || route.Nodes.IsEmpty() || route.Nodes[route.Nodes.Count() - 1].Carrier != unit.Radio || route.Failed || route.Sending || route.Cursor != route.Nodes.Count() - 1 || !unit.Waypoint) { Fail("delivered response lacks immutable accepted report and original physical recipient/downlink"); return; }
if (!proof.Accepted || !original || unit == proof.Source || !unit.Assignment || !unit.Waypoint) { Fail("delivered response lacks accepted original recipient/order"); return; }
if (unit.Assignment.Target != m_Player || unit.Assignment.Source != proof.Report.Source || unit.Assignment.ObservedAt != proof.Report.ObservedAt || vector.Distance(unit.Assignment.Position, proof.Report.Position) > 0.01) { Fail("delivered response changed immutable report"); return; }
if (unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || !route.Downlink || route.Nodes.IsEmpty()) { Fail("delivered response lacks original radio/downlink"); return; }
ERR_HQRadioNode recipientNode = route.Nodes[route.Nodes.Count() - 1];
if (recipientNode.Carrier != unit.Radio || route.Failed || route.Sending || route.Cursor != route.Nodes.Count() - 1) { Fail("delivered response lacks completed actual recipient downlink"); return; }
proof.Recipient = unit; proof.RecipientRadio = unit.Radio; proof.DeliveryId = route.MessageId; proof.Waypoint = unit.Waypoint; proof.DeliveredAt = now;
foreach (IEntity actor : original.Actors) if (ERR_Actors.CanFight(actor)) { proof.MovingActor = actor; break; }
if (proof.MovingActor) proof.DeliveredOrigin = proof.MovingActor.GetOrigin();
@@ -18,6 +18,8 @@ modded class ERR_GameMode
protected bool m_ERRTestHQResponse;
protected bool m_ERRTestMortar;
protected bool m_ERRTestMortarFire;
protected bool m_ERRTestMortarCourier;
protected ref ERR_NativeMortarCourierProbe m_ERRMortarCourier;
protected bool m_ERRTestMortarTransfer;
protected ref ERR_NativeMortarTransferProbe m_ERRMortarTransfer;
protected ref ERR_NativeMortarFiringProbe m_ERRMortarFire;
@@ -37,10 +39,12 @@ modded class ERR_GameMode
m_ERRTestHQResponse = outcome == "hq-response";
m_ERRTestMortar = outcome == "mortar-inventory";
m_ERRTestMortarFire = outcome == "mortar-fire" || outcome == "mortar-expiry";
m_ERRTestMortarCourier = outcome == "mortar-courier";
m_ERRTestMortarTransfer = outcome == "mortar-transfer";
m_ERRTestPatrolHQ = outcome == "patrol-hq";
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar;
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
if (m_ERRTestMortarTransfer) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
if (m_ERRTestMortarFire) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
if (m_ERRTestPatrolHQ) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 420000;
@@ -62,6 +66,7 @@ modded class ERR_GameMode
protected void ERR_StopHQPatrol()
{
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
if (m_ERRMortarCourier) m_ERRMortarCourier.Stop();
if (m_ERRMortarTransfer) m_ERRMortarTransfer.Stop();
if (m_ERRMortarFire) m_ERRMortarFire.Stop();
if (m_ERRHQPatrolMovement) m_ERRHQPatrolMovement.Stop();
@@ -99,6 +104,15 @@ modded class ERR_GameMode
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
{
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
if (m_ERRTestMortarCourier)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 181, "native finite courier reached an exhausted battery before explicit reset")) return;
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
ERR_StopHQPatrol();
Print("ERR SMOKE COMPLETE: native finite courier, real supply uplink/downlink, original patrol walking and exact physical shell delivery; original 27 shells sequestered in fixture boxes; reset requested");
return;
}
if (m_ERRTestMortarTransfer)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 161, "native exact shell transfers and original-owner restoration complete before explicit reset")) return;
@@ -179,6 +193,13 @@ modded class ERR_GameMode
if (m_ERRSmokeDone) return;
if (now > m_ERRSmokeDeadline) { ERR_Check(false, "timeout at step " + m_ERRSmokeStep.ToString()); return; }
if (now < m_ERRSmokeWait) return;
if (m_ERRSmokeStep == 180)
{
int result = m_ERRMortarCourier.Tick(now);
if (result < 0) { ERR_Check(false, "native finite courier fixture"); return; }
if (result == 1) { m_ERRSmokeStep = 181; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRSmokeStep == 160)
{
int result = m_ERRMortarTransfer.Tick(now);
@@ -225,6 +246,13 @@ modded class ERR_GameMode
m_ERRSmokeStep = 130;
return;
}
if (m_ERRTestMortarCourier)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRMortarCourier = new ERR_NativeMortarCourierProbe(this);
m_ERRSmokeStep = 180;
return;
}
if (m_ERRTestMortarTransfer)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
@@ -16,6 +16,15 @@ class ERR_UnlockPrisonAction : ScriptedUserAction
class ERR_PrisonDoorAction : SCR_DoorUserAction
{
// Native AI calls PerformAction directly, bypassing the UI availability query.
// Recheck the same permission here; preserve vanilla execution/broadcasting
// on both server and clients once the replicated prison phase is unlocked.
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
if (!CanBePerformedScript(pUserEntity)) return;
super.PerformAction(pOwnerEntity, pUserEntity);
}
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
@@ -137,6 +137,7 @@ class ERR_CasualtyLoadingService
protected ERR_GameMode m_Mode;
protected bool m_Stopped;
protected int m_NextNonce;
protected bool m_MaintenanceQueued;
protected ref array<ref ERR_CasualtySettling> m_Settling = {};
protected ref array<ref ERR_CasualtyLoadTicket> m_Pending = {};
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
@@ -220,6 +221,7 @@ class ERR_CasualtyLoadingService
m_Pending.Insert(ticket);
ticket.Seat.SetReserved(casualty);
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
ScheduleMaintenance();
// Preparation never moves the actor. Its owner starts a local real-time lease.
controller.ERR_PrepareCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), ticket.Nonce);
return true;
@@ -267,6 +269,7 @@ class ERR_CasualtyLoadingService
int index = m_Pending.Find(ticket); if (index < 0) return;
m_Pending.Remove(index);
if (ticket.Seat && ticket.Casualty && ticket.Seat.IsReservedBy(ticket.Casualty)) ticket.Seat.SetReserved(null);
if (m_Pending.IsEmpty()) CancelMaintenance();
m_OnResolved.Invoke(ticket.Casualty, success);
}
void Tick(float worldNow)
@@ -277,6 +280,28 @@ class ERR_CasualtyLoadingService
if (roundAction == 2) { Stop(); return; }
int now = System.GetTickCount();
Observe(now);
MaintainPending();
}
protected void ScheduleMaintenance()
{
if (m_MaintenanceQueued || m_Stopped || m_Pending.IsEmpty() || !GetGame()) return;
// Only ticket observation runs independently of player population. No owner
// request is retried and neither local nor server leases are refreshed.
m_MaintenanceQueued = true;
GetGame().GetCallqueue().CallLater(MaintainPending, 250, true);
}
protected void CancelMaintenance()
{
if (!m_MaintenanceQueued) return;
m_MaintenanceQueued = false;
if (GetGame()) GetGame().GetCallqueue().Remove(MaintainPending);
}
protected void MaintainPending()
{
if (!Replication.IsServer() || m_Stopped) { CancelMaintenance(); return; }
if (!m_Mode || ERR_MissionRules.Terminal(m_Mode.Phase())) { Stop(); return; }
if (m_Pending.IsEmpty()) { CancelMaintenance(); return; }
int now = System.GetTickCount();
array<ref ERR_CasualtyLoadTicket> snapshot = {}; foreach (ERR_CasualtyLoadTicket item : m_Pending) snapshot.Insert(item);
foreach (ERR_CasualtyLoadTicket ticket : snapshot)
{
@@ -300,6 +325,7 @@ class ERR_CasualtyLoadingService
{
if (!Replication.IsServer() || m_Stopped) return;
m_Stopped = true; m_Settling.Clear();
CancelMaintenance();
while (!m_Pending.IsEmpty()) Resolve(m_Pending[m_Pending.Count() - 1], false);
}
}
@@ -1,9 +1,24 @@
// A first clue identifies a stable search square, not the exact objective.
// Stable grid centers prevent repeated updates from triangulating the site.
class ERR_DiscoveryMapRules
{
static int Coordinate(float coordinate, int knowledge)
{
if (knowledge >= 2) return Math.Round(coordinate);
return Math.Floor(coordinate / 500.0) * 500 + 250;
}
}
// Discovery markers are authored objective information, not player annotations.
// Native local insertion overwrites owner ID before OnCreateMarker; clear it at
// that boundary so native edit/drag cannot move a discovered objective.
class ERR_DiscoveryMarker : SCR_MapMarkerBase
{
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
void SetDiscoveryPosition(vector position, int knowledge)
{
SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge));
}
override void OnCreateMarker(bool skipProfanityFilter = false)
{
SetMarkerOwnerID(-1);
@@ -11,6 +26,14 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
// overwriting a newer discovery label after the location becomes identified.
super.OnCreateMarker(true);
}
override void OnDelete()
{
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
super.OnDelete();
m_wRoot = null;
m_MarkerWidgetComp = null;
}
override void SetCustomText(string text)
{
super.SetCustomText(text);
@@ -21,11 +44,24 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
}
}
// Vanilla local deletion removes only its active list. A culled local objective
// must also leave the disabled list or map movement can resurrect it later.
modded class SCR_MapMarkerManagerComponent
{
void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker)
{
if (!marker || marker.GetMarkerID() != -1) return;
m_aStaticMarkers.RemoveItem(marker);
m_aDisabledMarkers.RemoveItem(marker);
marker.OnDelete();
}
}
// Read replicated discovery locally. A server-wide marker list never leaks
// locations which have not been recovered through gameplay.
modded class ERR_LocationComponent
{
protected ref SCR_MapMarkerBase m_ERRMarker;
protected ref ERR_DiscoveryMarker m_ERRMarker;
protected int m_ERRShown;
protected string m_ERRShownLabel;
@@ -40,7 +76,7 @@ modded class ERR_LocationComponent
if (!GetGame().GetPlayerController() || Knowledge() <= 0 || (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel)) return;
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (!manager || !manager.GetMarkerConfig()) return;
string label = "Unknown location";
string label = "Search area (500 m square)";
if (Knowledge() == 2)
{
if (!Label().IsEmpty()) label = Label();
@@ -54,13 +90,17 @@ modded class ERR_LocationComponent
{
m_ERRMarker = new ERR_DiscoveryMarker();
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
vector p = Position();
m_ERRMarker.SetWorldPos(Math.Round(p[0]), Math.Round(p[2]));
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
m_ERRMarker.SetMarkerConfigID(0);
m_ERRMarker.SetCustomText(label);
manager.InsertStaticMarker(m_ERRMarker, true);
}
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
m_ERRMarker.SetCustomText(label);
// Reactivate a previously culled rough marker if the exact point moves
// into the open map's view. Native update performs normal visibility checks.
SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance();
if (mapEntity && mapEntity.IsOpen()) manager.SetStaticMarkerDisabled(m_ERRMarker, false);
m_ERRShown = Knowledge();
m_ERRShownLabel = Label();
}
@@ -68,7 +108,11 @@ modded class ERR_LocationComponent
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateMarker);
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (manager && m_ERRMarker) manager.RemoveStaticMarker(m_ERRMarker);
if (m_ERRMarker)
{
if (manager) manager.ERR_RemoveDiscoveryMarker(m_ERRMarker);
else m_ERRMarker.OnDelete();
}
m_ERRMarker = null;
super.OnDelete(owner);
}
@@ -30,6 +30,8 @@ class ERR_DiscoveryService
}
protected void Recover(IEntity player)
{
if (!Replication.IsServer() || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return;
array<IEntity> folders = {};
+9 -1
View File
@@ -31,6 +31,7 @@ class ERR_GameMode : SCR_BaseGameMode
[RplProp()]
protected ERR_RadioFeedback m_RadioFeedback;
protected ref ERR_RecoveryService m_Recovery;
protected ref ERR_PrisonEscapeService m_PrisonEscape;
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
@@ -55,6 +56,7 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_EnvironmentService Environment() { return m_Environment; }
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
ERR_RecoveryService Recovery() { return m_Recovery; }
ERR_PrisonEscapeService PrisonEscape() { return m_PrisonEscape; }
string RecoveryStatus() { return m_RecoveryStatus; }
ERR_CommandService Command() { return m_Command; }
ERR_Objectives Objectives() { return m_Objectives; }
@@ -229,7 +231,8 @@ class ERR_GameMode : SCR_BaseGameMode
bool UnlockPrison(IEntity player)
{
if (!Replication.IsServer() || !ERR_Actors.Player(player) || !ERR_Actors.CanFight(player)) return false;
if (!Replication.IsServer() || !m_Roster || !m_Roster.CurrentActor(player) || !ERR_Actors.CanFight(player)) return false;
if (!PlayerFaction() || SCR_FactionManager.SGetFaction(player) != PlayerFaction()) return false;
if (vector.DistanceXZ(player.GetOrigin(), PrisonPosition()) > 15) return false;
return m_Phase == ERR_Phase.PRISON && Transition(ERR_Phase.RADIO_SEARCH);
}
@@ -282,6 +285,11 @@ class ERR_GameMode : SCR_BaseGameMode
PublishEscapeResults(true);
m_Extraction.OnResolved().Insert(ExtractionResolved);
OnStop().Insert(m_Extraction.Stop);
m_PrisonEscape = new ERR_PrisonEscapeService(this);
OnPhaseChanged().Insert(m_PrisonEscape.PhaseChanged);
OnTick().Insert(m_PrisonEscape.Tick);
OnStop().Insert(m_PrisonEscape.Stop);
m_Roster.OnChanged().Insert(m_PrisonEscape.RosterChanged);
m_Discovery = new ERR_DiscoveryService(this);
OnTick().Insert(m_Discovery.Tick);
OnStop().Insert(m_Discovery.Stop);
@@ -0,0 +1,62 @@
// A previously initialized off-world medical kit can outlive its weak catalog.
// Runtime/startup validation remains vanilla. Only owned teardown bypasses the
// unrelated initialization gate; no catalog is kept alive or fabricated.
class ERR_ItemStationTeardownRules
{
static bool Recover(bool initialized, bool scoped, bool offWorld, bool catalogPresent, bool editMode)
{
return initialized && scoped && offWorld && !catalogPresent && !editMode;
}
}
modded class SCR_ResupplyMedicalGadgetSupportStationComponent
{
protected bool m_ERRTeardownScope;
protected bool m_ERRTeardownInitializing;
protected bool m_ERRTeardownInitialized;
protected bool m_ERRTeardownDone;
protected SCR_ResourceComponent m_ERRTeardownResource;
protected bool ERR_KnownOffWorldKit(IEntity owner)
{
if (!GetGame() || !owner || owner.GetWorld() == GetGame().GetWorld() || !owner.GetPrefabData()) return false;
ResourceName prefab = owner.GetPrefabData().GetPrefabName();
return prefab == "{AE578EEA4244D41F}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et" || prefab == "{21EF98BFC1EB3793}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et";
}
protected override void DelayedInit(IEntity owner)
{
m_ERRTeardownInitializing = ERR_GameMode.Get() && ERR_KnownOffWorldKit(owner);
super.DelayedInit(owner);
m_ERRTeardownInitializing = false;
}
protected override void AddRemoveOnDamageStateChanged(IEntity owner, bool subscribe)
{
super.AddRemoveOnDamageStateChanged(owner, subscribe);
// Vanilla reaches this only after the complete virtual InitValidSetup passed.
if (!subscribe || !m_ERRTeardownInitializing) return;
m_ERRTeardownScope = true;
m_ERRTeardownInitialized = true;
if (m_ResourceConsumer || m_ResourceGenerator) m_ERRTeardownResource = GetResourceComponent();
}
override void OnDelete(IEntity owner)
{
bool offWorld = owner && (!GetGame() || owner.GetWorld() != GetGame().GetWorld());
if (!ERR_ItemStationTeardownRules.Recover(m_ERRTeardownInitialized, m_ERRTeardownScope, offWorld, m_EntityCatalogManager != null, SCR_Global.IsEditMode()))
{
super.OnDelete(owner);
return;
}
if (m_ERRTeardownDone) return;
m_ERRTeardownDone = true;
if (GetGame()) GetGame().GetCallqueue().Remove(DelayedInit);
AddRemoveOnDamageStateChanged(owner, false);
// These exact stations reject UsesRange at initialization and were never
// registered with the range manager. Native resource subscription is separate
// from range, and still needs removing when its component remains alive.
if (m_ERRTeardownResource) m_ERRTeardownResource.TEMP_GetOnInteractorReplicated().Remove(TEMP_OnInteractorReplicated);
m_ERRTeardownResource = null;
}
}
@@ -19,6 +19,8 @@ modded class ERR_GameMode
protected ref ERR_NativeGuardCancelProbe m_ERRGuardCancelProbe;
protected ref ERR_NativeLocalAdmissionProbe m_ERRLocalAdmissionProbe;
protected ref ERR_NativePoweredFlightProbe m_ERRPoweredFlightProbe;
protected ref ERR_NativePrisonBreachProbe m_ERRPrisonBreachProbe;
protected ref ERR_NativeItemStationTeardownProbe m_ERRItemStationTeardownProbe;
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
protected ref ERR_NativeCasualtyLoadProbe m_ERRCasualtyLoadProbe;
@@ -52,6 +54,7 @@ modded class ERR_GameMode
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound();
string outcome;
m_ERRStartupFailure = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "startup-failure";
m_ERRExtractionCase = outcome;
@@ -83,6 +86,7 @@ modded class ERR_GameMode
protected void ERR_StopHealthProbes()
{
ERR_NativeDiscoveryMapProbe.Stop();
ERR_NativeProgressionIdentityProbe.Stop();
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
@@ -91,6 +95,8 @@ modded class ERR_GameMode
if (m_ERRGuardCancelProbe) m_ERRGuardCancelProbe.Stop();
if (m_ERRLocalAdmissionProbe) m_ERRLocalAdmissionProbe.Stop();
if (m_ERRPoweredFlightProbe) m_ERRPoweredFlightProbe.Stop();
if (m_ERRPrisonBreachProbe) m_ERRPrisonBreachProbe.Stop();
if (m_ERRItemStationTeardownProbe) m_ERRItemStationTeardownProbe.Stop();
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
if (m_ERRBaseMedicalProbe) m_ERRBaseMedicalProbe.Stop();
@@ -162,6 +168,20 @@ modded class ERR_GameMode
{
if (!ERR_MissionRules.Terminal(current)) return;
GetGame().GetCallqueue().Remove(ERR_BaseTick);
if (m_ERRExtractionCase == "station-teardown")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2002, "native station teardown fixture finished before explicit reset")) return;
ERR_StopHealthProbes();
Print("ERR SMOKE COMPLETE: isolated off-world station catalog-expiry injection and native callback teardown; not restart reproduction");
return;
}
if (m_ERRExtractionCase == "prison-breach")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1902, "native destroyed-gate crossing finished before explicit reset")) return;
ERR_StopHealthProbes();
Print("ERR SMOKE COMPLETE: actual gate destruction and sampled prison crossing; repositioned fixture, not walking or multiplayer");
return;
}
if (m_ERRExtractionCase == "powered-flight")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1802, "native powered-flight fixture finished before explicit test reset")) return;
@@ -277,6 +297,7 @@ modded class ERR_GameMode
protected void ERR_BaseTick()
{
float now = GetWorld().GetWorldTime();
if (ERR_NativeProgressionIdentityProbe.Failed()) { ERR_BaseCheck(false, "native progression identity rejection fixture"); return; }
if (now > m_ERRBaseDeadline) { ERR_BaseCheck(false, "timeout at " + m_ERRBaseStep.ToString()); return; }
if (m_ERRExtractionCase == "extraction-startup-failure" || m_ERRExtractionCase == "extraction-boundary-failure")
{
@@ -305,6 +326,20 @@ modded class ERR_GameMode
Print("ERR SMOKE COMPLETE: expected startup-failure");
return;
}
if (m_ERRBaseStep == 2000)
{
int result = m_ERRItemStationTeardownProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "native isolated station teardown fixture"); return; }
if (result == 1) { m_ERRBaseStep = 2002; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1900)
{
int result = m_ERRPrisonBreachProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "native destroyed-gate progression fixture"); return; }
if (result == 1) { m_ERRBaseStep = 1902; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1800)
{
int result = m_ERRPoweredFlightProbe.Tick(now);
@@ -467,6 +502,19 @@ modded class ERR_GameMode
if (!ERR_NativeDiscoveryMapProbe.Run()) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRDiscoveryMapChecked = true;
}
if (m_ERRExtractionCase == "station-teardown")
{
m_ERRItemStationTeardownProbe = new ERR_NativeItemStationTeardownProbe();
m_ERRBaseStep = 2000;
return;
}
if (m_ERRExtractionCase == "prison-breach")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPrisonBreachProbe = new ERR_NativePrisonBreachProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 1900;
return;
}
if (m_ERRExtractionCase == "powered-flight")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
@@ -630,6 +678,7 @@ modded class ERR_GameMode
if (!ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "primary cache lost before any clues are recovered")) return;
}
if (!ERR_BaseCheck(lock && m_ERRBaseRadio && m_ERRBaseDocuments.Count() == 2 && ERR_Actors.Alive(ExtractionVehicle()), "gate, clues, radio and helicopter created by core")) return;
if (!ERR_NativeProgressionIdentityProbe.Begin(this, m_ERRBasePlayer)) { ERR_BaseCheck(false, "native progression identity gate baseline"); return; }
ERR_BaseMove(lock.GetOwner().GetOrigin() + "1 0 1");
lock.PerformAction(lock.GetOwner(), m_ERRBasePlayer);
if (!ERR_BaseCheck(Phase() == ERR_Phase.RADIO_SEARCH, "gate action begins escape")) return;
@@ -704,6 +753,7 @@ modded class ERR_GameMode
foreach (IEntity item : stock) if (item) SCR_EntityHelper.DeleteEntityAndChildren(item);
if (!ERR_BaseCheck(m_ERRMedicalDiscoveries == 1, "discovery ticks do not repeat the location event")) return;
if (m_ERRCacheLoss && !ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "lost primary cache stays absent while alternate documents identify the radio")) return;
if (!ERR_BaseCheck(ERR_NativeProgressionIdentityProbe.DocumentPassed(), "actual held-document rejection and restored recovery were exercised this round")) return;
if (!ERR_BaseCheck(m_ERRBaseRadio.Knowledge() == 2, "documents identify radio without Intel addon")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 30;
@@ -74,6 +74,13 @@ class ERR_NativeObjectiveMapMarker : ERR_DiscoveryMarker
m_MarkerWidgetComp = null;
}
}
modded class SCR_MapMarkerManagerComponent
{
bool ERR_ProbeRetainsDiscoveryMarker(ERR_DiscoveryMarker marker)
{
return m_aStaticMarkers.Contains(marker) || m_aDisabledMarkers.Contains(marker);
}
}
class ERR_NativeDiscoveryMapProbe
{
protected static ref ERR_NativePlainMapMarker s_Plain;
@@ -89,7 +96,7 @@ class ERR_NativeDiscoveryMapProbe
if (s_Objective)
{
s_Objective.ERR_ProbeDetachMapWidget();
if (manager) manager.RemoveStaticMarker(s_Objective);
if (manager) manager.ERR_RemoveDiscoveryMarker(s_Objective);
}
s_Plain = null;
s_Objective = null;
@@ -125,6 +132,13 @@ class ERR_NativeDiscoveryMapProbe
manager.InsertStaticMarker(s_Objective, true);
if (!Check(s_Objective.GetMarkerOwnerID() == -1 && !SCR_MapMarkersUI.IsOwnedMarker(s_Objective) && !s_Objective.CanBeRemovedByOwner(), "local insertion retains objective identity and disables native edit/drag/delete eligibility")) return false;
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Unknown location", "objective first label rendered through actual native layout")) return false;
int coordinates[2];
s_Objective.SetDiscoveryPosition("6123.25 0 7345.75", 1);
s_Objective.GetWorldPos(coordinates);
if (!Check(coordinates[0] == 6250 && coordinates[1] == 7250, "first clue stores only stable 500m search-square center")) return false;
s_Objective.SetDiscoveryPosition("6123.25 0 7345.75", 2);
s_Objective.GetWorldPos(coordinates);
if (!Check(coordinates[0] == 6123 && coordinates[1] == 7346, "identification moves same native marker to exact rounded coordinates")) return false;
Widget originalWidget = s_Objective.GetRootWidget();
s_Objective.SetCustomText("Radio station");
if (!Check(s_Objective.GetRootWidget() == originalWidget && s_Objective.ERR_ProbeMapText() == "Radio station", "identification refreshes existing native widget without recreating marker")) return false;
@@ -133,6 +147,12 @@ class ERR_NativeDiscoveryMapProbe
s_Objective.SetCustomText("Initial helicopter pickup site");
if (!Check(!s_Objective.GetRootWidget() && s_Objective.GetCustomText() == "Initial helicopter pickup site", "closed widget accepts data-only update safely")) return false;
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Initial helicopter pickup site", "new native widget renders latest saved label")) return false;
manager.SetStaticMarkerDisabled(s_Objective, true);
if (!Check(manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective) && !manager.GetStaticMarkers().Contains(s_Objective), "native culling retains objective only in disabled list")) return false;
manager.ERR_RemoveDiscoveryMarker(s_Objective);
if (!Check(!manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective) && !s_Objective.GetRootWidget(), "deleting culled objective removes both memberships and its widget")) return false;
manager.ERR_RemoveDiscoveryMarker(s_Objective);
if (!Check(!manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective), "duplicate owned cleanup cannot reactivate objective")) return false;
Stop();
return Check(manager.GetStaticMarkers().Count() == originalCount, "owned probe widgets and markers removed; mission markers preserved");
}
@@ -0,0 +1,141 @@
#ifdef WORKBENCH
// Test-only horizontal envelope, independent of aircraft yaw/pitch/roll.
// All distances are around the native physics origin, not the rotor centre.
class ERR_FlightFootprintRules
{
static float CylinderSphere(float originDistance, float radius, float thickness)
{
// Triangle inequality deliberately overbounds the one-sided rotor cylinder.
return originDistance + radius + thickness;
}
static bool Fits(float radius, float initialDrift, float initialHeight)
{
if (!(radius > 0 && radius < 32 && initialDrift >= 0 && initialDrift < 32)) return false;
if (!(initialHeight >= -3 && initialHeight <= 5)) return false;
return radius + initialDrift + 8 + 1 <= 40 && radius + initialHeight + 18 + 1 <= 60;
}
}
class ERR_NativeFlightFootprint
{
static const int MAX_ENTITIES = 256;
static const int MAX_DEPTH = 16;
static const int MAX_HITZONES = 512;
protected static bool Refuse(string reason)
{
Print("ERR POWERED FOOTPRINT: refused " + reason);
return false;
}
protected static bool PointRadius(vector point, vector origin, inout float radius)
{
float distance = vector.Distance(point, origin);
if (!(distance >= 0 && distance < 1000)) return false;
radius = Math.Max(radius, distance);
return true;
}
static bool Measure(Vehicle aircraft, VehicleHelicopterSimulation simulation, vector pad, out vector origin, out float radius)
{
radius = 0;
if (!aircraft || !simulation || !simulation.IsValid() || !aircraft.GetPrefabData()) return Refuse("missing initialized native airframe");
if (aircraft.GetPrefabData().GetPrefabName() != "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et") return Refuse("different airframe resource");
// Model bounds and damage pivots are world-space. The simulation getter
// exposes internal physics space (identity at the airport in 100422), so
// use this same physical aircraft's actual entity world transform.
vector worldTransform[4]; aircraft.GetWorldTransform(worldTransform);
origin = worldTransform[3];
for (int axis = 0; axis < 3; axis++)
if (!(Math.AbsFloat(worldTransform[axis].Length() - 1) < 0.001)) return Refuse("non-unit native entity world basis");
array<IEntity> entities = {aircraft};
array<int> depths = {0};
array<HitZone> seenZones = {};
int meshes, mainSweeps, tailSweeps;
float modelRadius, damageRadius, simulationRadius;
for (int index = 0; index < entities.Count(); index++)
{
IEntity entity = entities[index];
if (!entity) return Refuse("hierarchy changed during measurement");
if (entity.GetVObject())
{
vector mins, maxs; entity.GetWorldBounds(mins, maxs);
for (int dimension = 0; dimension < 3; dimension++)
if (!(mins[dimension] <= maxs[dimension])) return Refuse("invalid model bounds");
// A sphere around every corner encloses the complete world AABB. Its radius
// remains conservative under any rigid orientation of the whole aircraft.
for (int x = 0; x < 2; x++)
for (int y = 0; y < 2; y++)
for (int z = 0; z < 2; z++)
{
vector corner = mins;
if (x == 1) corner[0] = maxs[0];
if (y == 1) corner[1] = maxs[1];
if (z == 1) corner[2] = maxs[2];
if (!PointRadius(corner, origin, modelRadius)) return Refuse("non-finite or excessive model bounds");
}
meshes++;
}
HitZoneContainerComponent container = HitZoneContainerComponent.Cast(entity.FindComponent(HitZoneContainerComponent));
if (container)
{
int count = container.CountAllHitZones();
if (count < 0 || count + seenZones.Count() > MAX_HITZONES) return Refuse("hitzone traversal bound");
array<HitZone> zones = {};
if (container.GetAllHitZones(zones) != count || zones.Count() != count) return Refuse("hitzones changed during measurement");
foreach (HitZone zone : zones)
{
if (!zone || seenZones.Contains(zone)) continue;
seenZones.Insert(zone);
SCR_RotorHitZone rotorZone = SCR_RotorHitZone.Cast(zone);
if (!rotorZone || rotorZone.GetRotorRadius() <= 0) continue; // transmission zones have no sweep
int rotorIndex = rotorZone.GetRotorIndex();
float expectedRadius, thickness;
if (rotorIndex == 0) { expectedRadius = 12; thickness = 1.5; mainSweeps++; }
else if (rotorIndex == 1) { expectedRadius = 2.3; thickness = 0.75; tailSweeps++; }
else return Refuse("unknown rotor sweep index");
// Stock thickness is protected with no getter. Scope to the exact prefab
// and verify its public sweep radius against the audited stock constants.
if (!(Math.AbsFloat(rotorZone.GetRotorRadius() - expectedRadius) < 0.001)) return Refuse("stock damage sweep radius changed");
PointInfo point = rotorZone.GetPointInfo();
if (!point) return Refuse("missing actual rotor sweep pivot");
vector rotorWorld[4]; point.GetWorldTransform(rotorWorld);
// Frobenius norm bounds the transform's maximum stretch even with scale
// or shear; do not assume the PointInfo basis is orthonormal.
float scaleSquared = 0;
for (int basis = 0; basis < 3; basis++) scaleSquared += vector.Dot(rotorWorld[basis], rotorWorld[basis]);
float scale = Math.Sqrt(scaleSquared);
if (!(scale > 0 && scale < 4)) return Refuse("invalid damage sweep basis");
float distance = vector.Distance(origin, rotorWorld[3]);
float sweepBound = ERR_FlightFootprintRules.CylinderSphere(distance, expectedRadius * scale, thickness * scale);
if (!(sweepBound > 0 && sweepBound < 1000)) return Refuse("invalid damage sweep extent");
damageRadius = Math.Max(damageRadius, sweepBound);
PrintFormat("ERR POWERED FOOTPRINT ROTOR: index=%1 pivot=%2 damageRadius=%3 thickness=%4 scale=%5 sphereBound=%6", rotorIndex, rotorWorld[3], expectedRadius, thickness, scale, sweepBound);
}
}
IEntity child = entity.GetChildren();
while (child)
{
if (entities.Count() >= MAX_ENTITIES || depths[index] >= MAX_DEPTH || entities.Contains(child)) return Refuse("entity/depth/cycle traversal bound");
entities.Insert(child); depths.Insert(depths[index] + 1);
child = child.GetSibling();
}
}
if (meshes == 0 || mainSweeps != 1 || tailSweeps != 1) return Refuse("complete model and both actual damage sweeps required");
if (simulation.RotorCount() != 2) return Refuse("stock simulation rotor count changed");
for (int rotor = 0; rotor < 2; rotor++)
{
vector localRotor[4]; simulation.RotorGetTransform(rotor, localRotor);
float physicalRadius = simulation.RotorGetRadius(rotor);
float physicalBound = localRotor[3].Length() + physicalRadius;
if (!(physicalRadius > 0 && physicalBound > 0 && physicalBound < 1000)) return Refuse("invalid native simulation rotor extent");
simulationRadius = Math.Max(simulationRadius, physicalBound);
PrintFormat("ERR POWERED FOOTPRINT PHYSICS ROTOR: index=%1 localPivot=%2 radius=%3 sphereBound=%4", rotor, localRotor[3], physicalRadius, physicalBound);
}
radius = Math.Max(modelRadius, Math.Max(damageRadius, simulationRadius));
float initialDrift = vector.DistanceXZ(origin, pad);
float initialHeight = origin[1] - pad[1];
PrintFormat("ERR POWERED FOOTPRINT: entities=%1 meshes=%2 zones=%3 modelSphere=%4 damageSphere=%5 simulationSphere=%6 completeSphere=%7 initialDrift=%8 initialHeight=%9", entities.Count(), meshes, seenZones.Count(), modelRadius, damageRadius, simulationRadius, radius, initialDrift, initialHeight);
if (!ERR_FlightFootprintRules.Fits(radius, initialDrift, initialHeight)) return Refuse("measured footprint plus initial placement,8m drift and1m margin exceeds40m corridor or60m ceiling");
return true;
}
}
#endif
@@ -0,0 +1,90 @@
#ifdef WORKBENCH
// Optional observations only. Never writes pilot controls, signals or settings.
modded class ERR_NativePoweredFlightProbe
{
protected bool m_ERRInputDiagChecked;
protected bool m_ERRInputDiagEnabled;
protected int m_ERRInputDiagSamples;
protected int m_ERRWrittenSequence;
protected int m_ERRAppliedSequence;
protected int m_ERRApplyCallbacks;
protected float m_ERRWrittenAt;
protected float m_ERRAppliedAt;
protected float m_ERRWrittenCollective;
protected float m_ERRAppliedCollective;
protected float m_ERRWrittenThrottle;
protected float m_ERRAppliedThrottle;
// Vector elements are cyclic-forward, cyclic-aside, anti-torque, respectively.
protected vector m_ERRWrittenCyclic;
protected vector m_ERRAppliedCyclic;
protected bool ERR_InputDiagActive()
{
if (!m_ERRInputDiagChecked)
{
string enabled;
m_ERRInputDiagEnabled = System.GetCLIParam("errFlightInputDiag", enabled) && enabled == "1";
m_ERRInputDiagChecked = true;
}
if (!m_ERRInputDiagEnabled || !m_Bound || m_Stopped || !m_Input) return false;
return PoweredAuthority();
}
override void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
{
// Snapshot native context before this callback's owned final writes.
ERR_ObserveAppliedFlightInput(controller, owner);
int before = m_ControlUpdates;
super.ApplyPilot(controller, owner, dt);
if (m_ControlUpdates == before || controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
m_ERRWrittenSequence = m_ControlUpdates;
m_ERRWrittenAt = GetGame().GetWorld().GetWorldTime();
m_ERRWrittenCollective = m_Input.GetVehicleCollective();
m_ERRWrittenThrottle = m_Input.GetVehicleThrottle();
m_ERRWrittenCyclic = Vector(m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque());
}
void ERR_ObserveAppliedFlightInput(CharacterControllerComponent controller, IEntity owner)
{
if (controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
m_ERRApplyCallbacks++;
m_ERRAppliedSequence = m_ERRWrittenSequence;
m_ERRAppliedAt = GetGame().GetWorld().GetWorldTime();
m_ERRAppliedCollective = m_Input.GetVehicleCollective();
m_ERRAppliedThrottle = m_Input.GetVehicleThrottle();
m_ERRAppliedCyclic = Vector(m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque());
}
protected string ERR_ExistingRotorSignal(SignalsManagerComponent signals, string name)
{
if (!signals) return "no-manager";
int index = signals.FindSignal(name);
if (index < 0) return "missing";
return string.Format("%1", signals.GetSignalValue(index));
}
protected override void Diagnostic(float now)
{
bool due = m_Simulation && m_Aircraft && now >= m_NextLog;
super.Diagnostic(now);
// The existing diagnostic gate is two seconds. Cap at120 batches as well,
// and never call native getters for an unowned or player-claimed fixture.
if (!due || m_ERRInputDiagSamples >= 120 || !ERR_InputDiagActive()) return;
m_ERRInputDiagSamples++;
float writtenAge = -1;
float appliedAge = -1;
if (m_ERRWrittenSequence > 0) writtenAge = now - m_ERRWrittenAt;
if (m_ERRApplyCallbacks > 0) appliedAge = now - m_ERRAppliedAt;
PrintFormat("ERR POWERED INPUT OBSERVATION: sample=%1 writtenSequence=%2 applyCallbacks=%3 beforeWriteSequence=%4 writtenAgeMs=%5 beforeWriteAgeMs=%6", m_ERRInputDiagSamples, m_ERRWrittenSequence, m_ERRApplyCallbacks, m_ERRAppliedSequence, writtenAge, appliedAge);
PrintFormat("ERR POWERED INPUT STAGES: writtenCollective=%1 beforeWriteCollective=%2 writtenFRA=%3 beforeWriteFRA=%4 writtenThrottle=%5 beforeWriteThrottle=%6", m_ERRWrittenCollective, m_ERRAppliedCollective, m_ERRWrittenCyclic, m_ERRAppliedCyclic, m_ERRWrittenThrottle, m_ERRAppliedThrottle);
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
string collective = ERR_ExistingRotorSignal(signals, "MainRotorCollective");
string forward = ERR_ExistingRotorSignal(signals, "MainRotorCyclicForward");
string aside = ERR_ExistingRotorSignal(signals, "MainRotorCyclicAside");
string tail = ERR_ExistingRotorSignal(signals, "TailRotorCollective");
bool autohover = m_Controller.GetAutohoverEnabled();
int mode = m_Controller.GetCollectiveMode();
PrintFormat("ERR POWERED ROTOR SIGNALS: collective=%1 cyclicForward=%2 cyclicAside=%3 tailCollective=%4 autohover=%5 collectiveMode=%6", collective, forward, aside, tail, autohover, mode);
}
}
#endif
@@ -0,0 +1,162 @@
#ifdef WORKBENCH
// Opt-in fault injection on a fixture-owned stock kit in a private preview world.
// Calls the actual teardown boundary while the native callback sources still
// exist, so an unsubscribe failure remains observable before entity destruction.
modded class SCR_ResupplyMedicalGadgetSupportStationComponent
{
protected bool m_ERRTeardownProbeWatch;
protected int m_ERRTeardownProbeResourceCalls;
protected int m_ERRTeardownProbeDamageCalls;
protected int m_ERRTeardownProbeInitCalls;
protected override void DelayedInit(IEntity owner)
{
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeInitCalls++;
super.DelayedInit(owner);
}
protected override void TEMP_OnInteractorReplicated()
{
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeResourceCalls++;
super.TEMP_OnInteractorReplicated();
}
protected override void OnDamageStateChanged(EDamageState damageState)
{
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeDamageCalls++;
super.OnDamageStateChanged(damageState);
}
bool ERR_TeardownProbeInitialize()
{
// Preview worlds need not process normal world events. Execute the unchanged
// native initialization routine once if EOnInit has not already done so.
if (!m_ERRTeardownInitialized)
{
GetGame().GetCallqueue().Remove(DelayedInit);
DelayedInit(GetOwner());
}
return m_ERRTeardownInitialized && m_ERRTeardownScope && m_EntityCatalogManager && !UsesRange();
}
SCR_ResourceComponent ERR_TeardownProbeResource() { return m_ERRTeardownResource; }
void ERR_TeardownProbeWatch() { m_ERRTeardownProbeWatch = true; }
int ERR_TeardownProbeResourceCalls() { return m_ERRTeardownProbeResourceCalls; }
int ERR_TeardownProbeDamageCalls() { return m_ERRTeardownProbeDamageCalls; }
int ERR_TeardownProbeInitCalls() { return m_ERRTeardownProbeInitCalls; }
bool ERR_TeardownProbeDone() { return m_ERRTeardownDone; }
void ERR_TeardownProbeExpireAndDelete()
{
// Fault injection only: do not mutate/delete the global manager or any
// existing preview/cache entity. Nothing calls InitValidSetup with this null.
m_EntityCatalogManager = null;
GetGame().GetCallqueue().CallLater(DelayedInit, 100, false, GetOwner());
OnDelete(GetOwner());
OnDelete(GetOwner());
}
}
class ERR_NativeItemStationTeardownProbe
{
protected ref SharedItemRef m_WorldRef;
protected BaseWorld m_World;
protected IEntity m_Kit;
protected SCR_ResupplyMedicalGadgetSupportStationComponent m_Station;
protected SCR_ResourceComponent m_Resource;
protected SCR_DamageManagerComponent m_Damage;
protected SCR_HitZone m_HitZone;
protected SCR_EntityCatalogManagerComponent m_OriginalCatalog;
protected int m_Index;
protected int m_Step;
protected int m_Result;
protected bool m_Stopped;
protected int m_ExpectedDamageCalls;
protected float m_Wait;
protected float m_Deadline;
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: station teardown " + detail); return true; }
Print("ERR SMOKE FAIL: station teardown " + detail, LogLevel.ERROR);
m_Result = -1;
Stop();
return false;
}
void Stop()
{
m_Stopped = true;
ReleaseKit();
}
protected void ReleaseKit()
{
// All objects belong to this fixture, never a resource or preview cache.
m_Station = null;
m_Resource = null;
m_Damage = null;
m_HitZone = null;
if (m_Kit) delete m_Kit;
m_Kit = null;
m_World = null;
m_WorldRef = null;
}
protected void EmitNativeCallbacks()
{
m_Resource.TEMP_GetOnInteractorReplicated().Invoke();
if (m_Damage) m_Damage.GetOnDamageStateChanged().Invoke(m_Damage.GetState());
if (m_HitZone) m_HitZone.GetOnDamageStateChanged().Invoke(m_HitZone);
}
int Tick(float now)
{
if (m_Result != 0) return m_Result;
if (m_Stopped) return -1;
if (m_Deadline == 0) m_Deadline = now + 10000;
if (now >= m_Deadline) { Check(false, "bounded fixture deadline"); return -1; }
if (now < m_Wait) return 0;
if (m_Step == 0)
{
if (!Check(Replication.IsServer() && ERR_GameMode.Get() && SCR_EntityCatalogManagerComponent.GetInstance(), "successor server and real global catalog available")) return -1;
m_OriginalCatalog = SCR_EntityCatalogManagerComponent.GetInstance();
m_WorldRef = BaseWorld.CreateWorld("Preview", "ERRStationTeardownProbe");
if (m_WorldRef) m_World = m_WorldRef.GetRef();
if (!Check(m_World && m_World != GetGame().GetWorld(), "owns an independent off-world preview world")) return -1;
ResourceName prefab = "{AE578EEA4244D41F}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et";
if (m_Index == 1) prefab = "{21EF98BFC1EB3793}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et";
Resource resource = Resource.Load(prefab);
if (!Check(resource && resource.IsValid(), "stock kit resource resolves")) return -1;
m_Kit = GetGame().SpawnEntityPrefab(resource, m_World);
if (!Check(m_Kit != null, "isolated stock kit spawned")) return -1;
m_Wait = now + 500;
m_Step = 1;
return 0;
}
if (m_Step == 1)
{
m_Station = SCR_ResupplyMedicalGadgetSupportStationComponent.Cast(m_Kit.FindComponent(SCR_ResupplyMedicalGadgetSupportStationComponent));
if (!Check(m_Station && m_Station.ERR_TeardownProbeInitialize(), "actual native initialization completed with valid catalog and no range registration")) return -1;
m_Resource = m_Station.ERR_TeardownProbeResource();
if (!Check(m_Resource != null, "actual subscribed native resource component captured")) return -1;
m_Damage = SCR_DamageManagerComponent.Cast(m_Kit.FindComponent(SCR_DamageManagerComponent));
HitZoneContainerComponent hitZones = HitZoneContainerComponent.Cast(m_Kit.FindComponent(HitZoneContainerComponent));
if (hitZones) m_HitZone = SCR_HitZone.Cast(hitZones.GetDefaultHitZone());
m_ExpectedDamageCalls = 0;
if (m_Damage) m_ExpectedDamageCalls++;
if (m_HitZone) m_ExpectedDamageCalls++;
PrintFormat("ERR STATION TEARDOWN PROBE: kit=%1 nativeDamageSources=%2 resourcePresent=%3", m_Index, m_ExpectedDamageCalls, m_Resource != null);
m_Station.ERR_TeardownProbeWatch();
EmitNativeCallbacks();
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "native invokers reach initialized station exactly once per installed source")) return -1;
m_Station.ERR_TeardownProbeExpireAndDelete();
if (!Check(m_Station.ERR_TeardownProbeDone(), "expired cached catalog takes production cleanup exactly once")) return -1;
EmitNativeCallbacks();
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "resource and installed damage sources no longer invoke detached station")) return -1;
m_Wait = now + 500;
m_Step = 2;
return 0;
}
if (!Check(m_Station.ERR_TeardownProbeInitCalls() == 0, "queued native initialization did not run after teardown")) return -1;
EmitNativeCallbacks();
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "no late callbacks after 500 ms")) return -1;
if (!Check(m_OriginalCatalog && SCR_EntityCatalogManagerComponent.GetInstance() == m_OriginalCatalog, "same global catalog remains alive and unchanged by fault injection")) return -1;
ReleaseKit();
m_Index++;
if (m_Index < 2) { m_Step = 0; return 0; }
m_Result = 1;
return 1;
}
}
#endif
@@ -7,7 +7,13 @@ modded class SCR_CharacterControllerComponent
{
super.OnPrepareControls(owner, am, dt, player);
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
if (probe && !player) probe.PreparePilot(this, owner, dt);
if (probe && !player) probe.ObservePilotPreparation(this, owner);
}
protected override void OnApplyControls(IEntity owner, float timeSlice)
{
super.OnApplyControls(owner, timeSlice);
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
if (probe) probe.ApplyPilot(this, owner, timeSlice);
}
}
modded class SCR_HelicopterControllerComponent
@@ -21,6 +27,47 @@ modded class SCR_HelicopterControllerComponent
}
class ERR_PoweredFlightRules
{
static const int PAD_CANDIDATES = 128;
static const int PAD_REFINEMENTS = 80;
static const int AIRPORT_CANDIDATES = 169;
static vector AirportOffset(int index)
{
if (index < 0 || index >= AIRPORT_CANDIDATES) return vector.Zero;
int x = Math.Mod(index, 13) - 6;
int z = index / 13;
z -= 6;
return Vector(x * 50, 0, z * 50);
}
static vector RefineOffset(int index)
{
if (index < 0 || index >= PAD_REFINEMENTS) return vector.Zero;
int sample = index;
if (sample >= 40) sample++;
int x = Math.Mod(sample, 9) - 4;
int z = sample / 9;
z -= 4;
return Vector(x * 10, 0, z * 10);
}
static vector PadOffset(int index)
{
if (index < 0 || index >= PAD_CANDIDATES) return vector.Zero;
float radius = 120;
int segments = 12;
int sample = index;
float phase = 0;
if (index >= 12 && index < 108)
{
int ring = (index - 12) / 24;
radius = 200 + ring * 80;
segments = 24; sample = Math.Mod(index - 12, 24); phase = Math.Mod(ring, 2) * 0.5;
}
else if (index >= 108)
{
radius = 500; segments = 20; sample = index - 108; phase = 0.25;
}
float angle = (sample + phase) * 2 * Math.PI / segments;
return Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius);
}
static bool Authority(bool server, bool enabled, bool stopped, bool ownedPilot, bool ownedSeat, bool playerPresent, bool alive, bool fueled)
{
return server && enabled && !stopped && ownedPilot && ownedSeat && !playerPresent && alive && fueled;
@@ -29,6 +76,17 @@ class ERR_PoweredFlightRules
{
return Math.Clamp(value, -limit, limit);
}
static float NativeCollective(float signedDemand)
{
// The native apply stage clamps negative collective to zero (100422).
// Keep the feedback controller's signed state; encode its final demand in
// the nonnegative native collective domain, unlike signed cyclic/torque.
return (ClampInput(signedDemand) + 1) * 0.5;
}
static bool CollectiveObserved(float command, float signal)
{
return command >= 0.04 && command <= 0.06 && Math.AbsFloat(signal - command) <= 0.01;
}
static float DesiredVertical(float height, bool landing)
{
float target = 8;
@@ -59,6 +117,7 @@ class ERR_NativePoweredFlightProbe
protected PilotCompartmentSlot m_Seat;
protected CollectiveMode m_OriginalMode;
protected vector m_Home;
protected vector m_TestPad;
protected float m_Heading;
protected float m_Deadline;
protected float m_StageDeadline;
@@ -72,14 +131,24 @@ class ERR_NativePoweredFlightProbe
protected int m_Wakes;
protected int m_StartRequests;
protected int m_ControlUpdates;
protected int m_PilotPreparations;
protected int m_PositiveGateUpdates;
protected float m_PositiveSignalSince = -1;
protected int m_HelicopterUpdates;
protected bool m_PilotSpawnRequested;
protected AIAgent m_PilotAgent;
protected int m_OriginalPilotLOD;
protected bool m_PilotLODSaved;
protected bool m_PilotSeatsLogged;
protected float m_NextInitializationLog;
protected float m_NextSeatingLog;
protected bool m_Bound;
protected bool m_ModeSaved;
protected bool m_Stopped;
protected bool m_Complete;
protected bool m_ReadbackOK = true;
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
static const ResourceName GROUP = "{8DE0C0830FE0C33D}Prefabs/Groups/OPFOR/Group_USSR_Base.et";
static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
static bool Enabled()
{
@@ -131,15 +200,23 @@ class ERR_NativePoweredFlightProbe
// Override only this owned fixture controller, never global/user settings.
controller.SetCollectiveMode(CollectiveMode.Direct); m_HelicopterUpdates++;
}
void PreparePilot(CharacterControllerComponent controller, IEntity owner, float dt)
void ObservePilotPreparation(CharacterControllerComponent controller, IEntity owner)
{
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority()) return;
m_PilotPreparations++;
}
// Native prepare inputs are overwritten before application (101342).
// Integrate and write once here, after vanilla OnApplyControls, never in both.
void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
{
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority() || !m_Input || !m_Simulation || dt <= 0 || dt > 0.2) return;
vector transform[4]; m_Simulation.GetWorldTransform(transform);
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
vector velocity = m_Simulation.GetVelocity();
vector angular = m_Simulation.GetAngularVelocityLocal();
float height = transform[3][1] - m_Home[1];
float forward, aside, torque;
float collective = -1;
if (m_Stage == 9) collective = -0.9; // native0.05, bounded signal-consumption proof
if (m_Stage >= 4 && m_Stage <= 6 && m_Controller.IsEngineOn())
{
float desiredVertical = ERR_PoweredFlightRules.DesiredVertical(height, m_Stage == 6);
@@ -158,6 +235,7 @@ class ERR_NativePoweredFlightProbe
torque = ERR_PoweredFlightRules.ClampInput(headingError * 0.01 - angular[1] * 0.3, 0.3);
if (m_Stage == 6 && ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, velocity.Length(), angular.Length(), transform[1][1])) collective = -1;
}
collective = ERR_PoweredFlightRules.NativeCollective(collective);
m_Input.SetVehicleThrottle(1);
m_Input.SetVehicleCollective(collective);
m_Input.SetVehicleCyclicForward(forward);
@@ -169,34 +247,132 @@ class ERR_NativePoweredFlightProbe
protected bool FindPad(out vector result)
{
vector anchor = m_Mode.ExtractionVehicle().GetOrigin();
for (int index = 0; index < 12; index++)
int tested, lowGround, slopeRejected, cylinderRejected, sweepRejected;
float bestSlope = float.MAX;
vector bestCandidate;
vector refinementAnchor;
int localCandidates = ERR_PoweredFlightRules.PAD_CANDIDATES + ERR_PoweredFlightRules.PAD_REFINEMENTS;
for (int index = 0; index < localCandidates + ERR_PoweredFlightRules.AIRPORT_CANDIDATES; index++)
{
float angle = index * Math.PI / 6;
vector candidate = ERR_Actors.Ground(anchor + Vector(Math.Cos(angle) * 120, 0, Math.Sin(angle) * 120));
if (candidate[1] < 2) continue;
bool flat = true;
if (index == ERR_PoweredFlightRules.PAD_CANDIDATES)
{
if (bestSlope == float.MAX) refinementAnchor = anchor;
else refinementAnchor = bestCandidate;
// Freeze the refinement centre; improvements must not move the grid.
}
tested++;
vector candidate;
if (index < ERR_PoweredFlightRules.PAD_CANDIDATES) candidate = ERR_Actors.Ground(anchor + ERR_PoweredFlightRules.PadOffset(index));
else if (index < localCandidates) candidate = ERR_Actors.Ground(refinementAnchor + ERR_PoweredFlightRules.RefineOffset(index - ERR_PoweredFlightRules.PAD_CANDIDATES));
// Installed vanilla Eden/Airport location centre; candidate only, never
// accepted without exactly the same terrain and physical clearance queries.
else candidate = ERR_Actors.Ground("4900.953 0 11787.412" + ERR_PoweredFlightRules.AirportOffset(index - localCandidates));
if (candidate[1] < 2) { lowGround++; continue; }
float maximumDelta = 0;
for (int edge = 0; edge < 4; edge++)
{
float side = edge * Math.PI / 2;
vector point = candidate + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
if (Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]) > 0.8) flat = false;
maximumDelta = Math.Max(maximumDelta, Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]));
}
if (!flat || !SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 15, 4, TraceFlags.ENTS)) continue;
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 35 0";
clearance.Mins = "-15 -1 -15"; clearance.Maxs = "15 1 15"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(clearance, null) < 1) continue;
if (maximumDelta < bestSlope) { bestSlope = maximumDelta; bestCandidate = candidate; }
if (maximumDelta > 0.8) { slopeRejected++; continue; }
// Retain slope and exact native rejection semantics; widen the tested
// column for a runtime-measured whole-aircraft footprint plus8m drift.
if (!SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 40, 4, TraceFlags.ENTS)) { cylinderRejected++; continue; }
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 60 0";
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(clearance, null) < 1) { sweepRejected++; continue; }
PrintFormat("ERR POWERED PAD: found=%1 tested=%2 distance=%3m maximumHeightDelta=%4m lowGround=%5 slopeRejected=%6 cylinderRejected=%7 sweepRejected=%8", candidate, tested, vector.DistanceXZ(anchor, candidate), maximumDelta, lowGround, slopeRejected, cylinderRejected, sweepRejected);
result = candidate; return true;
}
if (bestSlope == float.MAX) bestSlope = -1;
PrintFormat("ERR POWERED PAD: no safe candidate; tested=%1 localRadius=557m plus airport300m-grid lowGround=%2 slopeRejected=%3 cylinderRejected=%4 sweepRejected=%5 bestHeightDelta=%6m bestCandidate=%7 (diagnostic only; never a fallback)", tested, lowGround, slopeRejected, cylinderRejected, sweepRejected, bestSlope, bestCandidate);
return false;
}
protected void Diagnostic(float now)
{
if (!m_Simulation || !m_Aircraft || now < m_NextLog) return;
m_NextLog = now + 2000;
vector transform[4]; m_Simulation.GetWorldTransform(transform);
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
PrintFormat("ERR POWERED FLIGHT: stage=%1 height=%2 drift=%3 speed=%4 vertical=%5 up=%6 groundContact=%7 fuel=%8 engine=%9", m_Stage, transform[3][1] - m_Home[1], vector.DistanceXZ(transform[3], m_Home), m_Simulation.GetVelocity().Length(), m_Simulation.GetVelocity()[1], transform[1][1], m_Simulation.HasAnyGroundContact(), m_Fuel.GetTotalFuel(), m_Controller.IsEngineOn());
PrintFormat("ERR POWERED INPUT: pilotHooks=%1 vehicleHooks=%2 readback=%3 collective=%4 cyclicF=%5 cyclicR=%6 antiTorque=%7 rotorRPM=%8 targetRPM=%9", m_ControlUpdates, m_HelicopterUpdates, m_ReadbackOK, m_Input.GetVehicleCollective(), m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque(), m_Simulation.RotorGetRPM(0), m_Simulation.RotorGetRPMTarget(0));
}
protected bool PollNativeFlightComponents(float now)
{
// Component pointers, simulation and tank contents may initialize after spawn.
// Enumerate the real empty primary cockpit slot; an active pilot assignment
// is a later occupancy requirement, not a prerequisite to creating its pilot.
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
m_Simulation = null;
if (m_Controller) m_Simulation = m_Controller.GetSimulation();
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Aircraft.FindComponent(BaseCompartmentManagerComponent));
array<BaseCompartmentSlot> slots = {};
if (manager) manager.GetCompartments(slots);
m_Seat = null;
if (slots.Count() <= 64)
{
foreach (BaseCompartmentSlot slot : slots)
{
PilotCompartmentSlot pilotSlot = PilotCompartmentSlot.Cast(slot);
if (!pilotSlot || pilotSlot.GetOwner() != m_Aircraft || pilotSlot.GetVehicle() != m_Aircraft) continue;
if (!m_PilotSeatsLogged) PrintFormat("ERR POWERED INITIAL SEAT: name=%1 unique=%2 type=%3 occupied=%4 accessible=%5 piloting=%6", pilotSlot.GetCompartmentName(), pilotSlot.GetCompartmentUniqueName(), pilotSlot.GetType(), pilotSlot.GetOccupant() != null, pilotSlot.IsCompartmentAccessible(), pilotSlot.IsPiloting());
// This is the actual object name in the audited stock Mi8 prefab. Do not
// choose the copilot merely because it has the same PilotCompartment type.
if (pilotSlot.GetCompartmentName() != "PilotCompartment") continue;
if (pilotSlot.GetType() != ECompartmentType.PILOT || !pilotSlot.IsCompartmentAccessible()) continue;
if (pilotSlot.GetOccupant() || pilotSlot.IsReserved() || pilotSlot.IsGetInLocked()) continue;
m_Seat = pilotSlot;
}
}
if (slots.Count() > 0) m_PilotSeatsLogged = true;
bool validSimulation = m_Simulation && m_Simulation.IsValid();
bool fueled = m_Fuel && m_Fuel.HasFuel();
PilotCompartmentSlot active;
if (m_Controller) active = m_Controller.GetPilotCompartmentSlot();
if (now >= m_NextInitializationLog || now >= m_StageDeadline)
{
m_NextInitializationLog = now + 2000;
float fuel = -1;
if (m_Fuel) fuel = m_Fuel.GetTotalFuel();
PrintFormat("ERR POWERED INITIALIZATION: controller=%1 simulation=%2 valid=%3 manager=%4 slots=%5 primaryEmptySeat=%6 activeSeat=%7 fuelManager=%8", m_Controller != null, m_Simulation != null, validSimulation, manager != null, slots.Count(), m_Seat != null, active != null, m_Fuel != null);
PrintFormat("ERR POWERED INITIALIZATION STOCK: fueled=%1 totalFuel=%2 agents=%3 pilotSpawnRequested=%4", fueled, fuel, m_Group.GetAgentsCount(), m_PilotSpawnRequested);
}
return m_Controller && validSimulation && m_Seat && fueled;
}
protected void SeatingDiagnostic(float now)
{
// Observe each existing stage-1 predicate without changing seating, AI,
// physics, health, fuel, or the shared preparation deadline.
if (now < m_NextSeatingLog && now < m_StageDeadline) return;
m_NextSeatingLog = now + 2000;
bool ownPilot = OwnPilot();
bool seatMatches = m_Seat.GetOccupant() == m_Pilot;
bool activeMatches = m_Controller.GetPilotCompartmentSlot() == m_Seat;
bool vehicleMatches = m_Pilot && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
bool aircraftReady = ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
bool pilotAlive = ERR_Actors.Alive(m_Pilot);
bool pilotConscious = ERR_Actors.CanFight(m_Pilot);
bool aircraftAlive = ERR_Actors.Alive(m_Aircraft);
bool sameGroup = ERR_Actors.Group(m_Pilot) == m_Group;
bool player = ERR_Actors.Player(m_Pilot);
PrintFormat("ERR POWERED SEATING: ownPilot=%1 seatOccupantMatches=%2 activeSlotMatches=%3 vehicleMatches=%4 aircraftReady=%5 wakes=%6 stableSince=%7 remainingMs=%8", ownPilot, seatMatches, activeMatches, vehicleMatches, aircraftReady, m_Wakes, m_Stable, m_StageDeadline - now);
PrintFormat("ERR POWERED SEATING ACTORS: pilotExists=%1 alive=%2 conscious=%3 sameGroup=%4 player=%5 groupAgents=%6 aircraftAlive=%7", m_Pilot != null, pilotAlive, pilotConscious, sameGroup, player, m_Group.GetAgentsCount(), aircraftAlive);
PrintFormat("ERR POWERED SEATING IDENTITIES: pilot=%1 occupant=%2 selectedSeat=%3 activeSeat=%4 group=%5 actualGroup=%6", m_Pilot, m_Seat.GetOccupant(), m_Seat, m_Controller.GetPilotCompartmentSlot(), m_Group, ERR_Actors.Group(m_Pilot));
if (m_PilotAgent) PrintFormat("ERR POWERED SEATING AGENT: actualLOD=%1 forcedLOD=%2 activated=%3", m_PilotAgent.GetLOD(), m_PilotAgent.GetPermanentLOD(), m_PilotAgent.IsAIActivated());
CompartmentAccessComponent access;
if (m_Pilot) access = CompartmentAccessComponent.Cast(m_Pilot.FindComponent(CompartmentAccessComponent));
if (access) PrintFormat("ERR POWERED SEATING ACCESS: gettingIn=%1 gettingOut=%2", access.IsGettingIn(), access.IsGettingOut());
Physics physics = m_Aircraft.GetPhysics();
vector entityTransform[4]; m_Aircraft.GetWorldTransform(entityTransform);
if (physics) PrintFormat("ERR POWERED SEATING PHYSICS: dynamic=%1 awake=%2 speed=%3 angularSpeed=%4 entityUp=%5 entityOrigin=%6", physics.IsDynamic(), physics.IsActive(), physics.GetVelocity().Length(), physics.GetAngularVelocity().Length(), entityTransform[1][1], entityTransform[3]);
vector nativeTransform[4]; m_Simulation.GetWorldTransform(nativeTransform);
bool valid = m_Simulation.IsValid();
bool nativeContact = m_Simulation.HasAnyGroundContact();
bool parkedSupport = ERR_CasualtyGroundSupport.Parked(m_Aircraft, now >= m_StageDeadline);
PrintFormat("ERR POWERED SEATING SIMULATION: valid=%1 nativeContact=%2 parkedSupport=%3 engineOn=%4 nativeUp=%5 nativeOrigin=%6", valid, nativeContact, parkedSupport, m_Controller.IsEngineOn(), nativeTransform[1][1], nativeTransform[3]);
}
void Stop()
{
if (m_Stopped) return;
@@ -210,6 +386,7 @@ class ERR_NativePoweredFlightProbe
m_Input.SetVehicleCyclicForward(0); m_Input.SetVehicleCyclicAside(0); m_Input.SetVehicleAntiTorque(0);
if (m_Controller && m_ModeSaved) m_Controller.SetCollectiveMode(m_OriginalMode);
}
if (m_PilotLODSaved && m_PilotAgent && OwnPilot() && !PlayerPresent()) m_PilotAgent.SetPermanentLOD(m_OriginalPilotLOD);
if (m_Group && m_Group.GetPlayerCount(true) == 0) m_Group.SetPermanentLOD(-1);
// Keep real airborne/failed test actors for diagnosis and mode-owned cleanup.
// Stop never repairs, stabilizes, teleports, refuels or replaces them.
@@ -219,34 +396,53 @@ class ERR_NativePoweredFlightProbe
if (m_Complete) return 1;
if (m_Stopped) return -1;
if (!Enabled() || !m_Mode || now >= m_Deadline || ERR_MissionRules.Terminal(m_Mode.Phase())) { Check(false, "server fixture active within 240-second bound"); return -1; }
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) m_Stable = -1;
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) { m_Stable = -1; m_PositiveSignalSince = -1; }
m_PreviousTick = now;
if (m_Stage == 0)
{
if (!m_Mode.ExtractionVehicle()) return 0;
vector pad;
if (!Check(FindPad(pad), "separate clear flat test pad found; extraction vehicle untouched")) return -1;
m_TestPad = pad;
m_Aircraft = Vehicle.Cast(ERR_Actors.Spawn(AIRCRAFT, pad));
m_Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(GROUP, pad + "20 0 0"));
m_Mode.Own(m_Aircraft); m_Mode.Own(m_Group);
if (!Check(m_Aircraft && m_Group, "one stock unarmed airframe and own empty group created once")) return -1;
// Group_USSR_Base authors six queued members; use the memberless native
// base group so adding one pilot cannot initiate a hidden crew allocation.
if (!Check(m_Group.InitFactionKey("USSR") && m_Group.GetFactionName() == "USSR", "memberless native group receives explicit USSR faction")) return -1;
m_Group.SetPermanentLOD(0); m_Group.ActivateAI();
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
if (m_Controller) { m_Simulation = m_Controller.GetSimulation(); m_Seat = m_Controller.GetPilotCompartmentSlot(); }
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
if (!Check(m_Simulation && m_Seat && !m_Seat.GetOccupant() && m_Fuel && m_Fuel.HasFuel(), "native flight simulation, actual active pilot seat and finite fuel present")) return -1;
m_Stage = 8; m_StageDeadline = now + 30000; return 0;
}
if (m_Stage == 8)
{
if (!m_Aircraft || !m_Group || PlayerPresent()) { Check(false, "initial fixture airframe/group remain owned and unclaimed"); return -1; }
bool componentsReady = PollNativeFlightComponents(now);
if (now >= m_StageDeadline) { Check(false, "native flight components and empty primary pilot seat initialize within shared 30-second preparation bound"); return -1; }
if (!componentsReady) return 0;
if (!Check(!m_PilotSpawnRequested && m_Group.GetAgentsCount() == 0, "native components initialized before the only pilot allocation")) return -1;
m_PilotSpawnRequested = true;
AIGroup nativeGroup = m_Group;
m_Pilot = m_Seat.SpawnCharacterInCompartment(PILOT, nativeGroup, GROUP);
if (m_Pilot) m_Mode.Own(m_Pilot);
if (!Check(m_Pilot && nativeGroup == m_Group, "one existing vanilla character spawned through native pilot-seat mechanism")) return -1;
m_Mode.Own(m_Pilot);
m_Stage = 1; m_StageDeadline = now + 30000; return 0;
AIControlComponent pilotControl = AIControlComponent.Cast(m_Pilot.FindComponent(AIControlComponent));
if (pilotControl) m_PilotAgent = pilotControl.GetControlAIAgent();
if (!Check(m_PilotAgent && OwnPilot(), "single native pilot owns its actual control agent and fixture group")) return -1;
// Group LOD does not establish each member's own native LOD. Keep the
// distant fixture pilot's actual control agent active, as physical crews do.
m_OriginalPilotLOD = m_PilotAgent.GetPermanentLOD(); m_PilotLODSaved = true;
m_PilotAgent.SetPermanentLOD(0); m_PilotAgent.ActivateAI();
m_Stage = 1; return 0;
}
if (m_Stage == 1)
{
if (!m_Aircraft || !m_Group || !m_Controller || !m_Simulation || !m_Seat) { Check(false, "same native flight objects survive seating"); return -1; }
SeatingDiagnostic(now);
Physics physics = m_Aircraft.GetPhysics();
if (physics && physics.IsDynamic() && m_Simulation.IsValid() && m_Wakes == 0) { m_Wakes = 1; physics.SetActive(ActiveState.ACTIVE); m_Stable = -1; return 0; }
if (now >= m_StageDeadline) { Check(false, "initial native seating and ground settling complete in 30 seconds"); return -1; }
if (!m_Wakes || !OwnPilot() || m_Seat.GetOccupant() != m_Pilot || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
if (!m_Wakes || !OwnPilot() || m_Seat.GetOccupant() != m_Pilot || m_Controller.GetPilotCompartmentSlot() != m_Seat || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
if (m_Stable < 0) m_Stable = now;
if (now - m_Stable < 1000) return 0;
m_Character = CharacterControllerComponent.Cast(m_Pilot.FindComponent(CharacterControllerComponent));
@@ -262,28 +458,52 @@ class ERR_NativePoweredFlightProbe
if (m_Stage == 2)
{
if (now >= m_StageDeadline) { Check(false, "native AI input and helicopter preparation hooks execute before engine start"); return -1; }
if (m_ControlUpdates < 3 || m_HelicopterUpdates < 3) return 0;
if (!Check(m_Controller.GetCollectiveMode() == CollectiveMode.Direct && m_Input.GetVehicleCollective() == -1, "owned native input hooks apply minimum collective in Direct mode")) return -1;
if (m_PilotPreparations < 3 || m_ControlUpdates < 3 || m_HelicopterUpdates < 3) return 0;
if (!Check(m_Controller.GetCollectiveMode() == CollectiveMode.Direct && m_Input.GetVehicleCollective() == 0, "owned native input hooks apply minimum collective in Direct mode")) return -1;
if (!Check(!m_Controller.IsEngineOn(), "native engine remains off until the complete footprint is qualified")) return -1;
vector measuredOrigin;
float measuredRadius;
if (!Check(ERR_NativeFlightFootprint.Measure(m_Aircraft, m_Simulation, m_TestPad, measuredOrigin, measuredRadius), "actual complete model and rotor footprint fits the cleared flight column before engine start")) return -1;
// Baseline the actual entity world origin used by geometry and telemetry,
// after native placement; this never changes an entity transform.
m_Home = measuredOrigin;
m_StartRequests = 1;
bool started = m_Controller.StartEngine();
PrintFormat("ERR POWERED ENGINE REQUEST: StartEngine returned=%1; normal procedure, no force-start or simulation EngineStart", started);
m_Stage = 3; m_StageDeadline = now + 65000; return 0;
}
if (now - m_LastControl > 1000) { Check(false, "native control callback remains live during test flight"); return -1; }
vector transform[4]; m_Simulation.GetWorldTransform(transform);
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
float height = transform[3][1] - m_Home[1];
float speed = m_Simulation.GetVelocity().Length();
float drift = vector.DistanceXZ(transform[3], m_Home);
if (height > 18 || height < -3 || drift > 22 || transform[1][1] < 0.85) { Check(false, "physical test stays inside height, drift and tilt safety envelope"); return -1; }
if (height > 18 || height < -3 || drift >= 8 || transform[1][1] < 0.85) { Check(false, "physical test stays inside height, drift and tilt safety envelope"); return -1; }
if (m_Stage == 3)
{
if (now >= m_StageDeadline) { Check(false, "normal engine startup and native rotor RPM complete within 65 seconds"); return -1; }
float targetRPM = m_Simulation.RotorGetRPMTarget(0);
if (!m_Controller.IsEngineOn() || targetRPM <= 0 || m_Simulation.RotorGetRPM(0) < targetRPM * 0.9 || m_Fuel.GetTotalFuel() >= m_InitialFuel) return 0;
if (!Check(m_StartRequests == 1 && m_Fuel.GetTotalFuel() < m_InitialFuel, "single normal engine start spins physical rotor and consumes native fuel")) return -1;
m_PositiveGateUpdates = m_ControlUpdates; m_PositiveSignalSince = -1;
m_Stage = 9; m_StageDeadline = now + 3000; return 0;
}
if (((m_Stage >= 4 && m_Stage <= 6) || m_Stage == 9) && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
if (m_Stage == 9)
{
if (now >= m_StageDeadline) { Check(false, "positive native collective reaches actual rotor signal before climb within three seconds"); return -1; }
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
int signalIndex = -1;
if (signals) signalIndex = signals.FindSignal("MainRotorCollective");
if (!Check(signalIndex >= 0, "existing native main rotor collective signal available for consumption proof")) return -1;
float observed = signals.GetSignalValue(signalIndex);
bool matched = m_ControlUpdates >= m_PositiveGateUpdates + 3 && ERR_PoweredFlightRules.CollectiveObserved(m_Input.GetVehicleCollective(), observed);
PrintFormat("ERR POWERED COLLECTIVE GATE: command=%1 rotorSignal=%2 applyUpdates=%3 matched=%4", m_Input.GetVehicleCollective(), observed, m_ControlUpdates - m_PositiveGateUpdates, matched);
if (!matched) { m_PositiveSignalSince = -1; return 0; }
if (m_PositiveSignalSince < 0) m_PositiveSignalSince = now;
if (now - m_PositiveSignalSince < 500) return 0;
if (!Check(true, "positive collective persists in existing native rotor signal across actual ticks before climb")) return -1;
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
}
if (m_Stage >= 4 && m_Stage <= 6 && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
if (m_Stage == 4 || m_Stage == 5)
{
bool hover = !m_Simulation.HasAnyGroundContact() && ERR_PoweredFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1], drift);
@@ -0,0 +1,167 @@
#ifdef WORKBENCH
// Disposable-world fixture. Direct native door boundary, gate damage and real admitted character;
// deliberate asynchronous repositioning tests sampling, not walking/input/MP.
class ERR_NativePrisonBreachProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected vector m_Center;
protected vector m_Target;
protected int m_Step;
protected int m_Transitions;
protected float m_Wait;
protected float m_Deadline;
void ERR_NativePrisonBreachProbe(ERR_GameMode mode, IEntity player)
{
m_Mode = mode; m_Player = player;
m_Mode.OnPhaseChanged().Insert(PhaseChanged);
}
protected void PhaseChanged(ERR_Phase previous, ERR_Phase current)
{
if (previous == ERR_Phase.PRISON && current == ERR_Phase.RADIO_SEARCH) m_Transitions++;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: prison-breach " + detail); return true; }
Print("ERR SMOKE FAIL: prison-breach " + detail, LogLevel.ERROR);
Stop();
return false;
}
protected void Move(vector offset)
{
vector transform[4];
m_Player.GetWorldTransform(transform);
m_Target = ERR_Actors.Ground(m_Center + offset);
transform[3] = m_Target;
ChimeraCharacter.Cast(m_Player).Teleport(transform);
}
protected bool Arrived() { return vector.Distance(m_Player.GetOrigin(), m_Target) < 1; }
void Stop() { if (m_Mode) m_Mode.OnPhaseChanged().Remove(PhaseChanged); }
int Tick(float now)
{
if (m_Deadline == 0) m_Deadline = now + 45000;
if (now > m_Deadline || !m_Mode.Active() || !ERR_Actors.CanFight(m_Player) || !m_Mode.Players().CurrentActor(m_Player))
{
vector observed;
if (m_Player) observed = m_Player.GetOrigin();
PrintFormat("ERR PRISON BREACH STATE: step=%1 player=%2 target=%3 phase=%4", m_Step, observed, m_Target, m_Mode.Phase());
Check(false, "deadline or canonical conscious participant lost");
return -1;
}
if (now < m_Wait) return 0;
ERR_PrisonEscapeService escape = m_Mode.PrisonEscape();
if (m_Step == 0)
{
if (escape) Print("ERR PRISON BREACH REGISTRATION: " + escape.RegistrationReason());
if (!Check(escape && escape.Registered() && escape.Gate() && !escape.GateLost() && m_Mode.Phase() == ERR_Phase.PRISON, "actual authored gate registered before any breach")) return -1;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
foreach (ERR_LocationComponent location : locations)
if (location.Kind() == ERR_LocationKind.PRISON) m_Center = location.Position();
Move("1.5 0 -4");
m_Step = 1; m_Wait = now + 1500;
return 0;
}
if (m_Step == 1)
{
if (!Arrived() || !escape.ProbeArmed(m_Player)) return 0;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "inside sample alone does not complete prison")) return -1;
IEntity gate = escape.Gate();
BaseDoorComponent door = BaseDoorComponent.Cast(gate.FindComponent(BaseDoorComponent));
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(gate.FindComponent(BaseActionsManagerComponent));
array<BaseUserAction> actions = {};
if (manager) manager.GetActionsList(actions);
ERR_PrisonDoorAction guardedAction;
foreach (BaseUserAction action : actions)
{
ERR_PrisonDoorAction candidate = ERR_PrisonDoorAction.Cast(action);
if (candidate && candidate.GetOwner() == gate) guardedAction = candidate;
}
ERR_SiteRuntime prison = m_Mode.Sites().Find("everon-prison-1");
array<AIAgent> guards = {};
if (prison && prison.Group) prison.Group.GetAgents(guards);
IEntity actualGuard;
foreach (AIAgent guard : guards)
if (guard && ERR_Actors.CanFight(guard.GetControlledEntity()) && !ERR_Actors.Player(guard.GetControlledEntity())) { actualGuard = guard.GetControlledEntity(); break; }
if (!Check(door && guardedAction && actualGuard && Math.AbsFloat(door.GetNormalizedDoorState()) < 0.01 && !door.IsOpening(), "actual intact gate is closed with successor door action and original living guard")) return -1;
if (!Check(!guardedAction.CanBePerformedScript(actualGuard) && !guardedAction.CanBePerformedScript(m_Player), "locked phase rejects both original AI and actual prisoner action permission")) return -1;
// Direct calls deliberately match SCR_AIOpenDoor's execution boundary.
// This is not autonomous AI navigation or a player held-input test.
guardedAction.PerformAction(gate, actualGuard);
guardedAction.PerformAction(gate, m_Player);
m_Step = 11; m_Wait = now + 1500;
return 0;
}
if (m_Step == 11)
{
IEntity gate = escape.Gate();
BaseDoorComponent door;
if (gate) door = BaseDoorComponent.Cast(gate.FindComponent(BaseDoorComponent));
if (!Check(door && !escape.GateLost() && Math.AbsFloat(door.GetNormalizedDoorState()) < 0.01 && !door.IsOpening() && m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "direct AI/player PerformAction cannot open locked native gate or advance progression")) return -1;
SCR_DestructionMultiPhaseComponent damage = SCR_DestructionMultiPhaseComponent.Cast(gate.FindComponent(SCR_DestructionMultiPhaseComponent));
if (!Check(damage && damage.GetDefaultHitZone(), "actual gate has its inherited multiphase destruction component and hit zone")) return -1;
PrintFormat("ERR PRISON BREACH DAMAGE: health=%1 enabled=%2 threshold=%3", damage.GetHealth(), damage.IsDamageHandlingEnabled(), damage.GetTotalDestructionThreshold());
vector hit[3]; hit[0] = gate.GetOrigin(); hit[1] = "0 0 1"; hit[2] = "0 0 -1";
SCR_DamageContext context = new SCR_DamageContext(EDamageType.TRUE, 100000, hit, gate, damage.GetDefaultHitZone(), Instigator.CreateInstigator(m_Player), null, -1, -1);
damage.HandleDamage(context);
m_Step = 2; m_Wait = now + 1500;
return 0;
}
if (m_Step == 2)
{
if (!escape.GateLost()) return 0;
PrintFormat("ERR PRISON BREACH GATE: remaining=%1 lost=%2", escape.Gate(), escape.GateLost());
ERR_PrisonEscapeService late = new ERR_PrisonEscapeService(m_Mode);
late.PhaseChanged(ERR_Phase.PREPARING, ERR_Phase.PRISON);
bool refusedLate = !late.Registered() && !late.GateLost();
late.Stop();
if (!Check(refusedLate, "fresh registration after native gate loss stays unregistered and cannot grant escape")) return -1;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "native gate destruction alone cannot advance the round")) return -1;
Move("1.5 0 -60");
m_Step = 3; m_Wait = now + 2500;
return 0;
}
if (m_Step == 3)
{
if (!Arrived()) return 0;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && !escape.ProbeArmed(m_Player), "actual far displacement is rejected and clears inside continuity")) return -1;
Move("1.5 0 -9");
m_Step = 4; m_Wait = now + 2500;
return 0;
}
if (m_Step == 4)
{
if (!Arrived()) return 0;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && !escape.ProbeArmed(m_Player), "outside-only arrival cannot finish prison")) return -1;
Move("1.5 0 -4");
m_Step = 5; m_Wait = now + 1500;
return 0;
}
if (m_Step == 5)
{
if (!Arrived() || !escape.ProbeArmed(m_Player)) return 0;
Move("1.5 0 -9");
m_Step = 6; m_Wait = now + 1500;
return 0;
}
if (m_Step == 6)
{
if (!Arrived() || m_Mode.Phase() == ERR_Phase.PRISON) return 0;
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Transitions == 1, "bounded inside-to-outside crossing advances once through ordinary phase event")) return -1;
ERR_SiteRuntime first = m_Mode.Sites().Find("everon-supply-1");
ERR_SiteRuntime second = m_Mode.Sites().Find("everon-supply-2");
if (!Check(first && second && first.Location.Knowledge() == 2 && second.Location.Knowledge() == 2, "ordinary escape handlers reveal both scavenging sites")) return -1;
m_Step = 7; m_Wait = now + 2500;
return 0;
}
if (m_Step == 7)
{
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Transitions == 1, "later production ticks do not repeat prison completion")) return -1;
Stop();
m_Step = 8;
return 1;
}
return 1;
}
}
#endif
@@ -0,0 +1,105 @@
#ifdef WORKBENCH
// Synchronous roster-body fault injection against the real admitted controller.
// No disconnect, possession, account change, network event or native rebind is simulated.
class ERR_NativeProgressionIdentityProbe
{
protected static ERR_GameMode s_Mode;
protected static IEntity s_Actor;
protected static bool s_DocumentAttempted;
protected static bool s_DocumentPassed;
protected static bool s_Failed;
static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: progression identity " + detail); return true; }
s_Failed = true;
Print("ERR SMOKE FAIL: progression identity " + detail, LogLevel.ERROR);
return false;
}
static bool Failed() { return s_Failed; }
static bool DocumentPassed() { return s_DocumentPassed; }
static void Stop() { s_Mode = null; s_Actor = null; }
static void BeginRound() { Stop(); s_DocumentAttempted = false; s_DocumentPassed = false; s_Failed = false; }
static bool Begin(ERR_GameMode mode, IEntity actor)
{
BeginRound();
string enabled;
if (!Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return false;
if (!mode || !actor || !mode.Players()) return false;
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
PlayerController controller = GetGame().GetPlayerController();
if (!Check(life && controller && controller.GetControlledEntity() == actor && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(actor) && SCR_FactionManager.SGetFaction(actor) == mode.PlayerFaction() && vector.DistanceXZ(actor.GetOrigin(), mode.PrisonPosition()) < 5, "actual local conscious prisoner is canonical and inside lock range")) return false;
IEntity savedActor = life.Actor;
UUID savedIdentity = life.AccountId;
int savedConnection = life.ConnectionId;
bool savedConnected = life.Connected;
bool savedUsedLife = life.UsedLife;
// No callback, assertion, early return or yield between mutation and restore.
life.Actor = null;
bool nativeRosterRefused = mode.Players().CurrentActor(actor) == null;
bool refused = !mode.UnlockPrison(actor);
life.Actor = savedActor;
if (!Check(nativeRosterRefused && refused && mode.Phase() == ERR_Phase.PRISON, "synchronous body-mismatch fault injection rejects normal prison unlock without progression")) return false;
if (!Check(mode.Players().CurrentActor(actor) == life && controller.GetControlledEntity() == savedActor && life.AccountId == savedIdentity && life.ConnectionId == savedConnection && life.Connected == savedConnected && life.UsedLife == savedUsedLife, "exact actor, account, connection and used-life state restored before assertions")) return false;
s_Mode = mode; s_Actor = actor;
Print("ERR IDENTITY FIXTURE LIMIT: synchronous roster Actor=null fault injection, immediately restored; no actual possession, reconnect, foreign faction or multiplayer test");
return true;
}
static bool WantsDocument(ERR_GameMode mode, IEntity actor)
{
return !s_Failed && !s_DocumentAttempted && mode && mode == s_Mode && actor == s_Actor;
}
static void AttemptDocument() { s_DocumentAttempted = true; }
static void CompleteDocument(bool success) { s_DocumentPassed = success; }
}
modded class ERR_DiscoveryService
{
protected override void Recover(IEntity player)
{
if (!ERR_NativeProgressionIdentityProbe.WantsDocument(m_Mode, player)) { super.Recover(player); return; }
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) { super.Recover(player); return; }
array<IEntity> folders = {}; Folders(inventory, folders);
array<ERR_LocationComponent> locations = {}; m_Mode.Locations(locations);
IEntity candidate;
ERR_LocationComponent target;
foreach (IEntity folder : folders)
{
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
InventoryItemComponent item = InventoryItemComponent.Cast(folder.FindComponent(InventoryItemComponent));
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
if (!item || !item.GetParentSlot() || !item.GetParentSlot().GetStorage() || !item.GetParentSlot().GetStorage().Contains(folder) || !document) continue;
foreach (ERR_LocationComponent location : locations)
if (location && location.Id() == document.TargetId() && location.Knowledge() < 2) { candidate = folder; target = location; break; }
if (candidate) break;
}
if (!candidate) { super.Recover(player); return; }
ERR_NativeProgressionIdentityProbe.AttemptDocument();
ERR_PlayerLife life = m_Mode.Players().CurrentActor(player);
PlayerController controller = GetGame().GetPlayerController();
if (!ERR_NativeProgressionIdentityProbe.Check(life && controller && controller.GetControlledEntity() == player && ERR_Actors.Alive(player) && SCR_FactionManager.SGetFaction(player) == m_Mode.PlayerFaction(), "real held unspent targeted document has an eligible unidentified site and canonical carrier")) return;
array<int> knowledge = {};
foreach (ERR_LocationComponent location : locations) knowledge.Insert(location.Knowledge());
int spent = m_Spent.Count();
int previousKnowledge = target.Knowledge();
IEntity savedActor = life.Actor;
UUID savedIdentity = life.AccountId;
int savedConnection = life.ConnectionId;
bool savedConnected = life.Connected;
bool savedUsedLife = life.UsedLife;
// Call the actual production implementation directly: no recursion into this
// hook. Restore synchronously before any check, positive call or return.
life.Actor = null;
super.Recover(player);
life.Actor = savedActor;
bool unchanged = m_Spent.Count() == spent && !m_Spent.Contains(candidate);
for (int index = 0; index < locations.Count(); index++)
if (!locations[index] || locations[index].Knowledge() != knowledge[index]) unchanged = false;
if (!ERR_NativeProgressionIdentityProbe.Check(unchanged, "synchronous body-mismatch fault injection neither spends the held document nor changes any discovery knowledge")) return;
if (!ERR_NativeProgressionIdentityProbe.Check(m_Mode.Players().CurrentActor(player) == life && controller.GetControlledEntity() == savedActor && life.AccountId == savedIdentity && life.ConnectionId == savedConnection && life.Connected == savedConnected && life.UsedLife == savedUsedLife, "document rejection restores exact canonical actor and unchanged used-life identity before normal recovery")) return;
super.Recover(player);
bool recovered = m_Spent.Contains(candidate) && m_Spent.Count() > spent && target.Knowledge() > previousKnowledge;
ERR_NativeProgressionIdentityProbe.CompleteDocument(ERR_NativeProgressionIdentityProbe.Check(recovered, "immediately restored ordinary recovery spends the same actual document and advances its target"));
}
}
#endif
@@ -0,0 +1,199 @@
// Physical escape from the authored 12m-square prison, including alternate
// exits. Normal lock picking remains available. No additional timer or respawn.
class ERR_PrisonEscapeRules
{
static bool Inside(float x, float z, float halfSize) { return Math.AbsFloat(x) <= halfSize && Math.AbsFloat(z) <= halfSize; }
static bool Continuous(float elapsedMs, float displacement)
{
return elapsedMs > 0 && elapsedMs <= 2500 && displacement >= 0 && displacement <= 1.5 + 12.0 * elapsedMs / 1000.0;
}
static bool Registration(bool ownedDefinition, int gates, bool bounded, bool intact) { return ownedDefinition && gates == 1 && bounded && intact; }
static bool Grounded(float gap) { return gap >= -0.5 && gap <= 2.5; }
static bool Crossing(bool armed, float previousX, float previousZ, float x, float z, float elapsedMs, float displacement)
{
return armed && Inside(previousX, previousZ, 8) && !Inside(x, z, 8) && Continuous(elapsedMs, displacement);
}
}
class ERR_PrisonEscapeSample
{
IEntity Actor;
PlayerController Controller;
int ConnectionId;
vector Position;
float ObservedAt;
bool Armed;
}
class ERR_PrisonEscapeService
{
protected ERR_GameMode m_Mode;
protected IEntity m_Gate;
protected bool m_Registered;
protected bool m_Stopped;
protected vector m_Center;
protected string m_RegistrationReason = "prison phase not entered";
protected ref map<UUID, ref ERR_PrisonEscapeSample> m_Samples = new map<UUID, ref ERR_PrisonEscapeSample>();
void ERR_PrisonEscapeService(ERR_GameMode mode) { m_Mode = mode; }
IEntity Gate() { return m_Gate; }
bool Registered() { return m_Registered; }
string RegistrationReason() { return m_RegistrationReason; }
bool GateLost()
{
if (!m_Registered) return false;
if (!m_Gate) return true;
SCR_DestructionMultiPhaseComponent damage = SCR_DestructionMultiPhaseComponent.Cast(m_Gate.FindComponent(SCR_DestructionMultiPhaseComponent));
return damage && (damage.GetDestroyed() || damage.IsDestructionQueued());
}
protected bool HasSuccessorLock(IEntity entity)
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (!manager) return false;
array<BaseUserAction> actions = {};
manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
if (lock && lock.GetOwner() == entity) return true;
}
return false;
}
// Native evaluated child prefabs need not expose the same raw prefab name as
// the authored reference. Follow the owned composition and its actual action.
protected bool FindGate(IEntity prison, out IEntity found, out int matches)
{
found = null; matches = 0;
array<IEntity> pending = {}; pending.Insert(prison);
int visited;
while (!pending.IsEmpty() && visited < 64)
{
IEntity entity = pending[0]; pending.Remove(0); visited++;
if (!entity) continue;
bool lock = HasSuccessorLock(entity);
bool door = entity.FindComponent(BaseDoorComponent) != null;
if (lock || door)
{
ResourceName raw;
if (entity.GetPrefabData()) raw = entity.GetPrefabData().GetPrefabName();
PrintFormat("Rereintroduced [prison]: gate candidate raw=%1 resolved=%2 lock=%3 door=%4 position=%5", raw, SCR_ResourceNameUtils.GetPrefabName(entity), lock, door, entity.GetOrigin());
}
if (entity != prison && lock && door) { found = entity; matches++; }
IEntity child = entity.GetChildren();
while (child)
{
// Both queued and visited nodes count against the fixed scan bound.
if (pending.Count() + visited >= 64) return false;
pending.Insert(child);
child = child.GetSibling();
}
}
return pending.IsEmpty();
}
// One initial registration from the prepared, server-owned authored site.
// No world-nearby fallback or later retry can adopt a replacement/lost gate.
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
{
m_Samples.Clear();
if (!Replication.IsServer() || m_Stopped || current != ERR_Phase.PRISON || m_Registered) return;
m_RegistrationReason = "no unique owned authored prison";
if (!m_Mode || !m_Mode.Sites()) return;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
IEntity prison;
int prisons;
foreach (ERR_LocationComponent location : locations)
{
if (!location || location.Kind() != ERR_LocationKind.PRISON) continue;
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
if (!site || site.Location != location || !site.Definition) continue;
if (site.Definition.Kind != ERR_LocationKind.PRISON || site.Definition.Prefab != "{BE6B8D56349038A6}Prefabs/ERR/Prison.et") continue;
IEntity owner = location.GetOwner();
if (!owner || !m_Mode.Sites().Owns(owner)) continue;
prison = owner; prisons++;
}
if (prisons != 1) return;
ResourceName raw;
if (prison.GetPrefabData()) raw = prison.GetPrefabData().GetPrefabName();
PrintFormat("Rereintroduced [prison]: owned composition raw=%1 resolved=%2 position=%3", raw, SCR_ResourceNameUtils.GetPrefabName(prison), prison.GetOrigin());
IEntity found;
int gates;
bool bounded = FindGate(prison, found, gates);
SCR_DestructionMultiPhaseComponent damage;
if (found) damage = SCR_DestructionMultiPhaseComponent.Cast(found.FindComponent(SCR_DestructionMultiPhaseComponent));
bool intact = damage && damage.GetDefaultHitZone() && !damage.GetDestroyed() && !damage.IsDestructionQueued();
if (damage) PrintFormat("Rereintroduced [prison]: destruction health=%1 destroyed=%2 queued=%3", damage.GetHealth(), damage.GetDestroyed(), damage.IsDestructionQueued());
if (!ERR_PrisonEscapeRules.Registration(true, gates, bounded, intact))
{
m_RegistrationReason = string.Format("gate registration refused: candidates=%1 bounded=%2 intact=%3", gates, bounded, intact);
Print("Rereintroduced [prison]: " + m_RegistrationReason);
return;
}
m_Gate = found;
m_Center = prison.GetOrigin();
m_Registered = true;
m_RegistrationReason = "one intact successor lock in owned prison";
Print("Rereintroduced [prison]: " + m_RegistrationReason);
}
void RosterChanged(int connection)
{
// Invalidate only this connection; a simultaneous new join must not erase
// another prisoner's genuine inside-to-outside crossing.
array<UUID> removed = {};
foreach (UUID account, ERR_PrisonEscapeSample sample : m_Samples)
if (sample.ConnectionId == connection) removed.Insert(account);
foreach (UUID accountToRemove : removed) m_Samples.Remove(accountToRemove);
}
void Tick(float now)
{
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode.Phase() != ERR_Phase.PRISON || !m_Mode.Active() || !m_Registered) return;
ref map<UUID, ref ERR_PrisonEscapeSample> next = new map<UUID, ref ERR_PrisonEscapeSample>();
array<ref ERR_PlayerLife> lives = {};
m_Mode.Players().Participants(lives);
PlayerManager manager = GetGame().GetPlayerManager();
foreach (ERR_PlayerLife life : lives)
{
IEntity actor = life.Actor;
if (!actor || m_Mode.Players().CurrentActor(actor) != life || !ERR_Actors.CanFight(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(actor) != m_Mode.PlayerFaction()) continue;
PlayerController controller = manager.GetPlayerController(life.ConnectionId);
if (!controller || controller.GetControlledEntity() != actor) continue;
vector position = actor.GetOrigin();
float ground = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
if (!ERR_PrisonEscapeRules.Grounded(position[1] - ground)) continue;
vector offset = position - m_Center;
ERR_PrisonEscapeSample sample = m_Samples.Get(life.AccountId);
bool armed;
if (sample && sample.Actor == actor && sample.Controller == controller)
{
float elapsed = now - sample.ObservedAt;
float displacement = vector.Distance(position, sample.Position);
bool continuous = ERR_PrisonEscapeRules.Continuous(elapsed, displacement);
vector previousOffset = sample.Position - m_Center;
if (ERR_PrisonEscapeRules.Crossing(sample.Armed, previousOffset[0], previousOffset[2], offset[0], offset[2], elapsed, displacement))
{
// An actual qualifying exit is sufficient even if the gate remains intact.
// Use the existing authority transition so cache reveal/tasks/HQ events
// remain exactly the same as successful lock picking.
m_Mode.Transition(ERR_Phase.RADIO_SEARCH);
return;
}
armed = continuous && sample.Armed;
}
if (!ERR_PrisonEscapeRules.Inside(offset[0], offset[2], 8)) continue;
if (!armed) armed = ERR_PrisonEscapeRules.Inside(offset[0], offset[2], 5.5);
ERR_PrisonEscapeSample updated = new ERR_PrisonEscapeSample();
updated.Actor = actor; updated.Controller = controller; updated.ConnectionId = life.ConnectionId;
updated.Position = position; updated.ObservedAt = now; updated.Armed = armed;
next.Insert(life.AccountId, updated);
}
m_Samples = next;
}
void Stop() { m_Stopped = true; m_Samples.Clear(); m_Gate = null; m_Registered = false; }
#ifdef WORKBENCH
bool ProbeArmed(IEntity actor)
{
ERR_PlayerLife life = m_Mode.Players().CurrentActor(actor);
if (!life) return false;
ERR_PrisonEscapeSample sample = m_Samples.Get(life.AccountId);
return sample && sample.Actor == actor && sample.Armed;
}
#endif
}