Harden base progression and cleanup; validate finite physical resupply
This commit is contained in:
@@ -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 = {};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user