Integrate startup optics and supply vehicle lock picking
This commit is contained in:
@@ -65,6 +65,7 @@ class ERR_EnemyEquipmentEntry
|
||||
IEntity Actor;
|
||||
SCR_AIGroup Group;
|
||||
SCR_AICombatComponent Combat;
|
||||
ref ERR_EnemyOptics Optics;
|
||||
int Checks;
|
||||
int SkillChecks;
|
||||
bool Rolled;
|
||||
@@ -120,6 +121,7 @@ class ERR_EnemyEquipmentEntry
|
||||
{
|
||||
Complete = true;
|
||||
Released = true;
|
||||
if (Optics) Optics.Stop();
|
||||
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
||||
Combat = null;
|
||||
// Do not interpret Stop as cancellation of an accepted native insertion.
|
||||
@@ -176,6 +178,7 @@ class ERR_EnemyEquipment
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (Known(actor) || !ERR_EnemyEquipmentRules.Eligible(m_Mode, state.Group, actor)) continue;
|
||||
ERR_EnemyEquipmentEntry entry = new ERR_EnemyEquipmentEntry(); entry.Actor = actor; entry.Group = state.Group;
|
||||
entry.Optics = new ERR_EnemyOptics(actor, state.Group);
|
||||
entry.Combat = SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent));
|
||||
if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, state.Group);
|
||||
m_Entries.Insert(entry);
|
||||
@@ -193,6 +196,7 @@ class ERR_EnemyEquipment
|
||||
if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, entry.Group);
|
||||
}
|
||||
entry.Check(m_Mode);
|
||||
if (entry.Optics) entry.Optics.Check(m_Mode);
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Owner-authorized startup optics adaptation; provenance belongs in equipment migration docs.
|
||||
// Native attachment storage owns fitting/replication; no loose optics or replacement grants.
|
||||
class ERR_EnemyOpticsInspection
|
||||
{
|
||||
int Remaining = 128;
|
||||
bool Complete = true;
|
||||
bool Visit(IEntity entity, int depth = 0)
|
||||
{
|
||||
if (!entity) return false;
|
||||
if (depth > 4 || Remaining <= 0) { Complete = false; return false; }
|
||||
Remaining--;
|
||||
if (entity.FindComponent(SCR_2DOpticsComponent) || entity.FindComponent(SCR_2DPIPSightsComponent) || entity.FindComponent(SCR_PIPSightsComponent)) return true;
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (Visit(child, depth + 1)) return true;
|
||||
if (!Complete) return false;
|
||||
child = child.GetSibling();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool Inspect(BaseWeaponComponent weapon)
|
||||
{
|
||||
if (!weapon) { Complete = false; return false; }
|
||||
if (Visit(weapon.GetOwner())) return true;
|
||||
BaseSightsComponent sights = weapon.GetAttachedSights();
|
||||
if (sights && Visit(sights.GetOwner())) return true;
|
||||
array<AttachmentSlotComponent> slots = {}; weapon.GetAttachments(slots);
|
||||
if (slots.Count() > 16) { Complete = false; return false; }
|
||||
foreach (AttachmentSlotComponent slot : slots)
|
||||
{
|
||||
if (slot && Visit(slot.GetAttachedEntity())) return true;
|
||||
if (!Complete) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_EnemyOpticsJob
|
||||
{
|
||||
IEntity Rifle;
|
||||
bool Submitted;
|
||||
bool Terminal;
|
||||
bool Confirmed;
|
||||
int Attempt = -1;
|
||||
ref array<int> Rejected = {};
|
||||
ref ERR_EnemyEquipmentCallback Operation;
|
||||
string Reason;
|
||||
}
|
||||
|
||||
class ERR_EnemyOptics
|
||||
{
|
||||
protected IEntity m_Actor;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected ref array<ref ERR_EnemyOpticsJob> m_Jobs = {};
|
||||
protected int m_Checks;
|
||||
protected bool m_Captured;
|
||||
protected bool m_Stopped;
|
||||
void ERR_EnemyOptics(IEntity actor, SCR_AIGroup group) { m_Actor = actor; m_Group = group; }
|
||||
static bool EligibleWeapon(EWeaponType type)
|
||||
{
|
||||
return type == EWeaponType.WT_SNIPERRIFLE || type == EWeaponType.WT_RIFLE || type == EWeaponType.WT_MACHINEGUN;
|
||||
}
|
||||
static void Candidates(out array<ResourceName> result)
|
||||
{
|
||||
result = {
|
||||
"{C850A33226B8F9C1}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et",
|
||||
"{D2018EDB1BBF4C88}Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et",
|
||||
"{A1D5833FF10C6503}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1M2.et",
|
||||
"{695AAA77F84EB238}Prefabs/Weapons/Attachments/Optics/1p21/Optic_1P21.et",
|
||||
"{333A794F0B55C019}Prefabs/Weapons/Attachments/Optics/m8541/Optic_M8541.et",
|
||||
"{BD496EE1B40DC510}Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et",
|
||||
"{8D4BAA235F7EDBEA}Prefabs/Weapons/Attachments/Optics/1p78/Optic_1P78.et"
|
||||
};
|
||||
}
|
||||
protected void Finish(ERR_EnemyOpticsJob job, string reason)
|
||||
{
|
||||
if (job.Terminal) return;
|
||||
job.Terminal = true; job.Reason = reason;
|
||||
ERR_HQ.Event("OPTICS", reason);
|
||||
}
|
||||
protected void Fit(ERR_GameMode mode, ERR_EnemyOpticsJob job, InventoryStorageManagerComponent inventory)
|
||||
{
|
||||
if (job.Terminal) return;
|
||||
// Re-read the canonical actor and native weapon roster before every grant.
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, m_Group, m_Actor)) { Finish(job, "startup optics cancelled: actor ownership changed"); return; }
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(m_Actor.FindComponent(BaseWeaponManagerComponent));
|
||||
array<IEntity> current = {}; if (manager) manager.GetWeaponsList(current);
|
||||
if (!job.Rifle || !current.Contains(job.Rifle)) { Finish(job, "startup rifle left original actor; no replacement"); return; }
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(job.Rifle.FindComponent(BaseWeaponComponent));
|
||||
if (!weapon || !EligibleWeapon(weapon.GetWeaponType())) { Finish(job, "original weapon no longer eligible"); return; }
|
||||
ERR_EnemyOpticsInspection inspection = new ERR_EnemyOpticsInspection();
|
||||
if (inspection.Inspect(weapon)) { job.Confirmed = true; Finish(job, "physical magnified optic preserved/confirmed"); return; }
|
||||
// Callback success is terminal even if someone removes the optic before our next observation.
|
||||
if (job.Submitted && job.Operation && job.Operation.Finished && job.Operation.Succeeded) { Finish(job, "native optic insertion completed; no replenishment"); return; }
|
||||
if (m_Checks >= 5)
|
||||
{
|
||||
if (job.Submitted && (!job.Operation || !job.Operation.Finished)) Finish(job, "startup optics deadline; unresolved insertion remains quarantined");
|
||||
else Finish(job, "startup optics deadline; compatible magnified optic not confirmed");
|
||||
return;
|
||||
}
|
||||
if (job.Submitted)
|
||||
{
|
||||
if (!job.Operation || !job.Operation.Finished) return;
|
||||
job.Rejected.Insert(job.Attempt); job.Submitted = false;
|
||||
}
|
||||
if (!inspection.Complete) { job.Reason = "existing optics inspection incomplete; no grant"; return; }
|
||||
WeaponAttachmentsStorageComponent storage = WeaponAttachmentsStorageComponent.Cast(job.Rifle.FindComponent(WeaponAttachmentsStorageComponent));
|
||||
if (!storage || storage.GetSlotsCount() > 16) return;
|
||||
array<ResourceName> candidates = {}; Candidates(candidates);
|
||||
array<int> remaining = {0, 1, 2, 3, 4, 5, 6};
|
||||
while (!remaining.IsEmpty())
|
||||
{
|
||||
int draw = Math.RandomInt(0, remaining.Count());
|
||||
int index = remaining[draw]; remaining.Remove(draw);
|
||||
ResourceName prefab = candidates[index];
|
||||
for (int slotIndex = 0; slotIndex < storage.GetSlotsCount(); slotIndex++)
|
||||
{
|
||||
InventoryStorageSlot slot = storage.GetSlot(slotIndex);
|
||||
int attempt = index * 16 + slotIndex;
|
||||
if (!slot || slot.GetAttachedEntity() || job.Rejected.Contains(attempt)) continue;
|
||||
if (!inventory.CanInsertResourceInStorage(prefab, storage, slotIndex)) continue;
|
||||
job.Attempt = attempt; job.Operation = new ERR_EnemyEquipmentCallback(); job.Submitted = true;
|
||||
bool accepted = inventory.TrySpawnPrefabToStorage(prefab, storage, slotIndex, EStoragePurpose.PURPOSE_ANY, job.Operation);
|
||||
// A synchronous success is authoritative even if the return value is inconsistent.
|
||||
if (!accepted && !(job.Operation.Finished && job.Operation.Succeeded))
|
||||
{ job.Rejected.Insert(attempt); job.Submitted = false; }
|
||||
return; // At most one native submission per original rifle per startup check.
|
||||
}
|
||||
}
|
||||
}
|
||||
bool Check(ERR_GameMode mode)
|
||||
{
|
||||
if (m_Stopped) return true;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, m_Group, m_Actor)) { Stop(); return true; }
|
||||
m_Checks++;
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(m_Actor.FindComponent(BaseWeaponManagerComponent));
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!manager || !inventory) { if (m_Checks >= 5) Stop(); return m_Stopped; }
|
||||
if (!m_Captured)
|
||||
{
|
||||
array<IEntity> current = {}; manager.GetWeaponsList(current);
|
||||
if (current.Count() > 8) { Stop(); return true; }
|
||||
array<IEntity> seen = {};
|
||||
foreach (IEntity rifle : current)
|
||||
{
|
||||
if (!rifle || rifle == m_Actor || seen.Contains(rifle)) continue;
|
||||
seen.Insert(rifle);
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent));
|
||||
if (!weapon || !EligibleWeapon(weapon.GetWeaponType())) continue;
|
||||
ERR_EnemyOpticsJob job = new ERR_EnemyOpticsJob(); job.Rifle = rifle;
|
||||
m_Jobs.Insert(job);
|
||||
}
|
||||
m_Captured = !m_Jobs.IsEmpty();
|
||||
}
|
||||
bool complete = m_Captured;
|
||||
foreach (ERR_EnemyOpticsJob job : m_Jobs)
|
||||
{
|
||||
Fit(mode, job, inventory);
|
||||
if (!job.Terminal) complete = false;
|
||||
}
|
||||
if (complete || m_Checks >= 5) Stop();
|
||||
return m_Stopped;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_EnemyOpticsJob job : m_Jobs) job.Terminal = true;
|
||||
// Keep exact-weapon tombstones/callbacks. Accepted native inserts cannot be cancelled here.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in controlled arrangement of the existing local prisoner only. Production
|
||||
// perception, radio intake, planning and downlink execution remain unchanged.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected override void Observe(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_HQReport before = unit.PendingReport;
|
||||
super.Observe(unit, now);
|
||||
ERR_NativeConvoyCoverProbe proof = ERR_NativeConvoyCoverProbe.Active();
|
||||
if (proof && unit.PendingReport && unit.PendingReport != before) proof.Observed(unit, now);
|
||||
}
|
||||
protected override void Accept(ERR_HQReport report, float now)
|
||||
{
|
||||
super.Accept(report, now);
|
||||
ERR_NativeConvoyCoverProbe proof = ERR_NativeConvoyCoverProbe.Active();
|
||||
if (!proof || !report) return;
|
||||
foreach (ERR_HQReport accepted : m_Contacts)
|
||||
if (accepted.MessageId == report.MessageId) { proof.Accepted(accepted, now); break; }
|
||||
}
|
||||
protected override void DeliverConvoyOrder(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_ConvoyCombatOrder order = unit.PendingConvoyOrder;
|
||||
ERR_HQRoute route = unit.Downlink;
|
||||
bool hadWaypoint;
|
||||
ERR_ConvoyTask task = ConvoyTask();
|
||||
if (task && !task.Vehicles.IsEmpty()) hadWaypoint = task.Vehicles[0].Waypoint != null;
|
||||
super.DeliverConvoyOrder(unit, now);
|
||||
ERR_NativeConvoyCoverProbe proof = ERR_NativeConvoyCoverProbe.Active();
|
||||
if (proof && order && route) proof.Delivered(order, route, hadWaypoint, now);
|
||||
}
|
||||
}
|
||||
class ERR_NativeConvoyCoverReport
|
||||
{
|
||||
ref ERR_HQReport Report;
|
||||
ref ERR_HQRoute Uplink;
|
||||
bool Accepted;
|
||||
}
|
||||
class ERR_NativeConvoyCoverProbe
|
||||
{
|
||||
protected static ERR_NativeConvoyCoverProbe s_Active;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_ConvoyTask m_Task;
|
||||
protected ERR_ConvoyVehicle m_Lead;
|
||||
protected IEntity m_Player;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected ERR_PlayerLife m_Life;
|
||||
protected DamageManagerComponent m_Damage;
|
||||
protected bool m_DamageWasEnabled;
|
||||
protected bool m_Protected;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Failed;
|
||||
protected bool m_Held;
|
||||
protected bool m_Resumed;
|
||||
protected int m_RouteIndex;
|
||||
protected int m_LedgerCount;
|
||||
protected float m_Deadline;
|
||||
protected float m_HeldAt;
|
||||
protected float m_ResumedAt;
|
||||
protected float m_NextLog;
|
||||
protected vector m_HoldPosition;
|
||||
protected vector m_ResumePosition;
|
||||
protected vector m_HidePosition;
|
||||
protected ref array<IEntity> m_Forces = {};
|
||||
protected ref array<ref ERR_NativeConvoyCoverReport> m_Reports = {};
|
||||
protected int m_Step;
|
||||
static bool Enabled()
|
||||
{
|
||||
string flag;
|
||||
return ERR_NativeConvoyProbe.Enabled() && System.GetCLIParam("errConvoyCover", flag) && flag == "1";
|
||||
}
|
||||
static ERR_NativeConvoyCoverProbe Active() { return s_Active; }
|
||||
void ERR_NativeConvoyCoverProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: convoy cover " + detail); return true; }
|
||||
m_Failed = true; Print("ERR SMOKE FAIL: convoy cover " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool Canonical()
|
||||
{
|
||||
return m_Player && m_Controller && m_Life && m_Mode.Players().CurrentActor(m_Player) == m_Life
|
||||
&& m_Controller == GetGame().GetPlayerController() && m_Controller.GetControlledEntity() == m_Player
|
||||
&& m_Life.Actor == m_Player && m_Life.Connected && m_Life.UsedLife;
|
||||
}
|
||||
protected bool UnchangedState()
|
||||
{
|
||||
if (!Canonical() || !m_Task || m_Task.Stopped || m_Task.Vehicles.Count() != 6
|
||||
|| m_Mode.Forces().Count() != m_LedgerCount || m_Task.Cargo.Count() != 56) return false;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
if (!vehicle || vehicle.Released || !vehicle.CrewValid(m_Mode)) return false;
|
||||
array<IEntity> current = {}; m_HQ.PhysicalForces(current);
|
||||
if (current.Count() != m_Forces.Count()) return false;
|
||||
foreach (IEntity entity : current) if (!m_Forces.Contains(entity)) return false;
|
||||
return true;
|
||||
}
|
||||
protected bool Unchanged()
|
||||
{
|
||||
if (UnchangedState()) return true;
|
||||
// Read-only, failure-only evidence. Preserve the original invariant result.
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: canonical=%1 player=%2 controller=%3 life=%4 ledger=%5 expectedLedger=%6",
|
||||
Canonical(), m_Player != null, m_Controller != null, m_Life != null, m_Mode.Forces().Count(), m_LedgerCount);
|
||||
if (!m_Task) { Print("ERR CONVOY COVER INVARIANT: missing original task"); return false; }
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: stopped=%1 reason=%2 ready=%3 vehicles=%4 cargoCallbacks=%5",
|
||||
m_Task.Stopped, m_Task.Reason, m_Task.Ready, m_Task.Vehicles.Count(), m_Task.Cargo.Count());
|
||||
for (int index = 0; index < m_Task.Vehicles.Count() && index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
if (!vehicle) { PrintFormat("ERR CONVOY COVER INVARIANT: missing vehicle record=%1", index); continue; }
|
||||
bool crewValid = vehicle.CrewValid(m_Mode);
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: vehicle=%1 released=%2 arrived=%3 crewValid=%4 entityAlive=%5 group=%6 routeIndex=%7 waypoint=%8",
|
||||
index, vehicle.Released, vehicle.Arrived, crewValid, ERR_Actors.Alive(vehicle.Entity), vehicle.Group, vehicle.RouteIndex, vehicle.Waypoint != null);
|
||||
if (crewValid) continue;
|
||||
IEntity driverVehicle;
|
||||
if (vehicle.Driver) driverVehicle = CompartmentAccessComponent.GetVehicleIn(vehicle.Driver);
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: vehicle=%1 driverConscious=%2 driverPlayer=%3 driverAboardOriginal=%4",
|
||||
index, ERR_Actors.CanFight(vehicle.Driver), ERR_Actors.Player(vehicle.Driver),
|
||||
ERR_ConvoyRules.PartOf(driverVehicle, vehicle.Entity));
|
||||
for (int memberIndex = 0; memberIndex < vehicle.Members.Count() && memberIndex < 19; memberIndex++)
|
||||
{
|
||||
IEntity member = vehicle.Members[memberIndex];
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: vehicle=%1 member=%2 actor=%3 alive=%4 player=%5 sameGroup=%6",
|
||||
index, memberIndex, member, ERR_Actors.Alive(member), ERR_Actors.Player(member), ERR_Actors.Group(member) == vehicle.Group);
|
||||
}
|
||||
}
|
||||
array<IEntity> current = {}; m_HQ.PhysicalForces(current);
|
||||
PrintFormat("ERR CONVOY COVER INVARIANT: currentForces=%1 initialForces=%2", current.Count(), m_Forces.Count());
|
||||
int differences;
|
||||
foreach (IEntity added : current)
|
||||
if (!m_Forces.Contains(added) && differences++ < 8) PrintFormat("ERR CONVOY COVER INVARIANT: newly listed physical entity=%1", added);
|
||||
differences = 0;
|
||||
foreach (IEntity missing : m_Forces)
|
||||
if (!current.Contains(missing) && differences++ < 8) PrintFormat("ERR CONVOY COVER INVARIANT: no longer listed physical entity=%1", missing);
|
||||
return false;
|
||||
}
|
||||
protected void MovePrisoner(vector position)
|
||||
{
|
||||
vector transform[4]; m_Player.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(position);
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
}
|
||||
protected ERR_NativeConvoyCoverReport Find(int id)
|
||||
{
|
||||
foreach (ERR_NativeConvoyCoverReport entry : m_Reports)
|
||||
if (entry.Report.MessageId == id) return entry;
|
||||
return null;
|
||||
}
|
||||
void Observed(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_Stopped || !unit.PendingReport || unit.PendingReport.Target != m_Player || !unit.Uplink) return;
|
||||
IEntity observer = unit.Utility.ERR_ResponseVisualObserver(unit.PendingReport, now);
|
||||
if (!observer || !m_Forces.Contains(observer) || m_Reports.Count() >= 64) return;
|
||||
ERR_NativeConvoyCoverReport entry = new ERR_NativeConvoyCoverReport();
|
||||
entry.Report = unit.PendingReport.Snapshot(); entry.Uplink = unit.Uplink; m_Reports.Insert(entry);
|
||||
}
|
||||
void Accepted(ERR_HQReport report, float now)
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
ERR_NativeConvoyCoverReport entry = Find(report.MessageId);
|
||||
if (!entry) return;
|
||||
entry.Accepted = !entry.Uplink.Failed && entry.Uplink.StartedAt >= 0
|
||||
&& entry.Uplink.Cursor >= entry.Uplink.Nodes.Count();
|
||||
}
|
||||
void Delivered(ERR_ConvoyCombatOrder order, ERR_HQRoute route, bool hadWaypoint, float now)
|
||||
{
|
||||
if (m_Stopped || !m_Lead || order.Group != m_Lead.Group
|
||||
|| m_Lead.Combat.LastCommandId != order.CommandId || route.Failed) return;
|
||||
if (!Check(route.Downlink && route.StartedAt >= 0 && route.Cursor == route.Nodes.Count() - 1,
|
||||
"original crew received its real completed downlink")) return;
|
||||
if (order.Hold)
|
||||
{
|
||||
ERR_NativeConvoyCoverReport entry = Find(order.ReportId);
|
||||
if (!Check(entry && entry.Accepted && entry.Report.Target == m_Player,
|
||||
"HOLD traces to actual original observer perception and delivered sighting")) return;
|
||||
if (m_Held) return;
|
||||
if (!Check(hadWaypoint && m_Lead.Combat.Held && !m_Lead.Waypoint,
|
||||
"delivered HOLD removed the owned native road waypoint")) return;
|
||||
m_Held = true; m_HeldAt = now; m_RouteIndex = m_Lead.RouteIndex;
|
||||
m_HoldPosition = m_Lead.Entity.GetOrigin();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Check(m_Held && !hadWaypoint && !m_Lead.Combat.Held && m_Lead.Waypoint
|
||||
&& m_Lead.RouteIndex == m_RouteIndex && now - order.ObservedAt >= 60000,
|
||||
"delivered RESUME restores one road waypoint at the preserved index after quiet evidence")) return;
|
||||
m_Resumed = true; m_ResumedAt = now; m_ResumePosition = m_Lead.Entity.GetOrigin();
|
||||
}
|
||||
}
|
||||
int Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (!Enabled() || !hq || m_Failed || m_Stopped) return -1;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_HQ = hq; m_Task = hq.ConvoyTask();
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (m_Controller) m_Player = m_Controller.GetControlledEntity();
|
||||
m_Life = m_Mode.Players().CurrentActor(m_Player);
|
||||
if (!Check(Canonical() && m_Task && m_Task.Ready && m_Task.Vehicles.Count() == 6,
|
||||
"canonical local prisoner and previously proven physical convoy selected")) return -1;
|
||||
m_Lead = m_Task.Vehicles[0];
|
||||
ERR_SiteRuntime hide = m_Mode.Sites().Find("everon-extraction-1");
|
||||
if (!Check(hide && hide.Definition && !m_Lead.Combat.Held && m_Lead.Waypoint
|
||||
&& vector.DistanceXZ(hide.Definition.Position, m_Lead.Entity.GetOrigin()) >= 800,
|
||||
"lead has a live road order and authored distant concealment destination")) return -1;
|
||||
m_HidePosition = hide.Definition.Position;
|
||||
m_Damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
||||
if (!Check(m_Damage != null, "existing prisoner native damage component found")) return -1;
|
||||
m_DamageWasEnabled = m_Damage.IsDamageHandlingEnabled(); m_Damage.EnableDamageHandling(false); m_Protected = true;
|
||||
m_HQ.PhysicalForces(m_Forces); m_LedgerCount = m_Mode.Forces().Count();
|
||||
vector vehicleTransform[4]; m_Lead.Entity.GetWorldTransform(vehicleTransform);
|
||||
s_Active = this; m_Deadline = now + 300000; m_Step = 1;
|
||||
MovePrisoner(m_Lead.Entity.GetOrigin() + vehicleTransform[0] * 20);
|
||||
Print("ERR CONVOY COVER arrangement: existing prisoner moved beside original lead; temporary damage protection; no AI, inventory, reports, timers or radio routes changed.");
|
||||
}
|
||||
if (!Unchanged()) { Check(false, "original physical forces or canonical player changed"); return -1; }
|
||||
if (now >= m_Deadline) { Check(false, "bounded sighting/hold/resume deadline"); return -1; }
|
||||
if (now >= m_NextLog)
|
||||
{
|
||||
m_NextLog = now + 10000;
|
||||
PrintFormat("ERR CONVOY COVER: step=%1 held=%2 resumed=%3 report=%4 observed=%5 routeIndex=%6 waypoint=%7 reports=%8",
|
||||
m_Step, m_Lead.Combat.Held, m_Resumed, m_Lead.Combat.ReportId, m_Lead.Combat.ObservedAt, m_Lead.RouteIndex, m_Lead.Waypoint != null, m_Reports.Count());
|
||||
}
|
||||
if (m_Step == 1 && m_Held)
|
||||
{
|
||||
if (m_Lead.Waypoint || m_Lead.RouteIndex != m_RouteIndex) { Check(false, "held native road order unexpectedly advanced"); return -1; }
|
||||
if (now - m_HeldAt < 5000) return 0;
|
||||
if (!Check(true, "owned road waypoint stayed absent and route index stable for five seconds")) return -1;
|
||||
PrintFormat("ERR CONVOY COVER held native movement=%1m (native combat movement is not suppressed)", vector.DistanceXZ(m_HoldPosition, m_Lead.Entity.GetOrigin()));
|
||||
MovePrisoner(m_HidePosition); m_Step = 2;
|
||||
Print("ERR CONVOY COVER arrangement: same prisoner moved to distant authored extraction site; native visibility and radio must expire nearby evidence.");
|
||||
}
|
||||
if (m_Step == 2 && m_Resumed)
|
||||
{
|
||||
if (vector.DistanceXZ(m_ResumePosition, m_Lead.Entity.GetOrigin()) < 20) return 0;
|
||||
if (!Check(!m_Lead.Combat.Held && now > m_ResumedAt && Unchanged(),
|
||||
"same native lead vehicle drove twenty metres after delivered RESUME; forces and cargo unchanged")) return -1;
|
||||
Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Protected && m_Damage) m_Damage.EnableDamageHandling(m_DamageWasEnabled);
|
||||
m_Protected = false;
|
||||
if (s_Active == this) s_Active = null;
|
||||
}
|
||||
void ~ERR_NativeConvoyCoverProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
@@ -59,6 +59,7 @@ class ERR_NativeConvoyRoadSurvey
|
||||
class ERR_NativeConvoyProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_NativeConvoyCoverProbe m_Cover;
|
||||
protected ref ERR_ConvoyTask m_Task;
|
||||
protected ref array<vector> m_Origins = {};
|
||||
protected ref array<IEntity> m_OriginalVehicles = {};
|
||||
@@ -165,6 +166,38 @@ class ERR_NativeConvoyProbe
|
||||
PrintFormat("ERR CONVOY MOTION: vehicle=%1 groupAction=%2 driverBehavior=%3", i, groupAction, driverBehavior);
|
||||
}
|
||||
}
|
||||
// Failure-only readout distinguishes incapacitation, ownership, native seat
|
||||
// switching and dismounts without changing the exact original-seat assertion.
|
||||
protected void SeatDiagnostic(ERR_ConvoyVehicle vehicle, int index, IEntity actor, BaseCompartmentSlot intended, bool duplicateActor, bool duplicateSeat)
|
||||
{
|
||||
IEntity aboard, occupant;
|
||||
SCR_AIGroup actualGroup;
|
||||
BaseCompartmentSlot actual;
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(actor);
|
||||
if (actor) { aboard = CompartmentAccessComponent.GetVehicleIn(actor); actualGroup = ERR_Actors.Group(actor); }
|
||||
if (intended) occupant = intended.GetOccupant();
|
||||
if (character && character.GetCompartmentAccessComponent()) actual = character.GetCompartmentAccessComponent().GetCompartment();
|
||||
int intendedType = -1, actualType = -1, actualManager = -1, actualSlot = -1;
|
||||
if (intended) intendedType = intended.GetType();
|
||||
if (actual) { actualType = actual.GetType(); actualManager = actual.GetCompartmentMgrID(); actualSlot = actual.GetCompartmentSlotID(); }
|
||||
PrintFormat("ERR CONVOY SEAT: vehicle=%1 member=%2 actor=%3 alive=%4 conscious=%5 groupMatches=%6 duplicateActor=%7 duplicateSeat=%8",
|
||||
vehicle.Index, index, actor, ERR_Actors.Alive(actor), ERR_Actors.CanFight(actor), actualGroup == vehicle.Group, duplicateActor, duplicateSeat);
|
||||
PrintFormat("ERR CONVOY SEAT: intended=%1 occupant=%2 type=%3 actual=%4 actualType=%5 manager=%6 slot=%7 sameSeat=%8",
|
||||
intended, occupant, intendedType, actual, actualType, actualManager, actualSlot, actual == intended);
|
||||
PrintFormat("ERR CONVOY SEAT: aboard=%1 originalVehicle=%2 originalVehicleTree=%3",
|
||||
aboard, vehicle.Entity, ERR_ConvoyRules.PartOf(aboard, vehicle.Entity));
|
||||
AIControlComponent control;
|
||||
AIAgent agent;
|
||||
SCR_AIUtilityComponent utility;
|
||||
if (actor) control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
if (agent) utility = SCR_AIUtilityComponent.Cast(agent.FindComponent(SCR_AIUtilityComponent));
|
||||
string behavior = "no actor utility", groupAction = "no group utility";
|
||||
if (utility) behavior = MovementAction(utility.GetCurrentBehavior());
|
||||
SCR_AIGroupUtilityComponent groupUtility = vehicle.Group.GetGroupUtilityComponent();
|
||||
if (groupUtility) groupAction = MovementAction(groupUtility.GetCurrentAction());
|
||||
PrintFormat("ERR CONVOY SEAT: actorBehavior=%1 groupAction=%2", behavior, groupAction);
|
||||
}
|
||||
protected bool Population(ERR_HQ hq)
|
||||
{
|
||||
if (!Check(m_Task.Vehicles.Count() == 6, "six original vehicles remain tracked")) return false;
|
||||
@@ -191,10 +224,11 @@ class ERR_NativeConvoyProbe
|
||||
{
|
||||
IEntity actor = vehicle.Members[seatIndex];
|
||||
BaseCompartmentSlot seat = vehicle.Seats[seatIndex];
|
||||
if (!Check(actor && ERR_Actors.CanFight(actor) && !crew.Contains(actor)
|
||||
bool occupied = actor && ERR_Actors.CanFight(actor) && !crew.Contains(actor)
|
||||
&& ERR_Actors.Group(actor) == vehicle.Group && seat && !slots.Contains(seat) && seat.GetOccupant() == actor
|
||||
&& ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(actor), vehicle.Entity),
|
||||
"original crew occupies intended native compartment " + crew.Count().ToString())) return false;
|
||||
&& ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(actor), vehicle.Entity);
|
||||
if (!occupied) SeatDiagnostic(vehicle, seatIndex, actor, seat, crew.Contains(actor), slots.Contains(seat));
|
||||
if (!Check(occupied, "original crew occupies intended native compartment " + crew.Count().ToString())) return false;
|
||||
crew.Insert(actor); slots.Insert(seat);
|
||||
bool nativeMember;
|
||||
foreach (AIAgent agent : agents) if (agent && agent.GetControlledEntity() == actor) nativeMember = true;
|
||||
@@ -247,6 +281,7 @@ class ERR_NativeConvoyProbe
|
||||
int Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (!Enabled() || !hq || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Failed) return -1;
|
||||
if (m_Step == 4) return m_Cover.Tick(hq, now);
|
||||
if (m_Deadline == 0) m_Deadline = now + 480000;
|
||||
if (now >= m_Deadline) { Check(false, "native fixture deadline"); return -1; }
|
||||
if (m_Step == 0)
|
||||
@@ -340,7 +375,13 @@ class ERR_NativeConvoyProbe
|
||||
&& m_Mode.Forces().Count() == m_InitialLedger + 6 && m_Task.Cargo.Count() == 56,
|
||||
"later repeated dispatch preserves exact task, force and cargo counts")) return -1;
|
||||
Check(true, "all six native vehicles drove at least 20m and leader at least 100m without fixture movement");
|
||||
if (ERR_NativeConvoyCoverProbe.Enabled())
|
||||
{
|
||||
m_Cover = new ERR_NativeConvoyCoverProbe(m_Mode); m_Step = 4;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
void Stop() { if (m_Cover) m_Cover.Stop(); }
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -78,6 +78,7 @@ modded class ERR_GameMode
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;
|
||||
if (ERR_NativeConvoyCoverProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000;
|
||||
if (m_ERRTestHQMemory || m_ERRTestHQBody) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
|
||||
if (m_ERRTestAircraftControl || m_ERRTestAircraftRoute || m_ERRTestAircraftField) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
|
||||
@@ -169,6 +170,7 @@ modded class ERR_GameMode
|
||||
if (m_ERRAircraftRoute) m_ERRAircraftRoute.Stop();
|
||||
if (m_ERRAircraftField) m_ERRAircraftField.Stop();
|
||||
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
|
||||
if (m_ERRConvoyProbe) m_ERRConvoyProbe.Stop();
|
||||
if (m_ERRMortarSalvo) m_ERRMortarSalvo.Stop();
|
||||
if (m_ERRMortarCourier) m_ERRMortarCourier.Stop();
|
||||
if (m_ERRMortarTransfer) m_ERRMortarTransfer.Stop();
|
||||
|
||||
@@ -73,7 +73,7 @@ ERR_ScenarioDefinition {
|
||||
DiscoverRadius 25
|
||||
RevealOnEscape 1
|
||||
Transport ERR_SiteTransport {
|
||||
Prefab "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"
|
||||
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
|
||||
Offset 0 0 -8
|
||||
Heading 180
|
||||
ClearanceRadius 4
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
Vehicle : "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et" {
|
||||
components {
|
||||
ERR_VehicleSecurityComponent "{E51E9F624DB28C37}" {
|
||||
m_LockAnchor -0.95 1.05 0.3
|
||||
}
|
||||
SCR_BaseCompartmentManagerComponent "{20FB66C5DCB8DF72}" {
|
||||
DoorInfoList {
|
||||
CompartmentDoorInfo "{60E692D208F05B56}" {
|
||||
OpenDoorAction ERR_SecureOpenVehicleDoorAction "{60B8C998D577B72E}" {}
|
||||
}
|
||||
CompartmentDoorInfo "{60E692D20B077A08}" {
|
||||
OpenDoorAction ERR_SecureOpenVehicleDoorAction "{60B8C998D577B72E}" {}
|
||||
}
|
||||
CompartmentDoorInfo "{60E692D26EE5B7B1}" {
|
||||
OpenDoorAction ERR_SecureOpenVehicleDoorAction "{60B8C998D577B72E}" {}
|
||||
}
|
||||
CompartmentDoorInfo "{60E692D26E53A3BD}" {
|
||||
OpenDoorAction ERR_SecureOpenVehicleDoorAction "{60B8C998D577B72E}" {}
|
||||
}
|
||||
}
|
||||
CompartmentSlots {
|
||||
PilotCompartmentSlot PilotCompartment {
|
||||
CompartmentAction ERR_SecureGetInAction "{5086DB209BDE8253}" {}
|
||||
}
|
||||
CargoCompartmentSlot Passenger_r01 {
|
||||
CompartmentAction ERR_SecureGetInAction "{5086CC2FCCAA457D}" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
SlotManagerComponent "{55BCE45E438E4CFF}" {
|
||||
Slots {
|
||||
RegisteringComponentSlotInfo SeatsRear {
|
||||
Prefab "{E6A7C3924851BD0F}Prefabs/ERR/Vehicles/SupplyUAZRearSeats.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionsManagerComponent "{C97BE5489221AE18}" {
|
||||
ActionContexts {
|
||||
UserActionContext "{CB61D7F309A284E5}" {
|
||||
ContextName "ERR_supply_uaz_lock"
|
||||
Position PointInfo "{AB27893C456ED10F}" {
|
||||
Offset -0.95 1.05 0.3
|
||||
}
|
||||
Radius 0.35
|
||||
Omnidirectional 1
|
||||
}
|
||||
}
|
||||
additionalActions {
|
||||
ERR_ForceVehicleLockAction "{D42B680FEC3159A7}" {
|
||||
ParentContextList { "ERR_supply_uaz_lock" }
|
||||
UIInfo SCR_ActionUIInfo "{AC35E61B8D74029F}" {
|
||||
Name "Force vehicle lock"
|
||||
}
|
||||
Duration 4
|
||||
VisibilityRange 2
|
||||
"Sort Priority" 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
GenericEntity : "{780AAF0A51AEE61A}Prefabs/Vehicles/Wheeled/UAZ469/VehParts/Seats/UAZ469_seats_rear.et" {
|
||||
components {
|
||||
SCR_BaseCompartmentManagerComponent "{2CD4A765A7D52986}" {
|
||||
CompartmentSlots {
|
||||
CargoCompartmentSlot Passenger_l02 {
|
||||
CompartmentAction ERR_SecureGetInAction "{5086CC2FCCAA457D}" {}
|
||||
}
|
||||
CargoCompartmentSlot Passenger_r02 {
|
||||
CompartmentAction ERR_SecureGetInAction "{5086CC2FCCAA457D}" {}
|
||||
}
|
||||
CargoCompartmentSlot Passenger_m02 {
|
||||
CompartmentAction ERR_SecureGetInAction "{5086CC2FCCAA457D}" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{E6A7C3924851BD0F}Prefabs/ERR/Vehicles/SupplyUAZRearSeats.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
ERR_NativeDiscoveryMapProbe.Stop();
|
||||
ERR_NativeProgressionIdentityProbe.Stop();
|
||||
if (m_ERRTransportProbe) m_ERRTransportProbe.Stop();
|
||||
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
|
||||
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
|
||||
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
|
||||
@@ -627,7 +628,7 @@ modded class ERR_GameMode
|
||||
}
|
||||
if (m_ERRExtractionCase == "transport-loss")
|
||||
{
|
||||
m_ERRTransportProbe = new ERR_NativeTransportProbe(this);
|
||||
m_ERRTransportProbe = new ERR_NativeTransportProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 300;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ERR_NativeFactionCatalogProbe
|
||||
if (!Check(catalogManager && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE, "US") && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM, "USSR"), "global catalog manager retains faction vehicle and inventory access")) return false;
|
||||
ERR_SiteRuntime depot = mode.Sites().Find("everon-supply-2");
|
||||
Vehicle helicopter = mode.ExtractionVehicle();
|
||||
if (!Check(depot && depot.Transport && ERR_Actors.Alive(depot.Transport) && depot.Transport.GetPrefabData().GetPrefabName() == UAZ(), "physical finite depot vehicle is supported UAZ")) return false;
|
||||
if (!Check(depot && depot.Transport && ERR_Actors.Alive(depot.Transport) && depot.Transport.GetPrefabData().GetPrefabName() == "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et", "physical finite depot vehicle is successor supply UAZ")) return false;
|
||||
if (!Check(helicopter && ERR_Actors.Alive(helicopter) && helicopter.GetPrefabData().GetPrefabName() == Mi8() && helicopter.FindComponent(HelicopterControllerComponent), "physical extraction aircraft is supported Mi8")) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (!Check(mode.Extraction().Seats(seats) >= 12, "physical aircraft still has required capacity")) return false;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
class ERR_NativeTransportProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected ref ERR_NativeVehicleSecurityProbe m_SecurityProbe;
|
||||
protected bool m_SecurityChecked;
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected CarControllerComponent m_Controller;
|
||||
@@ -13,11 +16,13 @@ class ERR_NativeTransportProbe
|
||||
protected BaseCompartmentSlot m_Passenger;
|
||||
protected int m_Step;
|
||||
protected float m_Wait;
|
||||
void ERR_NativeTransportProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void ERR_NativeTransportProbe(ERR_GameMode mode, IEntity player) { m_Mode = mode; m_Player = player; }
|
||||
void Stop() { if (m_SecurityProbe) m_SecurityProbe.Stop(); }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: transport " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: transport " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected bool DefinitionChecks()
|
||||
@@ -61,6 +66,18 @@ class ERR_NativeTransportProbe
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (!m_SecurityChecked)
|
||||
{
|
||||
if (!m_SecurityProbe)
|
||||
{
|
||||
ERR_SiteRuntime securitySite = m_Mode.Sites().Find("everon-supply-2");
|
||||
if (!Check(securitySite && securitySite.Transport, "security prelude uses existing supply transport")) return -1;
|
||||
m_SecurityProbe = new ERR_NativeVehicleSecurityProbe(m_Mode, m_Player, securitySite.Transport);
|
||||
}
|
||||
int result = m_SecurityProbe.Tick(now);
|
||||
if (result <= 0) return result;
|
||||
m_SecurityChecked = true;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#ifdef WORKBENCH
|
||||
// Explicit transport-loss prelude. Diagnostic direct callbacks, not held input,
|
||||
// client action RPC, manual boarding, multiplayer or late-join replication proof.
|
||||
class ERR_NativeVehicleSecurityProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected ERR_VehicleSecurityComponent m_Security;
|
||||
protected ERR_ForceVehicleLockAction m_Breach;
|
||||
protected ERR_SecureGetInAction m_DriverAction;
|
||||
protected DamageManagerComponent m_Damage;
|
||||
protected bool m_PreviousDamageEnabled;
|
||||
protected bool m_DamageChanged;
|
||||
protected bool m_Moved;
|
||||
protected bool m_Stopped;
|
||||
protected vector m_Original[4];
|
||||
protected vector m_Destination;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
protected int m_Step;
|
||||
void ERR_NativeVehicleSecurityProbe(ERR_GameMode mode, IEntity player, Vehicle vehicle)
|
||||
{
|
||||
m_Mode = mode; m_Player = player; m_Vehicle = vehicle;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: vehicle-security " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: vehicle-security " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
// Restore only the original body whose state this fixture changed. Never
|
||||
// touch a replacement body/controller after loss of canonical ownership.
|
||||
if (m_Damage && m_DamageChanged) m_Damage.EnableDamageHandling(m_PreviousDamageEnabled);
|
||||
m_DamageChanged = false;
|
||||
m_Damage = null;
|
||||
if (!m_Moved || !m_Player || !m_Mode || !m_Mode.Players()) return;
|
||||
if (!m_Mode.Players().CurrentActor(m_Player)) return;
|
||||
if (!ERR_Actors.CanFight(m_Player) || CompartmentAccessComponent.GetVehicleIn(m_Player)) return;
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(m_Player);
|
||||
if (character) character.Teleport(m_Original);
|
||||
m_Moved = false;
|
||||
}
|
||||
protected bool DriverAvailable()
|
||||
{
|
||||
bool available = m_DriverAction.CanBePerformedScript(m_Player);
|
||||
if (available) return true;
|
||||
m_DriverAction.ERR_ProbeRejectedEntry(m_Player);
|
||||
// A direct diagnostic call has no selected UI context. Bind only the unique
|
||||
// actual driver door if the stock resolver confirms the context is missing.
|
||||
if (m_DriverAction.ERR_ProbeResolvedDoor(m_Player) != -1) return false;
|
||||
return m_DriverAction.ERR_ProbeDeclaredDriverDoor(m_Player);
|
||||
}
|
||||
protected bool Inspect()
|
||||
{
|
||||
m_Security = ERR_VehicleSecurityComponent.Find(m_Vehicle);
|
||||
if (!Check(m_Security && m_Security.GetOwner() == m_Vehicle && m_Security.Locked(), "authored supply vehicle initially has its locked successor component")) return false;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (manager) manager.GetCompartments(seats);
|
||||
if (!Check(seats.Count() == 5, "native root manager aggregates all five original seats")) return false;
|
||||
int front, rear;
|
||||
foreach (BaseCompartmentSlot slot : seats)
|
||||
{
|
||||
if (!Check(slot && !slot.GetOccupant(), "original seat is present and initially empty")) return false;
|
||||
ERR_SecureGetInAction action = ERR_SecureGetInAction.Cast(slot.GetGetInAction());
|
||||
if (!Check(action && action.HasLocalEffectOnlyScript(), "native seat contains scoped successor local entry action")) return false;
|
||||
if (!Check(ERR_VehicleSecurityComponent.Find(action.GetOwner()) == m_Security, "seat action resolves root security through physical hierarchy")) return false;
|
||||
if (!Check(!action.CanBePerformedScript(m_Player), "locked entry availability refuses the canonical prisoner")) return false;
|
||||
action.PerformAction(action.GetOwner(), m_Player);
|
||||
if (action.GetOwner() == m_Vehicle) front++; else rear++;
|
||||
if (SCR_CompartmentAccessComponent.GetCompartmentType(slot) == ECompartmentType.PILOT) m_DriverAction = action;
|
||||
}
|
||||
if (!Check(front == 2 && rear == 3 && m_DriverAction, "two root and three attached rear entry substitutions are all present")) return false;
|
||||
BaseActionsManagerComponent actionsManager = BaseActionsManagerComponent.Cast(m_Vehicle.FindComponent(BaseActionsManagerComponent));
|
||||
array<BaseUserAction> actions = {};
|
||||
if (actionsManager) actionsManager.GetActionsList(actions);
|
||||
array<ERR_SecureOpenVehicleDoorAction> doors = {};
|
||||
foreach (BaseUserAction candidate : actions)
|
||||
{
|
||||
if (!candidate || candidate.GetOwner() != m_Vehicle) continue;
|
||||
ERR_ForceVehicleLockAction breach = ERR_ForceVehicleLockAction.Cast(candidate);
|
||||
if (breach) m_Breach = breach;
|
||||
ERR_SecureOpenVehicleDoorAction door = ERR_SecureOpenVehicleDoorAction.Cast(candidate);
|
||||
if (door && !doors.Contains(door)) doors.Insert(door);
|
||||
}
|
||||
if (!Check(doors.Count() == 4 && m_Breach, "four scoped open-door actions and held breach action exist in native prefab")) return false;
|
||||
foreach (ERR_SecureOpenVehicleDoorAction door : doors)
|
||||
{
|
||||
if (!Check(door.HasLocalEffectOnlyScript() && !door.CanBePerformedScript(m_Player), "native local door action refuses locked use")) return false;
|
||||
door.PerformAction(door.GetOwner(), m_Player);
|
||||
}
|
||||
if (!Check(!m_Breach.HasLocalEffectOnlyScript() && m_Breach.CheckOnServerFirstScript() && !m_Breach.CanBroadcastScript(), "breach action advertises nonlocal server-first single execution")) return false;
|
||||
if (!Check(!m_Security.TryBreach(null) && m_Security.Locked(), "missing current player cannot directly breach native component")) return false;
|
||||
string reason;
|
||||
if (!Check(!m_Security.CanBreach(m_Player, reason), "canonical prisoner at original distant position cannot breach")) return false;
|
||||
Print("ERR VEHICLE SECURITY DISTANT REASON: " + reason);
|
||||
return true;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (m_Deadline == 0) m_Deadline = now + 30000;
|
||||
if (!m_Mode || !m_Mode.Active() || !m_Mode.Players() || !m_Mode.Players().CurrentActor(m_Player))
|
||||
{
|
||||
Check(false, "canonical admitted prisoner and active mission retained"); return -1;
|
||||
}
|
||||
if (!ERR_Actors.CanFight(m_Player) || !ERR_Actors.Alive(m_Vehicle) || now > m_Deadline)
|
||||
{
|
||||
Check(false, "living actors and bounded probe deadline retained"); return -1;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(!CompartmentAccessComponent.GetVehicleIn(m_Player), "canonical prisoner is on foot before diagnostic relocation")) return -1;
|
||||
if (!Inspect()) return -1;
|
||||
m_Damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
||||
if (!Check(m_Damage && ChimeraCharacter.Cast(m_Player), "canonical prisoner supports protected diagnostic repositioning")) return -1;
|
||||
m_PreviousDamageEnabled = m_Damage.IsDamageHandlingEnabled();
|
||||
m_Damage.EnableDamageHandling(false);
|
||||
m_DamageChanged = true;
|
||||
m_Player.GetWorldTransform(m_Original);
|
||||
vector transform[4]; m_Player.GetWorldTransform(transform);
|
||||
m_Destination = ERR_Actors.Ground(m_Vehicle.CoordToParent("-2 0 0.3"));
|
||||
transform[3] = m_Destination;
|
||||
m_Moved = true;
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
m_Step = 1; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (vector.Distance(m_Player.GetOrigin(), m_Destination) > 0.75) return 0;
|
||||
if (!Check(!CompartmentAccessComponent.GetVehicleIn(m_Player), "locked direct entry callbacks left the prisoner outside")) return -1;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
for (int index = 0; index < 4; index++)
|
||||
if (!Check(manager && !manager.IsDoorOpen(index), "locked direct door callbacks left actual native door closed")) return -1;
|
||||
string reason;
|
||||
bool canBreach = m_Security.CanBreach(m_Player, reason);
|
||||
PrintFormat("ERR VEHICLE SECURITY REACH: player=%1 vehicle=%2 available=%3 reason=%4", m_Player.GetOrigin(), m_Vehicle.GetOrigin(), canBreach, reason);
|
||||
if (!Check(canBreach && m_Breach.CanBePerformedScript(m_Player), "configured native stationary vehicle passes actual driver-lock distance and LOS")) return -1;
|
||||
m_Breach.PerformAction(m_Player, m_Player);
|
||||
if (!Check(m_Security.Locked(), "wrong native action owner cannot unlock")) return -1;
|
||||
if (!Check(m_Security.TryBreach(m_Player) && !m_Security.Locked(), "direct canonical server breach unlocks the existing vehicle")) return -1;
|
||||
if (!Check(!m_Security.TryBreach(m_Player) && !m_Security.Locked(), "duplicate completion does not relock or repeat breach")) return -1;
|
||||
if (!Check(!m_Breach.CanBeShownScript(m_Player), "completed held action disappears after unlock")) return -1;
|
||||
// This invokes ordinary native eligibility, not forced scripted boarding.
|
||||
if (!Check(DriverAvailable(), "unlocked driver seat passes ordinary native boarding availability")) return -1;
|
||||
Stop();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// Failure logger reads the same protected stock-action caches and computed door.
|
||||
// The separate explicit-context fallback temporarily binds a declared native
|
||||
// driver door and restores it; faction, reservations and admission are untouched.
|
||||
modded class ERR_SecureGetInAction
|
||||
{
|
||||
int ERR_ProbeResolvedDoor(IEntity user) { return GetRelevantDoorIndex(user); }
|
||||
bool ERR_ProbeDeclaredDriverDoor(IEntity user)
|
||||
{
|
||||
if (GetRelevantDoorIndex(user) != -1) return false;
|
||||
BaseCompartmentSlot seat = GetCompartmentSlot();
|
||||
if (!seat || !m_CompartmentManager) return false;
|
||||
if (SCR_CompartmentAccessComponent.GetCompartmentType(seat) != ECompartmentType.PILOT) return false;
|
||||
array<int> doors = {};
|
||||
seat.GetAvailableDoorIndices(doors);
|
||||
if (doors.Count() != 1 || !m_CompartmentManager.GetDoorInfo(doors[0])) return false;
|
||||
int previous = GetCompartmentDoorInfoIndex();
|
||||
SetCompartmentDoorInfoIndex(doors[0]);
|
||||
// Run the unchanged successor + vanilla eligibility chain with that exact
|
||||
// native seat's declared door. No GetInVehicle call or eligibility bypass.
|
||||
bool available = CanBePerformedScript(user);
|
||||
PrintFormat("ERR VEHICLE ENTRY EXPLICIT CONTEXT: declaredDriverDoor=%1 previous=%2 available=%3", doors[0], previous, available);
|
||||
if (!available) ERR_ProbeRejectedEntry(user);
|
||||
SetCompartmentDoorInfoIndex(previous);
|
||||
return available;
|
||||
}
|
||||
|
||||
void ERR_ProbeRejectedEntry(IEntity user)
|
||||
{
|
||||
Print("ERR VEHICLE ENTRY REJECT: reason=" + GetCannotPerformReason());
|
||||
BaseCompartmentSlot seat = GetCompartmentSlot();
|
||||
SCR_ChimeraCharacter character = SCR_ChimeraCharacter.Cast(user);
|
||||
PrintFormat("ERR VEHICLE ENTRY OBJECTS: owner=%1 seat=%2 character=%3 manager=%4 damage=%5", GetOwner(), seat, character, m_CompartmentManager, m_DamageManager);
|
||||
if (m_DamageManager) PrintFormat("ERR VEHICLE ENTRY DAMAGE: state=%1", m_DamageManager.GetState());
|
||||
if (!seat || !character) return;
|
||||
SCR_CharacterControllerComponent controller = SCR_CharacterControllerComponent.Cast(character.GetCharacterController());
|
||||
CompartmentAccessComponent access = character.GetCompartmentAccessComponent();
|
||||
if (controller) PrintFormat("ERR VEHICLE ENTRY ACTOR: loitering=%1 access=%2", controller.IsLoitering(), access);
|
||||
if (access) PrintFormat("ERR VEHICLE ENTRY TRANSITION: in=%1 gettingIn=%2 gettingOut=%3", access.IsInCompartment(), access.IsGettingIn(), access.IsGettingOut());
|
||||
Faction playerFaction = character.GetFaction();
|
||||
Vehicle vehicle = Vehicle.Cast(SCR_EntityHelper.GetMainParent(seat.GetOwner(), true));
|
||||
Faction vehicleFaction;
|
||||
if (vehicle) vehicleFaction = vehicle.GetFaction();
|
||||
string playerKey, vehicleKey;
|
||||
bool hostile;
|
||||
if (playerFaction) playerKey = playerFaction.GetFactionKey();
|
||||
if (vehicleFaction) vehicleKey = vehicleFaction.GetFactionKey();
|
||||
if (playerFaction && vehicleFaction) hostile = playerFaction.IsFactionEnemy(vehicleFaction);
|
||||
PrintFormat("ERR VEHICLE ENTRY FACTIONS: player=%1 vehicle=%2 hostile=%3", playerKey, vehicleKey, hostile);
|
||||
SCR_VehicleFactionAffiliationComponent affiliation;
|
||||
if (vehicle) affiliation = SCR_VehicleFactionAffiliationComponent.Cast(vehicle.FindComponent(SCR_VehicleFactionAffiliationComponent));
|
||||
if (affiliation) PrintFormat("ERR VEHICLE ENTRY AFFILIATION: current=%1 default=%2 occupied=%3", affiliation.GetAffiliatedFactionKey(), affiliation.GetDefaultFactionKey(), affiliation.IsVehicleOccupied());
|
||||
PrintFormat("ERR VEHICLE ENTRY SEAT: occupant=%1 reserved=%2 reservedByCaller=%3 accessible=%4", seat.GetOccupant(), seat.IsReserved(), seat.IsReservedBy(user), seat.IsCompartmentAccessible());
|
||||
if (m_pLockComp) PrintFormat("ERR VEHICLE ENTRY BASELOCK: locked=%1 reason=%2", m_pLockComp.IsLocked(user, seat), m_pLockComp.GetCannotPerformReason(user));
|
||||
else Print("ERR VEHICLE ENTRY BASELOCK: absent");
|
||||
float supplies;
|
||||
bool suppliesKnown;
|
||||
if (m_ResourceComp) suppliesKnown = SCR_ResourceSystemHelper.GetStoredResources(m_ResourceComp, supplies);
|
||||
bool blocksSupplies;
|
||||
if (m_CompartmentManager) blocksSupplies = m_CompartmentManager.BlockSuppliesIfOccupied();
|
||||
PrintFormat("ERR VEHICLE ENTRY SUPPLIES: cachedResource=%1 blocks=%2 known=%3 amount=%4", m_ResourceComp, blocksSupplies, suppliesKnown, supplies);
|
||||
int door = GetRelevantDoorIndex(user);
|
||||
PrintFormat("ERR VEHICLE ENTRY DOOR: stored=%1 resolved=%2", GetCompartmentDoorInfoIndex(), door);
|
||||
if (!m_CompartmentManager || !access) return;
|
||||
if (door >= 0 && m_CompartmentManager.GetDoorInfo(door))
|
||||
PrintFormat("ERR VEHICLE ENTRY RESOLVED DOOR: blockedByUser=%1 doorUser=%2 canGetIn=%3", m_CompartmentManager.IsGetInAndOutBlockedByDoorUser(door), m_CompartmentManager.GetDoorUser(door), access.CanGetInVehicleViaDoor(seat.GetOwner(), m_CompartmentManager, door));
|
||||
else Print("ERR VEHICLE ENTRY RESOLVED DOOR: missing or invalid context; no indexed native query issued");
|
||||
array<int> doors = {};
|
||||
seat.GetAvailableDoorIndices(doors);
|
||||
if (doors.Count() > 8) { Print("ERR VEHICLE ENTRY DOORS: unexpected count above diagnostic bound"); return; }
|
||||
foreach (int index : doors)
|
||||
{
|
||||
if (index < 0 || !m_CompartmentManager.GetDoorInfo(index))
|
||||
{
|
||||
PrintFormat("ERR VEHICLE ENTRY CANDIDATE DOOR: invalid declared index=%1; no indexed native query issued", index);
|
||||
continue;
|
||||
}
|
||||
PrintFormat("ERR VEHICLE ENTRY CANDIDATE DOOR: index=%1 blockedByUser=%2 canGetIn=%3", index, m_CompartmentManager.IsGetInAndOutBlockedByDoorUser(index), access.CanGetInVehicleViaDoor(seat.GetOwner(), m_CompartmentManager, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -11,7 +11,7 @@ class ERR_ObjectiveText
|
||||
}
|
||||
static string Extraction(float radius)
|
||||
{
|
||||
return string.Format("The map marks the initial helicopter pickup site. Board the helicopter and fly beyond %1 kilometres from the prison. To load an unconscious prisoner, place them on the ground beside the grounded, stationary helicopter and use 'Load casualty into extraction helicopter' on them. Living prisoners aboard escape; dead or absent prisoners do not.", radius / 1000.0);
|
||||
return string.Format("The map marks the initial helicopter pickup site. A prisoner must pilot the helicopter. Gather everyone you intend to rescue before flying beyond %1 kilometres from the prison: crossing that boundary ends the round for everyone. Only living prisoners aboard escape, including unconscious passengers; anyone left behind does not escape. To load an unconscious prisoner, place them on the ground beside the grounded, stationary helicopter and use 'Load casualty into extraction helicopter' on them.", radius / 1000.0);
|
||||
}
|
||||
static string RadioFeedbackMessage(ERR_RadioFeedback feedback)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,13 @@ modded class SCR_PlayerController
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
// A new/reclaimed character needs the current objective again even if the
|
||||
// phase has not changed. The existing monitor still owns delivery/retries.
|
||||
if (to && from != to)
|
||||
{
|
||||
m_ERRGuidePhase = -1;
|
||||
m_ERRGuideMessage = "";
|
||||
}
|
||||
ERR_StartGuide();
|
||||
}
|
||||
protected override void ERR_OnObserverMonitorStarted()
|
||||
|
||||
@@ -166,6 +166,8 @@ class ERR_SiteService
|
||||
CarControllerComponent controller = CarControllerComponent.Cast(site.Transport.FindComponent(CarControllerComponent));
|
||||
if (!controller) return Fail("transport must be wheeled: " + site.Definition.Id);
|
||||
controller.SetPersistentHandBrake(true);
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(site.Transport);
|
||||
if (security && !security.Configure()) return Fail("transport security configuration: " + site.Definition.Id);
|
||||
return true;
|
||||
}
|
||||
protected bool TransportReady(ERR_SiteRuntime site)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Native held progress/cancellation is authored by Duration in the prefab.
|
||||
// Server-first execution revalidates the current admitted body and physical
|
||||
// conditions. It does not independently attest four seconds of server hold time.
|
||||
class ERR_ForceVehicleLockAction : ScriptedUserAction
|
||||
{
|
||||
override bool HasLocalEffectOnlyScript() { return false; }
|
||||
override bool CheckOnServerFirstScript() { return true; }
|
||||
override bool CanBroadcastScript() { return false; }
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(GetOwner());
|
||||
return security && security.Locked();
|
||||
}
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
SetCannotPerformReason(string.Empty);
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(GetOwner());
|
||||
if (!security) { SetCannotPerformReason("Vehicle lock is unavailable"); return false; }
|
||||
string reason;
|
||||
bool allowed = security.CanBreach(user, reason);
|
||||
SetCannotPerformReason(reason);
|
||||
return allowed;
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (!Replication.IsServer() || pOwnerEntity != GetOwner()) return;
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(pOwnerEntity);
|
||||
if (security) security.TryBreach(pUserEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Both native parent classes are LOCAL-effect actions. Keep native boarding and
|
||||
// door animation behavior; these scoped guards run on the executing client.
|
||||
class ERR_SecureGetInAction : SCR_GetInUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
SetCannotPerformReason(string.Empty);
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(GetOwner());
|
||||
if (security && security.Locked())
|
||||
{
|
||||
SetCannotPerformReason("Vehicle locked: hold Force vehicle lock at the driver door");
|
||||
return false;
|
||||
}
|
||||
return super.CanBePerformedScript(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (pOwnerEntity != GetOwner() || !CanBePerformedScript(pUserEntity)) return;
|
||||
super.PerformAction(pOwnerEntity, pUserEntity);
|
||||
}
|
||||
}
|
||||
class ERR_SecureOpenVehicleDoorAction : SCR_OpenVehicleDoorUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
SetCannotPerformReason(string.Empty);
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(GetOwner());
|
||||
if (security && security.Locked())
|
||||
{
|
||||
SetCannotPerformReason("Vehicle locked: hold Force vehicle lock at the driver door");
|
||||
return false;
|
||||
}
|
||||
return super.CanBePerformedScript(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (pOwnerEntity != GetOwner() || !CanBePerformedScript(pUserEntity)) return;
|
||||
super.PerformAction(pOwnerEntity, pUserEntity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
class ERR_VehicleSecurityComponentClass : ScriptComponentClass {}
|
||||
|
||||
// Only attached to the successor supply UAZ. This is an interaction lock, not
|
||||
// an anti-cheat boundary around the engine's local vehicle-entry API.
|
||||
class ERR_VehicleSecurityComponent : ScriptComponent
|
||||
{
|
||||
[RplProp()] protected bool m_Configured;
|
||||
[RplProp()] protected bool m_Breached;
|
||||
// Same authored local point as the dedicated exterior action context.
|
||||
[Attribute("-0.95 1.05 0.3")] protected vector m_LockAnchor;
|
||||
|
||||
static ERR_VehicleSecurityComponent Find(IEntity entity)
|
||||
{
|
||||
while (entity)
|
||||
{
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Cast(entity.FindComponent(ERR_VehicleSecurityComponent));
|
||||
if (security) return security;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
bool Locked() { return !m_Breached; }
|
||||
bool Configure()
|
||||
{
|
||||
if (!Replication.IsServer()) return false;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || mode.Phase() != ERR_Phase.PREPARING) return false;
|
||||
if (!Vehicle.Cast(GetOwner()) || !ERR_Actors.Alive(GetOwner())) return false;
|
||||
// Duplicate registration never resets a breached or configured lock.
|
||||
if (m_Configured) return true;
|
||||
m_Configured = true;
|
||||
Replication.BumpMe();
|
||||
return true;
|
||||
}
|
||||
static bool Motion(float speed, float angularSpeed)
|
||||
{
|
||||
return speed >= 0 && speed <= 0.25 && angularSpeed >= 0 && angularSpeed <= 0.1;
|
||||
}
|
||||
static bool Reach(float distance)
|
||||
{
|
||||
return distance >= 0 && distance <= 2;
|
||||
}
|
||||
protected bool EmptyVehicle()
|
||||
{
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(GetOwner().FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
manager.GetCompartments(seats);
|
||||
if (seats.IsEmpty() || seats.Count() > 8) return false;
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
{
|
||||
if (!seat || seat.GetOccupant()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool ClearReach(IEntity user, vector anchor)
|
||||
{
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||
if (!character || !GetGame()) return false;
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = character.AimingPosition();
|
||||
trace.End = anchor;
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = user;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
|
||||
IEntity hit = trace.TraceEnt;
|
||||
while (hit)
|
||||
{
|
||||
if (hit == GetOwner()) return true;
|
||||
hit = hit.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool CanBreach(IEntity user, out string reason)
|
||||
{
|
||||
reason = string.Empty;
|
||||
if (!m_Configured) { reason = "Vehicle lock is not ready"; return false; }
|
||||
if (!Locked()) { reason = "Vehicle lock has already been forced"; return false; }
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active()) { reason = "Mission is not active"; return false; }
|
||||
if (!ERR_Actors.Player(user) || !ERR_Actors.CanFight(user)) { reason = "You must be alive and conscious"; return false; }
|
||||
if (CompartmentAccessComponent.GetVehicleIn(user)) { reason = "Leave the vehicle first"; return false; }
|
||||
if (!ERR_Actors.Alive(GetOwner())) { reason = "Vehicle is destroyed"; return false; }
|
||||
Physics physics = GetOwner().GetPhysics();
|
||||
if (!physics || !Motion(physics.GetVelocity().Length(), physics.GetAngularVelocity().Length())) { reason = "Vehicle must be stationary"; return false; }
|
||||
if (!EmptyVehicle()) { reason = "Vehicle must be unoccupied"; return false; }
|
||||
vector anchor = GetOwner().CoordToParent(m_LockAnchor);
|
||||
if (!Reach(vector.Distance(user.GetOrigin(), anchor))) { reason = "Move closer to the driver door lock"; return false; }
|
||||
if (!ClearReach(user, anchor)) { reason = "The driver door lock is obstructed"; return false; }
|
||||
return true;
|
||||
}
|
||||
bool TryBreach(IEntity user)
|
||||
{
|
||||
if (!Replication.IsServer()) return false;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Players() || !mode.Players().CurrentActor(user)) return false;
|
||||
string reason;
|
||||
if (!CanBreach(user, reason)) return false;
|
||||
m_Breached = true;
|
||||
Replication.BumpMe();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user