Advance standalone admission, finite artillery and native validation

This commit is contained in:
2026-09-19 09:02:31 +02:00
parent af5146a317
commit 40a474934c
532 changed files with 56456 additions and 199 deletions
@@ -0,0 +1,8 @@
SCR_AIGroup : "{8DE0C0830FE0C33D}Prefabs/Groups/OPFOR/Group_USSR_Base.et" {
m_aUnitPrefabSlots {
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,339 @@
class ERR_HQUnit
{
int Id;
SCR_AIGroup Group;
SCR_AIGroupUtilityComponent Utility;
IEntity Radio;
float NextReport;
ref ERR_HQRoute Uplink;
ref ERR_HQReport PendingReport;
ref ERR_HQRoute Downlink;
ref ERR_HQReport PendingOrder;
ref ERR_HQReport Assignment;
AIWaypoint Waypoint;
bool ReservedForSupport;
bool Eligible(ERR_GameMode mode)
{
if (ReservedForSupport || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
array<AIAgent> agents = {};
Group.GetAgents(agents);
bool survivor;
foreach (AIAgent agent : agents)
{
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) continue;
survivor = true;
if (CompartmentAccessComponent.GetVehicleIn(agent.GetControlledEntity())) return false;
}
return survivor;
}
void Release()
{
if (Waypoint)
{
if (Group) Group.RemoveWaypoint(Waypoint);
SCR_EntityHelper.DeleteEntityAndChildren(Waypoint);
}
Waypoint = null;
Assignment = null;
PendingOrder = null;
Downlink = null;
}
}
class ERR_HQ : ERR_CommandService
{
protected ERR_GameMode m_Mode;
protected ref ERR_MortarSystem m_Mortars;
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();
protected ref ERR_HQJournal m_Journal = new ERR_HQJournal();
protected int m_NextId;
protected float m_NextNetwork;
protected float m_NextAssignment;
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); }
static ERR_HQ Get()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode) return null;
return ERR_HQ.Cast(mode.Command());
}
static bool MissionActive()
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Active();
}
static void Event(string kind, string message, int messageId = 0)
{
ERR_HQ hq = Get();
if (hq) hq.Journal().Write(kind, message, messageId);
}
ERR_MortarSystem Mortars() { return m_Mortars; }
bool PrepareSupport() { return m_Mortars.Prepare(); }
void ReserveGroup(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group) continue;
unit.Release();
unit.ReservedForSupport = true;
}
}
void PhysicalForces(out array<IEntity> entities)
{
entities.Clear();
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Group) continue;
array<AIAgent> agents = {};
unit.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity();
if (!entities.Contains(actor)) entities.Insert(actor);
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(actor);
if (vehicle && !entities.Contains(vehicle)) entities.Insert(vehicle);
}
}
m_Mortars.AppendGuns(entities);
}
bool MortarConnected(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units)
if (unit.Group == group && unit.Radio) return m_Network.Route(unit.Radio) != null;
return false;
}
ERR_HQRoute Downlink(SCR_AIGroup group, string kind, string detail, int sourceId)
{
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group || !unit.Radio) continue;
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
if (!route) return null;
route.MessageId = m_Journal.Message();
route.Payload = string.Format("%1 from report M%2; %3", kind, sourceId, detail);
m_Journal.Write("ORDER QUEUED", route.Payload, route.MessageId);
return route;
}
return null;
}
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
{
contacts.Clear();
foreach (ERR_HQReport contact : m_Contacts)
if (ERR_MortarPrediction.Fresh(contact, now)) contacts.Insert(contact.Snapshot());
}
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();
}
override void RegisterGroup(SCR_AIGroup group)
{
if (!Replication.IsServer() || !group || m_Units.Count() >= 128) return;
foreach (ERR_HQUnit known : m_Units) if (known.Group == group) return;
SCR_AIGroupUtilityComponent utility = SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent));
if (!utility || group.GetFaction() != m_Mode.EnemyFaction()) return;
ERR_HQUnit unit = new ERR_HQUnit();
unit.Id = ++m_NextId;
unit.Group = group;
unit.Utility = utility;
int phase = unit.Id % 5;
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
m_Units.Insert(unit);
m_Journal.Write("REGISTER", string.Format("group %1", unit.Id));
}
override void UnregisterGroup(SCR_AIGroup group)
{
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group) continue;
unit.Release();
unit.Group = null;
unit.Utility = null;
}
}
protected int Assigned()
{
int total;
foreach (ERR_HQUnit unit : m_Units) if (unit.Assignment || unit.PendingOrder) total++;
return total;
}
protected void IndexRadios()
{
m_Network.Clear();
foreach (ERR_HQUnit unit : m_Units)
{
unit.Radio = null;
if (!unit.Group || unit.Group.GetPlayerCount(true) > 0 || unit.Group.GetFaction() != m_Mode.EnemyFaction()) continue;
array<AIAgent> agents = {};
unit.Group.GetAgents(agents);
int best;
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity actor = agent.GetControlledEntity();
int capability = ERR_HQRadio.Capability(actor);
if (capability <= 0) continue;
m_Network.Add(actor, unit.Group, unit.Id, capability);
if (capability > best) { unit.Radio = actor; best = capability; }
}
}
m_Network.Build();
}
protected void Observe(ERR_HQUnit unit, float now)
{
if (!unit.Utility || !unit.Radio || unit.Uplink || now < unit.NextReport) return;
unit.NextReport = now + m_Mode.Settings().Get("reportIntervalSeconds") * 1000;
array<ref ERR_HQVisualContact> samples = {};
ERR_HQObservation.Gather(unit.Utility, now, samples);
if (samples.IsEmpty()) return;
ERR_HQVisualContact seen = samples[0];
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route) return;
ERR_HQReport report = new ERR_HQReport();
report.MessageId = m_Journal.Message();
report.Target = seen.Entity;
report.Source = unit.Group;
report.Position = seen.Position;
report.ObservedAt = seen.ObservedAt;
ERR_HQObservation.Describe(report, samples, now);
route.MessageId = report.MessageId;
route.Payload = report.Description(now);
unit.PendingReport = report;
unit.Uplink = route;
m_Journal.Write("REPORT", route.Payload, report.MessageId);
}
protected void Accept(ERR_HQReport report, float now)
{
if (!report || !ERR_MissionRules.ContactFresh(report.ObservedAt, now)) return;
for (int i = 0; i < m_Contacts.Count(); i++)
{
ERR_HQReport previous = m_Contacts[i];
if (previous.Target != report.Target) continue;
if (previous.ObservedAt >= report.ObservedAt) return;
float seconds = (report.ObservedAt - previous.ObservedAt) / 1000;
if (seconds > 0 && seconds < 30 && vector.DistanceXZ(previous.Position, report.Position) < 500)
{
report.Velocity = (report.Position - previous.Position) / seconds;
report.HasVelocity = report.Velocity.Length() <= 100;
}
m_Contacts[i] = report.Snapshot();
return;
}
if (m_Contacts.Count() >= 64) m_Contacts.Remove(0);
m_Contacts.Insert(report.Snapshot());
}
protected void DeliverOrder(ERR_HQUnit unit, float now)
{
ERR_HQReport order = unit.PendingOrder;
if (!order || !ERR_MissionRules.ContactFresh(order.ObservedAt, now) || !unit.Eligible(m_Mode)) { unit.Release(); return; }
SCR_AIGroupPerception perception = unit.Utility.m_Perception;
if (perception && !perception.m_aTargets.IsEmpty()) { unit.Release(); return; }
int deliveryId;
if (unit.Downlink) deliveryId = unit.Downlink.MessageId;
unit.Release();
vector position = order.Position;
if (order.HasVelocity) position = ERR_HQReport.Predict(position, order.Velocity, (now - order.ObservedAt) / 1000);
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), ERR_Actors.Ground(position)));
if (!waypoint) { m_Journal.Write("ORDER FAILED", "waypoint spawn failed", order.MessageId); return; }
unit.Waypoint = waypoint;
unit.Assignment = order;
unit.Group.AddWaypointAt(waypoint, 0);
m_Journal.Write("ORDER DELIVERED", string.Format("group %1 from report M%2: %3", unit.Id, order.MessageId, order.Description(now)), deliveryId);
}
protected void Assign(float now)
{
int limit = m_Mode.Settings().Get("supportSquadLimit");
int radius = m_Mode.Settings().Get("responseRadiusMetres");
int assigned = Assigned();
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Assignment && (!ERR_MissionRules.ContactFresh(unit.Assignment.ObservedAt, now) || !unit.Eligible(m_Mode) || assigned > limit))
{
unit.Release(); assigned--;
}
if (unit.PendingOrder && (!ERR_MissionRules.ContactFresh(unit.PendingOrder.ObservedAt, now) || !unit.Eligible(m_Mode) || assigned > limit))
{
unit.Release(); assigned--;
}
}
foreach (ERR_HQReport report : m_Contacts)
{
if (!ERR_MissionRules.ContactFresh(report.ObservedAt, now)) continue;
foreach (ERR_HQUnit unit : m_Units)
{
if (assigned >= limit) return;
if (unit.Assignment || unit.PendingOrder || !unit.Eligible(m_Mode) || !unit.Radio) continue;
if (vector.DistanceXZ(unit.Group.GetOrigin(), report.Position) > radius) continue;
SCR_AIGroupPerception perception = unit.Utility.m_Perception;
if (perception && !perception.m_aTargets.IsEmpty()) continue;
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
if (!route) continue;
unit.PendingOrder = report.Snapshot();
unit.Downlink = route;
route.MessageId = m_Journal.Message();
route.Payload = "order from report M" + report.MessageId.ToString();
assigned++;
}
}
}
override void Tick(float now)
{
if (!Replication.IsServer()) return;
if (now >= m_NextNetwork) { IndexRadios(); m_NextNetwork = now + 5000; }
for (int i = m_Contacts.Count() - 1; i >= 0; i--)
if (!ERR_MissionRules.ContactFresh(m_Contacts[i].ObservedAt, now)) m_Contacts.Remove(i);
for (int i = m_Units.Count() - 1; i >= 0; i--)
{
ERR_HQUnit unit = m_Units[i];
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 (unit.Downlink)
{
int delivered = unit.Downlink.Advance(now, m_Journal);
if (delivered == 1) DeliverOrder(unit, now);
else if (delivered < 0) unit.Release();
}
if (!unit.Group && !unit.Uplink) { unit.Release(); m_Units.Remove(i); continue; }
Observe(unit, now);
}
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
m_Mortars.Tick(now);
m_Journal.Flush();
}
override void Stop()
{
m_Mortars.Stop();
foreach (ERR_HQUnit unit : m_Units) unit.Release();
m_Units.Clear();
m_Contacts.Clear();
m_Network.Clear();
m_Journal.Write("STOP", "HQ stopped; tactical orders released");
m_Journal.Flush();
}
}
modded class ERR_GameMode
{
protected override ERR_CommandService CreateCommandService() { return new ERR_HQ(this); }
}
@@ -0,0 +1,101 @@
// Server-owned audit stream. The file contains every event; chat reads a bounded
// recent-history window with explicit gap notices for clients that fall behind.
class ERR_HQJournal
{
protected ref array<string> m_Recent = {};
protected ref array<string> m_Pending = {};
protected int m_Sequence;
protected int m_Message;
protected static int s_Part;
protected static int s_Session;
protected string m_Session;
void ERR_HQJournal()
{
s_Session++;
m_Session = string.Format("%1-%2", System.GetUnixTime(), s_Session);
}
protected float m_RetryAt;
protected int m_Lost;
string Path()
{
if (s_Part == 0)
return "$logs:rereintroduced-hq.log";
return "$logs:rereintroduced-hq-" + s_Part.ToString() + ".log";
}
int Message()
{
return ++m_Message;
}
int Sequence()
{
return m_Sequence;
}
void Write(string kind, string message, int messageId = 0)
{
if (!Replication.IsServer())
return;
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
if (m_Recent.Count() >= 300)
m_Recent.Remove(0);
m_Recent.Insert(line);
if (m_Pending.Count() < 4096)
m_Pending.Insert(line);
else
m_Lost++;
}
void Read(int after, out array<string> lines, out int cursor, int limit = 8)
{
lines.Clear();
cursor = Math.Clamp(after, 0, m_Sequence);
int first = m_Sequence - m_Recent.Count();
if (cursor < first)
{
lines.Insert(string.Format("HQ log: %1 older entries are outside chat history; see %2", first - cursor, Path()));
cursor = first;
}
int characters;
for (int i = cursor - first; i < m_Recent.Count() && lines.Count() < Math.Clamp(limit, 1, 40); i++)
{
// Bound each menu RPC without skipping unread records. Chat still
// requests eight; the live menu can drain a busy journal faster.
if (characters + m_Recent[i].Length() > 16000 && !lines.IsEmpty())
break;
lines.Insert(m_Recent[i]);
characters += m_Recent[i].Length();
cursor++;
}
}
void Flush()
{
if (m_Pending.IsEmpty() || !Replication.IsServer())
return;
float now = GetGame().GetWorld().GetWorldTime();
if (now < m_RetryAt)
return;
FileHandle file = FileIO.OpenFile(Path(), FileMode.APPEND);
if (!file)
{
m_RetryAt = now + 30000;
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
return;
}
foreach (string line : m_Pending)
file.WriteLine(line);
if (m_Lost > 0)
file.WriteLine(string.Format("HQ log: %1 entries lost while file was unavailable and buffer full", m_Lost));
bool rotate = file.GetLength() >= 5242880;
file.Close();
m_Pending.Clear();
m_Lost = 0;
if (rotate)
s_Part++;
}
}
@@ -0,0 +1,241 @@
// Radio propagation is simulated using actual living carriers and inventory.
// A manpack is the HQ uplink; short-range hops are limited to 500 m and three
// retransmissions. No virtual relay is fabricated when the network is broken.
class ERR_HQRadioNode
{
IEntity Carrier;
SCR_AIGroup Group;
int UnitId;
int Capability;
protected float m_CheckedAt = -1;
protected int m_LiveCapability;
bool Valid(bool manpack = false)
{
ERR_GameMode factions = ERR_GameMode.Get();
if (!Carrier || !Group || Group.GetPlayerCount(true) > 0 || !factions || Group.GetFaction() != factions.EnemyFaction() || ERR_Actors.Group(Carrier) != Group)
return false;
int required = 1;
if (manpack)
required = 2;
float now = GetGame().GetWorld().GetWorldTime();
if (m_CheckedAt < 0 || now < m_CheckedAt || now - m_CheckedAt >= 1000)
{
m_CheckedAt = now;
m_LiveCapability = ERR_HQRadio.Capability(Carrier);
}
return m_LiveCapability >= required;
}
string Label()
{
return "unit " + UnitId.ToString();
}
}
class ERR_HQRoute
{
ref array<ref ERR_HQRadioNode> Nodes = {};
bool Downlink;
int Cursor;
bool Sending;
float Due;
vector SentFrom;
float StartedAt = -1;
int MessageId;
string Payload;
bool Failed;
// -1 failed, 0 travelling, 1 delivered. Each hop really gates delivery.
int Advance(float now, ERR_HQJournal journal)
{
if (Failed)
return -1;
if (StartedAt < 0)
{
StartedAt = now;
if (Downlink)
Cursor = -1; // first leg is HQ to the manpack
}
if (now - StartedAt > 30000)
return Fail(journal, "route timed out");
int target = Cursor + 1;
if (Sending)
{
if (now < Due)
return 0;
if (target < Nodes.Count())
{
if (!Nodes[target].Valid(Downlink && target == 0))
return Fail(journal, "receiving carrier lost radio, died or changed ownership");
if (Cursor >= 0 && vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin()) > 500)
return Fail(journal, "receiver moved beyond the 500m link");
journal.Write("RECEIVE", Nodes[target].Label() + " received from " + SenderLabel(), MessageId);
}
else
journal.Write("RECEIVE", "HQ received from " + SenderLabel(), MessageId);
Cursor++;
Sending = false;
if ((!Downlink && Cursor >= Nodes.Count()) || (Downlink && Cursor == Nodes.Count() - 1))
return 1;
target = Cursor + 1;
}
if (Nodes.IsEmpty())
return Fail(journal, "no physical radio route");
if (Cursor >= 0 && !Nodes[Cursor].Valid(!Downlink && Cursor == Nodes.Count() - 1))
return Fail(journal, "transmitting carrier lost radio, died or changed ownership");
string recipient = "HQ";
string link = "long-range";
if (target < Nodes.Count())
{
if (!Nodes[target].Valid(Downlink && target == 0))
return Fail(journal, "next relay unavailable");
recipient = Nodes[target].Label();
if (Cursor >= 0)
{
SentFrom = Nodes[Cursor].Carrier.GetOrigin();
float distance = vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin());
if (distance > 500)
return Fail(journal, "next relay beyond 500m");
link = Math.Round(distance).ToString() + "m short-range";
}
}
journal.Write("TRANSMIT", SenderLabel() + " -> " + recipient + " (" + link + ")", MessageId);
Sending = true;
Due = now + 2000;
return 0;
}
protected string SenderLabel()
{
if (Cursor < 0)
return "HQ";
return Nodes[Cursor].Label();
}
protected int Fail(ERR_HQJournal journal, string reason)
{
Failed = true;
journal.Write("LOST", reason + "; " + Payload, MessageId);
return -1;
}
}
class ERR_HQNetwork
{
protected ref array<ref ERR_HQRadioNode> m_Nodes = {};
protected ref array<int> m_Parents = {};
protected ref array<int> m_Depths = {};
protected ref map<IEntity, int> m_Indices = new map<IEntity, int>();
protected ref map<string, ref array<int>> m_Cells = new map<string, ref array<int>>();
bool Overflow;
void Clear()
{
m_Nodes.Clear();
m_Parents.Clear();
m_Depths.Clear();
m_Indices.Clear();
m_Cells.Clear();
Overflow = false;
}
protected string Cell(int x, int z)
{
return string.Format("%1:%2", x, z);
}
void Add(IEntity carrier, SCR_AIGroup group, int unitId, int capability)
{
if (m_Indices.Contains(carrier))
return;
if (m_Nodes.Count() >= 2048)
{
Overflow = true;
return;
}
ERR_HQRadioNode node = new ERR_HQRadioNode();
node.Carrier = carrier;
node.Group = group;
node.UnitId = unitId;
node.Capability = capability;
int index = m_Nodes.Count();
m_Nodes.Insert(node);
m_Indices.Insert(carrier, index);
m_Parents.Insert(-2);
m_Depths.Insert(99);
vector position = carrier.GetOrigin();
string key = Cell(Math.Floor(position[0] / 500), Math.Floor(position[2] / 500));
array<int> cell = m_Cells.Get(key);
if (!cell)
{
cell = {};
m_Cells.Insert(key, cell);
}
cell.Insert(index);
}
void Build()
{
array<int> queue = {};
int checks;
for (int root = 0; root < m_Nodes.Count(); root++)
{
if (m_Nodes[root].Capability == 2)
{
m_Parents[root] = -1;
m_Depths[root] = 0;
queue.Insert(root);
}
}
for (int head = 0; head < queue.Count(); head++)
{
int from = queue[head];
if (m_Depths[from] >= 3)
continue;
vector position = m_Nodes[from].Carrier.GetOrigin();
int x = Math.Floor(position[0] / 500);
int z = Math.Floor(position[2] / 500);
for (int dx = -1; dx <= 1; dx++)
{
for (int dz = -1; dz <= 1; dz++)
{
array<int> neighbors = m_Cells.Get(Cell(x + dx, z + dz));
if (!neighbors)
continue;
foreach (int candidate : neighbors)
{
if (++checks > 100000)
{
Overflow = true;
return;
}
if (m_Parents[candidate] != -2 || vector.DistanceXZ(position, m_Nodes[candidate].Carrier.GetOrigin()) > 500)
continue;
m_Parents[candidate] = from;
m_Depths[candidate] = m_Depths[from] + 1;
queue.Insert(candidate);
}
}
}
}
}
ERR_HQRoute Route(IEntity caller, bool downlink = false)
{
int index;
if (!m_Indices.Find(caller, index) || m_Parents[index] == -2)
return null;
ERR_HQRoute route = new ERR_HQRoute();
route.Downlink = downlink;
while (index >= 0)
{
if (downlink)
route.Nodes.InsertAt(m_Nodes[index], 0);
else
route.Nodes.Insert(m_Nodes[index]);
index = m_Parents[index];
}
return route;
}
}
@@ -0,0 +1,199 @@
// These samples exist only at intake. HQ never revisits their entity loadouts.
class ERR_HQVisualContact
{
IEntity Entity;
vector Position;
float ObservedAt;
bool Escapist;
string Travel = "unknown";
string Weapon = "unknown";
}
class ERR_HQObservation
{
// Require current native line of sight before reading visible equipment.
static bool IsClearSighting(float age, float traceFraction, float exposure)
{
return age >= 0 && age <= 0.5 && traceFraction >= 1 && exposure > 0;
}
static bool CanIdentifyWeapon(float distance, float exposure)
{
return distance >= 0 && distance <= 150 && exposure >= 0.5;
}
static string WeaponCategory(EWeaponType type)
{
switch (type)
{
case EWeaponType.WT_HANDGUN: return "pistol";
case EWeaponType.WT_RIFLE: return "rifle";
case EWeaponType.WT_SNIPERRIFLE: return "sniper rifle";
case EWeaponType.WT_MACHINEGUN: return "machine gun";
case EWeaponType.WT_ROCKETLAUNCHER: return "rocket launcher";
case EWeaponType.WT_GRENADELAUNCHER: return "grenade launcher";
case EWeaponType.WT_FRAGGRENADE: return "grenade";
case EWeaponType.WT_SMOKEGRENADE: return "smoke grenade";
}
return "unidentified weapon";
}
static ERR_HQVisualContact Capture(BaseTarget target, float now)
{
if (!target || target.GetTargetCategory() != ETargetCategory.ENEMY || !IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
return null;
DamageManagerComponent damage = target.GetDamageManagerComponent();
if (damage && damage.IsDestroyed())
return null;
IEntity entity = target.GetTargetEntity();
if (!entity)
return null;
ERR_HQVisualContact seen = new ERR_HQVisualContact();
seen.Entity = entity;
seen.Position = target.GetLastSeenPosition();
seen.ObservedAt = now - target.GetTimeSinceSeen() * 1000;
SCR_ChimeraCharacter character = SCR_ChimeraCharacter.Cast(entity);
if (character)
{
if (GetGame().GetPlayerManager())
seen.Escapist = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(entity) > 0;
seen.Travel = "on foot";
if (CompartmentAccessComponent.GetVehicleIn(entity))
seen.Travel = "in vehicle";
// Read only the drawn weapon; never enumerate weapons or inventory.
if (CanIdentifyWeapon(target.GetDistance(), target.GetExposure()))
{
CharacterControllerComponent controller = CharacterControllerComponent.Cast(entity.FindComponent(CharacterControllerComponent));
if (controller)
{
IEntity heldItem = controller.GetCurrentItemInHands();
BaseWeaponComponent weapon;
if (heldItem)
weapon = BaseWeaponComponent.Cast(heldItem.FindComponent(BaseWeaponComponent));
seen.Weapon = "none drawn";
if (weapon)
seen.Weapon = WeaponCategory(weapon.GetWeaponType());
}
}
}
else if (Vehicle.Cast(entity))
{
seen.Travel = "vehicle";
if (target.GetUnitType() == EAIUnitType.UnitType_Aircraft)
seen.Travel = "aircraft";
}
return seen;
}
static void Gather(SCR_AIGroupUtilityComponent utility, float now, out array<ref ERR_HQVisualContact> samples)
{
samples.Clear();
if (!utility)
return;
array<BaseTarget> visible = {};
utility.ERR_HQGetVisualTargets(visible);
foreach (BaseTarget target : visible)
{
ERR_HQVisualContact seen = Capture(target, now);
if (!seen)
continue;
ERR_HQVisualContact existing;
foreach (ERR_HQVisualContact sample : samples)
{
if (sample.Entity == seen.Entity)
{
existing = sample;
break;
}
}
if (!existing)
{
if (samples.Count() < 64)
samples.Insert(seen);
}
else if (seen.ObservedAt > existing.ObservedAt || (seen.ObservedAt == existing.ObservedAt && existing.Weapon == "unknown" && seen.Weapon != "unknown"))
{
existing.Position = seen.Position;
existing.ObservedAt = seen.ObservedAt;
existing.Travel = seen.Travel;
existing.Weapon = seen.Weapon;
}
}
}
static void Describe(ERR_HQReport report, array<ref ERR_HQVisualContact> samples, float now)
{
// New observations must not silently inherit old equipment/travel details.
report.SeenEscapists = -1;
report.TravelMode = "unknown";
report.SeenWeapons = "unknown";
report.DetailsObservedAt = -1;
if (report.Distress)
return;
ERR_HQVisualContact anchor;
foreach (ERR_HQVisualContact sample : samples)
{
if (sample.Entity == report.Target && now - sample.ObservedAt >= 0 && now - sample.ObservedAt <= 500)
{
anchor = sample;
break;
}
}
if (!anchor)
return;
report.DetailsObservedAt = anchor.ObservedAt;
report.TravelMode = anchor.Travel;
report.SeenWeapons = anchor.Weapon;
array<IEntity> counted = {};
array<string> weapons = {};
string travel;
foreach (ERR_HQVisualContact sighting : samples)
{
if (!sighting.Escapist || !sighting.Entity || counted.Contains(sighting.Entity) || now - sighting.ObservedAt < 0 || now - sighting.ObservedAt > 500 || vector.DistanceXZ(anchor.Position, sighting.Position) > 100)
continue;
counted.Insert(sighting.Entity);
if (travel.IsEmpty())
travel = sighting.Travel;
else if (travel != sighting.Travel)
travel = "mixed travel";
if (sighting.Weapon != "unknown" && !weapons.Contains(sighting.Weapon) && weapons.Count() < 4)
weapons.Insert(sighting.Weapon);
}
if (counted.IsEmpty())
return; // A visible vehicle does not reveal its hidden occupants.
report.SeenEscapists = counted.Count();
// A vehicle anchor remains a vehicle report, even with people near it.
if (anchor.Escapist)
report.TravelMode = travel;
report.SeenWeapons = "unknown";
if (!weapons.IsEmpty())
{
report.SeenWeapons = weapons[0];
for (int i = 1; i < weapons.Count(); i++)
report.SeenWeapons += ", " + weapons[i];
}
}
}
modded class SCR_AIGroupUtilityComponent
{
void ERR_HQGetVisualTargets(out array<BaseTarget> targets)
{
targets.Clear();
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
{
if (!info || !info.m_Perception)
continue;
AIAgent observer = AIAgent.Cast(info.GetOwner());
if (!observer || !ERR_Actors.CanFight(observer.GetControlledEntity()))
continue;
array<BaseTarget> own = {};
info.m_Perception.GetTargetsList(own, ETargetCategory.ENEMY);
foreach (BaseTarget target : own)
{
if (target && ERR_HQObservation.IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
targets.Insert(target);
}
}
}
}
@@ -0,0 +1,144 @@
// A radio's category is protected in the stock gadget component.
modded class SCR_RadioComponent
{
bool ERR_IsHQManpack()
{
return m_iRadioCategory == ERadioCategory.MANPACK;
}
}
class ERR_HQRadio
{
// 0: no radio, 1: basic radio, 2: manpack. Inventory is checked live.
static int Capability(IEntity character)
{
if (!ERR_Actors.CanFight(character))
return 0;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));
if (!inventory)
return 0;
array<IEntity> items = {};
inventory.GetItems(items);
int result;
foreach (IEntity item : items)
{
if (!item || !item.FindComponent(BaseRadioComponent))
continue;
result = 1;
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
if (gadget && gadget.ERR_IsHQManpack())
return 2;
}
return result;
}
}
// HQ uses entity identity only for matching observations, never for its origin.
// Visible details are captured at intake and copied as scalar snapshots.
// Reports and orders contain immutable observed coordinates and timestamps.
class ERR_HQReport
{
int MessageId;
IEntity Target;
SCR_AIGroup Source;
vector Position;
vector Velocity;
float ObservedAt = -1;
float LastSent = -60000;
float PerceptionStamp = -1;
bool HasVelocity;
bool Engaged;
bool Distress;
int SeenEscapists = -1;
string TravelMode = "unknown";
string SeenWeapons = "unknown";
float DetailsObservedAt = -1;
ERR_HQReport Snapshot()
{
ERR_HQReport copy = new ERR_HQReport();
copy.MessageId = MessageId;
copy.Target = Target;
copy.Source = Source;
copy.Position = Position;
copy.Velocity = Velocity;
copy.ObservedAt = ObservedAt;
copy.Engaged = Engaged;
copy.Distress = Distress;
copy.HasVelocity = HasVelocity;
copy.SeenEscapists = SeenEscapists;
copy.TravelMode = TravelMode;
copy.SeenWeapons = SeenWeapons;
copy.DetailsObservedAt = DetailsObservedAt;
return copy;
}
string TravelDirection()
{
if (!HasVelocity)
return "unknown";
if (Velocity.Length() < 0.5)
return "stationary";
float x = Math.AbsFloat(Velocity[0]);
float z = Math.AbsFloat(Velocity[2]);
string northSouth = "N";
string eastWest = "E";
if (Velocity[2] < 0)
northSouth = "S";
if (Velocity[0] < 0)
eastWest = "W";
if (x < z * 0.414214)
return northSouth;
if (z < x * 0.414214)
return eastWest;
return northSouth + eastWest;
}
string Description(float now, bool brief = false)
{
if (Distress)
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
string count = "count unknown";
if (SeenEscapists >= 0)
count = "at least " + SeenEscapists.ToString() + " spotted nearby";
string motion = "speed/direction unknown";
if (HasVelocity)
{
float speed = Math.Round(Velocity.Length() * 10) / 10.0;
float azimuth = Math.Atan2(Velocity[0], Velocity[2]) * Math.RAD2DEG;
if (azimuth < 0)
azimuth += 360;
int heading = Math.Round(azimuth);
motion = string.Format("%1 km/h, azimuth %2 degrees (%3)", Math.Round(speed * 3.6), heading % 360, TravelDirection());
}
string details = count + "; " + TravelMode + "; " + motion + "; seen weapons: " + SeenWeapons;
if (brief)
return details;
string velocity = "unknown";
if (HasVelocity)
velocity = string.Format("[%1,%2] m/s", Math.Round(Velocity[0] * 10) / 10.0, Math.Round(Velocity[2] * 10) / 10.0);
return string.Format("X=%1 Z=%2: %3; vector X/Z=%4 (%5s ago)", Math.Round(Position[0]), Math.Round(Position[2]), details, velocity, Math.Round(Math.Max(0, now - ObservedAt) / 1000));
}
static vector Predict(vector position, vector velocity, float seconds)
{
vector offset = velocity * Math.Clamp(seconds, 0, 20);
offset[1] = 0;
if (offset.Length() > 300)
offset = offset.Normalized() * 300;
return position + offset;
}
int Priority(float now)
{
if (Distress)
return 0;
if (now - ObservedAt <= 15000)
{
if (Engaged)
return 3;
return 2;
}
return 1;
}
}
@@ -0,0 +1,31 @@
// An idle rotor or zero speed alone is normal during takeoff, landing and reload.
// Confirm prolonged grounding, but recognize actual loss of flight capability.
class ERR_HelicopterAvailability
{
protected float m_fGroundedSince = -1;
string Check(float now, float spawnedAt, bool destroyed, bool disabled, bool livingPilot, bool playerControlled, bool servicing, float height, float speed)
{
if (destroyed) return "destroyed";
if (now - spawnedAt < 20000)
{
m_fGroundedSince = -1;
return "";
}
if (disabled) return "engine, rotor or airframe disabled";
if (playerControlled)
{
m_fGroundedSince = -1;
return "";
}
if (!livingPilot) return "no living pilot";
if (servicing || height > 5 || speed > 2)
{
m_fGroundedSince = -1;
return "";
}
if (m_fGroundedSince < 0) m_fGroundedSince = now;
if (now - m_fGroundedSince >= 60000) return "grounded for 60 seconds";
return "";
}
}
@@ -0,0 +1,461 @@
// Migrated from the independently authored AI extension; successor-owned physical groups.
// Each site/phase has its own repeat limiter. Changed reasons and lifecycle
// events are immediate; only identical polling results are summarized.
class ERR_MortarTraceState
{
string LastReason;
float NextLog;
int Suppressed;
bool Due(string reason, float now)
{
if (reason != LastReason || now >= NextLog)
{
LastReason = reason;
NextLog = now + 30000;
return true;
}
Suppressed++;
return false;
}
}
class ERR_MortarBattery
{
static const ResourceName GUN = "{D1FFE458E8AC4BDB}Prefabs/Weapons/Mortars/2B14/Mortar_2B14.et";
static const ResourceName SHELL = "{98EC9C526AFBA282}Prefabs/Weapons/Ammo/Ammo_Shell_82mm_HE_O832DU.et";
static const ResourceName CREW = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
static const ResourceName RADIO_OPERATOR = "{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et";
IEntity RadioOperator;
IEntity Gun;
SCR_AIGroup Group;
ref SCR_AIStaticArtilleryActivity Activity;
ref ERR_HQReport Report;
ref ERR_HQRoute RadioOrder;
ref ERR_HQReport PendingRadioReport;
ref ERR_HQReport PendingSupplyReport;
protected ref ERR_MortarReadiness m_Readiness;
protected ref ERR_MortarSupplyTransfer m_Transfer;
protected ref array<IEntity> m_AttemptedShells = {};
protected bool m_Stopped;
vector Position;
vector Impact;
vector AimOffset;
int AimedShot = -1;
float FlightTime;
float AimedAt = -1;
float AirspaceUntil;
vector AirspaceImpact;
float AirspaceRadius = 600;
float OrderedAt;
float NextOrder;
float SpawnedAt;
bool StartupActivated;
bool Released;
bool Rejected;
bool Claimed; // keeps the world slot occupied after the battery is destroyed
int SiteId;
int OrderId;
int RadioId;
int SourceId;
int OrderShots;
int TotalShots;
bool CommandOpen;
string LastFailure;
string SpawnFailure;
vector LastCandidate;
ref map<string, int> SearchCounts = new map<string, int>();
ref map<string, ref ERR_MortarTraceState> Traces = new map<string, ref ERR_MortarTraceState>();
void Audit(string kind, string detail)
{
ERR_HQ.Event("MORTAR " + kind, string.Format("site=%1 order=%2 radio=M%3 source=M%4 position=%5; %6", SiteId, OrderId, RadioId, SourceId, Position, detail), RadioId);
}
void Trace(string phase, string reason, string detail)
{
ERR_MortarTraceState state;
if (!Traces.Find(phase, state))
{
state = new ERR_MortarTraceState();
Traces.Insert(phase, state);
}
string previous = state.LastReason;
if (!state.Due(reason, GetGame().GetWorld().GetWorldTime()))
return;
Audit(phase, string.Format("reason=%1; %2; previous=%3 repeated polls omitted=%4", reason, detail, previous, state.Suppressed));
state.Suppressed = 0;
}
void FlushTraces()
{
foreach (string phase, ERR_MortarTraceState state : Traces)
{
if (state.Suppressed > 0)
Audit("REPEATS", string.Format("phase=%1 reason=%2 repeated polls omitted=%3", phase, state.LastReason, state.Suppressed));
}
Traces.Clear();
}
string OperationalReason()
{
if (m_Stopped) return "battery stopped";
if (!Gun) return "gun missing";
if (!SpawnFailure.IsEmpty()) return SpawnFailure;
if (!Group || !Group.GetGroupUtilityComponent()) return "crew group missing";
if (Group.GetFaction() != ERR_GameMode.Get().EnemyFaction()) return "crew changed faction";
if (Group.GetPlayerCount(true) > 0) return "player in crew group";
if (Group.GetAgentsCount() == 0) return "crew group empty";
DamageManagerComponent damage = DamageManagerComponent.Cast(Gun.FindComponent(DamageManagerComponent));
if (damage && damage.IsDestroyed()) return "gun destroyed";
return "";
}
void BeginRequest(ERR_HQReport report, float now)
{
Cancel("superseded by new HQ order");
FlushTraces();
OrderId++;
RadioId = 0;
SourceId = report.MessageId;
OrderShots = 0;
Released = false;
CommandOpen = true;
OrderedAt = now;
LastFailure = "";
Audit("COMMAND", string.Format("requested rounds=%1 HE radius=%2m; %3", ERR_MortarSalvo.ROUNDS, ERR_MortarSalvo.RADIUS_METRES, report.Description(now)));
}
void ObserveProjectile(IEntity projectileEntity, IEntity firingCharacter, bool playerGunner)
{
if (!projectileEntity)
{
Audit("FIRE CALLBACK", "native OnWeaponFired had no projectile; no confirmed shot counted");
return;
}
TotalShots++;
if (CommandOpen && Released && !playerGunner)
{
OrderShots++;
// Only a physical projectile unlocks the next load. It must be aimed again.
Released = false;
AimedAt = -1;
Audit("FIRED", string.Format("physical projectile=%1 origin=%2 order shots=%3 total shots=%4 predicted impact=%5 predicted flight=%6s gunner=%7 (impact not observed)", projectileEntity, projectileEntity.GetOrigin(), OrderShots, TotalShots, Impact, FlightTime, firingCharacter));
}
else
Audit("FIRED UNASSIGNED", string.Format("physical projectile=%1 origin=%2 player gunner=%3 order open=%4 committed=%5 total shots=%6; no active HQ order attribution", projectileEntity, projectileEntity.GetOrigin(), playerGunner, CommandOpen, Released, TotalShots));
}
void ReserveShellAirspace(float now)
{
// Enclose every still-airborne shell, including an earlier, longer flight.
// A later round must not move/shrink the existing exclusion or deadline.
if (now >= AirspaceUntil)
{
AirspaceImpact = Impact;
AirspaceRadius = 600;
}
AirspaceRadius = Math.Max(AirspaceRadius, vector.DistanceXZ(AirspaceImpact, Impact) + 600);
AirspaceUntil = Math.Max(AirspaceUntil, now + (FlightTime + 10) * 1000);
}
bool SpawnFailed(string reason)
{
SpawnFailure = reason;
Audit("DEPLOY FAILED", string.Format("reason=%1 claimed=%2 gun=%3 group=%4", reason, Claimed, Gun, Group));
return false;
}
void SearchResult(vector candidate, string reason)
{
LastCandidate = candidate;
int count;
SearchCounts.Find(reason, count);
SearchCounts.Set(reason, count + 1);
}
void SiteStatus(float now)
{
if (!SearchCounts.IsEmpty())
{
string counts;
foreach (string reason, int count : SearchCounts) counts += string.Format("%1=%2; ", reason, count);
Audit("SITE SEARCH", string.Format("candidate counts since last summary: %1 last candidate=%2", counts, LastCandidate));
SearchCounts.Clear();
}
int agents, living;
int actionState = -1;
if (Group)
{
array<AIAgent> crew = {};
Group.GetAgents(crew);
agents = crew.Count();
foreach (AIAgent agent : crew)
{
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) living++;
}
}
if (Activity) actionState = Activity.GetActionState();
bool connected;
ERR_HQ hq = ERR_HQ.Get();
if (hq && Group) connected = hq.MortarConnected(Group);
Audit("SITE STATUS", string.Format("claimed=%1 startup=%2 operational=%3 crew agents=%4 fighting=%5 inventory HE=%6 radio connected=%7 cooldown remaining=%8s", Claimed, StartupActivated, Operational(), agents, living, CountShells(), connected, Math.Max(0, NextOrder - now) / 1000));
Audit("ORDER STATUS", string.Format("open=%1 route pending=%2 native action=%3 shell committed=%4 order shots=%5 total shots=%6 age=%7s last failure=%8", CommandOpen, RadioOrder != null, actionState, Released, OrderShots, TotalShots, Math.Max(0, now - OrderedAt) / 1000, LastFailure));
if (!SpawnFailure.IsEmpty()) Audit("PARTIAL DEPLOYMENT", SpawnFailure);
if (!Operational()) Trace("SITE BLOCK", OperationalReason(), "spawn failure=" + SpawnFailure);
}
bool Operational()
{
return OperationalReason().IsEmpty();
}
int CountShells()
{
if (!Group)
return 0;
int count;
array<IEntity> items = {};
array<AIAgent> agents = {};
Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity()))
continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(agent.GetControlledEntity().FindComponent(InventoryStorageManagerComponent));
if (!inventory)
continue;
ERR_Inventory.AppendUnique(inventory, items);
}
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL) count++;
return count;
}
void Bind(int siteId, SCR_AIGroup group, IEntity gun, float now)
{
SiteId = siteId;
Group = group;
Gun = gun;
Position = gun.GetOrigin();
SpawnedAt = now;
Claimed = true;
m_Readiness = new ERR_MortarReadiness(this);
}
int InventoryShells(InventoryStorageManagerComponent inventory)
{
if (!inventory)
return 0;
int count;
array<IEntity> items = {};
ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
{
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL)
count++;
}
return count;
}
bool Stopped() { return m_Stopped; }
bool SupplyPending() { return m_Transfer && m_Transfer.Unresolved(); }
bool SupplyAuthorized(float now)
{
if (!Replication.IsServer() || m_Stopped || !StartupActivated || !Operational() || !CommandOpen || !ERR_HQ.MissionActive()) return false;
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (!system || system.FindGun(Gun) != this) return false;
ERR_HQReport suppliedReport = Report;
if (!suppliedReport) suppliedReport = PendingSupplyReport;
return ERR_MortarPrediction.Fresh(suppliedReport, now);
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
Cancel("HQ stopped");
if (m_Readiness) m_Readiness.Stop();
}
ERR_MortarSupplyState SupplyState(float now)
{
// Observe an existing native operation even after its order was canceled.
// No second request may race an unresolved transfer on this battery.
if (m_Transfer)
{
ERR_MortarSupplyState observed = m_Transfer.Poll(now);
if (m_Transfer.CapturedOrder() == OrderId)
{
if (observed == ERR_MortarSupplyState.READY) m_Transfer = null;
return observed;
}
if (m_Transfer.Unresolved()) return ERR_MortarSupplyState.FAILED;
m_Transfer = null;
}
if (!SupplyAuthorized(now)) return SupplyStateFailed("current HQ order, crew or report is unavailable");
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Gun.FindComponent(SCR_AIVehicleUsageComponent));
IEntity gunner;
if (usage && usage.GetTurretCompartmentSlot())
gunner = usage.GetTurretCompartmentSlot().GetOccupant();
array<AIAgent> agents = {};
Group.GetAgents(agents);
float nearest = float.MAX;
if (!gunner)
{
foreach (AIAgent candidate : agents)
{
if (!candidate || candidate.GetControlledEntity() == RadioOperator || !ERR_Actors.CanFight(candidate.GetControlledEntity()))
continue;
float distance = vector.DistanceXZ(Gun.GetOrigin(), candidate.GetControlledEntity().GetOrigin());
if (distance < nearest)
{
nearest = distance;
gunner = candidate.GetControlledEntity();
}
}
}
if (!gunner) return SupplyStateFailed("no fighting gunner found");
if (ERR_Actors.Group(gunner) != Group) return SupplyStateFailed("turret occupant belongs to another group");
if (!ERR_Actors.CanFight(gunner)) return SupplyStateFailed("gunner cannot fight");
InventoryStorageManagerComponent destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
if (!destination)
return SupplyStateFailed("gunner inventory missing");
if (InventoryShells(destination) > 0)
return ERR_MortarSupplyState.READY;
int nearbyDonors, donorShells, storageRejected;
array<IEntity> consideredShells = {};
foreach (AIAgent bearer : agents)
{
if (!bearer || !ERR_Actors.CanFight(bearer.GetControlledEntity()))
continue;
IEntity donor = bearer.GetControlledEntity();
if (donor == gunner || vector.DistanceXZ(donor.GetOrigin(), gunner.GetOrigin()) > 12)
continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(donor.FindComponent(InventoryStorageManagerComponent));
if (!inventory)
continue;
nearbyDonors++;
array<IEntity> items = {};
ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
{
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != SHELL)
continue;
if (consideredShells.Contains(item) || m_AttemptedShells.Contains(item)) continue;
consideredShells.Insert(item);
donorShells++;
BaseInventoryStorageComponent storage = destination.FindStorageForItem(item, EStoragePurpose.PURPOSE_DEPOSIT);
if (!storage) { storageRejected++; continue; }
m_AttemptedShells.Insert(item); // even rejected/ambiguous requests are never retried
m_Transfer = new ERR_MortarSupplyTransfer(this, donor, gunner, item, storage, now);
if (!m_Transfer.Begin(now)) return SupplyStateFailed("exact shell transfer request rejected; no retry");
Audit("AMMO TRANSFER", string.Format("one exact-item move pending donor=%1 gunner=%2 shell=%3 order=%4; awaiting callback AND physical destination", donor, gunner, item, OrderId));
return ERR_MortarSupplyState.PENDING;
}
}
return SupplyStateFailed(string.Format("gunner empty; fighting donors within 12m=%1 unattempted HE=%2 destination storage rejected=%3", nearbyDonors, donorShells, storageRejected));
}
// Compatibility for existing native callers; request acceptance is NOT ready.
bool SupplyGunner() { return SupplyState(GetGame().GetWorld().GetWorldTime()) == ERR_MortarSupplyState.READY; }
protected ERR_MortarSupplyState SupplyStateFailed(string reason)
{
SupplyFailed(reason);
return ERR_MortarSupplyState.FAILED;
}
bool SupplyFailed(string reason)
{
LastFailure = reason;
Audit("AMMO FAILED", reason);
return false;
}
void Cancel(string reason = "cancel requested")
{
if (CommandOpen)
{
FlushTraces();
Audit("ORDER END", string.Format("reason=%1 observed shots=%2 shell committed=%3 elapsed=%4s last failure=%5", reason, OrderShots, Released, (GetGame().GetWorld().GetWorldTime() - OrderedAt) / 1000, LastFailure));
}
CommandOpen = false;
RadioOrder = null;
PendingRadioReport = null;
PendingSupplyReport = null;
if (m_Transfer) m_Transfer.Invalidate();
Report = null;
AimedAt = -1;
if (Activity)
Activity.Complete();
Activity = null;
}
// Idle distant batteries still report through HQ's scheduler. Their native
// AI needs full detail while firing or when players can interact with them.
protected void UpdateCrewDetail(float now, bool firing = false)
{
if (!Group || Group.GetPlayerCount(true) > 0)
return;
bool active = firing || Activity != null || ERR_MortarSystem.NearPlayer(Position);
int lod = -1;
if (active)
lod = 0;
if (Group.GetPermanentLOD() != lod)
Group.SetPermanentLOD(lod);
if (active && !Group.IsAIActivated())
Group.ActivateAI();
array<AIAgent> agents = {};
Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || EntityUtils.IsPlayer(agent.GetControlledEntity()))
continue;
if (agent.GetPermanentLOD() != lod)
agent.SetPermanentLOD(lod);
if (active && !agent.IsAIActivated() && ERR_Actors.CanFight(agent.GetControlledEntity()))
agent.ActivateAI();
}
}
void Update(float now)
{
if (m_Stopped) return;
if (!StartupActivated)
{
if (!SpawnFailure.IsEmpty()) return;
if (!m_Readiness) { SpawnFailed("initial support readiness owner missing"); return; }
ERR_MortarSupplyState startup = m_Readiness.Poll(now);
if (startup != ERR_MortarSupplyState.READY) return;
StartupActivated = true;
}
if (m_Transfer) m_Transfer.Poll(now);
if (!Operational())
{
Cancel(OperationalReason());
return;
}
if (Activity)
{
Trace("ACTIVITY", string.Format("native state=%1", Activity.GetActionState()), string.Format("age=%1s aim age=%2s committed=%3 shots=%4", (now - OrderedAt) / 1000, (now - AimedAt) / 1000, Released, OrderShots));
if (Activity.GetActionState() == EAIActionState.FAILED) Cancel("native artillery activity FAILED");
else if (Activity.GetActionState() == EAIActionState.COMPLETED) Cancel("native artillery activity COMPLETED (shot count is separate)");
else if (now - OrderedAt > ERR_MortarSalvo.TIMEOUT_MS) Cancel("120s artillery salvo timeout");
}
if (StartupActivated)
UpdateCrewDetail(now);
}
void Order(ERR_HQReport report, float now)
{
if (m_Stopped || !StartupActivated || !Operational() || !CommandOpen || Activity || !ERR_MortarPrediction.Fresh(report, now)) return;
Report = report.Snapshot();
OrderedAt = now;
NextOrder = now + 30000;
Released = false;
UpdateCrewDetail(now, true);
Activity = new SCR_AIStaticArtilleryActivity(Group.GetGroupUtilityComponent(), null, report.Position, SCR_EAIArtilleryAmmoType.HIGH_EXPLOSIVE, ERR_MortarSalvo.ROUNDS);
Group.GetGroupUtilityComponent().AddAction(Activity);
Audit("AIM", string.Format("native activity created; latest source=M%1 observed=%2", report.MessageId, report.Description(now)));
}
}
@@ -0,0 +1,231 @@
// The dedicated manpack operator has no mortar-shell backpack. Preserve the
// native chooser and its caller's original index for every eligible gunner.
modded class SCR_AIStaticArtilleryActivity
{
protected override AIAgent FindBestAgentForArtillery(notnull array<AIAgent> agents, notnull IEntity artilleryEntity, out int outIndex)
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system) battery = system.FindGun(artilleryEntity);
if (!battery) return super.FindBestAgentForArtillery(agents, artilleryEntity, outIndex);
array<AIAgent> gunners = {};
foreach (AIAgent agent : agents)
{
if (agent && agent.GetControlledEntity() != battery.RadioOperator && ERR_Actors.CanFight(agent.GetControlledEntity()))
gunners.Insert(agent);
}
int filteredIndex;
AIAgent selected = super.FindBestAgentForArtillery(gunners, artilleryEntity, filteredIndex);
outIndex = -1;
if (selected) outIndex = agents.Find(selected);
return selected;
}
}
// Leave all other artillery (including player-operated captured guns) native.
modded class SCR_AIGetArtilleryAimDistanceCompensation
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
IEntity gun;
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(gun);
if (!battery)
return super.EOnTaskSimulate(owner, dt);
battery.AimedAt = -1;
float now = GetGame().GetWorld().GetWorldTime();
ERR_HQReport report = system.Latest(battery, now);
if (!ERR_HQ.MissionActive()) { system.Withhold(battery, "aim: mission inactive"); return ENodeResult.FAIL; }
if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) { system.Withhold(battery, "aim: salvo complete or order closed"); return ENodeResult.FAIL; }
if (battery.Released) { system.Withhold(battery, "aim: waiting for committed projectile"); return ENodeResult.FAIL; }
if (!report) { system.Withhold(battery, "aim: " + system.ContactFailure(battery)); return ENodeResult.FAIL; }
battery.Trace("AIM START", "solving moving intercept", string.Format("latest source=M%1; %2", report.MessageId, report.Description(now)));
ResourceName ammo;
GetVariableIn(PORT_AMMO_PREFAB, ammo);
if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return ENodeResult.FAIL; }
Resource resource = Resource.Load(ammo);
if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return ENodeResult.FAIL; }
IEntitySource source = resource.GetResource().ToEntitySource();
if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return ENodeResult.FAIL; }
array<float> coefficients = {};
GetAmmoInitialSpeedCoefficients(ammo, coefficients);
if (coefficients.IsEmpty())
coefficients.Insert(1.0);
float minimum, maximum;
GetVerticalAimLimits(gun, minimum, maximum);
float flight = 30;
float offset;
int charge;
vector impact;
vector salvoOffset = ERR_MortarSalvo.Offset(battery.SiteId, battery.OrderId, battery.OrderShots);
bool converged;
// Iterate the moving intercept using the native shell's flight time.
// Reject non-convergence rather than fire with an arbitrary lead time.
for (int iteration = 0; iteration < 4; iteration++)
{
if (!ERR_MortarPrediction.Predict(report, now, flight + 2, impact)) { system.Withhold(battery, "aim prediction: " + ERR_MortarPrediction.FailureReason, string.Format("flight lead=%1s; %2", flight + 2, report.Description(now))); return ENodeResult.FAIL; }
if (!system.OutsideSettlements(impact)) { system.Withhold(battery, "aim target inside settlement exclusion", system.SettlementReason); return ENodeResult.FAIL; }
impact += salvoOffset;
impact[1] = SCR_TerrainHelper.GetTerrainY(impact);
float distance = vector.DistanceXZ(gun.GetOrigin(), impact);
float altitude = impact[1] - gun.GetOrigin()[1];
if (distance < 350 || distance > 4000) { system.Withhold(battery, "predicted impact out of range", string.Format("iteration=%1 distance=%2m impact=%3 allowed=350..4000m", iteration, distance, impact)); return ENodeResult.FAIL; }
bool reachable;
float calculated;
for (int i = 0; i < coefficients.Count(); i++)
{
if (!BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(distance, offset, calculated, source, altitude, coefficients[i]))
continue;
float tangent = (offset + altitude) / distance;
if (calculated <= 0 || calculated > 75 || tangent < Math.Tan(minimum) || tangent > Math.Tan(maximum))
continue;
charge = i;
reachable = true;
break;
}
if (!reachable) { system.Withhold(battery, "no native ballistic solution within elevation/flight-time limits", string.Format("iteration=%1 distance=%2 altitude=%3 charges=%4 elevation radians=%5..%6 flight limit=75s", iteration, distance, altitude, coefficients.Count(), minimum, maximum)); return ENodeResult.FAIL; }
converged = Math.AbsFloat(calculated - flight) < 1;
flight = calculated;
if (converged)
break;
}
if (!converged) { system.Withhold(battery, "moving intercept did not converge in four iterations"); return ENodeResult.FAIL; }
if (!system.Safe(battery, impact, flight + 2)) { system.Withhold(battery, "aim impact safety rejected", system.SafetyReason); return ENodeResult.FAIL; }
battery.Impact = impact;
battery.AimOffset = salvoOffset;
battery.AimedShot = battery.OrderShots;
battery.FlightTime = flight;
battery.AimedAt = now;
battery.LastFailure = "";
battery.Trace("AIM SOLUTION", "ready", string.Format("predicted impact=%1 flight=%2s charge index=%3 aim height offset=%4m latest source=M%5", impact, flight, charge, offset, report.MessageId));
vector aim = impact + Vector(0, offset, 0);
SetVariableOut(PORT_AIM_POS, aim);
SetVariableOut(PORT_INIT_SPEED_ID, charge);
return ENodeResult.SUCCESS;
}
}
modded class SCR_AIShootStaticArtillery
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
IEntity gun;
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(gun);
if (battery && !system.AllowShot(battery))
{
battery.Trace("SHOOT NODE", "authorization rejected", battery.LastFailure);
return ENodeResult.FAIL;
}
ENodeResult result = super.EOnTaskSimulate(owner, dt);
if (battery)
battery.Trace("SHOOT NODE", string.Format("native result=%1", result), "authorization passed; node result does not confirm a physical shot");
return result;
}
}
modded class SCR_MortarMuzzleComponent
{
protected override void TransferShellToMortar()
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system && m_CharController && !EntityUtils.IsPlayer(m_CharController.GetOwner()))
battery = system.FindGun(GetOwner());
if (battery)
{
// The loading animation takes time. Recheck at the actual transfer,
// before native inventory insertion triggers the physical projectile.
if (!system.AllowShot(battery))
{
if (m_ShellComponent)
{
m_ShellComponent.GetOnShellUsed().Remove(TransferShellToMortar);
m_ShellComponent.SetLoadedState(false);
}
CharacterAnimationComponent animation = m_CharController.GetAnimationComponent();
if (animation && animation.GetCommandHandler())
animation.GetCommandHandler().FinishItemUse(false);
battery.Audit("ABORT", "final shell transfer rejected; " + battery.LastFailure);
return;
}
battery.Released = true;
battery.ReserveShellAirspace(GetGame().GetWorld().GetWorldTime());
battery.Audit("SHELL COMMIT", string.Format("final safety passed; gunner=%1 predicted impact=%2 flight=%3s; invoking native transfer, awaiting projectile event", m_CharController.GetOwner(), battery.Impact, battery.FlightTime));
}
super.TransferShellToMortar();
}
override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity)
{
IEntity firingCharacter;
if (m_CharController) firingCharacter = m_CharController.GetOwner();
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(GetOwner());
if (!battery)
return;
bool playerGunner;
if (firingCharacter) playerGunner = EntityUtils.IsPlayer(firingCharacter);
battery.ObserveProjectile(projectileEntity, firingCharacter, playerGunner);
}
}
// The native tree normally re-aims only every five shots (and skips the first
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
modded class SCR_AIStaticArtilleryBehavior
{
ERR_MortarBattery ERR_GetMortarBattery()
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (!system) return null;
return system.FindGun(m_ArtilleryEntity.m_Value);
}
}
modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
ERR_MortarBattery battery;
SCR_AIStaticArtilleryBehavior behavior;
if (m_Utility)
{
behavior = SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction());
if (behavior) battery = behavior.ERR_GetMortarBattery();
}
if (battery)
{
int gate = ERR_MortarSupplyRules.CycleGate(battery.CommandOpen, battery.OrderShots, behavior.GetShotCount(), ERR_MortarSalvo.ROUNDS, ERR_MortarSupplyState.READY);
if (gate < 0) return ENodeResult.FAIL;
if (gate == 0) return ENodeResult.RUNNING;
// Native super increments the behavior counter. Wait BEFORE calling it,
// so repeated pending ticks cannot count the same projectile twice.
if (battery.OrderShots < ERR_MortarSalvo.ROUNDS)
{
ERR_MortarSupplyState supply = battery.SupplyState(GetGame().GetWorld().GetWorldTime());
if (supply == ERR_MortarSupplyState.PENDING) return ENodeResult.RUNNING;
if (supply == ERR_MortarSupplyState.FAILED)
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (system) system.Withhold(battery, "next native load lacks confirmed physical gunner ammunition");
return ENodeResult.FAIL;
}
}
}
ENodeResult result = super.EOnTaskSimulate(owner, dt);
if (battery && result == ENodeResult.SUCCESS)
{
SetVariableOut(PORT_AIMING_NEEDED, true);
}
return result;
}
}
@@ -0,0 +1,131 @@
// All target coordinates come from delivered HQ observations, never a player's
// current transform. Road projection stops at an unknown junction/road end.
class ERR_MortarPrediction
{
// Read immediately after a failed synchronous prediction; no extra world scan.
static string FailureReason;
// Includes observation uplink, HQ downlink and native aiming. All stages
// retain the original observation time; radio delivery never renews it.
static const float MAX_REPORT_AGE_MS = 60000;
static bool Fresh(ERR_HQReport report, float now)
{
return report && report.Target && !report.Distress && now >= report.ObservedAt && now - report.ObservedAt <= MAX_REPORT_AGE_MS && report.TravelMode != "aircraft";
}
static bool Reject(string reason)
{
FailureReason = reason;
return false;
}
static vector ClosestOnSegment(vector point, vector start, vector end)
{
vector segment = end - start;
segment[1] = 0;
vector offset = point - start;
offset[1] = 0;
float lengthSq = segment.LengthSq();
if (lengthSq < 0.01)
return start;
return vector.Lerp(start, end, Math.Clamp(vector.Dot(offset, segment) / lengthSq, 0, 1));
}
static bool FollowRoad(array<vector> points, vector observed, vector velocity, float seconds, out vector impact)
{
if (!points || points.Count() < 2)
return Reject("road polyline has fewer than two points");
int segment = -1;
float closest = 20;
vector projected;
for (int i = 0; i < points.Count() - 1; i++)
{
vector candidate = ClosestOnSegment(observed, points[i], points[i + 1]);
float distance = vector.DistanceXZ(candidate, observed);
if (distance < closest)
{
closest = distance;
segment = i;
projected = candidate;
}
}
if (segment < 0)
return Reject("observed vehicle is not within 20m of a road segment");
vector forward = points[segment + 1] - points[segment];
forward[1] = 0;
forward.Normalize();
vector travel = velocity;
travel[1] = 0;
float speed = travel.Length();
travel.Normalize();
float alignment = vector.Dot(forward, travel);
if (Math.AbsFloat(alignment) < 0.7)
return Reject("vehicle heading crosses road; alignment below 0.7");
int step = 1;
int next = segment + 1;
if (alignment < 0)
{
step = -1;
next = segment;
}
float remaining = speed * seconds;
vector position = projected;
while (next >= 0 && next < points.Count())
{
float length = vector.DistanceXZ(position, points[next]);
if (length >= remaining && length > 0.01)
{
impact = vector.Lerp(position, points[next], remaining / length);
return true;
}
remaining -= length;
position = points[next];
next += step;
}
return Reject("prediction reaches unknown road end/junction");
}
static bool Predict(ERR_HQReport report, float now, float flightSeconds, out vector impact)
{
FailureReason = "";
if (!report) return Reject("target report missing");
if (report.Distress) return Reject("distress report cannot authorize fire");
if (now < report.ObservedAt) return Reject("observation timestamp is in the future");
if (now - report.ObservedAt > MAX_REPORT_AGE_MS) return Reject("observation older than 60s");
float seconds = (now - report.ObservedAt) / 1000 + flightSeconds;
if (seconds < 0 || seconds > 90)
return Reject("observation age plus flight outside 0..90s");
if (report.TravelMode == "aircraft") return Reject("aircraft target unsupported");
if (report.TravelMode == "mixed travel") return Reject("mixed travel cannot be predicted");
impact = report.Position;
if (!report.HasVelocity || report.Velocity.Length() < 0.5)
return true;
if (report.Velocity.Length() * seconds > 1800)
return Reject("predicted travel exceeds 1800m");
bool vehicle = report.TravelMode == "vehicle" || report.TravelMode == "in vehicle";
if (vehicle)
{
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!world || !world.GetRoadNetworkManager())
return Reject("road network unavailable");
BaseRoad road;
float roadDistance;
world.GetRoadNetworkManager().GetClosestRoad(report.Position, road, roadDistance, skipNavlinks: true);
if (!road) return Reject("no road near reported vehicle position");
if (roadDistance > 20) return Reject("reported vehicle farther than 20m from road");
array<vector> points = {};
road.GetPoints(points);
return FollowRoad(points, report.Position, report.Velocity, seconds, impact);
}
impact += report.Velocity * seconds;
return true;
}
static bool ThreatensFriendly(vector impact, vector position, vector velocity, float flightSeconds)
{
velocity[1] = 0;
vector future = position + velocity * Math.Clamp(flightSeconds, 0, 90);
// Protect the entire predicted path, not only where a soldier is now.
return vector.DistanceXZ(impact, ClosestOnSegment(impact, position, future)) <= 100;
}
}
@@ -0,0 +1,177 @@
enum ERR_MortarSupplyState { FAILED = -1, PENDING, READY }
class ERR_MortarSupplyRules
{
static int CycleGate(bool commandOpen, int confirmedShots, int behaviorShots, int limit, int supplyState)
{
if (!commandOpen) return -1;
if (confirmedShots <= behaviorShots) return 0;
if (confirmedShots < limit) return supplyState;
return 1;
}
}
class ERR_MortarInitialItem : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
void Rejected() { Finish(false); }
}
// One finite allocation, after the ENTIRE original support crew exists. Failed
// or stopped setup retains physical assets and retires only its pending spawn intent.
class ERR_MortarReadiness
{
protected ERR_MortarBattery m_Battery;
protected ref array<IEntity> m_Members = {};
protected ref array<ref ERR_MortarInitialItem> m_Operations = {};
protected bool m_Allocated;
protected bool m_Ready;
protected bool m_Stopped;
protected bool m_Retired;
protected int m_RequestedShells;
void ERR_MortarReadiness(ERR_MortarBattery battery) { m_Battery = battery; }
static ERR_MortarSupplyState Population(int physical, int gunners, int operators, bool complete, bool expired)
{
if (physical > 4 || gunners > 3 || operators > 1) return ERR_MortarSupplyState.FAILED;
if (physical == 4 && gunners == 3 && operators == 1 && complete && !expired) return ERR_MortarSupplyState.READY;
if (expired) return ERR_MortarSupplyState.FAILED;
return ERR_MortarSupplyState.PENDING;
}
static bool AllocationValid(int shells) { return shells >= 0 && shells <= 9; }
int RequestedShells() { return m_RequestedShells; }
bool Ready() { return m_Ready && !m_Stopped; }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Ready || m_Retired || !m_Battery || !m_Battery.Group || !Replication.IsServer()) return;
m_Retired = true;
// As in Core initial guard cleanup: stopping the current request alone does
// not retire the native group's still-authored future expansion intent.
RetireGroup(m_Battery.Group);
}
static void RetireGroup(SCR_AIGroup group)
{
if (!group || !Replication.IsServer()) return;
group.SetSpawnImmediately(false);
group.m_aUnitPrefabSlots = new array<ResourceName>();
if (!GetGame()) return;
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
if (world) world.PurgeSpawnRequestsForGroup(group);
}
protected ERR_MortarSupplyState Fail(string reason)
{
m_Battery.SpawnFailed(reason);
Stop();
return ERR_MortarSupplyState.FAILED;
}
protected bool Allocate()
{
m_Allocated = true; // before native requests; never refill or retry a failure
foreach (IEntity actor : m_Members)
{
if (actor == m_Battery.RadioOperator) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
int existing = m_Battery.InventoryShells(inventory);
if (!AllocationValid(existing)) return false;
if (ERR_HQRadio.Capability(actor) == 0 && !inventory.TrySpawnPrefabToStorage("{E1A5D4B878AA8980}Prefabs/Items/Equipment/Radios/Radio_R148.et"))
m_Battery.Audit("RADIO EQUIP", "optional crew handset did not fit; dedicated manpack remains required");
for (int i = existing; i < 9; i++)
{
ERR_MortarInitialItem operation = new ERR_MortarInitialItem();
m_Operations.Insert(operation);
m_RequestedShells++;
if (!inventory.TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL, purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: operation))
{
operation.Rejected();
return false;
}
}
}
return true;
}
ERR_MortarSupplyState Poll(float now)
{
if (m_Stopped || !Replication.IsServer() || !m_Battery || m_Battery.Stopped()) return ERR_MortarSupplyState.FAILED;
if (m_Ready) return ERR_MortarSupplyState.READY;
SCR_AIGroup group = m_Battery.Group;
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed");
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent));
if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed");
bool expired = now < m_Battery.SpawnedAt || now - m_Battery.SpawnedAt >= 90000;
array<AIAgent> agents = {}; group.GetAgents(agents);
array<IEntity> physical = {};
int gunners, operators;
IEntity operatorActor;
foreach (AIAgent agent : agents)
{
if (!agent || !agent.GetControlledEntity()) continue;
IEntity actor = agent.GetControlledEntity();
if (physical.Contains(actor)) return Fail("initial support group reports duplicate physical actor");
physical.Insert(actor);
if (!ERR_Actors.CanFight(actor) || SCR_FactionManager.SGetFaction(actor) != mode.EnemyFaction() || !actor.GetPrefabData()) return Fail("initial support actor unavailable or has wrong faction/resource");
ResourceName prefab = actor.GetPrefabData().GetPrefabName();
if (prefab == ERR_MortarBattery.CREW) gunners++;
else if (prefab == ERR_MortarBattery.RADIO_OPERATOR) { operators++; operatorActor = actor; }
else return Fail("initial support actor does not belong to authored crew catalog");
}
ERR_MortarSupplyState population = Population(physical.Count(), gunners, operators, group.IsExpandComplete(), expired);
if (population == ERR_MortarSupplyState.FAILED) return Fail("initial four-person support population incomplete/invalid within 90 seconds");
if (population == ERR_MortarSupplyState.PENDING) return population;
if (m_Allocated)
{
foreach (IEntity original : m_Members)
if (!physical.Contains(original)) return Fail("initial support member replaced after finite allocation");
}
foreach (IEntity member : physical)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(member.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return ERR_MortarSupplyState.PENDING;
if (!m_Allocated && member != operatorActor && m_Battery.InventoryShells(inventory) < 9 && !inventory.CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT)) return ERR_MortarSupplyState.PENDING;
}
if (ERR_HQRadio.Capability(operatorActor) != 2) return ERR_MortarSupplyState.PENDING;
// Preserve the existing earliest activation delay, but it is not readiness.
if (now - m_Battery.SpawnedAt < 10000) return ERR_MortarSupplyState.PENDING;
if (!m_Allocated)
{
if (!group.GetGroupUtilityComponent() || !group.GetGroupUtilityComponent().m_VehicleMgr) return ERR_MortarSupplyState.PENDING;
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Battery.Gun.FindComponent(SCR_AIVehicleUsageComponent));
if (!usage || !group.GetGroupUtilityComponent().m_VehicleMgr.TryAddVehicle(usage)) return Fail("native support gun registration failed");
m_Members.Copy(physical); m_Battery.RadioOperator = operatorActor;
if (!Allocate()) return Fail("finite initial support shell allocation rejected; no retry");
return ERR_MortarSupplyState.PENDING; // never accept mutation-frame readiness
}
foreach (ERR_MortarInitialItem operation : m_Operations)
{
if (!operation.Finished) return ERR_MortarSupplyState.PENDING;
if (!operation.Succeeded) return Fail("finite initial support shell operation failed; no retry");
}
foreach (IEntity equipped : m_Members)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(equipped.FindComponent(InventoryStorageManagerComponent));
int shells = m_Battery.InventoryShells(inventory);
if (equipped == operatorActor)
{
if (shells != 0) return Fail("dedicated initial manpack operator unexpectedly carries mortar shells");
}
else if (shells != 9) return ERR_MortarSupplyState.PENDING;
}
if (m_Battery.CountShells() != 27) return ERR_MortarSupplyState.PENDING;
m_Ready = true;
group.ActivateAI();
foreach (AIAgent readyAgent : agents)
{
if (readyAgent) { readyAgent.SetPermanentLOD(0); readyAgent.ActivateAI(); }
}
m_Battery.Audit("CREW READY", string.Format("four original physical members, one manpack, 27 confirmed HE; new shell requests=%1; allocation permanently closed", m_RequestedShells));
return ERR_MortarSupplyState.READY;
}
}
@@ -0,0 +1,16 @@
// One delivered HQ order drives a continuous native loading/firing cycle.
class ERR_MortarSalvo
{
static const int ROUNDS = 8;
static const float RADIUS_METRES = 40;
static const float TIMEOUT_MS = 120000;
static vector Offset(int site, int order, int shot)
{
// Stable throughout one ballistic solve; successive shells cover the disk
// instead of clustering on the same point or changing every iteration.
float radius = RADIUS_METRES * Math.Sqrt((shot + 1.0) / ROUNDS);
float angle = (site * 53 + order * 97 + shot * 137.5) * Math.DEG2RAD;
return Vector(Math.Sin(angle) * radius, 0, Math.Cos(angle) * radius);
}
}
@@ -0,0 +1,108 @@
class ERR_MortarSupplyCallback : ScriptedInventoryOperationCallback
{
ERR_MortarSupplyTransfer Transfer;
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
}
// Exact-item native transaction. Callbacks only record outcomes; the HQ tick
// authorizes progress after checking actual inventory ownership and the order.
class ERR_MortarSupplyTransfer
{
protected ERR_MortarBattery m_Battery;
protected IEntity m_Gun;
protected SCR_AIGroup m_Group;
protected IEntity m_Donor;
protected IEntity m_Gunner;
protected IEntity m_Shell;
protected InventoryStorageManagerComponent m_Source;
protected InventoryStorageManagerComponent m_Destination;
protected BaseInventoryStorageComponent m_Storage;
protected ref ERR_MortarSupplyCallback m_Callback;
protected int m_Order;
protected int m_Acknowledgement;
protected float m_Deadline;
protected bool m_Accepted;
protected bool m_Invalidated;
protected bool m_Resolved;
protected bool m_Ready;
void ERR_MortarSupplyTransfer(ERR_MortarBattery battery, IEntity donor, IEntity gunner, IEntity shell, BaseInventoryStorageComponent storage, float now)
{
m_Battery = battery; m_Gun = battery.Gun; m_Group = battery.Group;
m_Donor = donor; m_Gunner = gunner; m_Shell = shell; m_Storage = storage;
m_Order = battery.OrderId; m_Deadline = now + 8000;
m_Source = InventoryStorageManagerComponent.Cast(donor.FindComponent(InventoryStorageManagerComponent));
m_Destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
}
static bool Authority(bool valid, bool invalidated, int capturedOrder, int currentOrder)
{
return valid && !invalidated && capturedOrder == currentOrder;
}
static bool Proven(bool callbackComplete, bool sourceContains, bool destinationContains, bool exactSlot)
{
return callbackComplete && !sourceContains && destinationContains && exactSlot;
}
protected bool Contains(InventoryStorageManagerComponent inventory)
{
if (!inventory || !m_Shell) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
return items.Contains(m_Shell);
}
protected bool Valid(float now)
{
if (!m_Battery || !Authority(m_Battery.SupplyAuthorized(now), m_Invalidated, m_Order, m_Battery.OrderId)) return false;
if (m_Battery.Gun != m_Gun || m_Battery.Group != m_Group || !m_Donor || !m_Gunner || m_Donor == m_Gunner || !m_Shell || !m_Source || !m_Destination || !m_Storage) return false;
if (!ERR_Actors.CanFight(m_Donor) || !ERR_Actors.CanFight(m_Gunner) || ERR_Actors.Group(m_Donor) != m_Group || ERR_Actors.Group(m_Gunner) != m_Group) return false;
Faction faction = m_Group.GetFaction();
if (SCR_FactionManager.SGetFaction(m_Donor) != faction || SCR_FactionManager.SGetFaction(m_Gunner) != faction || vector.Distance(m_Donor.GetOrigin(), m_Gunner.GetOrigin()) > 12) return false;
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Gun.FindComponent(SCR_AIVehicleUsageComponent));
if (!usage || !usage.GetTurretCompartmentSlot()) return false;
IEntity occupant = usage.GetTurretCompartmentSlot().GetOccupant();
return !occupant || occupant == m_Gunner;
}
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() || !Valid(now) || !Contains(m_Source) || Contains(m_Destination)) return false;
m_Callback = new ERR_MortarSupplyCallback(); m_Callback.Transfer = this;
// Do not use callback.GetItem(): the native contract excludes Move operations.
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; m_Ready = false; }
bool Unresolved() { return m_Accepted && !m_Resolved; }
int CapturedOrder() { return m_Order; }
ERR_MortarSupplyState Poll(float now)
{
if (!Replication.IsServer()) return ERR_MortarSupplyState.FAILED;
bool valid = Valid(now);
bool sourceContains = Contains(m_Source);
bool destinationContains = Contains(m_Destination);
bool exactSlot;
InventoryItemComponent item;
if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));
if (item && item.GetParentSlot()) exactSlot = item.GetParentSlot().GetStorage() == m_Storage;
bool proven = Proven(m_Acknowledgement == 1, sourceContains, destinationContains, exactSlot);
if (proven || m_Acknowledgement < 0) m_Resolved = true;
// A successful callback without physical destination proof is ambiguous.
// Keep it reserved through timeout/cancellation; never start a second move.
if (!valid || now >= m_Deadline)
{
m_Invalidated = true; m_Ready = false;
return ERR_MortarSupplyState.FAILED;
}
if (proven)
{
m_Ready = true;
return ERR_MortarSupplyState.READY;
}
if (m_Acknowledgement < 0 || !m_Accepted) return ERR_MortarSupplyState.FAILED;
return ERR_MortarSupplyState.PENDING;
}
}
@@ -0,0 +1,244 @@
// Owned and ticked by HQ. Initial assets are authored once; losses are never replaced.
class ERR_MortarSystem
{
protected ERR_HQ m_HQ;
protected ref array<ref ERR_MortarBattery> m_Batteries = {};
protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety();
protected bool m_Prepared;
protected bool m_Stopped;
protected float m_NextDecision;
string SafetyReason;
string SettlementReason;
void ERR_MortarSystem(ERR_HQ hq) { m_HQ = hq; }
static ERR_MortarSystem Get()
{
ERR_HQ hq = ERR_HQ.Get();
if (!hq || !Replication.IsServer()) return null;
return hq.Mortars();
}
static bool NearPlayer(vector position)
{
array<IEntity> players = {};
ERR_Actors.Players(players);
foreach (IEntity player : players)
if (vector.DistanceXZ(player.GetOrigin(), position) <= 1200) return true;
return false;
}
bool Prepare()
{
if (!Replication.IsServer() || m_Prepared || m_Stopped) return false;
m_Prepared = true;
vector position = ERR_Actors.Ground("6000 0 7000");
if (position[1] < 1) return false;
IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position);
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
if (!gun || !group)
{
ERR_Actors.DeleteSpawned(gun);
ERR_MortarReadiness.RetireGroup(group);
ERR_Actors.DeleteSpawned(group);
return false;
}
if (group.GetFaction() != ERR_GameMode.Get().EnemyFaction())
{
ERR_Actors.DeleteSpawned(gun);
ERR_MortarReadiness.RetireGroup(group);
ERR_Actors.DeleteSpawned(group);
return false;
}
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(gun.FindComponent(FactionAffiliationComponent));
if (affiliation) affiliation.SetAffiliatedFaction(ERR_GameMode.Get().EnemyFaction());
ERR_MortarBattery battery = new ERR_MortarBattery();
battery.Bind(1, group, gun, GetGame().GetWorld().GetWorldTime());
m_Batteries.Insert(battery);
ERR_GameMode mode = ERR_GameMode.Get();
mode.Own(gun);
mode.RegisterGroup(group);
m_HQ.ReserveGroup(group);
battery.Audit("DEPLOY", "authored gun and four-person crew; finite ammunition initialization pending");
return true;
}
static ResourceName MortarGroup() { return "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"; }
ERR_MortarBattery FindGun(IEntity gun)
{
foreach (ERR_MortarBattery battery : m_Batteries)
if (gun && battery.Gun == gun) return battery;
return null;
}
ERR_MortarBattery FindGroup(SCR_AIGroup group)
{
foreach (ERR_MortarBattery battery : m_Batteries)
if (group && battery.Group == group) return battery;
return null;
}
ERR_HQReport Latest(ERR_MortarBattery battery, float now)
{
// Only the coordinates actually delivered to this battery are available.
// A newer HQ contact cannot update a gun's knowledge without another downlink.
if (!battery || !ERR_MortarPrediction.Fresh(battery.Report, now)) return null;
return battery.Report;
}
string ContactFailure(ERR_MortarBattery battery)
{
if (!battery.Report) return "no delivered target report";
return "delivered target report expired or invalid";
}
bool OutsideSettlements(vector impact)
{
bool clear = m_Settlements.OutsideSettlements(impact);
SettlementReason = m_Settlements.SettlementReason;
return clear;
}
bool Safe(ERR_MortarBattery battery, vector impact, float flight)
{
SafetyReason = "clear";
if (!battery.Operational()) { SafetyReason = battery.OperationalReason(); return false; }
if (!OutsideSettlements(impact)) { SafetyReason = SettlementReason; return false; }
if (SCR_TerrainHelper.GetTerrainY(impact) < 0) { SafetyReason = "impact below Everon sea level"; return false; }
Faction faction = battery.Group.GetFaction();
if (!faction) { SafetyReason = "battery faction missing"; return false; }
array<IEntity> friendlies = {};
m_HQ.PhysicalForces(friendlies);
foreach (IEntity entity : friendlies)
{
Faction friendly = SCR_FactionManager.SGetFaction(entity);
if (!friendly || faction.IsFactionEnemy(friendly) || !ERR_Actors.Alive(entity)) continue;
vector velocity;
IEntity moving = CompartmentAccessComponent.GetVehicleIn(entity);
if (!moving) moving = entity;
if (moving.GetPhysics()) velocity = moving.GetPhysics().GetVelocity();
if (ERR_MortarPrediction.ThreatensFriendly(impact, entity.GetOrigin(), velocity, flight))
{
SafetyReason = "friendly physical force can enter the impact area";
return false;
}
}
return true;
}
bool Withhold(ERR_MortarBattery battery, string reason, string detail = "")
{
battery.LastFailure = reason + "; " + detail;
battery.Trace("WITHHELD", reason, detail);
// Finish the activity from the next HQ tick, never from inside its own node.
battery.Rejected = true;
return false;
}
bool AllowShot(ERR_MortarBattery battery)
{
float now = GetGame().GetWorld().GetWorldTime();
if (m_Stopped || !ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive or HQ stopped");
if (!battery.Operational()) return Withhold(battery, battery.OperationalReason());
if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) return Withhold(battery, "salvo complete or order closed");
if (battery.Released) return Withhold(battery, "waiting for committed projectile");
if (battery.AimedShot != battery.OrderShots || battery.AimedAt < 0 || now - battery.AimedAt > 10000) return Withhold(battery, "fresh aim solution required");
ERR_HQReport report = Latest(battery, now);
if (!report) return Withhold(battery, ContactFailure(battery));
vector updated;
if (!ERR_MortarPrediction.Predict(report, now, battery.FlightTime, updated)) return Withhold(battery, ERR_MortarPrediction.FailureReason);
updated += battery.AimOffset;
if (vector.DistanceXZ(updated, battery.Impact) > 60) return Withhold(battery, "predicted target left aimed impact area");
if (!OutsideSettlements(updated)) return Withhold(battery, SettlementReason);
if (!Safe(battery, battery.Impact, battery.FlightTime)) return Withhold(battery, SafetyReason);
return true;
}
protected void AdvanceOrder(ERR_MortarBattery battery, float now)
{
if (battery.RadioOrder)
{
int deliveredState = battery.RadioOrder.Advance(now, m_HQ.Journal());
if (deliveredState == 0) return;
ERR_HQReport delivered = battery.PendingRadioReport;
battery.RadioOrder = null;
battery.PendingRadioReport = null;
if (deliveredState < 0 || !ERR_MortarPrediction.Fresh(delivered, now) || !battery.Operational() || battery.Activity || !battery.CommandOpen)
{
battery.Cancel("radio delivery, report freshness or battery availability failed");
battery.NextOrder = now + 10000;
return;
}
battery.PendingSupplyReport = delivered;
}
if (!battery.PendingSupplyReport) return;
if (!battery.CommandOpen || !ERR_MortarPrediction.Fresh(battery.PendingSupplyReport, now) || !battery.Operational() || battery.Activity)
{
battery.Cancel("delivered order expired or became unavailable while awaiting physical ammunition");
battery.NextOrder = now + 10000;
return;
}
ERR_MortarSupplyState supply = battery.SupplyState(now);
if (supply == ERR_MortarSupplyState.PENDING) return;
if (supply == ERR_MortarSupplyState.FAILED)
{
battery.Cancel("physical gunner ammunition unavailable or transfer failed");
battery.NextOrder = now + 10000;
return;
}
ERR_HQReport ready = battery.PendingSupplyReport;
battery.PendingSupplyReport = null;
battery.Order(ready, now);
}
void Tick(float now)
{
if (m_Stopped || !Replication.IsServer()) return;
foreach (ERR_MortarBattery battery : m_Batteries)
{
if (battery.Rejected && !battery.Released)
{
battery.Cancel("native aim or firing authorization rejected");
battery.NextOrder = now + 10000;
}
battery.Rejected = false;
battery.Update(now);
AdvanceOrder(battery, now);
}
if (now < m_NextDecision) return;
m_NextDecision = now + 10000;
array<ref ERR_HQReport> reports = {};
m_HQ.SupportContacts(reports, now);
ERR_MortarBattery selected;
ERR_HQReport target;
float distance = float.MAX;
foreach (ERR_MortarBattery battery : m_Batteries)
{
if (!battery.StartupActivated || !battery.Operational() || battery.Activity || battery.RadioOrder || battery.PendingSupplyReport || battery.SupplyPending() || now < battery.NextOrder || battery.CountShells() == 0) continue;
foreach (ERR_HQReport report : reports)
{
float candidate = vector.DistanceXZ(battery.Position, report.Position);
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
selected = battery;
target = report;
distance = candidate;
}
}
if (!selected || !target) return;
ERR_HQRoute route = m_HQ.Downlink(selected.Group, "MORTAR ORDER", target.Description(now), target.MessageId);
if (!route) return;
selected.BeginRequest(target, now);
selected.RadioId = route.MessageId;
selected.RadioOrder = route;
selected.PendingRadioReport = target.Snapshot();
m_NextDecision = now + 20000;
}
void AppendGuns(array<IEntity> entities)
{
foreach (ERR_MortarBattery battery : m_Batteries)
if (battery.Gun && !entities.Contains(battery.Gun)) entities.Insert(battery.Gun);
}
string Status()
{
int ready, shells;
foreach (ERR_MortarBattery battery : m_Batteries)
{
if (battery.StartupActivated && battery.Operational()) ready++;
shells += battery.CountShells();
}
return string.Format("mortars %1/%2; HE shells %3", ready, m_Batteries.Count(), shells);
}
void Stop()
{
m_Stopped = true;
foreach (ERR_MortarBattery battery : m_Batteries) battery.Stop();
}
}
@@ -0,0 +1,241 @@
#ifdef WORKBENCH
// Explicit synthetic observation only. Delivery, HQ scheduling and native fire
// are unchanged; this helper never writes directly into the HQ contact list.
modded class ERR_HQ
{
int ERR_QueueMortarFixtureReport(SCR_AIGroup source, IEntity target, vector position, float now)
{
if (!ERR_NativeMortarFiringProbe.Enabled() || !target || !m_Contacts.IsEmpty()) return 0;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != source || !unit.ReservedForSupport || !unit.Radio || unit.Uplink || unit.PendingReport) continue;
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route || route.Nodes.IsEmpty()) return 0;
ERR_HQReport report = new ERR_HQReport();
report.MessageId = m_Journal.Message(); report.Target = target; report.Source = source;
report.Position = position; report.ObservedAt = now; report.DetailsObservedAt = now;
report.HasVelocity = true; report.Velocity = vector.Zero;
report.TravelMode = "on foot"; report.SeenEscapists = 1; report.SeenWeapons = "fixture observation";
route.MessageId = report.MessageId; route.Payload = "EXPLICIT SYNTHETIC OBSERVATION: " + report.Description(now);
unit.PendingReport = report; unit.Uplink = route;
m_Journal.Write("FIXTURE REPORT", route.Payload, report.MessageId);
return report.MessageId;
}
return 0;
}
void ERR_RemoveMortarFixtureReport(IEntity target)
{
if (!target) return;
for (int i = m_Contacts.Count() - 1; i >= 0; i--)
if (m_Contacts[i].Target == target) m_Contacts.Remove(i);
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.PendingReport && unit.PendingReport.Target == target) { unit.PendingReport = null; unit.Uplink = null; }
if ((unit.PendingOrder && unit.PendingOrder.Target == target) || (unit.Assignment && unit.Assignment.Target == target)) unit.Release();
}
}
}
// Observe only the actual native event. Node SUCCESS, accepted inventory moves
// and committed shell loading are never counted as physical projectiles.
modded class SCR_MortarMuzzleComponent
{
override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity)
{
ERR_NativeMortarFiringProbe probe = ERR_NativeMortarFiringProbe.Active();
if (probe) probe.NativeProjectile(GetOwner(), projectileEntity);
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
}
}
class ERR_NativeMortarFiringProbe
{
protected static ERR_NativeMortarFiringProbe s_Active;
protected ERR_GameMode m_Mode;
protected ERR_MortarBattery m_Battery;
protected IEntity m_Gun;
protected SCR_AIGroup m_Group;
protected IEntity m_Target;
protected vector m_Impact;
protected ref array<IEntity> m_Crew = {};
protected ref array<IEntity> m_InitialShells = {};
protected ref array<IEntity> m_Projectiles = {};
protected int m_Source;
protected int m_Order;
protected int m_Step;
protected int m_Events;
protected int m_StoppedShells;
protected float m_Deadline;
protected float m_NextDiagnostic;
protected float m_StopUntil;
protected bool m_Unsafe;
protected bool m_BadProjectile;
protected bool m_SawUplink;
protected bool m_SawDownlink;
protected bool m_SawActivity;
protected bool m_Stopped;
protected string m_BadReason;
static bool Enabled()
{
string enabled, outcome;
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mortar-fire";
}
static ERR_NativeMortarFiringProbe Active() { return s_Active; }
void ERR_NativeMortarFiringProbe(ERR_GameMode mode)
{
m_Mode = mode;
m_Deadline = GetGame().GetWorld().GetWorldTime() + 180000;
if (Enabled() && !s_Active) s_Active = this;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: mortar firing " + detail); return true; }
Print("ERR SMOKE FAIL: mortar firing " + detail, LogLevel.ERROR); Stop(); return false;
}
protected void Shells(out array<IEntity> shells)
{
shells.Clear();
foreach (IEntity actor : m_Crew)
{
if (!actor) continue;
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 && !shells.Contains(item)) shells.Insert(item);
}
}
protected bool CheckNoOccupants(IEntity entity)
{
if (ChimeraCharacter.Cast(entity) || Vehicle.Cast(entity)) { m_Unsafe = true; return false; }
return true;
}
protected bool EmptyImpact(vector position)
{
m_Unsafe = false;
GetGame().GetWorld().QueryEntitiesByAABB(position - "200 5000 200", position + "200 5000 200", CheckNoOccupants, null, EQueryEntitiesFlags.ALL);
return !m_Unsafe;
}
protected bool SelectImpact(ERR_MortarSystem system)
{
// Bounded candidate survey: no clearance assumption from coordinates alone.
array<vector> offsets = {"-800 0 -600", "-600 0 -800", "0 0 -1000", "600 0 -800", "-1000 0 0", "800 0 -600", "-800 0 600", "0 0 1000"};
foreach (vector offset : offsets)
{
vector candidate = ERR_Actors.Ground(m_Battery.Position + offset);
if (candidate[1] < 1 || !EmptyImpact(candidate) || !system.Safe(m_Battery, candidate, 75)) continue;
bool clear = true;
for (int shot = 0; shot < ERR_MortarSalvo.ROUNDS; shot++)
{
vector spread = ERR_Actors.Ground(candidate + ERR_MortarSalvo.Offset(m_Battery.SiteId, m_Battery.OrderId + 1, shot));
if (!system.Safe(m_Battery, spread, 75)) { clear = false; break; }
}
if (!clear) continue;
m_Impact = candidate;
PrintFormat("ERR MORTAR FIXTURE IMPACT: position=%1 batteryDistance=%2; no characters/vehicles in 400m square; production settlement/friendly exclusions passed", candidate, vector.DistanceXZ(candidate, m_Battery.Position));
return true;
}
return false;
}
protected void Diagnose(float now)
{
if (now < m_NextDiagnostic) return;
m_NextDiagnostic = now + 5000;
if (!m_Battery) { Print("ERR MORTAR FIXTURE: waiting for one authored battery"); return; }
PrintFormat("ERR MORTAR FIXTURE: step=%1 startup=%2 source=M%3 observedEvents=%4 ready=%5 failure=%6", m_Step, m_Battery.StartupActivated, m_Source, m_Events, m_Battery.Operational(), m_Battery.SpawnFailure);
m_Battery.SiteStatus(now);
}
void NativeProjectile(IEntity gun, IEntity projectile)
{
if (m_Stopped || !m_Battery || gun != m_Gun) return;
if (!projectile) { m_BadProjectile = true; m_BadReason = "native fired event has no physical projectile"; return; }
if (m_Projectiles.Contains(projectile)) { m_BadProjectile = true; m_BadReason = "same physical projectile observed twice"; return; }
m_Projectiles.Insert(projectile); m_Events++;
float now = GetGame().GetWorld().GetWorldTime();
if (m_Step != 1 || !m_Battery.CommandOpen || !m_Battery.Released || m_Battery.SourceId != m_Source || m_Battery.AimedAt < 0 || now - m_Battery.AimedAt > 10000 || m_Battery.FlightTime <= 0)
{ m_BadProjectile = true; m_BadReason = "projectile lacks this delivered order and fresh native aim/commit"; }
PrintFormat("ERR MORTAR FIXTURE PROJECTILE: event=%1 entity=%2 origin=%3 order=%4 source=M%5 aimAge=%6 flight=%7; impact is not measured", m_Events, projectile, projectile.GetOrigin(), m_Battery.OrderId, m_Battery.SourceId, now - m_Battery.AimedAt, m_Battery.FlightTime);
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (!Enabled() || s_Active != this || !m_Mode || !m_Mode.Active()) { Check(false, "explicit active fixture context retained"); return -1; }
ERR_HQ hq = ERR_HQ.Get();
ERR_MortarSystem system;
if (hq) system = hq.Mortars();
if (!system) { Check(false, "real HQ mortar service available"); return -1; }
if (!m_Battery)
{
array<IEntity> guns = {}; system.AppendGuns(guns);
if (guns.Count() == 1) { m_Gun = guns[0]; m_Battery = system.FindGun(m_Gun); }
}
Diagnose(now);
if (now >= m_Deadline) { Check(false, "bounded deadline; inspect readiness/route/activity/aim diagnostics"); return -1; }
if (m_BadProjectile) { Check(false, m_BadReason); return -1; }
if (!m_Battery) return 0;
if (!m_Battery.SpawnFailure.IsEmpty()) { Check(false, "battery startup failed: " + m_Battery.SpawnFailure); return -1; }
if (m_Step == 0)
{
if (!m_Battery.StartupActivated || !m_Battery.Operational() || !hq.MortarConnected(m_Battery.Group)) return 0;
m_Group = m_Battery.Group;
if (!Check(m_Group && m_Group.IsExpandComplete() && m_Group.GetAgentsCount() == 4 && m_Battery.TotalShots == 0 && m_Battery.OrderId == 0 && !m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "untasked original gun and complete four-person crew start without shots")) return -1;
array<AIAgent> agents = {}; m_Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
IEntity actor; if (agent) actor = agent.GetControlledEntity();
if (!Check(ERR_Actors.CanFight(actor) && !m_Crew.Contains(actor), "initial physical crew member is conscious and unique")) return -1;
m_Crew.Insert(actor);
}
if (!Check(m_Battery.RadioOperator && ERR_HQRadio.Capability(m_Battery.RadioOperator) == 2, "dedicated actual manpack supplies the radio route")) return -1;
Shells(m_InitialShells);
if (!Check(m_InitialShells.Count() == 27 && m_Battery.CountShells() == 27, "exactly 27 distinct physical initial HE shells")) return -1;
if (!Check(SelectImpact(system), "bounded candidate survey finds an unoccupied production-safe impact area")) return -1;
m_Target = ERR_Actors.Spawn(ERR_Resources.IntelFolder(), m_Impact);
if (!Check(m_Target != null, "fixture-owned inert report identity exists; no enemy perception is claimed")) return -1;
m_Source = hq.ERR_QueueMortarFixtureReport(m_Group, m_Target, m_Impact, now);
if (!Check(m_Source > 0, "synthetic observation enters actual existing radio uplink once")) return -1;
m_SawUplink = true; m_Deadline = now + 90000; m_Step = 1; return 0;
}
if (m_Battery.Gun != m_Gun || m_Battery.Group != m_Group) { Check(false, "original gun and crew identity retained"); return -1; }
if (!EmptyImpact(m_Impact)) { Check(false, "impact exclusion stays unoccupied before native firing/cancellation"); return -1; }
array<IEntity> remaining = {}; Shells(remaining);
foreach (IEntity item : remaining)
if (!m_InitialShells.Contains(item)) { Check(false, "no replacement HE shell appears after initialization"); return -1; }
if (m_Step == 1)
{
if (m_Battery.RadioOrder && m_Battery.SourceId == m_Source) { m_SawDownlink = true; m_Order = m_Battery.OrderId; }
if (m_Battery.Activity && m_Battery.Report && m_Battery.Report.MessageId == m_Source) m_SawActivity = true;
if (m_Battery.OrderId > 1 || (m_Battery.SourceId != 0 && m_Battery.SourceId != m_Source)) { Check(false, "one unchanged fixture source creates only one HQ order"); return -1; }
if (m_Events == 0) return 0;
if (!Check(m_Events == 1 && m_Battery.TotalShots == 1 && m_Battery.OrderShots == 1 && !m_Battery.Released && m_SawUplink && m_SawDownlink && m_SawActivity && m_Order == 1, "one physical projectile follows real uplink/downlink, native activity and fresh aim")) return -1;
if (!Check(remaining.Count() == 26 && m_Battery.CountShells() == 26, "one fired projectile consumes one of 27 original inventory shells")) return -1;
// Cancel on this later fixture tick, never reentrantly inside the firing node.
hq.ERR_RemoveMortarFixtureReport(m_Target);
system.Stop(); system.Stop();
if (!Check(!m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "explicit support Stop closes native order and route idempotently")) return -1;
m_StoppedShells = remaining.Count(); m_StopUntil = now + 12000; m_Deadline = m_StopUntil + 2000; m_Step = 2; return 0;
}
if (now < m_StopUntil) return 0;
if (!Check(m_Battery.TotalShots == 1 && m_Events == 1 && m_Battery.OrderId == m_Order && remaining.Count() == m_StoppedShells && !m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "stopped service issues no duplicate order, projectile or replenishment over 12 seconds")) return -1;
array<AIAgent> finalCrew = {}; m_Group.GetAgents(finalCrew);
if (!Check(finalCrew.Count() == 4 && m_Battery.Gun == m_Gun, "original group and gun retain four crew")) return -1;
foreach (AIAgent finalAgent : finalCrew)
if (!Check(finalAgent && m_Crew.Contains(finalAgent.GetControlledEntity()) && ERR_Actors.CanFight(finalAgent.GetControlledEntity()), "final conscious crew entity belongs to original physical roster")) return -1;
Stop(); return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
ERR_HQ hq = ERR_HQ.Get();
if (hq)
{
hq.ERR_RemoveMortarFixtureReport(m_Target);
if (m_Battery && m_Battery.SourceId == m_Source && m_Source > 0) m_Battery.Cancel("mortar firing fixture cleanup");
}
ERR_Actors.DeleteSpawned(m_Target); m_Target = null;
if (s_Active == this) s_Active = null;
}
void ~ERR_NativeMortarFiringProbe() { Stop(); }
}
#endif
@@ -0,0 +1,87 @@
#ifdef WORKBENCH
// Test access only: trigger the real delivered-order method on an existing
// registered squad. Radio transit is intentionally not simulated by this test.
modded class ERR_HQ
{
bool ERR_TestPatrolSearch(SCR_AIGroup group, out AIWaypoint search)
{
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group) continue;
if (unit.Assignment || unit.PendingOrder || unit.Waypoint || !unit.Eligible(m_Mode)) return false;
ERR_HQReport order = new ERR_HQReport();
order.Position = group.GetOrigin() + "40 0 0";
order.ObservedAt = GetGame().GetWorld().GetWorldTime();
order.MessageId = m_Journal.Message();
unit.PendingOrder = order;
DeliverOrder(unit, order.ObservedAt);
search = unit.Waypoint;
return search && unit.Assignment == order;
}
return false;
}
bool ERR_TestPatrolRelease(SCR_AIGroup group, AIWaypoint search)
{
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group || unit.Waypoint != search) continue;
unit.Release();
return !unit.Assignment && !unit.PendingOrder && !unit.Waypoint;
}
return false;
}
}
class ERR_NativePatrolHQProbe
{
protected ERR_GameMode m_Mode;
protected ERR_SiteRuntime m_Site;
protected AIWaypoint m_Search;
protected ref array<AIWaypoint> m_Baseline = {};
protected int m_Step;
protected float m_Wait;
void ERR_NativePatrolHQProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: patrol HQ " + detail); return true; }
Print("ERR SMOKE FAIL: patrol HQ " + detail, LogLevel.ERROR); return false;
}
int Tick(float now)
{
if (now < m_Wait) return 0;
ERR_HQ hq = ERR_HQ.Get();
if (!Check(hq != null, "real HQ available")) return -1;
if (m_Step == 0)
{
m_Site = m_Mode.Sites().Find("everon-outpost-1");
if (!Check(m_Site && m_Site.Group && m_Site.PatrolCycle, "existing outpost route available")) return -1;
m_Site.Group.GetWaypoints(m_Baseline);
if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Site.Group, m_Search), "real delivered-order method assigns existing squad")) return -1;
m_Step = 1; m_Wait = now + 750; return 0;
}
if (m_Step == 1)
{
array<AIWaypoint> queue = {};
m_Site.Group.GetWaypoints(queue);
if (!Check(m_Search && !queue.IsEmpty() && queue[0] == m_Search && m_Site.Group.GetCurrentWaypoint() == m_Search, "Search preempts native patrol")) return -1;
foreach (AIWaypoint waypoint : m_Baseline)
if (!Check(queue.Contains(waypoint), "preempted baseline route remains queued")) return -1;
if (!Check(hq.ERR_TestPatrolRelease(m_Site.Group, m_Search), "real HQ release removes only its assignment")) return -1;
m_Step = 2; m_Wait = now + 750; return 0;
}
if (m_Step == 2)
{
AIWaypoint current = m_Site.Group.GetCurrentWaypoint();
if (!Check(current && (current == m_Site.PatrolCycle || m_Site.PatrolWaypoints.Contains(current)), "native patrol resumes after Search release")) return -1;
m_Step = 3; return 1;
}
return 1;
}
void Stop()
{
ERR_HQ hq = ERR_HQ.Get();
if (hq && m_Site && m_Search) hq.ERR_TestPatrolRelease(m_Site.Group, m_Search);
}
void ~ERR_NativePatrolHQProbe() { Stop(); }
}
#endif
@@ -0,0 +1,63 @@
// Vanilla named settlements supply exclusion areas; no original mission location system.
// Shared town/village exclusion for mortar impacts, roadblocks and mine sites.
// Fixed distance from mapped settlement centres, independent of mission
// activation radii. Scattered buildings and props are not settlements.
class ERR_SettlementSafety
{
static const float RADIUS = 200;
string SettlementReason;
protected vector m_SafeImpact;
protected bool m_SettlementNear;
protected vector m_LastSettlementImpact;
protected float m_LastSettlementCheck = -1;
protected bool m_LastSettlementClear;
bool OutsideSettlements(vector impact)
{
float now = GetGame().GetWorld().GetWorldTime();
if (m_LastSettlementCheck >= 0 && now >= m_LastSettlementCheck && now - m_LastSettlementCheck < 1000 && vector.DistanceXZ(impact, m_LastSettlementImpact) == 0)
return m_LastSettlementClear;
m_LastSettlementCheck = now;
m_LastSettlementImpact = impact;
m_LastSettlementClear = false;
m_SafeImpact = impact;
m_SettlementNear = false;
SettlementReason = "clear";
// Also cover named native map settlements, independent of whether the
// mission placed a garrison marker there. Generic building entities also
// include deer stands, bridge pillars, rubble and electrical cabinets.
// Broad vertical bounds keep the 200m distance horizontal on hills.
GetGame().GetWorld().QueryEntitiesByAABB(impact - Vector(RADIUS, 5000, RADIUS), impact + Vector(RADIUS, 5000, RADIUS), CheckSettlement, FilterSettlement, EQueryEntitiesFlags.ALL);
m_LastSettlementClear = !m_SettlementNear;
return m_LastSettlementClear;
}
protected bool FilterSettlement(IEntity entity)
{
return entity && entity.FindComponent(MapDescriptorComponent);
}
protected bool CheckSettlement(IEntity entity)
{
float radius;
MapDescriptorComponent descriptor = MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent));
if (descriptor)
{
int type = descriptor.GetBaseType();
if (type == EMapDescriptorType.MDT_NAME_CITY)
radius = RADIUS;
else if (type == EMapDescriptorType.MDT_NAME_TOWN)
radius = RADIUS;
else if (type == EMapDescriptorType.MDT_NAME_VILLAGE || type == EMapDescriptorType.MDT_NAME_SETTLEMENT)
radius = RADIUS;
}
if (radius > 0 && vector.DistanceXZ(m_SafeImpact, entity.GetOrigin()) <= radius)
{
m_SettlementNear = true;
SettlementReason = string.Format("named map settlement entity=%1 position=%2 distance=%3m exclusion=%4m", entity, entity.GetOrigin(), vector.DistanceXZ(m_SafeImpact, entity.GetOrigin()), radius);
return false;
}
return true;
}
}
@@ -0,0 +1,9 @@
modded class ERR_ScenarioComponent
{
protected override bool PrepareAdditionalForces()
{
if (!super.PrepareAdditionalForces()) return false;
ERR_HQ hq = ERR_HQ.Get();
return hq && hq.PrepareSupport();
}
}
@@ -0,0 +1,9 @@
GameProject {
ID "ERR_Ai"
GUID "5192138CB9D5B911"
TITLE "Escapists Rereintroduced - Ai"
Dependencies {
"58D0FB3206B6F859"
"2528466DE220D91C"
}
}
@@ -0,0 +1,11 @@
SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Rereintroduced: Everon (development)"
m_sAuthor "Rereintroduced contributors"
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress."
m_sGameMode "Escape"
m_iPlayerCount 12
m_eSaveTypes 0
m_bShowInScenarioMenu 1
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{C1E20667DCE324ED}Missions/ERR_Everon.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,36 @@
// Companion-specific commands extend Core's authenticated transport.
modded class ERR_AdminService
{
protected override string Setting(string section, string option)
{
if (section == "hq")
{
if (option == "interval") return "reportIntervalSeconds";
if (option == "squads") return "supportSquadLimit";
if (option == "radius") return "responseRadiusMetres";
}
if (section == "intel")
{
if (option == "carrying") return "aiIntelChancePercent";
}
return super.Setting(section, option);
}
protected override bool Extension(ERR_GameMode mode, string arguments, array<string> replies)
{
if (arguments != "status" && arguments != "hq show" && arguments != "hq log") return super.Extension(mode, arguments, replies);
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
if (!hq) { replies.Insert("HQ unavailable"); return true; }
if (arguments == "hq show" || arguments == "status") replies.Insert(hq.Status());
else
{
int cursor;
hq.Journal().Read(Math.Max(0, hq.Journal().Sequence() - 8), replies, cursor);
if (replies.IsEmpty()) replies.Insert("HQ journal is empty.");
}
return true;
}
protected override string Help()
{
return super.Help() + " | hq show|log | hq interval|squads|radius show|set N | intel carrying show|set N";
}
}
@@ -0,0 +1,10 @@
#ifdef WORKBENCH
modded class ERR_MortarSystem
{
ERR_MortarBattery ERR_ProbeBattery()
{
if (m_Batteries.Count() != 1) return null;
return m_Batteries[0];
}
}
#endif
@@ -0,0 +1,114 @@
// Opt-in engine fixture: physical radios and production delivery routes only.
#ifdef WORKBENCH
modded class ERR_HQ
{
vector ERR_ProbeResponseOrigin()
{
foreach (ERR_HQUnit unit : m_Units)
if (!unit.ReservedForSupport && unit.Group) return unit.Group.GetOrigin() + "0 0 20";
return vector.Zero;
}
bool ERR_ProbeDeliveredResponse()
{
foreach (ERR_HQUnit unit : m_Units)
if (unit.Assignment && unit.Waypoint && unit.Group != unit.Assignment.Source) return true;
return false;
}
bool ERR_ProbeUniqueAssignments()
{
array<AIWaypoint> waypoints = {};
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Waypoint) continue;
if (!unit.Assignment || waypoints.Contains(unit.Waypoint)) return false;
waypoints.Insert(unit.Waypoint);
}
return waypoints.Count() > 0 && Assigned() <= m_Mode.Settings().Get("supportSquadLimit");
}
string ERR_ProbePerception()
{
int targets, sightings, radios;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Radio) radios++;
if (!unit.Utility) continue;
array<ref ERR_HQVisualContact> samples = {};
ERR_HQObservation.Gather(unit.Utility, GetGame().GetWorld().GetWorldTime(), samples);
sightings += samples.Count();
if (unit.Utility.m_Perception) targets += unit.Utility.m_Perception.m_aTargets.Count();
}
return string.Format("%1; radios=%2 targets=%3 fresh visual samples=%4", Status(), radios, targets, sightings);
}
ERR_HQRoute ERR_ProbeRoute(IEntity carrier = null, bool downlink = false)
{
if (carrier) return m_Network.Route(carrier, downlink);
foreach (ERR_HQUnit unit : m_Units)
if (unit.Radio) return m_Network.Route(unit.Radio, downlink);
return null;
}
bool ERR_ProbeSquadsReady()
{
int infantry;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.ReservedForSupport) continue;
infantry++;
if (!unit.Group || ERR_HQRadio.Capability(unit.Radio) != 2) return false;
array<AIAgent> agents = {};
unit.Group.GetAgents(agents);
if (agents.Count() != 6) return false;
}
return infantry == 6;
}
}
class ERR_NativeRadioProbe
{
protected ref ERR_HQRoute m_Route;
protected IEntity m_Carrier;
protected int m_Step;
int Tick(ERR_HQ hq, float now)
{
if (!hq) return -1;
if (m_Step == 0)
{
if (!hq.ERR_ProbeSquadsReady()) return 0;
Print("ERR SMOKE PASS: six physical six-person squads each have a manpack operator");
m_Route = hq.ERR_ProbeRoute();
if (!m_Route || m_Route.Nodes.IsEmpty()) return -1;
m_Carrier = m_Route.Nodes[0].Carrier;
m_Route.MessageId = hq.Journal().Message();
m_Step = 1;
}
int result = m_Route.Advance(now, hq.Journal());
if (result == 0) return 0;
if (m_Step == 3)
{
if (result >= 0) return -1;
Print("ERR SMOKE PASS: removing receiving manpack interrupts an in-flight HQ order");
return 1;
}
if (result < 0) return -1;
if (m_Step == 1) Print("ERR SMOKE PASS: physical manpack uplink delivers to HQ");
if (m_Step == 2) Print("ERR SMOKE PASS: HQ downlink delivers to a physical receiver");
m_Route = hq.ERR_ProbeRoute(m_Carrier, true);
if (!m_Route || m_Route.Nodes.IsEmpty()) return -1;
m_Route.MessageId = hq.Journal().Message();
m_Step++;
if (m_Step == 3)
{
// Start HQ transmission, then remove the actual receiving equipment.
if (m_Route.Advance(now, hq.Journal()) != 0) return -1;
IEntity receiver = m_Route.Nodes[0].Carrier;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(receiver.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return -1;
array<IEntity> items = {};
inventory.GetItems(items);
foreach (IEntity item : items)
if (item && item.FindComponent(BaseRadioComponent)) SCR_EntityHelper.DeleteEntityAndChildren(item);
}
return 0;
}
}
#endif
@@ -0,0 +1,434 @@
// Explicit, local Workbench diagnostic only. Never compiled into game/server builds.
#ifdef WORKBENCH
modded class ERR_GameMode
{
protected bool m_ERRTestPatrolHQ;
protected ref ERR_NativePatrolProbe m_ERRHQPatrolMovement;
protected ref ERR_NativePatrolHQProbe m_ERRHQPatrolOrder;
protected int m_ERRSmokeStep;
protected float m_ERRSmokeDeadline;
protected float m_ERRSmokeWait;
protected IEntity m_ERRSmokePlayer;
protected BaseDoorComponent m_ERRGate;
protected ERR_LocationComponent m_ERRSmokeRadio;
protected bool m_ERRSmokeDone;
protected bool m_ERRTestLostHelicopter;
protected bool m_ERRTestPlayerDeath;
protected bool m_ERRTestRadioNetwork;
protected bool m_ERRTestHQResponse;
protected bool m_ERRTestMortar;
protected bool m_ERRTestMortarFire;
protected ref ERR_NativeMortarFiringProbe m_ERRMortarFire;
protected float m_ERRNextProbeLog;
protected ref ERR_NativeRadioProbe m_ERRRadioProbe;
protected ref array<IEntity> m_ERRCacheFolders = {};
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmoke", enabled) || enabled != "1") return;
string outcome;
m_ERRTestLostHelicopter = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "lost-helicopter";
m_ERRTestRadioNetwork = outcome == "radio-network";
m_ERRTestHQResponse = outcome == "hq-response";
m_ERRTestMortar = outcome == "mortar-inventory";
m_ERRTestMortarFire = outcome == "mortar-fire";
m_ERRTestPatrolHQ = outcome == "patrol-hq";
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar;
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
if (m_ERRTestMortarFire) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
if (m_ERRTestPatrolHQ) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 420000;
OnStop().Insert(ERR_StopHQPatrol);
OnPhaseChanged().Insert(ERR_SmokePhase);
GetGame().GetCallqueue().CallLater(ERR_SmokeTick, 500, true);
Print("ERR SMOKE: scripted integration test enabled; gameplay acceptance remains separate");
}
protected bool ERR_Check(bool condition, string name)
{
if (condition) { Print("ERR SMOKE PASS: " + name); return true; }
ERR_StopHQPatrol();
Print("ERR SMOKE FAIL: " + name, LogLevel.ERROR);
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
return false;
}
protected void ERR_StopHQPatrol()
{
if (m_ERRMortarFire) m_ERRMortarFire.Stop();
if (m_ERRHQPatrolMovement) m_ERRHQPatrolMovement.Stop();
if (m_ERRHQPatrolOrder) m_ERRHQPatrolOrder.Stop();
}
protected void ERR_MovePlayer(vector position)
{
vector transform[4];
m_ERRSmokePlayer.GetWorldTransform(transform);
transform[3] = ERR_Actors.Ground(position);
ChimeraCharacter.Cast(m_ERRSmokePlayer).Teleport(transform);
}
protected ERR_UnlockPrisonAction ERR_FindLock(IEntity entity)
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
array<BaseUserAction> actions = {};
manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
if (lock) return lock;
}
}
IEntity child = entity.GetChildren();
while (child)
{
ERR_UnlockPrisonAction lock = ERR_FindLock(child);
if (lock) return lock;
child = child.GetSibling();
}
return null;
}
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
{
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
if (m_ERRTestMortarFire)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 151, "native mortar first shot and stop complete before explicit reset")) return;
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
ERR_StopHQPatrol();
Print("ERR SMOKE COMPLETE: native HQ radio to first mortar projectile and explicit stop; fixture reset requested");
return;
}
if (m_ERRTestPatrolHQ)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 133, "HQ patrol preemption and physical resumption before explicit reset")) return;
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
Print("ERR SMOKE COMPLETE: native HQ patrol preemption and physical resumption");
return;
}
if (m_ERRTestPlayerDeath)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 91, "death exhausts round life and reaches defeat after spectator grace")) return;
}
else if (m_ERRTestLostHelicopter)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 8, "lost extraction helicopter causes defeat")) return;
}
else if (!ERR_Check(current == ERR_Phase.VICTORY && m_ERRSmokeStep == 7, "scripted escape reaches victory after physical vehicle occupancy")) return;
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
PrintFormat("ERR SMOKE COMPLETE: terminal=%1 lostHelicopterCase=%2 playerDeathCase=%3", current, m_ERRTestLostHelicopter, m_ERRTestPlayerDeath);
GetGame().GetCallqueue().CallLater(ERR_EndScreenshot, 4000);
}
protected bool ERR_CheckRoster()
{
ERR_PlayerRoster roster = new ERR_PlayerRoster();
UUID first = "00e77100-0000-0000-0000-000000000101";
UUID second = "00e77100-0000-0000-0000-000000000202";
if (!ERR_Check(roster.Bind(101, first) && roster.CanEnter(101, ERR_Phase.PRISON), "new identity may enter prison")) return false;
roster.Spawned(101, m_ERRSmokePlayer);
if (!ERR_Check(!roster.CanEnter(101, ERR_Phase.PRISON), "spawn consumes identity life")) return false;
roster.Disconnect(101);
if (!ERR_Check(roster.HasSurvivor() && !roster.Get(101), "disconnect retains living actor and clears connection")) return false;
if (!ERR_Check(roster.Bind(202, first) && !roster.CanEnter(202, ERR_Phase.PRISON), "reconnected identity retains spent life")) return false;
if (!ERR_Check(!roster.Bind(303, first), "duplicate active identity rejected")) return false;
if (!ERR_Check(roster.Bind(101, second) && roster.CanEnter(101, ERR_Phase.RADIO_DEFENCE) && !roster.CanEnter(101, ERR_Phase.EXTRACTION), "recycled connection receives independent life and extraction admission policy")) return false;
return true;
}
protected bool ERR_CheckCompanionCommands()
{
array<string> replies = {};
array<string> commands = {"hq interval show", "hq squads show", "hq radius show", "intel carrying show", "intel reveal show"};
array<string> keys = {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent"};
for (int i = 0; i < commands.Count(); i++)
{
Admin().Execute(this, true, commands[i], replies);
if (!ERR_Check(replies.Count() == 1 && replies[0] == keys[i] + "=" + Settings().Get(keys[i]).ToString(), "companion command retained: " + commands[i])) return false;
}
Admin().Execute(this, false, "hq log", replies);
if (!ERR_Check(replies.Count() == 1 && replies[0] == "Administrator access required.", "companion journal is behind Core admin authorization")) return false;
Admin().Execute(this, true, "hq show", replies);
ERR_HQ hq = ERR_HQ.Cast(Command());
if (!ERR_Check(hq && replies.Count() == 1 && replies[0] == hq.Status(), "companion HQ status uses the shared command service")) return false;
return true;
}
protected void ERR_PrisonScreenshot() { System.MakeScreenshot("$logs:err-prison"); }
protected void ERR_EndScreenshot() { System.MakeScreenshot("$logs:err-result"); }
protected void ERR_SmokeTick()
{
float now = GetWorld().GetWorldTime();
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 == 150)
{
int result = m_ERRMortarFire.Tick(now);
if (result < 0) { ERR_Check(false, "native mortar firing probe failed; see diagnostics"); return; }
if (result == 1) { m_ERRSmokeStep = 151; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRSmokeStep == 130 || m_ERRSmokeStep == 132)
{
int result = m_ERRHQPatrolMovement.Tick(now);
if (result < 0) { ERR_StopHQPatrol(); GetGame().GetCallqueue().Remove(ERR_SmokeTick); return; }
if (result == 1)
{
if (m_ERRSmokeStep == 130) { m_ERRHQPatrolOrder = new ERR_NativePatrolHQProbe(this); m_ERRSmokeStep = 131; }
else { m_ERRSmokeStep = 133; Transition(ERR_Phase.DEFEAT); }
}
return;
}
if (m_ERRSmokeStep == 131)
{
int result = m_ERRHQPatrolOrder.Tick(now);
if (result < 0) { ERR_StopHQPatrol(); GetGame().GetCallqueue().Remove(ERR_SmokeTick); return; }
if (result == 1) { m_ERRHQPatrolMovement = new ERR_NativePatrolProbe(this); m_ERRSmokeStep = 132; }
return;
}
if (m_ERRSmokeStep == 0)
{
array<IEntity> players = {};
ERR_Actors.Players(players, true);
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
m_ERRSmokePlayer = players[0];
if (!ERR_CheckRoster() || !ERR_NativeContractsProbe.Run(this, m_ERRSmokePlayer) || !ERR_CheckCompanionCommands()) return;
if (m_ERRTestPatrolHQ)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRHQPatrolMovement = new ERR_NativePatrolProbe(this);
m_ERRSmokeStep = 130;
return;
}
if (m_ERRTestMortarFire)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRMortarFire = new ERR_NativeMortarFiringProbe(this);
m_ERRSmokeStep = 150;
return;
}
if (m_ERRTestMortar)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRSmokeStep = 120;
return;
}
if (m_ERRTestHQResponse)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRSmokeStep = 110;
return;
}
if (m_ERRTestRadioNetwork)
{
m_ERRRadioProbe = new ERR_NativeRadioProbe();
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRSmokeStep = 100;
return;
}
if (m_ERRTestPlayerDeath)
{
m_ERRSmokeStep = 9;
SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent)).Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
return;
}
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!ERR_Check(vector.DistanceXZ(m_ERRSmokePlayer.GetOrigin(), PrisonPosition()) < 5, "initial prisoner position")) return;
foreach (ERR_LocationComponent location : m_Locations)
if (location.Kind() == ERR_LocationKind.RADIO) m_ERRSmokeRadio = location;
if (!ERR_Check(m_ERRSmokeRadio && ERR_Actors.Alive(ExtractionVehicle()), "radio and extraction vehicle exist")) return;
ERR_UnlockPrisonAction lock;
foreach (IEntity tracked : m_OwnedEntities)
{
if (tracked) lock = ERR_FindLock(tracked);
if (lock) break;
}
if (!ERR_Check(lock != null, "authored gate exposes lock action")) return;
ERR_MovePlayer(lock.GetOwner().GetOrigin() + "1 0 1");
vector gateLow, gateHigh;
lock.GetOwner().GetWorldBounds(gateLow, gateHigh);
PrintFormat("ERR GEOMETRY: gate low=%1 high=%2", gateLow, gateHigh);
IEntity wall = lock.GetOwner().GetParent().GetChildren();
while (wall)
{
vector wallLow, wallHigh;
wall.GetWorldBounds(wallLow, wallHigh);
PrintFormat("ERR GEOMETRY: child origin=%1 low=%2 high=%3", wall.GetOrigin(), wallLow, wallHigh);
wall = wall.GetSibling();
}
GetGame().GetCallqueue().CallLater(ERR_PrisonScreenshot, 2000);
if (!ERR_Check(lock.CanBePerformedScript(m_ERRSmokePlayer), "lock permits prisoner")) return;
m_ERRGate = BaseDoorComponent.Cast(lock.GetOwner().FindComponent(BaseDoorComponent));
lock.PerformAction(lock.GetOwner(), m_ERRSmokePlayer);
if (!ERR_Check(Phase() == ERR_Phase.RADIO_SEARCH, "lock action advances mission")) return;
array<IEntity> cacheStock = {};
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(Sites().Find("everon-supply-1").Container.FindComponent(InventoryStorageManagerComponent)), cacheStock);
foreach (IEntity tracked : cacheStock)
{
if (tracked && tracked.GetPrefabData() && tracked.GetPrefabData().GetPrefabName() == ERR_Resources.ObjectiveDocument()) m_ERRCacheFolders.Insert(tracked);
}
if (!ERR_Check(m_ERRCacheFolders.Count() == 2, "two guaranteed physical cache documents")) return;
if (!ERR_Check(m_Settings.Set("aiIntelChancePercent", 0), "test does not rely on random intel")) return;
if (!ERR_Check(m_Settings.Set("radioDefenceSeconds", 30), "isolated test profile defence duration")) return;
m_ERRSmokeStep = 1;
}
else if (m_ERRSmokeStep == 1 || m_ERRSmokeStep == 2)
{
if (m_ERRSmokeStep == 2 && !ERR_Check(m_ERRGate && m_ERRGate.IsOpen() && m_ERRGate.CanCharacterPass(0.6), "unlocked gate physically permits passage")) return;
ERR_MovePlayer(Sites().Find("everon-supply-1").Container.GetOrigin() + "0 0 2");
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(Sites().Find("everon-supply-1").Container.FindComponent(InventoryStorageManagerComponent));
BaseInventoryStorageComponent destination = inventory.FindStorageForItem(m_ERRCacheFolders[m_ERRSmokeStep - 1]);
if (!ERR_Check(destination && source.TryMoveItemToStorage(m_ERRCacheFolders[m_ERRSmokeStep - 1], destination), "authored cache document recovered into inventory")) return;
m_ERRSmokeStep++;
m_ERRSmokeWait = now + 2500;
}
else if (m_ERRSmokeStep == 3)
{
if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2, "carried intel identifies radio through intel service")) return;
if (!ERR_Check(!TuneRadio(m_ERRSmokePlayer, m_ERRSmokeRadio), "radio rejects distant player")) return;
ERR_MovePlayer(m_ERRSmokeRadio.Position() + "2 0 0");
m_ERRSmokeStep = 30;
m_ERRSmokeWait = now + 1000;
}
else if (m_ERRSmokeStep == 30)
{
PrintFormat("ERR SMOKE: radio=%1 player=%2", m_ERRSmokeRadio.Position(), m_ERRSmokePlayer.GetOrigin());
if (!ERR_Check(TuneRadio(m_ERRSmokePlayer, m_ERRSmokeRadio), "identified radio starts defence")) return;
m_ERRSmokeStep = 4;
m_ERRSmokeWait = now + 3000;
}
else if (m_ERRSmokeStep == 4)
{
if (!ERR_Check(DefenceOccupied(), "occupied defence timer running")) return;
ERR_MovePlayer(m_ERRSmokeRadio.Position() + "90 0 0");
m_ERRSmokeStep = 5;
m_ERRSmokeWait = now + 2500;
}
else if (m_ERRSmokeStep == 5)
{
if (!ERR_Check(!DefenceOccupied() && Phase() == ERR_Phase.RADIO_DEFENCE, "leaving area resets defence")) return;
ERR_MovePlayer(m_ERRSmokeRadio.Position() + "2 0 0");
m_ERRSmokeStep = 6;
m_ERRSmokeWait = now + 33000;
}
else if (m_ERRSmokeStep == 6)
{
if (!ERR_Check(Phase() == ERR_Phase.EXTRACTION, "continuous occupied defence completes")) return;
if (m_ERRTestLostHelicopter)
{
m_ERRSmokeStep = 8;
ERR_Actors.DeleteSpawned(ExtractionVehicle());
return;
}
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRSmokePlayer.FindComponent(CompartmentAccessComponent));
BaseCompartmentSlot slot = access.FindFreeCompartment(ExtractionVehicle(), ECompartmentType.PILOT);
if (!ERR_Check(slot && access.GetInVehicle(ExtractionVehicle(), slot, true, -1, ECloseDoorAfterActions.INVALID, false), "boarding extraction vehicle")) return;
m_ERRSmokeStep = 7;
m_ERRSmokeWait = now + 3000;
}
else if (m_ERRSmokeStep == 120)
{
ERR_MortarBattery battery = ERR_MortarSystem.Get().ERR_ProbeBattery();
if (!battery || !battery.StartupActivated) return;
if (!ERR_Check(battery.Operational() && battery.Group.GetAgentsCount() == 4, "authored physical mortar and four-person crew initialize")) return;
if (!ERR_Check(ERR_HQRadio.Capability(battery.RadioOperator) == 2, "mortar has a dedicated physical manpack operator")) return;
if (!ERR_Check(battery.CountShells() == 27, "mortar starts with exactly 27 physical HE shells")) return;
array<AIAgent> agents = {};
battery.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(agent.GetControlledEntity().FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {};
ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) SCR_EntityHelper.DeleteEntityAndChildren(item);
}
m_ERRSmokeStep = 121;
m_ERRSmokeWait = now + 15000;
}
else if (m_ERRSmokeStep == 121)
{
ERR_MortarBattery battery = ERR_MortarSystem.Get().ERR_ProbeBattery();
if (!ERR_Check(battery.CountShells() == 0 && battery.TotalShots == 0, "depleted mortar inventory stays empty without fabricated ammunition")) return;
ERR_Actors.DeleteSpawned(battery.Gun);
ERR_Actors.DeleteSpawned(battery.Group);
m_ERRSmokeStep = 122;
m_ERRSmokeWait = now + 12000;
}
else if (m_ERRSmokeStep == 122)
{
ERR_MortarBattery battery = ERR_MortarSystem.Get().ERR_ProbeBattery();
if (!ERR_Check(battery && !battery.Gun && !battery.Group && !battery.Operational(), "lost mortar and crew remain lost without replacement spawning")) return;
m_ERRSmokeStep = 9;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
damage.EnableDamageHandling(true);
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
}
else if (m_ERRSmokeStep == 110)
{
ERR_HQ hq = ERR_HQ.Cast(Command());
if (!hq || !hq.ERR_ProbeSquadsReady()) return;
ERR_MovePlayer(hq.ERR_ProbeResponseOrigin());
m_ERRSmokeStep = 111;
m_ERRSmokeWait = now + 1000;
}
else if (m_ERRSmokeStep == 111)
{
ERR_HQ hq = ERR_HQ.Cast(Command());
if (now >= m_ERRNextProbeLog)
{
Print("ERR HQ PROBE: " + hq.ERR_ProbePerception());
m_ERRNextProbeLog = now + 10000;
}
if (!hq.ERR_ProbeDeliveredResponse()) return;
if (!ERR_Check(true, "native visual contact reaches HQ and orders a different existing squad")) return;
if (!ERR_Check(hq.ERR_ProbeUniqueAssignments(), "HQ uses distinct waypoints within the existing force and order limits")) return;
m_ERRSmokeStep = 9;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
damage.EnableDamageHandling(true);
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
}
else if (m_ERRSmokeStep == 100)
{
int result = m_ERRRadioProbe.Tick(ERR_HQ.Cast(Command()), now);
if (result == 0) return;
if (!ERR_Check(result == 1, "native radio delivery and interruption sequence")) return;
m_ERRSmokeStep = 9;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
damage.EnableDamageHandling(true);
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
}
else if (m_ERRSmokeStep == 9)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!ERR_Check(!ERR_Actors.Alive(m_ERRSmokePlayer), "native damage kills prisoner")) return;
if (!ERR_Check(controller && controller.ERR_ObserverState() == ERR_ObserverReason.SPENT_LIFE, "death selects spectator without another life")) return;
if (!ERR_Check(controller.ERR_HasObserverCamera() && SCR_ManualCamera.Cast(GetGame().GetCameraManager().CurrentCamera()), "spectator camera activates after native death")) return;
m_ERRSmokeStep = 91;
}
else if (m_ERRSmokeStep == 7)
{
if (!ERR_Check(CompartmentAccessComponent.GetVehicleIn(m_ERRSmokePlayer) == ExtractionVehicle(), "physical extraction occupancy")) return;
// Controlled transport tests the boundary detector, not piloting skill.
ExtractionVehicle().SetOrigin(m_EscapeCenter + Vector(m_EscapeRadius + 100, 300, 0));
m_ERRSmokeWait = now + 5000;
}
}
void ~ERR_GameMode()
{
if (GetGame())
{
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
GetGame().GetCallqueue().Remove(ERR_PrisonScreenshot);
GetGame().GetCallqueue().Remove(ERR_EndScreenshot);
}
}
}
#endif
@@ -0,0 +1,16 @@
#ifndef WORKBENCH
modded class ArmaReforgerScripted
{
override protected ResourceName ERR_DiagnosticHeader(string project)
{
if (project == "chat") return "{C1E20667DCE324ED}Missions/ERR_Everon.conf";
return super.ERR_DiagnosticHeader(project);
}
override protected string ERR_DiagnosticAddon(string project)
{
if (project == "chat") return "FA804F411BDDF2C6";
return super.ERR_DiagnosticAddon(project);
}
}
#endif
@@ -0,0 +1,51 @@
[WorkbenchPluginAttribute(name: "Validate Rereintroduced scenario", wbModules: {"WorldEditor"})]
class ERR_ValidateWorldPlugin : WorldEditorPlugin
{
protected bool m_Obstructed;
protected bool CheckClearance(IEntity entity)
{
if (entity && entity.GetVObject()) m_Obstructed = true;
return !m_Obstructed;
}
override void Run() { Validate(); }
override void RunCommandline() { Validate(); }
protected void Validate()
{
WorldEditor editor = Workbench.GetModule(WorldEditor);
if (!editor) { Print("ERR VALIDATION: World Editor missing", LogLevel.ERROR); return; }
WorldEditorAPI api = editor.GetApi();
if (!api) { Print("ERR VALIDATION: world API missing", LogLevel.ERROR); return; }
PrintFormat("ERR VALIDATION: prison terrain=%1, radio terrain=%2, extraction terrain=%3", api.GetTerrainSurfaceY(6090,7160), api.GetTerrainSurfaceY(6500,7200), api.GetTerrainSurfaceY(7000,7500));
for (int dx = -6; dx <= 6; dx += 3)
for (int dz = -6; dz <= 6; dz += 3)
PrintFormat("ERR TERRAIN: x=%1 z=%2 y=%3", 6090 + dx, 7160 + dz, api.GetTerrainSurfaceY(6090 + dx, 7160 + dz));
float bestSlope = 1000;
vector bestPosition;
for (int x = 5800; x <= 6200; x += 10)
{
for (int z = 6800; z <= 7200; z += 10)
{
float low = 10000;
float high = -10000;
for (int sx = -9; sx <= 9; sx += 3)
for (int sz = -15; sz <= 9; sz += 3)
{
float height = api.GetTerrainSurfaceY(x + sx, z + sz);
low = Math.Min(low, height);
high = Math.Max(high, height);
}
if (low > 5 && high - low < bestSlope)
{
m_Obstructed = false;
api.GetWorld().QueryEntitiesByAABB(Vector(x - 10, low + 0.1, z - 16), Vector(x + 10, high + 5, z + 10), CheckClearance);
if (m_Obstructed) continue;
bestSlope = high - low;
bestPosition = Vector(x, api.GetTerrainSurfaceY(x, z), z);
}
}
}
PrintFormat("ERR SITE CANDIDATE: position=%1 elevationRange=%2", bestPosition, bestSlope);
string play;
if (System.GetCLIParam("errPlay", play) && play == "1") editor.SwitchToGameMode();
}
}
@@ -0,0 +1,12 @@
GameProject {
ID "ERR_Chat"
GUID "FA804F411BDDF2C6"
TITLE "Escapists Rereintroduced - Chat"
Dependencies {
"58D0FB3206B6F859"
"2528466DE220D91C"
"5192138CB9D5B911"
"D31039C7C53E348A"
"9AAEE9041D79809E"
}
}
@@ -0,0 +1,136 @@
ERR_ScenarioDefinition {
MinimumRadioSources 2
Sites {
ERR_SiteDefinition {
Id "everon-prison-1"
Label "Prison"
Kind 0
Prefab "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
Position 6090 0 7160
Guarded 1
GuardOffset 35 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset -4 0 0
Stock {
ERR_SiteStock { Prefab "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et" Count 5 }
ERR_SiteStock { Prefab "{8B853CDD11BA916E}Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et" Count 8 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
}
}
ERR_SiteDefinition {
Id "everon-supply-1"
Label "Southern supply cache"
Kind 4
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Position 6150 0 7050
Guarded 1
GuardOffset 18 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset 0 0 0
Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
}
RevealOnEscape 1
}
ERR_SiteDefinition {
Id "everon-radio-1"
Label "Radio station"
DiscoverRadius 8
Kind 1
Prefab "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
Position 6500 0 7200
Guarded 1
GuardOffset 30 0 0
}
ERR_SiteDefinition {
Id "everon-extraction-1"
Label "Initial helicopter pickup site"
Kind 2
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
Position 7000 0 7500
Guarded 1
GuardOffset 30 0 0
}
ERR_SiteDefinition {
Id "everon-supply-2"
Label "Western supply cache"
Kind 4
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Position 5960 0 7220
Guarded 1
GuardOffset 16 0 0
Patrol {
ERR_SitePatrolPoint { Offset 18 0 0 }
ERR_SitePatrolPoint { Offset 18 0 24 }
ERR_SitePatrolPoint { Offset -6 0 24 }
ERR_SitePatrolPoint { Offset -6 0 0 }
}
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
DiscoverRadius 25
RevealOnEscape 1
Transport ERR_SiteTransport {
Prefab "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"
Offset 0 0 -8
Heading 180
ClearanceRadius 4
MaximumTerrainRange 1
}
Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 4 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 8 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 6 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
}
}
ERR_SiteDefinition {
Id "everon-medical-1"
Label "Medical stash"
Kind 4
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Position 6340 0 7400
Guarded 0
GuardOffset 16 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
DiscoverRadius 25
RevealOnEscape 0
Stock {
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 6 }
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 3 }
}
}
ERR_SiteDefinition {
Id "everon-outpost-1"
Label "Eastern guard outpost"
Kind 3
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Position 6740 0 7300
Guarded 1
GuardOffset 16 0 0
Patrol {
ERR_SitePatrolPoint { Offset 18 0 0 }
ERR_SitePatrolPoint { Offset 18 0 24 }
ERR_SitePatrolPoint { Offset -6 0 24 }
ERR_SitePatrolPoint { Offset -6 0 0 }
}
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
DiscoverRadius 35
RevealOnEscape 0
Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
}
Props {
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 0 0 -4 }
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset -4 0 0 Angles 90 0 0 }
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 4 0 0 Angles 90 0 0 }
}
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,24 @@
SCR_EntityCatalogMultiList {
m_eEntityCatalogType VEHICLE
m_aMultiLists {
SCR_EntityCatalogMultiListEntry {
m_sIdentifier "Mission transport"
m_aEntities {
SCR_EntityCatalogEntry {
m_sEntityPrefab "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"
m_aEntityDataList {
SCR_EntityCatalogEditorData { m_eValidEditorModes 33 }
SCR_EntityCatalogSpawnerData { m_eSlotTypes 24 }
}
}
SCR_EntityCatalogEntry {
m_sEntityPrefab "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"
m_aEntityDataList {
SCR_EntityCatalogEditorData { m_eValidEditorModes 33 }
SCR_EntityCatalogSpawnerData { m_eSlotTypes VEHICLE_LARGE }
}
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,277 @@
SCR_Faction : "{E7B7EA4823DD81F0}Configs/Factions/BLUFOR.conf" {
FactionKey "US"
UIInfo SCR_FactionUIInfo "{528C961236B7DCD3}" {
Name "#AR-Faction_US"
Icon "{EB7F65DBC9392557}UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USA.edds"
m_sNameUpper "#AR-Faction_US-UC"
}
"Identity of soldiers" FactionIdentity "{570D128F4022EF5A}" : "{B600946E9ACC17C2}Configs/Identities/FactionIdentity_US.conf" {
}
"Radio encryption key" "chickenNuggets"
m_sFactionFlag "{46FB2FB2C2125A3C}UI/Textures/Flags/US-flag-base-3it.edds"
m_FactionFlagMaterial "{610803CC3F3D6649}Assets/Props/Fabric/Flags/Data/Flag_1_2_USA.emat"
m_FactionLabel FACTION_US
m_CallsignInfo SCR_FactionCallsignInfo "{5DA0F2A6677ADA9E}" : "{CB7824D572DD0D5C}Configs/Callsigns/Callsigns_US.conf" {
}
m_iFactionRadioFrequency 48000
m_aEntityCatalogs {
SCR_EntityCatalogMultiList "{5C68A0EE815FA292}" : "{B56E8B4D4B195559}Configs/EntityCatalog/US/Characters_EntityCatalog_US.conf" {
}
SCR_EntityCatalogMultiList "{5C9E82F29B327997}" : "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf" {
}
SCR_EntityCatalogMultiList "{5C68A0EE8B97CA50}" : "{4E70E7C4C2C17D4A}Configs/EntityCatalog/US/Groups_EntityCatalog_US.conf" {
}
SCR_EntityCatalog "{5C68A0EEB7DB3E84}" : "{0E4B62F186F97C43}Configs/EntityCatalog/US/WeaponTripod_EntityCatalog_US.conf" {
}
SCR_EntityCatalogMultiList "{5C68A0EEB3F124F9}" : "{5F7EC52FC40A03E2}Configs/EntityCatalog/US/InventoryItems_EntityCatalog_US.conf" {
}
SCR_EntityCatalog "{5C9EDDD37394AD40}" : "{0BF36EFF9FFAF92B}Configs/EntityCatalog/US/SupplyContainerItems_EntityCatalog_US.conf" {
}
}
m_aFlagNames +{
}
m_aBaseCallsigns {
SCR_MilitaryBaseCallsign "{5C90D92F493E764B}" {
m_sCallsign "#AR-Campaign_PointAlabama"
m_sCallsignShort "#AR-Comm_Variable_Base_Alabama_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Alabama_US_UC"
m_iSignalIndex 0
}
SCR_MilitaryBaseCallsign "{5C90D92F49F79967}" {
m_sCallsign "#AR-Campaign_PointAlaska"
m_sCallsignShort "#AR-Comm_Variable_Base_Alaska_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Alaska_US_UC"
m_iSignalIndex 1
}
SCR_MilitaryBaseCallsign "{5C90D92F482DA59B}" {
m_sCallsign "#AR-Campaign_PointArizona"
m_sCallsignShort "#AR-Comm_Variable_Base_Arizona_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arizona_US_UC"
m_iSignalIndex 2
}
SCR_MilitaryBaseCallsign "{5C90D92F483330D2}" {
m_sCallsign "#AR-Campaign_PointArkansas"
m_sCallsignShort "#AR-Comm_Variable_Base_Arkansas_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arkansas_US_UC"
m_iSignalIndex 3
}
SCR_MilitaryBaseCallsign "{5C90D92F48170DA8}" {
m_sCallsign "#AR-Campaign_PointCalifornia"
m_sCallsignShort "#AR-Comm_Variable_Base_California_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_California_US_UC"
m_iSignalIndex 4
}
SCR_MilitaryBaseCallsign "{5C90D92F487834E8}" {
m_sCallsign "#AR-Campaign_PointColorado"
m_sCallsignShort "#AR-Comm_Variable_Base_Colorado_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Colorado_US_UC"
m_iSignalIndex 5
}
SCR_MilitaryBaseCallsign "{5C90D92F48410A86}" {
m_sCallsign "#AR-Campaign_PointConnecticut"
m_sCallsignShort "#AR-Comm_Variable_Base_Connecticut_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Connecticut_US_UC"
m_iSignalIndex 6
}
SCR_MilitaryBaseCallsign "{5C90D92F48A4E31F}" {
m_sCallsign "#AR-Campaign_PointDelaware"
m_sCallsignShort "#AR-Comm_Variable_Base_Delaware_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Delaware_US_UC"
m_iSignalIndex 7
}
SCR_MilitaryBaseCallsign "{5C90D92F488C6B57}" {
m_sCallsign "#AR-Campaign_PointFlorida"
m_sCallsignShort "#AR-Comm_Variable_Base_Florida_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Florida_US_UC"
m_iSignalIndex 8
}
SCR_MilitaryBaseCallsign "{5C90D92F4896F764}" {
m_sCallsign "#AR-Campaign_PointGeorgia"
m_sCallsignShort "#AR-Comm_Variable_Base_Georgia_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Georgia_US_UC"
m_iSignalIndex 9
}
SCR_MilitaryBaseCallsign "{5C90D92F48FBD90E}" {
m_sCallsign "#AR-Campaign_PointHawaii"
m_sCallsignShort "#AR-Comm_Variable_Base_Hawaii_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Hawaii_US_UC"
m_iSignalIndex 10
}
SCR_MilitaryBaseCallsign "{5C90D92F48DFF562}" {
m_sCallsign "#AR-Campaign_PointIdaho"
m_sCallsignShort "#AR-Comm_Variable_Base_Idaho_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Idaho_US_UC"
m_iSignalIndex 11
}
SCR_MilitaryBaseCallsign "{5C90D92F7724C851}" {
m_sCallsign "#AR-Campaign_PointIllinois"
m_sCallsignShort "#AR-Comm_Variable_Base_Illinois_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Illinois_US_UC"
m_iSignalIndex 12
}
SCR_MilitaryBaseCallsign "{5C90D92F77097F3F}" {
m_sCallsign "#AR-Campaign_PointIndiana"
m_sCallsignShort "#AR-Comm_Variable_Base_Indiana_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Indiana_US_UC"
m_iSignalIndex 13
}
SCR_MilitaryBaseCallsign "{5C90D92F77102B55}" {
m_sCallsign "#AR-Campaign_PointIowa"
m_sCallsignShort "#AR-Comm_Variable_Base_Iowa_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Iowa_US_UC"
m_iSignalIndex 14
}
SCR_MilitaryBaseCallsign "{5C90D92F777ABD04}" {
m_sCallsign "#AR-Campaign_PointKansas"
m_sCallsignShort "#AR-Comm_Variable_Base_Kansas_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kansas_US_UC"
m_iSignalIndex 15
}
SCR_MilitaryBaseCallsign "{5C90D92F775EBBC1}" {
m_sCallsign "#AR-Campaign_PointKentucky"
m_sCallsignShort "#AR-Comm_Variable_Base_Kentucky_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kentucky_US_UC"
m_iSignalIndex 16
}
SCR_MilitaryBaseCallsign "{5C90D92F77A28EB6}" {
m_sCallsign "#AR-Campaign_PointLouisiana"
m_sCallsignShort "#AR-Comm_Variable_Base_Louisiana_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Louisiana_US_UC"
m_iSignalIndex 17
}
SCR_MilitaryBaseCallsign "{5C90D92F77871C23}" {
m_sCallsign "#AR-Campaign_PointMaine"
m_sCallsignShort "#AR-Comm_Variable_Base_Maine_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Maine_US_UC"
m_iSignalIndex 18
}
SCR_MilitaryBaseCallsign "{5C90D92F77EB96A0}" {
m_sCallsign "#AR-Campaign_PointMaryland"
m_sCallsignShort "#AR-Comm_Variable_Base_Maryland_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Maryland_US_UC"
m_iSignalIndex 19
}
SCR_MilitaryBaseCallsign "{5C90D92F77CC2518}" {
m_sCallsign "#AR-Campaign_PointMassachusetts"
m_sCallsignShort "#AR-Comm_Variable_Base_Massachusetts_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Massachusetts_US_UC"
m_iSignalIndex 20
}
SCR_MilitaryBaseCallsign "{5C90D92F77D3715B}" {
m_sCallsign "#AR-Campaign_PointMichigan"
m_sCallsignShort "#AR-Comm_Variable_Base_Michigan_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Michigan_US_UC"
m_iSignalIndex 21
}
SCR_MilitaryBaseCallsign "{5C90D92F7635D418}" {
m_sCallsign "#AR-Campaign_PointMinnesota"
m_sCallsignShort "#AR-Comm_Variable_Base_Minnesota_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Minnesota_US_UC"
m_iSignalIndex 22
}
SCR_MilitaryBaseCallsign "{5C90D92F761C3FF2}" {
m_sCallsign "#AR-Campaign_PointMississippi"
m_sCallsignShort "#AR-Comm_Variable_Base_Mississippi_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Mississippi_US_UC"
m_iSignalIndex 23
}
SCR_MilitaryBaseCallsign "{5C90D92F7660CFAA}" {
m_sCallsign "#AR-Campaign_PointMissouri"
m_sCallsignShort "#AR-Comm_Variable_Base_Missouri_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Missouri_US_UC"
m_iSignalIndex 24
}
SCR_MilitaryBaseCallsign "{5C90D92F764A6E6B}" {
m_sCallsign "#AR-Campaign_PointMontana"
m_sCallsignShort "#AR-Comm_Variable_Base_Montana_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Montana_US_UC"
m_iSignalIndex 25
}
SCR_MilitaryBaseCallsign "{5C90D92F76AD4D35}" {
m_sCallsign "#AR-Campaign_PointNebraska"
m_sCallsignShort "#AR-Comm_Variable_Base_Nebraska_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nebraska_US_UC"
m_iSignalIndex 26
}
SCR_MilitaryBaseCallsign "{5C90D92F76B1EF3A}" {
m_sCallsign "#AR-Campaign_PointNevada"
m_sCallsignShort "#AR-Comm_Variable_Base_Nevada_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nevada_US_UC"
m_iSignalIndex 27
}
SCR_MilitaryBaseCallsign "{5C90D92F7695FF2F}" {
m_sCallsign "#AR-Campaign_PointNewYork"
m_sCallsignShort "#AR-Comm_Variable_Base_NewYork_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_NewYork_US_UC"
m_iSignalIndex 28
}
SCR_MilitaryBaseCallsign "{5C90D92F76FF94B1}" {
m_sCallsign "#AR-Campaign_PointOhio"
m_sCallsignShort "#AR-Comm_Variable_Base_Ohio_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Ohio_US_UC"
m_iSignalIndex 29
}
SCR_MilitaryBaseCallsign "{5C90D92F76C68EED}" {
m_sCallsign "#AR-Campaign_PointOklahoma"
m_sCallsignShort "#AR-Comm_Variable_Base_Oklahoma_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Oklahoma_US_UC"
m_iSignalIndex 30
}
SCR_MilitaryBaseCallsign "{5C90D92F752A9F29}" {
m_sCallsign "#AR-Campaign_PointOregon"
m_sCallsignShort "#AR-Comm_Variable_Base_Oregon_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Oregon_US_UC"
m_iSignalIndex 31
}
SCR_MilitaryBaseCallsign "{5C90D92F750E6FB9}" {
m_sCallsign "#AR-Campaign_PointPennsylvania"
m_sCallsignShort "#AR-Comm_Variable_Base_Pennsylvania_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Pennsylvania_US_UC"
m_iSignalIndex 32
}
SCR_MilitaryBaseCallsign "{5C90D92F7517240F}" {
m_sCallsign "#AR-Campaign_PointTexas"
m_sCallsignShort "#AR-Comm_Variable_Base_Texas_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Texas_US_UC"
m_iSignalIndex 33
}
SCR_MilitaryBaseCallsign "{5C90D92F7574D867}" {
m_sCallsign "#AR-Campaign_PointUtah"
m_sCallsignShort "#AR-Comm_Variable_Base_Utah_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Utah_US_UC"
m_iSignalIndex 34
}
SCR_MilitaryBaseCallsign "{5C90D92F755C4871}" {
m_sCallsign "#AR-Campaign_PointVermont"
m_sCallsignShort "#AR-Comm_Variable_Base_Vermont_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Vermont_US_UC"
m_iSignalIndex 35
}
SCR_MilitaryBaseCallsign "{5C90D92F75A0502B}" {
m_sCallsign "#AR-Campaign_PointVirginia"
m_sCallsignShort "#AR-Comm_Variable_Base_Virginia_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Virginia_US_UC"
m_iSignalIndex 36
}
SCR_MilitaryBaseCallsign "{5C90D92F758ABFD3}" {
m_sCallsign "#AR-Campaign_PointWashington"
m_sCallsignShort "#AR-Comm_Variable_Base_Washington_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Washington_US_UC"
m_iSignalIndex 37
}
SCR_MilitaryBaseCallsign "{5C90D92F75EF8263}" {
m_sCallsign "#AR-Campaign_PointWisconsin"
m_sCallsignShort "#AR-Comm_Variable_Base_Wisconsin_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Wisconsin_US_UC"
m_iSignalIndex 38
}
SCR_MilitaryBaseCallsign "{5C90D92F75F0F224}" {
m_sCallsign "#AR-Campaign_PointWyoming"
m_sCallsignShort "#AR-Comm_Variable_Base_Wyoming_US"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Wyoming_US_UC"
m_iSignalIndex 39
}
}
m_FactionHomeTerritoryConfig SCR_FactionHomeTerritoryConfig "{5AEE94D54FB58C33}" : "{B548A5B264939A98}Configs/Identities/Territories/FactionHomeTerritory_US.conf" {
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{5C67A1DDE0044806}Configs/ERR/Factions/US.conf"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,276 @@
SCR_Faction : "{B122E3B10BB3404B}Configs/Factions/OPFOR.conf" {
FactionKey "USSR"
UIInfo SCR_FactionUIInfo "{528C96127F8B6B08}" {
Name "#AR-Faction_USSR"
Icon "{40B12B0DF911B856}UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USSR.edds"
m_sNameUpper "#AR-Faction_USSR-UC"
}
"Identity of soldiers" FactionIdentity "{570CFDC6685B6B25}" : "{FBBCD73926989580}Configs/Identities/FactionIdentity_USSR.conf" {
}
"Radio encryption key" "coldBorscht"
m_sFactionFlag "{50824EB9F2C215AB}UI/Textures/Flags/USSR-flag-base-3it.edds"
m_FactionFlagMaterial "{B47B52D9DA68905D}Assets/Props/Fabric/Flags/Data/Flag_1_2_SovietArmy.emat"
m_FactionLabel FACTION_USSR
m_CallsignInfo SCR_FactionCallsignInfo "{5DA0F2A67DFB8809}" : "{958043CE1648EC43}Configs/Callsigns/Callsigns_USSR.conf" {
}
m_iFactionRadioFrequency 42000
m_aEntityCatalogs {
SCR_EntityCatalogMultiList "{5C68A0E9BD5E5DD9}" : "{3B679F1799D55F6F}Configs/EntityCatalog/USSR/Characters_EntityCatalog_USSR.conf" {
}
SCR_EntityCatalogMultiList "{5C9E82F285C09E24}" : "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf" {
}
SCR_EntityCatalogMultiList "{5C68A0E9B9AA3C2D}" : "{37D5B75AAC3E72EA}Configs/EntityCatalog/USSR/Groups_EntityCatalog_USSR.conf" {
}
SCR_EntityCatalog "{5C68A0E9A51460AB}" : "{DDF17C8C140BD1F2}Configs/EntityCatalog/USSR/WeaponTripod_EntityCatalog_USSR.conf" {
}
SCR_EntityCatalogMultiList "{5C68A0E9AFE1E631}" : "{C53421647C3D0D2E}Configs/EntityCatalog/USSR/InventoryItems_EntityCatalog_USSR.conf" {
}
SCR_EntityCatalog "{5C9EDDD378BA3298}" : "{B4CA6761AC935808}Configs/EntityCatalog/USSR/SupplyContainerItems_EntityCatalog_USSR.conf" {
}
}
m_iIdentityVoiceSignal 100
m_aBaseCallsigns {
SCR_MilitaryBaseCallsign "{5C9108F95F3FB520}" {
m_sCallsign "#AR-Campaign_PointArena"
m_sCallsignShort "#AR-Comm_Variable_Base_Arena_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arena_RU_UC"
m_iSignalIndex 0
}
SCR_MilitaryBaseCallsign "{5C9108F95F0034DB}" {
m_sCallsign "#AR-Campaign_PointAvrora"
m_sCallsignShort "#AR-Comm_Variable_Base_Avrora_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Avrora_RU_UC"
m_iSignalIndex 1
}
SCR_MilitaryBaseCallsign "{5C9108F95F6514E7}" {
m_sCallsign "#AR-Campaign_PointBekas"
m_sCallsignShort "#AR-Comm_Variable_Base_Bekas_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bekas_RU_UC"
m_iSignalIndex 2
}
SCR_MilitaryBaseCallsign "{5C9108F95F4EE8BF}" {
m_sCallsign "#AR-Campaign_PointBizon"
m_sCallsignShort "#AR-Comm_Variable_Base_Bizon_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bizon_RU_UC"
m_iSignalIndex 3
}
SCR_MilitaryBaseCallsign "{5C9108F95F5021F9}" {
m_sCallsign "#AR-Campaign_PointBolero"
m_sCallsignShort "#AR-Comm_Variable_Base_Bolero_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bolero_RU_UC"
m_iSignalIndex 4
}
SCR_MilitaryBaseCallsign "{5C9108F95FBA4C89}" {
m_sCallsign "#AR-Campaign_PointCafftan"
m_sCallsignShort "#AR-Comm_Variable_Base_Cafftan_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Cafftan_RU_UC"
m_iSignalIndex 5
}
SCR_MilitaryBaseCallsign "{5C9108F95F9C00A9}" {
m_sCallsign "#AR-Campaign_PointKaban"
m_sCallsignShort "#AR-Comm_Variable_Base_Kaban_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kaban_RU_UC"
m_iSignalIndex 6
}
SCR_MilitaryBaseCallsign "{5C9108F95FE17E10}" {
m_sCallsign "#AR-Campaign_PointConsul"
m_sCallsignShort "#AR-Comm_Variable_Base_Consul_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Consul_RU_UC"
m_iSignalIndex 7
}
SCR_MilitaryBaseCallsign "{5C9108F95FCBA475}" {
m_sCallsign "#AR-Campaign_PointDimok"
m_sCallsignShort "#AR-Comm_Variable_Base_Dimok_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Dimok_RU_UC"
m_iSignalIndex 8
}
SCR_MilitaryBaseCallsign "{5C9108F95E2B2963}" {
m_sCallsign "#AR-Campaign_PointEffir"
m_sCallsignShort "#AR-Comm_Variable_Base_Effir_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Effir_RU_UC"
m_iSignalIndex 9
}
SCR_MilitaryBaseCallsign "{5C9108F95E3040F5}" {
m_sCallsign "#AR-Campaign_PointFakir"
m_sCallsignShort "#AR-Comm_Variable_Base_Fakir_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Fakir_RU_UC"
m_iSignalIndex 10
}
SCR_MilitaryBaseCallsign "{5C9108F95E17C67D}" {
m_sCallsign "#AR-Campaign_PointGips"
m_sCallsignShort "#AR-Comm_Variable_Base_Gips_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Gips_RU_UC"
m_iSignalIndex 11
}
SCR_MilitaryBaseCallsign "{5C9108F95E797F8A}" {
m_sCallsign "#AR-Campaign_PointGussar"
m_sCallsignShort "#AR-Comm_Variable_Base_Gussar_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Gussar_RU_UC"
m_iSignalIndex 12
}
SCR_MilitaryBaseCallsign "{5C9108F95E431F83}" {
m_sCallsign "#AR-Campaign_PointIgla"
m_sCallsignShort "#AR-Comm_Variable_Base_Igla_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Igla_RU_UC"
m_iSignalIndex 13
}
SCR_MilitaryBaseCallsign "{5C9108F95EA7F87E}" {
m_sCallsign "#AR-Campaign_PointKhek"
m_sCallsignShort "#AR-Comm_Variable_Base_Khek_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Khek_RU_UC"
m_iSignalIndex 14
}
SCR_MilitaryBaseCallsign "{5C9108F95E88C6B9}" {
m_sCallsign "#AR-Campaign_PointLednik"
m_sCallsignShort "#AR-Comm_Variable_Base_Lednik_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Lednik_RU_UC"
m_iSignalIndex 15
}
SCR_MilitaryBaseCallsign "{5C9108F95EEDE322}" {
m_sCallsign "#AR-Campaign_PointMatros"
m_sCallsignShort "#AR-Comm_Variable_Base_Matros_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Matros_RU_UC"
m_iSignalIndex 16
}
SCR_MilitaryBaseCallsign "{5C9108F95EF6244A}" {
m_sCallsign "#AR-Campaign_PointMuskat"
m_sCallsignShort "#AR-Comm_Variable_Base_Muskat_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Muskat_RU_UC"
m_iSignalIndex 17
}
SCR_MilitaryBaseCallsign "{5C9108F95EDAF04A}" {
m_sCallsign "#AR-Campaign_PointNagan"
m_sCallsignShort "#AR-Comm_Variable_Base_Nagan_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nagan_RU_UC"
m_iSignalIndex 18
}
SCR_MilitaryBaseCallsign "{5C9108F95D3CCF27}" {
m_sCallsign "#AR-Campaign_PointNitka"
m_sCallsignShort "#AR-Comm_Variable_Base_Nitka_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nitka_RU_UC"
m_iSignalIndex 19
}
SCR_MilitaryBaseCallsign "{5C9108F95D076483}" {
m_sCallsign "#AR-Campaign_PointOmega"
m_sCallsignShort "#AR-Comm_Variable_Base_Omega_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Omega_RU_UC"
m_iSignalIndex 20
}
SCR_MilitaryBaseCallsign "{5C9108F95D68D0F3}" {
m_sCallsign "#AR-Campaign_PointPlazma"
m_sCallsignShort "#AR-Comm_Variable_Base_Plazma_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Plazma_RU_UC"
m_iSignalIndex 21
}
SCR_MilitaryBaseCallsign "{5C9108F95D4C0F8E}" {
m_sCallsign "#AR-Campaign_PointPolyn"
m_sCallsignShort "#AR-Comm_Variable_Base_Polyn_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Polyn_RU_UC"
m_iSignalIndex 22
}
SCR_MilitaryBaseCallsign "{5C9108F95D56EEEE}" {
m_sCallsign "#AR-Campaign_PointProlog"
m_sCallsignShort "#AR-Comm_Variable_Base_Prolog_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Prolog_RU_UC"
m_iSignalIndex 23
}
SCR_MilitaryBaseCallsign "{5C9108F95DBA92EE}" {
m_sCallsign "#AR-Campaign_PointRabkor"
m_sCallsignShort "#AR-Comm_Variable_Base_Rabkor_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Rabkor_RU_UC"
m_iSignalIndex 24
}
SCR_MilitaryBaseCallsign "{5C9108F95C69680D}" {
m_sCallsign "#AR-Campaign_PointRadon"
m_sCallsignShort "#AR-Comm_Variable_Base_Radon_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Radon_RU_UC"
m_iSignalIndex 25
}
SCR_MilitaryBaseCallsign "{5C9108F95C4FD930}" {
m_sCallsign "#AR-Campaign_PointSadko"
m_sCallsignShort "#AR-Comm_Variable_Base_Sadko_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Sadko_RU_UC"
m_iSignalIndex 26
}
SCR_MilitaryBaseCallsign "{5C9108F95C5255FD}" {
m_sCallsign "#AR-Campaign_PointSekret"
m_sCallsignShort "#AR-Comm_Variable_Base_Sekret_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Sekret_RU_UC"
m_iSignalIndex 27
}
SCR_MilitaryBaseCallsign "{5C9108F95CB621AE}" {
m_sCallsign "#AR-Campaign_PointShtanga"
m_sCallsignShort "#AR-Comm_Variable_Base_Shtanga_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Shtanga_RU_UC"
m_iSignalIndex 28
}
SCR_MilitaryBaseCallsign "{5C9108F95C9545BC}" {
m_sCallsign "#AR-Campaign_PointShuka"
m_sCallsignShort "#AR-Comm_Variable_Base_Shuka_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Shuka_RU_UC"
m_iSignalIndex 29
}
SCR_MilitaryBaseCallsign "{5C9108F95CFE26C3}" {
m_sCallsign "#AR-Campaign_PointTabor"
m_sCallsignShort "#AR-Comm_Variable_Base_Tabor_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tabor_RU_UC"
m_iSignalIndex 30
}
SCR_MilitaryBaseCallsign "{5C9108F95CDD263C}" {
m_sCallsign "#AR-Campaign_PointTenor"
m_sCallsignShort "#AR-Comm_Variable_Base_Tenor_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tenor_RU_UC"
m_iSignalIndex 31
}
SCR_MilitaryBaseCallsign "{5C9108F95B27128C}" {
m_sCallsign "#AR-Campaign_PointTsikson"
m_sCallsignShort "#AR-Comm_Variable_Base_Tsikson_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tsikson_RU_UC"
m_iSignalIndex 32
}
SCR_MilitaryBaseCallsign "{5C9108F95B08D83D}" {
m_sCallsign "#AR-Campaign_PointUlan"
m_sCallsignShort "#AR-Comm_Variable_Base_Ulan_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Ulan_RU_UC"
m_iSignalIndex 33
}
SCR_MilitaryBaseCallsign "{5C9108F95B6D3FC2}" {
m_sCallsign "#AR-Campaign_PointVermout"
m_sCallsignShort "#AR-Comm_Variable_Base_Vermout_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Vermout_RU_UC"
m_iSignalIndex 34
}
SCR_MilitaryBaseCallsign "{5C9108F95B712949}" {
m_sCallsign "#AR-Campaign_PointViympel"
m_sCallsignShort "#AR-Comm_Variable_Base_Viympel_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Viympel_RU_UC"
m_iSignalIndex 35
}
SCR_MilitaryBaseCallsign "{5C9108F95B57D185}" {
m_sCallsign "#AR-Campaign_PointYantar"
m_sCallsignShort "#AR-Comm_Variable_Base_Yantar_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Yantar_RU_UC"
m_iSignalIndex 36
}
SCR_MilitaryBaseCallsign "{5C9108F95BB17114}" {
m_sCallsign "#AR-Campaign_PointYolka"
m_sCallsignShort "#AR-Comm_Variable_Base_Yolka_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Yolka_RU_UC"
m_iSignalIndex 37
}
SCR_MilitaryBaseCallsign "{5C9108F95B97C054}" {
m_sCallsign "#AR-Campaign_PointZheton"
m_sCallsignShort "#AR-Comm_Variable_Base_Zheton_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Zheton_RU_UC"
m_iSignalIndex 38
}
SCR_MilitaryBaseCallsign "{5C9108F95BFC33E5}" {
m_sCallsign "#AR-Campaign_PointZurna"
m_sCallsignShort "#AR-Comm_Variable_Base_Zurna_RU"
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Zurna_RU_UC"
m_iSignalIndex 39
}
}
m_FactionHomeTerritoryConfig SCR_FactionHomeTerritoryConfig "{5AEE94D5517C30BD}" : "{31FC78AF00523A99}Configs/Identities/Territories/FactionHomeTerritory_USSR.conf" {
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{D7A5CFA62A36480D}Configs/ERR/Factions/USSR.conf"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,21 @@
SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOverScreensConfig.conf" {
m_aGameOverScreens {
ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" {
m_GameOverScreenId VICTORY
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{4984A5C1BFEFC04E}" {
m_sTitle "Escape complete"
m_sAudioOneShot "SOUND_ONVICTORY"
}
}
ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" {
m_GameOverScreenId LOOSE
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
m_sTitle "Escape failed"
m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost."
m_sAudioOneShot "SOUND_ONDEFEAT"
}
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,7 @@
ERR_FactionCatalog {
PlayerFactionKey "US"
EnemyFactionKey "USSR"
PrisonerPrefab "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et"
GuardGroupPrefab "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
ExtractionPrefab "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,11 @@
SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Rereintroduced: Everon Base (development)"
m_sAuthor "Rereintroduced contributors"
m_sDescription "Independent base escape mission using vanilla Reforger. Companion addons optional. Development scenario."
m_sGameMode "Escape"
m_iPlayerCount 12
m_eSaveTypes 0
m_bShowInScenarioMenu 1
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{AD62322D5213C440}Missions/ERR_Everon_Base.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Kind 2 }
RplComponent "{0BF5BAAA5FBFEECE}" { Streamable Disabled }
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,10 @@
SCR_FactionManager : "{A242612865F2A48E}Prefabs/MP/Managers/Factions/FactionManager_Base.et" {
Factions {
SCR_Faction : "{5C67A1DDE0044806}Configs/ERR/Factions/US.conf" {
m_sFactionBackground "{F8AC305446D5D480}UI/Textures/DeployMenu/Loadout-BG/Loadout_US_BG.imageset"
}
SCR_Faction : "{D7A5CFA62A36480D}Configs/ERR/Factions/USSR.conf" {
m_sFactionBackground "{21533F2089005056}UI/Textures/DeployMenu/Loadout-BG/Loadout_BG_USSR.imageset"
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,13 @@
GenericEntity {
components {
MeshObject "{A557F6DC012E5101}" {
Object "{268DBDABB3C94AD7}Assets/Props/Military/Sandbags/Sandbag_01_short.xob"
Materials {
MaterialAssignClass "{A557F6DC012E5102}" { SourceMaterial "Sandbags_01_Short" AssignedMaterial "{31529E064AA7C917}Assets/Props/Military/Sandbags/Data/Sandbags_01_short_Burlap.emat" }
MaterialAssignClass "{A557F6DC012E5103}" { SourceMaterial "Sandbags_01_MLOD" AssignedMaterial "{2169E8EB73E1C127}Assets/Props/Military/Sandbags/Data/Sandbags_01_Burlap_MLOD.emat" }
}
}
RigidBody "{A557F6DC012E5104}" { ModelGeometry 1 Static 1 }
RplComponent "{A557F6DC012E5105}" {}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,14 @@
ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
components {
SCR_GameOverScreenManagerComponent "{56DEC646298AD219}" {
m_GameOverScreenConfig SCR_GameOverScreenConfig "{56DEC6462FFDD82D}" : "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf" {}
}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_Definition "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
}
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
}
}
m_bUseSpawnPreload 0
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,5 @@
GenericEntity : "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et" {
components {
ERR_DocumentComponent "{E4598D0BE2C251A3}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,10 @@
SCR_AIGroup : "{E552DABF3636C2AD}Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et" {
m_aUnitPrefabSlots {
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
"{23ADBBC31B6A3DC6}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et"
"{1C78331E156A3D65}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et"
"{333DA6244C7DA34C}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et"
"{BF643BE4ADBDFDD3}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et"
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,90 @@
GenericEntity {
components {
ERR_LocationComponent "{3B60CBEB3C04333C}" { m_Kind 0 }
Hierarchy "{30ADADE77D1EC3B8}" {}
RplComponent "{7FA5168C6FBD0C22}" {}
}
{
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "2879356FFFBE36E0"
coords -6 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "C99EF9AFCEBA863D"
coords -3 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "91048277F04ACA61"
coords 0 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "8F2200A922FB0A6E"
coords 3 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "3C442429BBCF5D2F"
coords -6 0 -6
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "9E855CC802C592CA"
coords 6 0 -6
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "778DAC42C9208F67"
coords -6 0 -3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "7E350EB9053673C4"
coords 6 0 -3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "AD17884D5EA37D37"
coords -6 0 0
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "1A157E23E1A995D8"
coords 6 0 0
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "3A78A6F3ACB7ACB1"
coords -6 0 3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "275B472B565FFF6C"
coords 6 0 3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "1B20E11B3FC7B59A"
coords -6 0 -6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "2735B52246DD6213"
coords -3 0 -6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "28101A0E33470B38"
coords 3 0 -6
angles 0 0 0
}
GenericEntity : "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et" {
ID "54D8F96ECC743335"
scale 1.5
coords 0 -0.1 -6
angles 0 0 0
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,19 @@
GenericEntity : "{34B08027DBDE322A}Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et" {
components {
DoorComponent "{221C667A9F7D4B69}" {
DoorAction ERR_PrisonDoorAction "{50888D3646A61640}" {
ParentContextList { "SCR_DoorUserAction" }
}
}
ActionsManagerComponent "{F32B667C3C823800}" {
ActionContexts { UserActionContext "{50888D37F529F720}" { ContextName "SCR_DoorUserAction" Radius 2 } }
additionalActions {
ERR_UnlockPrisonAction "{CA7918FEA177BFB8}" {
ParentContextList { "SCR_DoorUserAction" }
UIInfo SCR_ActionUIInfo "{F734C81E11AA6BEF}" { Name "Pick prison lock" }
Duration 4
}
}
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,8 @@
GenericEntity {
components {
Hierarchy "{4F20A90EDBD625A4}" {}
MeshObject "{4F20A90EDBD625A1}" { Object "{0B96D139FEBCA006}Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob" }
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
RplComponent "{4F20A90EDBD625A3}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,55 @@
SCR_ChimeraCharacter : "{836E7E39AAC5888B}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Base.et" {
components {
ActionsManagerComponent "{520EA1D2F659CE02}" {
additionalActions {
ERR_LoadCasualtyAction "{E7016B8C499EC8A3}" {
ParentContextList { "Chest" "LowerTorso" }
UIInfo SCR_ActionUIInfo "{E7016B8C499EC8A4}" { Name "Load casualty into extraction helicopter" }
Duration 5
}
}
}
SCR_EditableCharacterComponent "{520EA1D2F659C85C}" {
m_UIInfo SCR_EditableEntityUIInfo "{5298E609432D192D}" {
Name "Prisoner"
}
}
SCR_CharacterInventoryStorageComponent "{520EA1D2DB118DAC}" {
components {
SCR_EquipmentStorageComponent "{56B49DA9722B635D}" {
InitialStorageSlots {
SCR_EquipmentStorageSlot BinocularSlot { Prefab "" }
SCR_EquipmentStorageSlot WristwatchSlot {
Prefab "{78ED4FEF62BBA728}Prefabs/Items/Equipment/Watches/Watch_SandY184A.et"
}
}
}
}
}
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
Slots {
LoadoutSlotInfo Hat { Prefab "{F545C1BE7C99954A}Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et" }
LoadoutSlotInfo Jacket { Prefab "{3CCA7A9BB4FD3197}Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et" }
LoadoutSlotInfo Pants { Prefab "{604BB72BE8E023C2}Prefabs/Characters/Uniforms/Pants_US_BDU.et" }
SCR_FeetLoadoutSlotInfo Boots { Prefab "{DAAFD15478BDE1C3}Prefabs/Characters/Footwear/CombatBoots_US_01.et" }
LoadoutSlotInfo ArmoredVest { Prefab "" }
LoadoutSlotInfo Back { Prefab "" }
LoadoutSlotInfo Vest { Prefab "" }
LoadoutSlotInfo HandWear { Prefab "" }
}
}
SCR_InventoryStorageManagerComponent "{520EA1D2DB118DE5}" {
InitialInventoryItems {
ItemsInitConfigurationItem "{EA7AC3C2078B68C1}" {
TargetStorage "Prefabs/Characters/Uniforms/Pants_US_BDU.et"
PrefabsToSpawn {
"{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et"
"{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et"
"{D70216B1B2889129}Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et"
"{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et"
}
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,16 @@
GenericEntity {
components {
MeshObject "{111C06CB740ED195}" { Object "{77C9FCF3FB1246E9}Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob" }
RigidBody "{4C174A44D07B91BA}" { ModelGeometry 1 Static 1 }
ERR_LocationComponent "{C19CA6CC83470879}" { m_Kind 1 }
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
ActionsManagerComponent "{B081EC0768636EAB}" {
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
ParentContextList { "ERR_Radio" }
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
Duration 8
} }
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,7 @@
SCR_ManualCamera {
m_bInputEnabled 0
m_bStartAtEntity 1
m_aComponents {
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,28 @@
GenericEntity : "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et" {
components {
MeshObject "{6530845A09309E37}" {
Object "{819ED3266EE5113B}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/EquipmentBoxWooden_Weapon_01.xob"
Materials {
MaterialAssignClass "{6530845A093091BB}" {
SourceMaterial "EquipmentBoxWooden_Weapon_01"
AssignedMaterial "{7D2E831AFA5CF50D}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/Data/EquipmentBoxWooden_Weapon_01_USSR.emat"
}
MaterialAssignClass "{6530845A093091B2}" {
SourceMaterial "EquipmentBoxWooden_Weapon_01_Decal"
}
}
}
SCR_UniversalInventoryStorageComponent "{6530845A09302DE0}" {
MaxCumulativeVolume 120000
MaxItemSize 120 40 40
m_fMaxWeight 80
Attributes SCR_ItemAttributeCollection "{6530845A09302DFF}" {
ItemDisplayName SCR_InventoryUIInfo "{6530845A09302D03}" {
Name "Supply cache"
Description "Limited equipment shared between prisoners."
}
}
}
InventoryStorageManagerComponent "{18A1D42A468F584F}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{8E7CFEA92C1811F0}" { m_Kind 4 }
RplComponent "{98A4A31435A9CA04}" { Streamable Disabled }
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,39 @@
class ERR_UnlockPrisonAction : ScriptedUserAction
{
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(user);
}
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !mode.UnlockPrison(pUserEntity)) return;
BaseDoorComponent door = BaseDoorComponent.Cast(pOwnerEntity.FindComponent(BaseDoorComponent));
if (door) door.UseDoorAction(pUserEntity);
}
}
class ERR_PrisonDoorAction : SCR_DoorUserAction
{
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Phase() > ERR_Phase.PRISON && super.CanBePerformedScript(user);
}
}
class ERR_TuneRadioAction : ScriptedUserAction
{
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
ERR_LocationComponent location = ERR_LocationComponent.Cast(GetOwner().FindComponent(ERR_LocationComponent));
return mode && mode.Phase() == ERR_Phase.RADIO_SEARCH && location && location.Knowledge() == 2 && ERR_Actors.CanFight(user);
}
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode) mode.TuneRadio(pUserEntity, ERR_LocationComponent.Cast(pOwnerEntity.FindComponent(ERR_LocationComponent)));
}
}
@@ -0,0 +1,81 @@
class ERR_Actors
{
static bool Alive(IEntity actor)
{
if (!actor) return false;
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
return damage && !damage.IsDestroyed();
}
static bool CanFight(IEntity actor)
{
if (!Alive(actor)) return false;
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
return controller && !controller.IsUnconscious();
}
static SCR_AIGroup Group(IEntity actor)
{
if (!actor) return null;
AIControlComponent control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
if (!control || !control.GetAIAgent()) return null;
return SCR_AIGroup.Cast(control.GetAIAgent().GetParentGroup());
}
static bool Player(IEntity actor)
{
return actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor) > 0;
}
static IEntity Spawn(ResourceName prefab, vector position, vector angles = "0 0 0")
{
if (!Replication.IsServer() || prefab.IsEmpty()) return null;
Resource resource = Resource.Load(prefab);
if (!resource || !resource.IsValid()) return null;
EntitySpawnParams params = new EntitySpawnParams();
params.TransformMode = ETransformMode.WORLD;
Math3D.AnglesToMatrix(angles, params.Transform);
params.Transform[3] = position;
return GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
}
static void DeleteSpawned(IEntity entity)
{
if (!Replication.IsServer() || !entity || Player(entity)) return;
SCR_AIGroup group = SCR_AIGroup.Cast(entity);
if (group)
{
// Never remove a group that has acquired a player since mission creation.
if (group.GetPlayerCount(true) > 0) return;
group.DespawnMembers();
}
SCR_EntityHelper.DeleteEntityAndChildren(entity);
}
static vector Ground(vector position)
{
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.1;
return position;
}
static void Players(out array<IEntity> players, bool conscious = false)
{
players.Clear();
array<int> ids = {};
GetGame().GetPlayerManager().GetPlayers(ids);
foreach (int id : ids)
{
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
if (Alive(actor) && (!conscious || CanFight(actor))) players.Insert(actor);
}
}
}
// One server-owned implementation is installed by the AI addon.
class ERR_CommandService
{
void Tick(float now) {}
void Stop() {}
void RegisterGroup(SCR_AIGroup group) {}
void UnregisterGroup(SCR_AIGroup group) {}
}
@@ -0,0 +1,111 @@
// Transport-independent administration. Only the owning controller supplies
// authorization, derived on the server from its authenticated player ID.
class ERR_CommandArguments
{
static string CoreSetting(string section, string option)
{
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
return "";
}
static bool Integer(string text, out int value)
{
if (text.IsEmpty() || text.Length() > 6) return false;
for (int i = 0; i < text.Length(); i++)
if ("0123456789".IndexOf(text.Substring(i, 1)) < 0) return false;
value = text.ToInt();
return true;
}
}
class ERR_AdminService
{
protected string Setting(string section, string option)
{
return ERR_CommandArguments.CoreSetting(section, option);
}
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
string key = "startTimeMinutes";
if (parts[1] == "speed") key = "timeMultiplier";
if (parts.Count() == 3 && parts[2] == "show")
{
if (mode.Time()) replies.Insert(mode.Time().Status());
replies.Insert("Next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression.");
return true;
}
int value;
bool valid;
if (parts.Count() == 4 && parts[2] == "set")
{
if (key == "startTimeMinutes") valid = ERR_TimeService.ParseClock(parts[3], value);
else valid = ERR_CommandArguments.Integer(parts[3], value);
}
if (valid && mode.Settings().Set(key, value))
replies.Insert("Saved for next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression. Current round clock unchanged.");
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59) or mission speed set N (1-24). Previous setting retained.");
return true;
}
protected string SettingHelp(string key)
{
if (key == "intelRevealChancePercent")
return "Use /esc intel reveal set N (0-100). This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
return "";
}
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
{
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + settings.Get(key).ToString()); return; }
string help = SettingHelp(key);
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && settings.Set(key, value))
replies.Insert(key + "=" + value.ToString() + " saved");
else
{
string failure = "Invalid value or settings could not be saved; previous setting retained.";
if (!help.IsEmpty()) failure += " " + help;
replies.Insert(failure);
}
}
protected string Help()
{
return "/esc status | settings | mission abandon | mission radio show|set N | mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain | mission time show|set HH:MM | mission speed show|set N | intel reveal show|set N|help | spectate next|previous";
}
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
{
replies.Clear();
if (!Replication.IsServer() || !mode) return;
if (!authorized) { replies.Insert("Administrator access required."); return; }
if (arguments.Length() > 160) { replies.Insert("Command is too long."); return; }
arguments = arguments.Trim(); arguments.ToLower();
if (arguments == "status") { replies.Insert(mode.Status()); Extension(mode, arguments, replies); return; }
if (arguments == "mission abandon")
{
if (mode.AbandonRound()) replies.Insert("Round abandoned. The normal defeat and restart sequence has begun.");
else replies.Insert("Only an active round can be abandoned.");
return;
}
if (!mode.Settings()) { replies.Insert("Mission settings are not ready."); return; }
if (arguments == "settings")
{
foreach (string key : ERR_Settings.Keys()) replies.Insert(key + "=" + mode.Settings().Get(key).ToString());
return;
}
if (Extension(mode, arguments, replies)) return;
array<string> parts = {};
arguments.Split(" ", parts, true);
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4)
{
string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty())
{
SettingCommand(mode.Settings(), key, parts, replies);
return;
}
}
replies.Insert(Help());
}
}
@@ -0,0 +1,82 @@
// Conservative fallback for a sleeping, engine-off Mi8 whose native simulation
// has not yet populated contact flags. Reads physical support; never wakes it.
class ERR_CasualtyGroundSupport
{
static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit)
{
return fraction >= 0 && fraction < 1 && normalY >= 0.85 && !selfHit && !dynamicHit;
}
static bool BelongsTo(IEntity entity, IEntity root)
{
while (entity)
{
if (entity == root) return true;
entity = entity.GetParent();
}
return false;
}
static bool WheelCenter(Vehicle aircraft, string bone, out vector center, bool diagnostics = false)
{
if (!aircraft) return false;
if (aircraft.GetBoneIndex(bone) == -1)
{
if (diagnostics) Print("ERR GEAR PIVOT MISSING: " + bone);
return false;
}
vector identity[4]; Math3D.MatrixIdentity4(identity);
PointInfo pivot = new PointInfo();
pivot.Set(aircraft, bone, identity);
vector world[4]; pivot.GetWorldTransform(world);
center = world[3];
if (diagnostics) PrintFormat("ERR GEAR PIVOT: bone=%1 node=%2 center=%3", bone, pivot.GetNodeId(), center);
return pivot.GetNodeId() != -1;
}
static bool WheelSupported(Vehicle aircraft, vector center, float radius, bool diagnostics = false)
{
vector bottom = center - Vector(0, radius, 0);
TraceParam trace = new TraceParam();
trace.Start = bottom + "0 0.10 0";
trace.End = bottom - "0 0.12 0";
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = aircraft;
float fraction = aircraft.GetWorld().TraceMove(trace, null);
bool selfHit = trace.TraceEnt && BelongsTo(trace.TraceEnt, aircraft);
bool dynamicHit;
if (trace.TraceEnt)
{
Physics surface = trace.TraceEnt.GetPhysics();
// Unknown/entity-only support and moving actors/vehicles are not parking.
dynamicHit = !surface || surface.IsDynamic();
}
bool supported = SupportHit(fraction, trace.TraceNorm[1], selfHit, dynamicHit);
if (diagnostics)
{
vector contact = vector.Lerp(trace.Start, trace.End, fraction);
float terrainY = aircraft.GetWorld().GetSurfaceY(bottom[0], bottom[2]);
PrintFormat("ERR GEAR TERRAIN: bottom=%1 terrainY=%2 tireTerrainGap=%3", bottom, terrainY, bottom[1] - terrainY);
PrintFormat("ERR GEAR SUPPORT: center=%1 radius=%2 bottom=%3 fraction=%4 normal=%5 self=%6 dynamic=%7 supported=%8 gap=%9", center, radius, bottom, fraction, trace.TraceNorm, selfHit, dynamicHit, supported, bottom[1] - contact[1]);
}
return supported;
}
static bool Parked(Vehicle aircraft, bool diagnostics = false)
{
if (!aircraft || !aircraft.GetPrefabData() || !aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return false;
Physics physics = aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
if (!physics || !controller || !controller.GetSimulation()) return false;
if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact());
if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false;
vector front, left, right;
if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false;
// Fail closed if native pivot resolution collapses to an owner origin or
// otherwise cannot represent the authored three-point landing footprint.
if (vector.DistanceXZ(front, left) < 1 || vector.DistanceXZ(front, right) < 1 || vector.DistanceXZ(left, right) < 1) return false;
// Radii come from installed Mi8_base.et's authored landing-gear configuration.
// Every wheel needs a short, upward-facing static collision directly below.
bool a = WheelSupported(aircraft, front, 0.30, diagnostics);
bool b = WheelSupported(aircraft, left, 0.45, diagnostics);
bool c = WheelSupported(aircraft, right, 0.45, diagnostics);
return a && b && c;
}
}
@@ -0,0 +1,48 @@
// Round-owned server service; only the aircraft reference is replicated for action visibility.
modded class ERR_GameMode
{
protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading;
[RplProp()]
protected RplId m_ERRCasualtyAircraftId = RplId.Invalid();
ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; }
Vehicle CasualtyLoadingAircraft()
{
if (Replication.IsServer()) return ExtractionVehicle();
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRCasualtyAircraftId));
if (!component) return null;
return Vehicle.Cast(component.GetEntity());
}
protected void ERR_UpdateCasualtyAircraft(float now)
{
Vehicle aircraft = ExtractionVehicle();
if (!aircraft)
{
if (m_ERRCasualtyAircraftId != RplId.Invalid()) { m_ERRCasualtyAircraftId = RplId.Invalid(); Replication.BumpMe(); }
return;
}
RplComponent component = RplComponent.Cast(aircraft.FindComponent(RplComponent));
if (!component || m_ERRCasualtyAircraftId == component.Id()) return;
m_ERRCasualtyAircraftId = component.Id();
Replication.BumpMe();
}
protected void ERR_StopCasualtyLoading()
{
if (m_ERRCasualtyLoading)
{
Players().OnChanged().Remove(m_ERRCasualtyLoading.RosterChanged);
OnTick().Remove(m_ERRCasualtyLoading.Tick);
m_ERRCasualtyLoading.Stop();
}
OnTick().Remove(ERR_UpdateCasualtyAircraft);
}
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (!Replication.IsServer() || SCR_Global.IsEditMode()) return;
m_ERRCasualtyLoading = new ERR_CasualtyLoadingService(this);
OnTick().Insert(ERR_UpdateCasualtyAircraft);
OnTick().Insert(m_ERRCasualtyLoading.Tick);
Players().OnChanged().Insert(m_ERRCasualtyLoading.RosterChanged);
OnStop().Insert(ERR_StopCasualtyLoading);
}
}
@@ -0,0 +1,75 @@
// Only successor service methods initiate this controller-owned handshake.
// No caller-supplied entity/seat RPC can ask the server to load another player.
modded class SCR_PlayerController
{
protected RplId m_ERRPreparedLoadMode = RplId.Invalid();
protected int m_ERRPreparedLoadNonce;
protected int m_ERRPreparedLoadClock;
protected int m_ERRLastLoadNonce;
void ERR_PrepareCasualtyLoad(RplId modeId, int nonce)
{
if (!Replication.IsServer()) return;
if (this == GetGame().GetPlayerController()) ERR_PrepareCasualtyLoadOwner(modeId, nonce);
else Rpc(ERR_PrepareCasualtyLoadOwner, modeId, nonce);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_PrepareCasualtyLoadOwner(RplId modeId, int nonce)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (this != GetGame().GetPlayerController() || !mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !ERR_CasualtyLoadingRules.GroundSettled(GetControlledEntity())) return;
if (m_ERRPreparedLoadMode == modeId && nonce <= m_ERRLastLoadNonce) return;
if (nonce <= 0) return;
m_ERRPreparedLoadMode = modeId;
m_ERRPreparedLoadNonce = nonce;
m_ERRLastLoadNonce = nonce;
m_ERRPreparedLoadClock = System.GetTickCount();
if (Replication.IsServer()) ERR_CasualtyLoadReady(modeId, nonce);
else Rpc(ERR_CasualtyLoadReady, modeId, nonce);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_CasualtyLoadReady(RplId modeId, int nonce)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
mode.CasualtyLoading().OwnerReady(this, nonce);
}
void ERR_CommitCasualtyLoad(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
{
if (!Replication.IsServer()) return;
if (this == GetGame().GetPlayerController()) ERR_CommitCasualtyLoadOwner(modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
else Rpc(ERR_CommitCasualtyLoadOwner, modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_CommitCasualtyLoadOwner(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
{
bool lease = this == GetGame().GetPlayerController() && m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce && ERR_CasualtyLoadingRules.Within(m_ERRPreparedLoadClock, System.GetTickCount(), 3000);
if (m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce) m_ERRPreparedLoadNonce = 0;
// Consume the local lease before the native call so duplicates cannot act.
bool accepted;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId);
Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId));
IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId);
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && vehicle == mode.CasualtyLoadingAircraft() && ERR_CasualtyLoadingRules.Nearby(mode, caller, actor) && owner)
{
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
BaseCompartmentSlot seat;
if (manager) seat = manager.FindCompartment(slotId);
if (access && ERR_CasualtyLoadingRules.TransportBench(seat, vehicle) && seat.GetOwner() == owner && !seat.GetOccupant() && seat.IsCompartmentAccessible() && !seat.IsGetInLockedFor(actor) && (!seat.IsReserved() || seat.IsReservedBy(actor)))
accepted = access.GetInVehicle(owner, seat, true, -1, ECloseDoorAfterActions.INVALID, false);
}
// Missing replicated entities or rejected native entry fail once; never retry.
if (Replication.IsServer()) ERR_CasualtyLoadResult(modeId, nonce, accepted);
else Rpc(ERR_CasualtyLoadResult, modeId, nonce, accepted);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_CasualtyLoadResult(RplId modeId, int nonce, bool accepted)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
mode.CasualtyLoading().OwnerResult(this, nonce, accepted);
}
}
@@ -0,0 +1,305 @@
// First release supports only the registered Mi8 extraction aircraft's physical
// child transport benches. Nothing changes health, ownership, or seat classes.
class ERR_CasualtyLoadingRules
{
static int RoundAction(int phase)
{
if (phase == ERR_Phase.PREPARING) return 0;
if (ERR_MissionRules.Terminal(phase)) return 2;
return 1;
}
static bool ObserveTicket(bool committed, bool physical, bool authorized)
{
return physical && (committed || authorized);
}
static bool Within(int started, int now, int limit) { return now >= started && now - started < limit; }
static bool SettledFor(int started, int observed, int now)
{
return started >= 0 && now >= started && now - started >= 5000 && Within(observed, now, 2000);
}
static bool Ranges(float callerToCasualty, float callerToAircraft, float casualtyToAircraft)
{
return callerToCasualty >= 0 && callerToCasualty <= 3 && callerToAircraft >= 0 && callerToAircraft <= 12 && casualtyToAircraft >= 0 && casualtyToAircraft <= 12;
}
static bool AircraftMotion(bool groundContact, float up, float speed, float angularSpeed)
{
return groundContact && up >= 0.85 && speed >= 0 && speed <= 0.5 && angularSpeed >= 0 && angularSpeed <= 0.1;
}
static RplId EntityId(IEntity entity)
{
if (!entity) return RplId.Invalid();
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component) return RplId.Invalid();
return component.Id();
}
static IEntity Entity(RplId id)
{
if (id == RplId.Invalid()) return null;
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component) return null;
return component.GetEntity();
}
static bool Down(IEntity actor)
{
if (!ERR_Actors.Alive(actor)) return false;
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
return controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED;
}
static bool GroundSettled(IEntity actor)
{
if (!Down(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(actor.FindComponent(CharacterAnimationComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
Physics physics = actor.GetPhysics();
return animation && !animation.PhysicsIsFalling() && physics && physics.GetVelocity().Length() <= 0.5 && access && !access.IsGettingIn() && !access.IsGettingOut();
}
static bool AircraftReady(Vehicle aircraft)
{
if (!ERR_Actors.Alive(aircraft)) return false;
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
Physics physics = aircraft.GetPhysics();
if (!controller || !controller.GetSimulation() || !physics) return false;
vector transform[4]; aircraft.GetWorldTransform(transform);
if (!AircraftMotion(true, transform[1][1], physics.GetVelocity().Length(), physics.GetAngularVelocity().Length())) return false;
if (controller.GetSimulation().HasAnyGroundContact()) return true;
return ERR_CasualtyGroundSupport.Parked(aircraft);
}
static bool LineOfSight(IEntity caller, IEntity casualty)
{
ChimeraCharacter rescuer = ChimeraCharacter.Cast(caller);
ChimeraCharacter patient = ChimeraCharacter.Cast(casualty);
if (!rescuer || !patient || !GetGame()) return false;
TraceParam trace = new TraceParam();
trace.Start = rescuer.AimingPosition();
trace.End = patient.AimingPosition();
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = caller;
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
// Hitting the intended body's clothing/attachment is still sight of that body.
IEntity hit = trace.TraceEnt;
while (hit)
{
if (hit == casualty) return true;
hit = hit.GetParent();
}
return false;
}
static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false;
CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent));
if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft();
if (!aircraft || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
Faction faction = mode.PlayerFaction();
return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty);
}
static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft)
{
if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false;
IEntity owner = seat.GetOwner();
if (!owner || owner == aircraft || !owner.GetPrefabData() || !owner.GetPrefabData().GetPrefabName().Contains("VehPart_MI8_seats")) return false;
while (owner)
{
if (owner == aircraft) return true;
owner = owner.GetParent();
}
return false;
}
}
class ERR_CasualtySettling
{
IEntity Actor;
UUID Account;
int Connection;
int Since;
int Observed;
}
class ERR_CasualtyLoadTicket
{
IEntity Caller;
IEntity Casualty;
Vehicle Aircraft;
BaseCompartmentSlot Seat;
UUID CallerAccount;
UUID CasualtyAccount;
int CallerConnection;
int CasualtyConnection;
int Nonce;
int Started;
int CommittedAt;
bool Committed;
bool OwnerReported;
}
class ERR_CasualtyLoadingService
{
protected ERR_GameMode m_Mode;
protected bool m_Stopped;
protected int m_NextNonce;
protected ref array<ref ERR_CasualtySettling> m_Settling = {};
protected ref array<ref ERR_CasualtyLoadTicket> m_Pending = {};
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
void ERR_CasualtyLoadingService(ERR_GameMode mode) { m_Mode = mode; }
ScriptInvoker OnResolved() { return m_OnResolved; } // (IEntity casualty, bool physicallyLoaded)
int PendingCount() { return m_Pending.Count(); }
protected ERR_PlayerLife CurrentLife(IEntity actor)
{
if (!m_Mode || !m_Mode.Players()) return null;
return m_Mode.Players().CurrentActor(actor);
}
protected bool SameLife(IEntity actor, UUID account, int connection)
{
ERR_PlayerLife life = CurrentLife(actor);
return life && life.AccountId == account && life.ConnectionId == connection;
}
protected bool Settled(IEntity actor, int now)
{
foreach (ERR_CasualtySettling sample : m_Settling)
if (sample.Actor == actor && SameLife(actor, sample.Account, sample.Connection)) return ERR_CasualtyLoadingRules.GroundSettled(actor) && ERR_CasualtyLoadingRules.SettledFor(sample.Since, sample.Observed, now);
return false;
}
protected void Observe(int now)
{
for (int index = m_Settling.Count() - 1; index >= 0; index--)
{
ERR_CasualtySettling sample = m_Settling[index];
if (!SameLife(sample.Actor, sample.Account, sample.Connection) || !ERR_CasualtyLoadingRules.GroundSettled(sample.Actor) || !ERR_CasualtyLoadingRules.Within(sample.Observed, now, 2000)) m_Settling.Remove(index);
else sample.Observed = now;
}
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives);
foreach (ERR_PlayerLife life : lives)
{
if (!CurrentLife(life.Actor) || !ERR_CasualtyLoadingRules.GroundSettled(life.Actor)) continue;
bool found;
foreach (ERR_CasualtySettling tracked : m_Settling) if (tracked.Actor == life.Actor) { found = true; break; }
if (found) continue;
ERR_CasualtySettling added = new ERR_CasualtySettling();
added.Actor = life.Actor; added.Account = life.AccountId; added.Connection = life.ConnectionId; added.Since = now; added.Observed = now;
m_Settling.Insert(added);
}
}
protected bool PendingActor(IEntity caller, IEntity casualty)
{
foreach (ERR_CasualtyLoadTicket pending : m_Pending)
if (pending.Caller == caller || pending.Casualty == casualty) return true;
return false;
}
protected BaseCompartmentSlot AvailableSeat(IEntity casualty, Vehicle aircraft)
{
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) || seat.GetOccupant() || seat.IsReserved() || !seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(casualty)) continue;
bool pendingSeat;
foreach (ERR_CasualtyLoadTicket pending : m_Pending) if (pending.Seat == seat) { pendingSeat = true; break; }
if (!pendingSeat) return seat;
}
return null;
}
bool CanLoad(IEntity caller, IEntity casualty)
{
if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false;
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
}
bool Begin(IEntity caller, IEntity casualty)
{
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
ERR_PlayerLife callerLife = CurrentLife(caller);
ERR_PlayerLife casualtyLife = CurrentLife(casualty);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId));
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(m_Mode.ExtractionVehicle()) == RplId.Invalid()) return false;
ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket();
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = m_Mode.ExtractionVehicle();
ticket.Seat = AvailableSeat(casualty, ticket.Aircraft);
if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false;
ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId;
ticket.CallerConnection = callerLife.ConnectionId; ticket.CasualtyConnection = casualtyLife.ConnectionId;
ticket.Started = System.GetTickCount(); ticket.Nonce = ++m_NextNonce;
m_Pending.Insert(ticket);
ticket.Seat.SetReserved(casualty);
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
// 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;
}
protected ERR_CasualtyLoadTicket Ticket(SCR_PlayerController controller, int nonce)
{
if (!controller || !GetGame() || !GetGame().GetPlayerManager() || GetGame().GetPlayerManager().GetPlayerController(controller.GetPlayerId()) != controller) return null;
foreach (ERR_CasualtyLoadTicket ticket : m_Pending)
if (ticket.Nonce == nonce && ticket.CasualtyConnection == controller.GetPlayerId() && ticket.Casualty == controller.GetControlledEntity()) return ticket;
return null;
}
void OwnerReady(SCR_PlayerController controller, int nonce)
{
if (!Replication.IsServer() || m_Stopped) return;
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || ticket.Committed) return;
int now = System.GetTickCount();
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
ticket.Committed = true; ticket.CommittedAt = now;
// Final authority/physical validation is the commit boundary. No retries.
controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID());
}
void OwnerResult(SCR_PlayerController controller, int nonce, bool accepted)
{
if (!Replication.IsServer() || m_Stopped) return;
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || !ticket.Committed || ticket.OwnerReported) return;
ticket.OwnerReported = true;
if (!accepted) Print("Rereintroduced [casualty]: owner declined loading; observing physical outcome until expiry.");
// Neither positive nor negative client acknowledgements release a reserved
// seat while an authorized owner command could still be in flight.
}
protected bool PhysicalTicket(ERR_CasualtyLoadTicket ticket)
{
// Death cannot revoke an in-flight commit. Keep the vacant seat reserved
// while the captured body exists; its owner may still see the prior state.
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected bool ValidTicket(ERR_CasualtyLoadTicket ticket)
{
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected void Resolve(ERR_CasualtyLoadTicket ticket, bool success)
{
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);
m_OnResolved.Invoke(ticket.Casualty, success);
}
void Tick(float worldNow)
{
if (!Replication.IsServer() || m_Stopped || !m_Mode) return;
int roundAction = ERR_CasualtyLoadingRules.RoundAction(m_Mode.Phase());
if (roundAction == 0) return;
if (roundAction == 2) { Stop(); return; }
int now = System.GetTickCount();
Observe(now);
array<ref ERR_CasualtyLoadTicket> snapshot = {}; foreach (ERR_CasualtyLoadTicket item : m_Pending) snapshot.Insert(item);
foreach (ERR_CasualtyLoadTicket ticket : snapshot)
{
if (m_Stopped) return;
// Commit authorizes one move of the captured body; a later caller/target
// disconnect cannot revoke that command or release its seat prematurely.
if (!ERR_CasualtyLoadingRules.ObserveTicket(ticket.Committed, PhysicalTicket(ticket), ValidTicket(ticket))) { Resolve(ticket, false); continue; }
bool aboard = CompartmentAccessComponent.GetVehicleIn(ticket.Casualty) == ticket.Aircraft && ticket.Seat.GetOccupant() == ticket.Casualty;
if (aboard && ticket.Committed) { Resolve(ticket, ERR_Actors.Alive(ticket.Casualty)); continue; }
if (ticket.Committed)
{
// Owner's prepare-to-commit lease is only 3s; this 8s observation budget
// leaves room for replication. Timeouts do not issue compensating teleports.
if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false);
}
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty)) Resolve(ticket, false);
}
}
void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); }
void Stop()
{
if (!Replication.IsServer() || m_Stopped) return;
m_Stopped = true; m_Settling.Clear();
while (!m_Pending.IsEmpty()) Resolve(m_Pending[m_Pending.Count() - 1], false);
}
}
@@ -0,0 +1,51 @@
// 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 int m_ERRShown;
protected string m_ERRShownLabel;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (RplSession.Mode() != RplMode.Dedicated && !SCR_Global.IsEditMode())
GetGame().GetCallqueue().CallLater(ERR_UpdateMarker, 1500, true);
}
protected void ERR_UpdateMarker()
{
if (!GetGame().GetPlayerController() || Knowledge() <= 0 || (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel)) return;
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (!manager || !manager.GetMarkerConfig()) return;
if (!m_ERRMarker)
{
m_ERRMarker = new SCR_MapMarkerBase();
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
vector p = Position();
m_ERRMarker.SetWorldPos(Math.Round(p[0]), Math.Round(p[2]));
m_ERRMarker.SetMarkerConfigID(0);
manager.InsertStaticMarker(m_ERRMarker, true);
}
string label = "Unknown location";
if (Knowledge() == 2)
{
if (!Label().IsEmpty()) label = Label();
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Initial helicopter pickup site";
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
else if (Kind() == ERR_LocationKind.PRISON) label = "Prison";
else label = Id();
}
m_ERRMarker.SetCustomText(label);
m_ERRShown = Knowledge();
m_ERRShownLabel = Label();
}
protected override void OnDelete(IEntity owner)
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateMarker);
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (manager && m_ERRMarker) manager.RemoveStaticMarker(m_ERRMarker);
m_ERRMarker = null;
super.OnDelete(owner);
}
}
@@ -0,0 +1,62 @@
// Base mission discovery: recovering physical documents works without companions.
// The Intel addon optionally assigns extra documents to enemy inventories.
class ERR_DiscoveryService
{
protected ERR_GameMode m_Mode;
protected ref set<IEntity> m_Spent = new set<IEntity>();
void ERR_DiscoveryService(ERR_GameMode mode) { m_Mode = mode; }
static void Folders(InventoryStorageManagerComponent inventory, out array<IEntity> folders)
{
folders.Clear();
SCR_PrefabNamePredicate predicate = new SCR_PrefabNamePredicate();
predicate.prefabName = ERR_Resources.IntelFolder();
array<IEntity> intel = {};
inventory.FindItems(intel, predicate, EStoragePurpose.PURPOSE_ANY);
foreach (IEntity folder : intel)
if (folder && !folders.Contains(folder)) folders.Insert(folder);
array<IEntity> objectives = {};
predicate.prefabName = ERR_Resources.ObjectiveDocument();
inventory.FindItems(objectives, predicate, EStoragePurpose.PURPOSE_ANY);
foreach (IEntity objective : objectives)
if (objective && !folders.Contains(objective)) folders.Insert(objective);
}
void Tick(float now)
{
if (!Replication.IsServer()) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
foreach (IEntity player : players) Recover(player);
}
protected void Recover(IEntity player)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return;
array<IEntity> folders = {};
Folders(inventory, folders);
foreach (IEntity folder : folders)
{
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
array<ERR_LocationComponent> eligible = {};
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
foreach (ERR_LocationComponent location : locations)
{
if (!location || location.Knowledge() >= 2) continue;
if (document)
{
if (location.Id() == document.TargetId()) eligible.Insert(location);
}
else if (location.Kind() != ERR_LocationKind.PRISON && location.Kind() != ERR_LocationKind.EXTRACTION) eligible.Insert(location);
}
if (eligible.IsEmpty()) continue;
ERR_LocationComponent chosen = eligible.GetRandomElement();
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Get("intelRevealChancePercent"))) continue;
m_Spent.Insert(folder);
PrintFormat("Rereintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
}
}
void Stop() { m_Spent.Clear(); }
}
@@ -0,0 +1,8 @@
[ComponentEditorProps(category: "Rereintroduced", description: "Physical document identifying one authored objective")]
class ERR_DocumentComponentClass : ScriptComponentClass {}
class ERR_DocumentComponent : ScriptComponent
{
protected string m_TargetId;
void SetTarget(string id) { if (Replication.IsServer()) m_TargetId = id; }
string TargetId() { return m_TargetId; }
}
@@ -0,0 +1,164 @@
// The server applies optional date/weather choices once at round preparation.
// Zero values preserve the world's authored environment and existing defaults.
class ERR_EnvironmentService
{
protected TimeAndWeatherManagerEntity m_Manager;
protected bool m_Started;
protected bool m_Stopped;
protected int m_DateCode;
protected int m_Profile;
static string Pad2(int value)
{
if (value < 10) return "0" + value.ToString();
return value.ToString();
}
static string Date(int code)
{
if (code == 0) return "authored";
if (!ERR_Settings.Valid("startDateCode", code)) return "unavailable";
int year = 1900 + code / 10000;
int month = (code % 10000) / 100;
int day = code % 100;
return year.ToString() + "-" + Pad2(month) + "-" + Pad2(day);
}
static bool ParseDate(string text, out int code)
{
if (text == "authored") { code = 0; return true; }
if (text.Length() != 10 || text.Substring(4, 1) != "-" || text.Substring(7, 1) != "-") return false;
int year, month, day;
if (!ERR_CommandArguments.Integer(text.Substring(0, 4), year) || !ERR_CommandArguments.Integer(text.Substring(5, 2), month) || !ERR_CommandArguments.Integer(text.Substring(8, 2), day)) return false;
if (year < 1900 || year > 2050 || month < 1 || month > 12 || day < 1 || day > 31) return false;
int parsed = (year - 1900) * 10000 + month * 100 + day;
if (!ERR_Settings.Valid("startDateCode", parsed)) return false;
code = parsed;
return true;
}
static string ProfileName(int profile)
{
if (profile == 0) return "authored";
if (profile == 1) return "clear";
if (profile == 2) return "overcast";
if (profile == 3) return "rain";
return "unavailable";
}
static string WeatherStateName(int profile)
{
if (profile == 1) return "Clear";
if (profile == 2) return "Overcast";
if (profile == 3) return "Rainy";
return "";
}
static bool ParseProfile(string text, out int profile)
{
if (text == "authored") { profile = 0; return true; }
if (text == "clear") { profile = 1; return true; }
if (text == "overcast") { profile = 2; return true; }
if (text == "rain" || text == "rainy") { profile = 3; return true; }
return false;
}
static bool HasWeatherState(TimeAndWeatherManagerEntity manager, string name)
{
if (!manager || name.IsEmpty()) return false;
array<ref WeatherState> states = {};
manager.GetWeatherStatesList(states);
foreach (WeatherState state : states)
if (state && state.GetStateName() == name) return true;
return false;
}
bool Start(ERR_GameMode mode)
{
if (!Replication.IsServer() || !mode || !mode.Settings()) return false;
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
if (!world) return false;
return StartOn(world.GetTimeAndWeatherManager(), mode.Settings().Get("startDateCode"), mode.Settings().Get("weatherProfile"));
}
protected bool StartOn(TimeAndWeatherManagerEntity manager, int dateCode, int profile)
{
if (!Replication.IsServer() || m_Stopped) return false;
if (m_Started) return true;
if (!manager || !ERR_Settings.Valid("startDateCode", dateCode) || !ERR_Settings.Valid("weatherProfile", profile)) return false;
int year = 1900 + dateCode / 10000;
int month = (dateCode % 10000) / 100;
int day = dateCode % 100;
if (dateCode != 0 && !manager.CheckValidDate(year, month, day)) return false;
string weather = WeatherStateName(profile);
BaseWeatherStateTransitionManager transitions = manager.GetTransitionManager();
if (profile != 0 && (!transitions || !manager.GetCurrentWeatherState() || !HasWeatherState(manager, weather))) return false;
int previousYear, previousMonth, previousDay;
manager.GetDate(previousYear, previousMonth, previousDay);
if (dateCode != 0 && !manager.SetDate(year, month, day, true)) return false;
if (profile != 0)
{
string previousWeather = manager.GetCurrentWeatherState().GetStateName();
bool previousLooping = manager.IsWeatherLooping();
// Use the native replicated weather route, never local editor previews.
// Loop the selected family; native variants/wind still follow vanilla rules.
manager.ForceWeatherTo(true, weather, 0, 24);
WeatherState current = manager.GetCurrentWeatherState();
WeatherState next = transitions.GetNextState();
if (!manager.IsWeatherLooping() || ((!current || current.GetStateName() != weather) && (!next || next.GetStateName() != weather)))
{
manager.ForceWeatherTo(previousLooping, previousWeather, 0, 24);
if (dateCode != 0) manager.SetDate(previousYear, previousMonth, previousDay, true);
return false;
}
}
m_Manager = manager;
m_DateCode = dateCode;
m_Profile = profile;
m_Started = true;
PrintFormat("Rereintroduced [environment]: date=%1 weather=%2", Date(dateCode), ProfileName(profile));
return true;
}
bool Started() { return m_Started && !m_Stopped; }
int DateCode() { return m_DateCode; }
int Profile() { return m_Profile; }
int CurrentDateCode()
{
if (!Started() || !m_Manager) return -1;
return (m_Manager.GetYear() - 1900) * 10000 + m_Manager.GetMonth() * 100 + m_Manager.GetDay();
}
string CurrentWeather()
{
if (!Started() || !m_Manager) return "unavailable";
WeatherState weather = m_Manager.GetCurrentWeatherState();
if (!weather) return "unavailable";
return weather.GetStateName();
}
string Status()
{
if (!Started()) return "Environment not started.";
return "Current date " + Date(CurrentDateCode()) + ", weather " + CurrentWeather() + " (round profile " + ProfileName(m_Profile) + ").";
}
void Stop() { m_Stopped = true; m_Manager = null; }
}
// Called after the server-owned admin authorization gate. Saving cannot change
// the active service; accepted changes are consumed by the next mission instance.
class ERR_EnvironmentCommands
{
static bool Execute(ERR_Settings settings, ERR_EnvironmentService environment, array<string> parts, array<string> replies)
{
if (!Replication.IsServer() || !settings || parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "date" && parts[1] != "weather")) return false;
if (parts.Count() == 3 && parts[2] == "show")
{
if (environment) replies.Insert(environment.Status());
replies.Insert("Next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ".");
return true;
}
int value;
bool valid;
string key = "startDateCode";
if (parts[1] == "weather") key = "weatherProfile";
if (parts.Count() == 4 && parts[2] == "set")
{
if (key == "startDateCode") valid = ERR_EnvironmentService.ParseDate(parts[3], value);
else valid = ERR_EnvironmentService.ParseProfile(parts[3], value);
}
if (valid && settings.Set(key, value))
replies.Insert("Saved for next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ". Current round unchanged.");
else replies.Insert("Invalid environment or settings could not be saved. Use mission date set YYYY-MM-DD (1900-2050) or authored; mission weather set authored|clear|overcast|rain. Previous setting retained.");
return true;
}
}
@@ -0,0 +1,72 @@
[BaseContainerProps()]
class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
{
protected ERR_PersonalEscapeResult PersonalResult(int playerId)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller || controller.GetPlayerId() != playerId) return ERR_PersonalEscapeResult.UNKNOWN;
return controller.ERR_EscapeResult();
}
override string GetTitle(int playerId, array<int> otherPlayerIds)
{
string title = "The helicopter escaped";
if (PersonalResult(playerId) == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
#ifdef WORKBENCH
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
#endif
return title;
}
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
{
ERR_PersonalEscapeResult result = PersonalResult(playerId);
if (result == ERR_PersonalEscapeResult.ESCAPED)
return "You crossed the extraction boundary aboard the helicopter.";
if (result == ERR_PersonalEscapeResult.MISSED) return "You did not escape with the helicopter.";
return "Your personal escape result is not available.";
}
}
// End data and the controller replicate independently. Refresh an already-open
// native result screen when the identity-derived controller result arrives.
modded class SCR_GameOverScreenContentUIComponent
{
protected bool m_ERREscapeContent;
override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent)
{
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
super.InitContent(endScreenUIContent);
m_ERREscapeContent = ERR_EscapeGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo) != null;
if (!m_ERREscapeContent) return;
SCR_PlayerController.ERR_OnEscapeResultChanged().Insert(ERR_RefreshEscapeContent);
ERR_RefreshEscapeContent();
}
protected void ERR_RefreshEscapeContent()
{
if (!m_ERREscapeContent || !m_wRoot) return;
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
int playerId = -1;
if (controller) playerId = controller.GetPlayerId();
ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();
TextWidget title = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sTileName));
TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName));
if (title) title.SetText(info.GetTitle(playerId, null));
if (debrief) debrief.SetText(info.GetDebriefing(playerId, null));
}
override void HandlerDeattached(Widget w)
{
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
m_ERREscapeContent = false;
super.HandlerDeattached(w);
}
}
[BaseContainerProps()]
class ERR_FailureGameOverScreenInfo : SCR_BaseGameOverScreenInfo
{
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && !mode.EndReason().IsEmpty()) return mode.EndReason();
return "No surviving prisoners can complete the escape, or the extraction helicopter has been lost.";
}
}
@@ -0,0 +1,112 @@
// Initial preparation only. A newly spawned vehicle may start asleep above its
// landing gear contact plane. Wake once, then observe native gravity/contact;
// never correct its transform, apply an impulse, or replenish its supplies.
class ERR_ExtractionPlacement
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Aircraft;
protected bool m_Stopped;
protected int m_WakeCount;
protected float m_Deadline;
protected float m_StableSince = -1;
protected float m_LastObservation = -1;
protected float m_NextDiagnostic;
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
protected string m_Failure;
void ERR_ExtractionPlacement(ERR_GameMode mode, Vehicle aircraft)
{
m_Mode = mode; m_Aircraft = aircraft;
m_Deadline = GetGame().GetWorld().GetWorldTime() + 30000;
}
static bool CanWake(bool initialized, int count, float now, float deadline)
{
return initialized && count == 0 && now < deadline;
}
static bool Fresh(float previous, float now)
{
return previous >= 0 && now >= previous && now - previous <= 1500;
}
static bool QualificationAllowed(bool previouslyReady, float now, float deadline)
{
return previouslyReady || now < deadline;
}
static bool Stable(float since, float previous, float now)
{
return since >= 0 && previous >= since && now >= previous && now - previous <= 1500 && now - since >= 1000;
}
int WakeCount() { return m_WakeCount; }
bool Ready() { return !m_Stopped && m_Result == ERR_PrepareResult.READY; }
string FailureReason() { return m_Failure; }
void Stop() { m_Stopped = true; }
protected ERR_PrepareResult Fail(string reason)
{
m_Failure = reason; m_Result = ERR_PrepareResult.FAILED;
Print("Rereintroduced [extraction placement]: " + reason, LogLevel.ERROR);
return m_Result;
}
protected void Diagnostic(bool ready)
{
if (!m_Aircraft) return;
Physics physics = m_Aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
vector entity[4]; m_Aircraft.GetWorldTransform(entity);
PrintFormat("ERR EXTRACTION PLACEMENT: wakes=%1 alive=%2 ready=%3 origin=%4 up=%5", m_WakeCount, ERR_Actors.Alive(m_Aircraft), ready, entity[3], entity[1][1]);
if (physics) PrintFormat("ERR EXTRACTION PHYSICS: dynamic=%1 awake=%2 speed=%3 angularSpeed=%4", physics.IsDynamic(), physics.IsActive(), physics.GetVelocity().Length(), physics.GetAngularVelocity().Length());
if (controller && controller.GetSimulation())
{
vector nativeTransform[4]; controller.GetSimulation().GetWorldTransform(nativeTransform);
PrintFormat("ERR EXTRACTION NATIVE: valid=%1 contact=%2 engine=%3 origin=%4 up=%5 entityDifference=%6", controller.GetSimulation().IsValid(), controller.GetSimulation().HasAnyGroundContact(), controller.GetSimulation().EngineIsOn(), nativeTransform[3], nativeTransform[1][1], vector.Distance(nativeTransform[3], entity[3]));
}
// Diagnostics read every wheel even while physics is awake. The strict parked
// fallback itself still requires sleep and never alters the vehicle.
vector front, left, right;
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_F", front, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, front, 0.30, true);
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bL", left, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, left, 0.45, true);
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bR", right, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, right, 0.45, true);
}
ERR_PrepareResult Poll(float now)
{
if (m_Stopped || m_Result == ERR_PrepareResult.FAILED) return ERR_PrepareResult.FAILED;
if (!Replication.IsServer() || !m_Mode || m_Mode.Phase() != ERR_Phase.PREPARING || m_Mode.ExtractionVehicle() != m_Aircraft) return Fail("initial aircraft ownership/phase changed");
if (!ERR_Actors.Alive(m_Aircraft) || !m_Aircraft.GetPrefabData() || !m_Aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return Fail("registered initial Mi8 is missing, destroyed or unsupported");
Physics physics = m_Aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
bool initialized = physics && physics.IsDynamic() && controller && controller.GetSimulation() && controller.GetSimulation().IsValid();
if (CanWake(initialized, m_WakeCount, now, m_Deadline))
{
// Production preparation owns the empty registered aircraft. The count is
// latched BEFORE the native call, preventing repeated wakes on any callback.
m_WakeCount = 1;
physics.SetActive(ActiveState.ACTIVE);
m_LastObservation = now;
Diagnostic(false);
m_NextDiagnostic = now + 1000;
return ERR_PrepareResult.PENDING; // never accept in the mutation frame
}
FuelManagerComponent fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
bool ready = initialized && m_WakeCount == 1 && fuel && fuel.HasFuel() && ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
if (now >= m_NextDiagnostic)
{
Diagnostic(ready);
PrintFormat("ERR EXTRACTION FUEL: manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
m_NextDiagnostic = now + 1000;
}
if (!ready || !Fresh(m_LastObservation, now)) m_StableSince = -1;
if (ready)
{
if (m_StableSince < 0) m_StableSince = now;
if (Stable(m_StableSince, m_LastObservation, now))
{
if (!QualificationAllowed(m_Result == ERR_PrepareResult.READY, now, m_Deadline)) return Fail("initial aircraft stable qualification missed the 30-second preparation deadline");
if (m_Result != ERR_PrepareResult.READY) Print("Rereintroduced [extraction placement]: native supported aircraft stable for one second, fuel present, exactly one initial wake");
m_Result = ERR_PrepareResult.READY;
m_LastObservation = now;
return m_Result;
}
}
m_Result = ERR_PrepareResult.PENDING;
m_LastObservation = now;
if (now >= m_Deadline) return Fail("initial aircraft did not become alive, fueled and physically supported/stable within 30 seconds; no replacement or repositioning requested");
return m_Result;
}
}
@@ -0,0 +1,101 @@
enum ERR_ExtractionState { WAITING, ESCAPED, LOST }
// Mission progression consumes the result; this service never spawns a vehicle.
class ERR_ExtractionService
{
static const int REQUIRED_SEATS = 12;
protected ref ERR_PlayerRoster m_Roster;
protected Vehicle m_Vehicle;
protected bool m_Assigned;
protected bool m_Begun;
protected bool m_Stopped;
protected vector m_Center;
protected float m_Radius;
protected ERR_ExtractionState m_State;
protected ref set<UUID> m_Escapees = new set<UUID>();
protected int m_ParticipantCount;
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; }
Vehicle VehicleEntity() { return m_Vehicle; }
ERR_ExtractionState State() { return m_State; }
ScriptInvoker OnResolved() { return m_OnResolved; }
int EscapedCount() { return m_Escapees.Count(); }
int LeftBehindCount() { return m_ParticipantCount - m_Escapees.Count(); }
bool DidEscape(UUID identity) { return m_Escapees.Contains(identity); }
bool Assign(Vehicle vehicle)
{
if (!Replication.IsServer() || m_Assigned || m_Stopped || !ERR_Actors.Alive(vehicle)) return false;
m_Vehicle = vehicle;
m_Assigned = true;
return true;
}
int Seats(out array<BaseCompartmentSlot> seats)
{
seats.Clear();
if (!m_Vehicle) return 0;
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
if (!manager) return 0;
array<BaseCompartmentSlot> all = {};
manager.GetCompartments(all);
foreach (BaseCompartmentSlot slot : all)
{
if (!slot) continue;
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
if (type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) seats.Insert(slot);
}
return seats.Count();
}
bool Ready()
{
if (!m_Assigned || !ERR_Actors.Alive(m_Vehicle) || !m_Vehicle.FindComponent(HelicopterControllerComponent)) return false;
array<BaseCompartmentSlot> seats = {};
if (Seats(seats) < REQUIRED_SEATS) return false;
foreach (BaseCompartmentSlot slot : seats)
if (SCR_CompartmentAccessComponent.GetCompartmentType(slot) == ECompartmentType.PILOT) return true;
return false;
}
bool Begin(vector center, float radius)
{
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !Ready()) return false;
m_Center = center;
m_Radius = radius;
m_Begun = true;
return true;
}
void Tick(ERR_Phase phase)
{
if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return;
if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return;
if (!ERR_Actors.Alive(m_Vehicle)) { Resolve(ERR_ExtractionState.LOST); return; }
if (phase != ERR_Phase.EXTRACTION || vector.DistanceXZ(m_Vehicle.GetOrigin(), m_Center) <= m_Radius) return;
array<ref ERR_PlayerLife> lives = {};
m_Roster.Participants(lives);
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor) || CompartmentAccessComponent.GetVehicleIn(life.Actor) != m_Vehicle) continue;
// Reserved disconnected characters and unconscious passengers remain alive.
// A recycled connection ID is never an identity for the escape snapshot.
m_Escapees.Insert(life.AccountId);
}
if (m_Escapees.IsEmpty()) return;
m_ParticipantCount = lives.Count();
Resolve(ERR_ExtractionState.ESCAPED);
}
protected void Resolve(ERR_ExtractionState result)
{
m_State = result;
m_OnResolved.Invoke(result);
}
void ConnectedWinners(out array<int> winners)
{
winners.Clear();
if (m_State != ERR_ExtractionState.ESCAPED || !m_Roster) return;
array<ref ERR_PlayerLife> lives = {};
m_Roster.Participants(lives);
foreach (ERR_PlayerLife life : lives)
if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId);
}
void Stop() { m_Stopped = true; }
}
@@ -0,0 +1,42 @@
// One authored faction/asset contract, supplied by the game mode resource.
[BaseContainerProps()]
class ERR_FactionCatalog
{
[Attribute()] string PlayerFactionKey;
[Attribute()] string EnemyFactionKey;
[Attribute("", UIWidgets.ResourceNamePicker, "Unarmed prisoner character", "et")]
ResourceName PrisonerPrefab;
[Attribute("", UIWidgets.ResourceNamePicker, "Initial physical guard squad", "et")]
ResourceName GuardGroupPrefab;
[Attribute("", UIWidgets.ResourceNamePicker, "Extraction helicopter", "et")]
ResourceName ExtractionPrefab;
bool Valid()
{
return !PlayerFactionKey.IsEmpty() && !EnemyFactionKey.IsEmpty() && PlayerFactionKey != EnemyFactionKey && !PrisonerPrefab.IsEmpty() && !GuardGroupPrefab.IsEmpty() && !ExtractionPrefab.IsEmpty();
}
Faction PlayerFaction()
{
if (!GetGame().GetFactionManager()) return null;
return GetGame().GetFactionManager().GetFactionByKey(PlayerFactionKey);
}
Faction EnemyFaction()
{
if (!GetGame().GetFactionManager()) return null;
return GetGame().GetFactionManager().GetFactionByKey(EnemyFactionKey);
}
bool WorldReady()
{
if (!Valid()) return false;
Faction player = PlayerFaction();
Faction enemy = EnemyFaction();
if (!player || !enemy || !player.IsFactionEnemy(enemy) || !enemy.IsFactionEnemy(player)) return false;
array<ResourceName> required = {PrisonerPrefab, GuardGroupPrefab, ExtractionPrefab};
foreach (ResourceName prefab : required)
{
Resource resource = Resource.Load(prefab);
if (!resource || !resource.IsValid()) return false;
}
return true;
}
}
@@ -0,0 +1,419 @@
class ERR_GameModeClass : SCR_BaseGameModeClass {}
class ERR_GameMode : SCR_BaseGameMode
{
[RplProp(), Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; configured from authored scenario")]
protected vector m_PrisonPosition;
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
protected vector m_EscapeCenter;
[RplProp(), Attribute("9000", UIWidgets.EditBox, "Helicopter must cross this radius to escape")]
protected float m_EscapeRadius;
[RplProp()]
protected ERR_Phase m_Phase;
[Attribute("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf", UIWidgets.ResourceNamePicker, "Faction and asset catalog", "conf")]
protected ResourceName m_FactionDefinition;
[RplProp()]
protected string m_PreparationError;
protected ref ERR_FactionCatalog m_Factions;
protected ref ERR_Settings m_Settings;
protected ref ERR_TimeService m_Time = new ERR_TimeService();
protected ref ERR_EnvironmentService m_Environment = new ERR_EnvironmentService();
protected ref ERR_DiscoveryService m_Discovery;
protected ref ERR_Objectives m_Objectives;
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
protected ref ERR_AdminService m_Admin = new ERR_AdminService();
protected ref ERR_CommandService m_Command;
protected ref array<ERR_LocationComponent> m_Locations = {};
protected ref array<IEntity> m_OwnedEntities = {};
protected ref ERR_ExtractionService m_Extraction;
[RplProp()]
protected string m_EndReason;
protected ref ERR_RadioDefenceService m_RadioDefence;
[RplProp()]
protected ERR_RadioFeedback m_RadioFeedback;
protected ref ERR_RecoveryService m_Recovery;
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
protected bool m_Stopped;
protected float m_NoSurvivorsSince = -1;
protected ref ScriptInvoker m_OnPhaseChanged = new ScriptInvoker();
protected ref ScriptInvoker m_OnTick = new ScriptInvoker();
protected ref ScriptInvoker m_OnStop = new ScriptInvoker();
protected ref ScriptInvoker m_OnLocationDiscovered = new ScriptInvoker();
protected ref ScriptInvoker m_OnGroup = new ScriptInvoker();
static ERR_GameMode Get()
{
if (!GetGame()) return null;
return ERR_GameMode.Cast(GetGame().GetGameMode());
}
ERR_Phase Phase() { return m_Phase; }
ERR_PlayerRoster Players() { return m_Roster; }
ERR_AdminService Admin() { return m_Admin; }
ERR_Settings Settings() { return m_Settings; }
ERR_TimeService Time() { return m_Time; }
ERR_EnvironmentService Environment() { return m_Environment; }
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
ERR_RecoveryService Recovery() { return m_Recovery; }
string RecoveryStatus() { return m_RecoveryStatus; }
ERR_CommandService Command() { return m_Command; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; }
Vehicle ExtractionVehicle()
{
if (!m_Extraction) return null;
return m_Extraction.VehicleEntity();
}
float EscapeRadius() { return m_EscapeRadius; }
ERR_RadioFeedback RadioFeedback() { return m_RadioFeedback; }
// Called only from existing authority transition/tick paths; no extra timer.
protected bool RefreshRadioFeedback()
{
if (!Replication.IsServer()) return false;
bool defending = m_Phase == ERR_Phase.RADIO_DEFENCE && m_RadioDefence != null && m_RadioDefence.State() == ERR_RadioDefenceState.DEFENDING;
bool occupied = defending && m_RadioDefence.Occupied();
ERR_RadioFeedback next = ERR_RadioFeedbackRules.Next(m_RadioFeedback, defending, occupied);
if (next == m_RadioFeedback) return false;
m_RadioFeedback = next;
return true;
}
bool DefenceOccupied() { return m_RadioDefence && m_RadioDefence.Occupied(); }
int DefenceSecondsRemaining()
{
if (!m_RadioDefence) return 0;
return m_RadioDefence.SecondsRemaining(GetWorld().GetWorldTime());
}
ScriptInvoker OnPhaseChanged() { return m_OnPhaseChanged; }
ScriptInvoker OnTick() { return m_OnTick; }
ScriptInvoker OnStop() { return m_OnStop; }
ScriptInvoker OnGroup() { return m_OnGroup; }
ScriptInvoker OnLocationDiscovered() { return m_OnLocationDiscovered; }
void LocationDiscovered(ERR_LocationComponent location, int previous, int current)
{
if (!Replication.IsServer() || !m_Locations.Contains(location) || current <= previous) return;
m_OnLocationDiscovered.Invoke(location, previous, current);
}
ERR_FactionCatalog Factions() { return m_Factions; }
Faction PlayerFaction()
{
if (!m_Factions) return null;
return m_Factions.PlayerFaction();
}
Faction EnemyFaction()
{
if (!m_Factions) return null;
return m_Factions.EnemyFaction();
}
string PreparationError() { return m_PreparationError; }
string Status()
{
if (!m_PreparationError.IsEmpty()) return "Mission startup failed: " + m_PreparationError;
if (!m_EndReason.IsEmpty()) return m_EndReason;
if (m_Phase == ERR_Phase.PREPARING) return "Preparing mission sites and physical supplies.";
string phase;
if (m_Phase == ERR_Phase.PRISON) phase = "Escape the prison";
else if (m_Phase == ERR_Phase.RADIO_SEARCH) phase = "Locate the radio station";
else if (m_Phase == ERR_Phase.RADIO_DEFENCE) phase = "Defend the radio transmission";
else if (m_Phase == ERR_Phase.EXTRACTION) phase = "Helicopter extraction";
else if (m_Phase == ERR_Phase.VICTORY) phase = "Escape complete";
else phase = "Mission lost";
if (!m_RecoveryStatus.IsEmpty()) phase += " " + m_RecoveryStatus;
return phase;
}
protected void RecoveryChanged(string message)
{
if (!Replication.IsServer() || m_RecoveryStatus == message) return;
m_RecoveryStatus = message;
Replication.BumpMe();
}
bool AbandonRound()
{
if (!Replication.IsServer() || !Active() || m_Stopped || !m_PreparationError.IsEmpty()) return false;
if (!ERR_MissionRules.CanTransition(m_Phase, ERR_Phase.DEFEAT)) return false;
m_EndReason = "An administrator abandoned this round.";
return Transition(ERR_Phase.DEFEAT);
}
protected void FailPreparation(string detail)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
m_PreparationError = detail;
Replication.BumpMe();
Print("Rereintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
StopServices();
ERR_SiteService sites = Sites();
if (sites) sites.Rollback();
for (int i = m_OwnedEntities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_OwnedEntities[i]);
m_OwnedEntities.Clear();
}
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
protected ERR_CommandService CreateCommandService() { return null; }
void ConfigureStart(vector position)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING) return;
m_PrisonPosition = position;
m_EscapeCenter = position;
Replication.BumpMe();
}
ERR_SiteService Sites()
{
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
if (!scenario) return null;
return scenario.Sites();
}
void RegisterLocation(ERR_LocationComponent location)
{
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
foreach (ERR_LocationComponent existing : m_Locations)
{
if (existing.Id() == location.Id())
{
Print("Rereintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
return;
}
}
m_Locations.Insert(location);
}
bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); }
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
bool SetExtractionVehicle(Vehicle vehicle)
{
return Replication.IsServer() && m_Phase == ERR_Phase.PREPARING && m_Extraction && m_Extraction.Assign(vehicle);
}
void PublishEscapeResults(bool reset = false)
{
if (!Replication.IsServer()) return;
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (!controller) continue;
if (reset) controller.ERR_ResetEscapeResult();
else controller.ERR_PublishEscapeResult(m_Roster, m_Extraction);
}
}
void RegisterGroup(SCR_AIGroup group)
{
if (!Replication.IsServer() || !group) return;
Own(group);
if (m_Command) m_Command.RegisterGroup(group);
m_OnGroup.Invoke(group);
}
bool Transition(ERR_Phase phase)
{
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
RefreshRadioFeedback();
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
Replication.BumpMe();
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
m_OnPhaseChanged.Invoke(previous, phase);
if (ERR_MissionRules.Terminal(phase))
{
StopServices();
array<int> winners = {};
if (phase == ERR_Phase.VICTORY && m_Extraction) m_Extraction.ConnectedWinners(winners);
EGameOverTypes reason = EGameOverTypes.LOOSE;
if (phase == ERR_Phase.VICTORY) reason = EGameOverTypes.VICTORY;
EndGameMode(SCR_GameModeEndData.Create(reason, winners));
}
return true;
}
bool UnlockPrison(IEntity player)
{
if (!Replication.IsServer() || !ERR_Actors.Player(player) || !ERR_Actors.CanFight(player)) return false;
if (vector.DistanceXZ(player.GetOrigin(), PrisonPosition()) > 15) return false;
return m_Phase == ERR_Phase.PRISON && Transition(ERR_Phase.RADIO_SEARCH);
}
bool TuneRadio(IEntity player, ERR_LocationComponent location)
{
return m_RadioDefence && m_RadioDefence.Begin(player, location);
}
protected void RadioStarted(ERR_LocationComponent location)
{
if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return;
if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop();
}
protected void RadioCompleted(ERR_LocationComponent location)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return;
foreach (ERR_LocationComponent candidate : m_Locations)
if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100);
Transition(ERR_Phase.EXTRACTION);
}
void PlayerSpawned(int id, IEntity actor)
{
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
m_Roster.Spawned(id, actor);
m_StartedPlayers = true;
m_NoSurvivorsSince = -1;
}
protected string SettingsPath() { return ERR_Settings.PATH; }
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (SCR_Global.IsEditMode()) return;
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!Replication.IsServer()) return;
m_Settings = new ERR_Settings(SettingsPath());
OnStop().Insert(m_Environment.Stop);
m_Recovery = new ERR_RecoveryService(this);
m_Recovery.OnChanged().Insert(RecoveryChanged);
OnStop().Insert(m_Recovery.Stop);
m_RadioDefence = new ERR_RadioDefenceService(this);
m_RadioDefence.OnStarted().Insert(RadioStarted);
m_RadioDefence.OnCompleted().Insert(RadioCompleted);
OnPhaseChanged().Insert(m_RadioDefence.PhaseChanged);
OnStop().Insert(m_RadioDefence.Stop);
m_Extraction = new ERR_ExtractionService(m_Roster);
PublishEscapeResults(true);
m_Extraction.OnResolved().Insert(ExtractionResolved);
OnStop().Insert(m_Extraction.Stop);
m_Discovery = new ERR_DiscoveryService(this);
OnTick().Insert(m_Discovery.Tick);
OnStop().Insert(m_Discovery.Stop);
m_Objectives = new ERR_Objectives(this);
OnPhaseChanged().Insert(m_Objectives.PhaseChanged);
OnTick().Insert(m_Objectives.Tick);
OnStop().Insert(m_Objectives.Stop);
m_Command = CreateCommandService();
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
}
protected void BindLocalObserver()
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller) return;
controller.ERR_StartObserverMonitor();
GetGame().GetCallqueue().Remove(BindLocalObserver);
}
protected void StartScenario()
{
if (m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
// Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
if (scenario) prepared = scenario.Prepare();
if (prepared == ERR_PrepareResult.PENDING)
{
GetGame().GetCallqueue().CallLater(StartScenario, 250, false);
return;
}
if (prepared == ERR_PrepareResult.FAILED)
{
string detail = "Scenario component is missing.";
if (scenario) detail = scenario.FailureReason();
FailPreparation(detail);
return;
}
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
{
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
return;
}
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
Transition(ERR_Phase.PRISON);
}
protected void ExtractionResolved(ERR_ExtractionState result)
{
if (!Replication.IsServer() || !Active()) return;
if (result == ERR_ExtractionState.LOST)
{
m_EndReason = "The extraction helicopter was lost. No replacement is available.";
Transition(ERR_Phase.DEFEAT);
}
else if (result == ERR_ExtractionState.ESCAPED)
{
m_EndReason = string.Format("Escape complete: %1 escaped; %2 did not escape.", m_Extraction.EscapedCount(), m_Extraction.LeftBehindCount());
Transition(ERR_Phase.VICTORY);
}
}
protected void ServerTick()
{
if (!Replication.IsServer() || !Active()) return;
float now = GetWorld().GetWorldTime();
m_Extraction.Tick(m_Phase);
if (!Active()) return;
m_RadioDefence.Tick(now);
if (RefreshRadioFeedback()) Replication.BumpMe();
m_Recovery.Tick();
if (!Active()) return;
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
{
m_NoSurvivorsSince = -1;
return;
}
if (m_StartedPlayers && !m_Roster.HasSurvivor())
{
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
if (now - m_NoSurvivorsSince >= 30000)
{
m_EndReason = "No surviving prisoners remain.";
Transition(ERR_Phase.DEFEAT);
return;
}
}
else m_NoSurvivorsSince = -1;
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_UpdateObserverServer();
}
if (m_Command) m_Command.Tick(now);
m_OnTick.Invoke(now);
}
protected void StopServices()
{
if (m_Stopped) return;
m_Stopped = true;
GetGame().GetCallqueue().Remove(ServerTick);
if (m_Command) m_Command.Stop();
m_OnStop.Invoke();
if (Replication.IsServer())
{
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
}
foreach (IEntity entity : m_OwnedEntities)
if (SCR_AIGroup.Cast(entity)) ERR_Actors.DeleteSpawned(entity);
}
}
void ~ERR_GameMode()
{
if (GetGame())
{
GetGame().GetCallqueue().Remove(StartScenario);
GetGame().GetCallqueue().Remove(BindLocalObserver);
StopServices();
}
m_Locations.Clear();
m_OwnedEntities.Clear();
}
}
@@ -0,0 +1,12 @@
// Initial preparation only. A READY site service never consults this rule again.
class ERR_InitialGuardRules
{
static float TimeoutMilliseconds() { return 90000; }
static ERR_PrepareResult State(int requested, int physical, int conscious, bool queueComplete, bool expired)
{
if (requested <= 0 || physical > requested || conscious > physical || conscious < 0 || physical < 0) return ERR_PrepareResult.FAILED;
if (expired) return ERR_PrepareResult.FAILED;
if (physical == requested && conscious == requested && queueComplete) return ERR_PrepareResult.READY;
return ERR_PrepareResult.PENDING;
}
}
@@ -0,0 +1,19 @@
// GetItems may enumerate the same physical entity through multiple storages.
// Identity is the entity reference, never its prefab: distinct rounds still count.
class ERR_Inventory
{
static int AppendUnique(InventoryStorageManagerComponent inventory, notnull array<IEntity> items)
{
if (!inventory) return items.Count();
array<IEntity> raw = {};
inventory.GetItems(raw);
foreach (IEntity item : raw)
if (item && !items.Contains(item)) items.Insert(item);
return items.Count();
}
static int Items(InventoryStorageManagerComponent inventory, out array<IEntity> items)
{
items.Clear();
return AppendUnique(inventory, items);
}
}
@@ -0,0 +1,23 @@
// Attach only to a successor-owned prisoner prefab, with a native action duration.
class ERR_LoadCasualtyAction : ScriptedUserAction
{
override bool CanBeShownScript(IEntity user)
{
return ERR_CasualtyLoadingRules.Nearby(ERR_GameMode.Get(), user, GetOwner());
}
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!ERR_CasualtyLoadingRules.Nearby(mode, user, GetOwner())) return false;
// Clients do not own the authoritative roster. Server always checks again.
if (!Replication.IsServer()) return true;
return mode.CasualtyLoading() && mode.CasualtyLoading().CanLoad(user, GetOwner());
}
override bool HasLocalEffectOnlyScript() { return false; }
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
if (!Replication.IsServer() || pOwnerEntity != GetOwner()) return;
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Begin(pUserEntity, pOwnerEntity);
}
}
@@ -0,0 +1,107 @@
// A round-local roster identity is not a Bohemia/platform identity. Only the
// real native admission callback may select it for the sole offline controller.
class ERR_LocalAdmissionRules
{
static bool Context(bool server, int sessionMode, int count, bool connected, bool sameLocal, bool sameManager, int connection, int controllerId)
{
return server && sessionMode == RplMode.None && count == 1 && connected && sameLocal && sameManager && connection > 0 && controllerId == connection;
}
}
class ERR_LocalAdmission
{
protected ERR_GameMode m_Mode;
protected SCR_PlayerController m_Controller;
protected int m_Connection;
protected UUID m_Key;
protected bool m_Selected;
protected bool m_OfflineMode;
protected bool m_LocalKey;
protected bool m_Failed;
protected bool m_Stopped;
protected bool m_RefusalLogged;
void ERR_LocalAdmission(ERR_GameMode mode) { m_Mode = mode; m_OfflineMode = RplSession.Mode() == RplMode.None; }
bool OfflineMode() { return m_OfflineMode; }
bool Selected() { return m_Selected; }
bool LocalKey() { return m_LocalKey; }
protected bool Context(ERR_GameMode mode, int connection, SCR_PlayerController controller)
{
if (!GetGame()) return false;
PlayerManager manager = GetGame().GetPlayerManager();
bool ownedMode = mode && mode == m_Mode && mode == ERR_GameMode.Get();
int count = -1;
bool connected, sameManager;
if (manager)
{
count = manager.GetPlayerCount();
connected = manager.IsPlayerConnected(connection);
sameManager = controller && manager.GetPlayerController(connection) == controller;
}
bool sameLocal = controller && controller == GetGame().GetPlayerController();
int controllerId;
if (controller) controllerId = controller.GetPlayerId();
bool valid = ownedMode && manager && controller && ERR_LocalAdmissionRules.Context(Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection, controllerId);
if (!valid && !m_RefusalLogged)
{
string project;
if (System.GetCLIParam("errOrdinaryScenario", project) && (project == "core" || project == "chat"))
{
m_RefusalLogged = true;
PrintFormat("ERR ORDINARY LOCAL CONTEXT: server=%1 sessionMode=%2 count=%3 connected=%4 sameLocal=%5 sameManager=%6 positiveMatchingId=%7 ownedMode=%8 controllerPresent=%9", Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection > 0 && controllerId == connection, ownedMode, controller != null);
}
}
return valid;
}
UUID Resolve(ERR_GameMode mode, int connection, SCR_PlayerController controller, UUID nativeIdentity)
{
// This helper is inert for modes created as Listen/Dedicated, even after Stop.
if (!m_OfflineMode) return nativeIdentity;
if (m_Failed || m_Stopped) return UUID.NULL_UUID;
if (m_Selected)
{
// A late backend value or name change never grants another local life.
if (mode != m_Mode || connection != m_Connection || controller != m_Controller || !Validate()) { m_Failed = true; return UUID.NULL_UUID; }
return m_Key;
}
if (!Context(mode, connection, controller)) { m_Failed = true; return UUID.NULL_UUID; }
m_Controller = controller; m_Connection = connection; m_Key = nativeIdentity;
m_Selected = true;
if (m_Key.IsNull()) { m_Key = UUID.GenV4(); m_LocalKey = true; }
if (m_Key.IsNull()) { m_Failed = true; return UUID.NULL_UUID; }
if (m_LocalKey) Print("Rereintroduced [admission]: sole offline controller uses a generated local-round roster key; backend identity and permissions unchanged.");
return m_Key;
}
bool Validate()
{
if (!m_OfflineMode) return true;
if (m_Failed || m_Stopped) return false;
if (!m_Selected) return true;
if (!Context(m_Mode, m_Connection, m_Controller)) m_Failed = true;
return !m_Failed;
}
bool Disconnected(int connection)
{
if (!m_Selected || m_Stopped || connection != m_Connection) return false;
m_Failed = true;
return true;
}
void Stop()
{
m_Stopped = true;
m_Controller = null;
m_Key = UUID.NULL_UUID;
// Keep selection/failure latched: this object cannot admit a replacement.
}
}
modded class ERR_GameMode
{
void ERR_FailLocalAdmission()
{
if (!Replication.IsServer()) return;
string reason = "The offline player context changed. Start a new scenario to establish a fresh session.";
if (Phase() == ERR_Phase.PREPARING) { FailPreparation(reason); return; }
if (!Active()) return;
m_EndReason = reason;
Transition(ERR_Phase.DEFEAT);
}
}
@@ -0,0 +1,69 @@
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
[ComponentEditorProps(category: "Rereintroduced", description: "Server-owned location with replicated discovery state")]
class ERR_LocationComponentClass : ScriptComponentClass {}
class ERR_LocationComponent : ScriptComponent
{
[RplProp(onRplName: "IdentityChanged"), Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
protected string m_Id;
[RplProp()]
protected string m_Label;
[RplProp(), Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
protected ERR_LocationKind m_Kind;
[RplProp()]
protected int m_Knowledge;
string Id() { return m_Id; }
string Label() { return m_Label; }
ERR_LocationKind Kind() { return m_Kind; }
int Knowledge() { return m_Knowledge; }
vector Position() { return GetOwner().GetOrigin(); }
bool Configure(string id, ERR_LocationKind kind, string label = "")
{
if (!Replication.IsServer() || id.IsEmpty()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || mode.Phase() != ERR_Phase.PREPARING) return false;
mode.UnregisterLocation(this);
m_Id = id;
m_Label = label;
m_Kind = kind;
mode.RegisterLocation(this);
Replication.BumpMe();
return true;
}
protected void IdentityChanged()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode) return;
mode.UnregisterLocation(this);
mode.RegisterLocation(this);
}
bool Discover(int roll, int chance)
{
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
int previous = m_Knowledge;
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
Replication.BumpMe();
ERR_GameMode mode = ERR_GameMode.Get();
if (mode) mode.LocationDiscovered(this, previous, m_Knowledge);
return true;
}
override void OnPostInit(IEntity owner)
{
super.OnPostInit(owner);
if (!SCR_Global.IsEditMode()) SetEventMask(owner, EntityEvent.INIT);
}
protected override void EOnInit(IEntity owner)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode) mode.RegisterLocation(this);
}
protected override void OnDelete(IEntity owner)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode) mode.UnregisterLocation(this);
super.OnDelete(owner);
}
}
@@ -0,0 +1,46 @@
enum ERR_Phase
{
PREPARING,
PRISON,
RADIO_SEARCH,
RADIO_DEFENCE,
EXTRACTION,
VICTORY,
DEFEAT
}
// Pure mission policy. Entity queries and authority checks belong to the owner.
class ERR_MissionRules
{
static bool Terminal(int phase)
{
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
}
static bool CanTransition(int from, int to)
{
if (Terminal(from) || from == to) return false;
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
return from == ERR_Phase.EXTRACTION && to == ERR_Phase.VICTORY;
}
static bool DefenceComplete(float now, float since, float duration, bool occupied)
{
return occupied && since >= 0 && duration > 0 && now >= since && now - since >= duration;
}
static bool ContactFresh(float observed, float now)
{
return observed >= 0 && now >= observed && now - observed <= 120000;
}
static int Discovery(int knowledge, int roll, int identifyPercent)
{
if (knowledge >= 1 || roll < identifyPercent) return 2;
return 1;
}
}
@@ -0,0 +1,76 @@
#ifdef WORKBENCH
// Test adapter exercises the production setting handler with a disposable file.
// Actual mode.Admin below still tests its server authorization and alias dispatch.
class ERR_NativeAdminSettingsAdapter : ERR_AdminService
{
void Probe(ERR_Settings settings, string arguments, array<string> replies)
{
replies.Clear();
arguments = arguments.Trim(); arguments.ToLower();
array<string> parts = {};
arguments.Split(" ", parts, true);
if (parts.Count() != 3 && parts.Count() != 4) return;
string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty()) SettingCommand(settings, key, parts, replies);
}
}
class ERR_NativeAdminSettingsProbe
{
protected static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: admin settings " + detail); return true; }
Print("ERR SMOKE FAIL: admin settings " + detail, LogLevel.ERROR);
return false;
}
protected static bool Storage(string path)
{
ERR_Settings settings = new ERR_Settings(path);
ERR_NativeAdminSettingsAdapter adapter = new ERR_NativeAdminSettingsAdapter();
array<string> replies = {};
map<string, int> initial = new map<string, int>();
foreach (string key : ERR_Settings.Keys()) initial.Set(key, settings.Get(key));
array<int> values = {0, 100, 15};
foreach (int value : values)
{
adapter.Probe(settings, " INTEL REVEAL SET " + value.ToString() + " ", replies);
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + value.ToString() + " saved" && settings.Get("intelRevealChancePercent") == value, "authorized setting handler accepts normalized value " + value.ToString())) return false;
ERR_Settings reloaded = new ERR_Settings(path);
if (!Check(reloaded.Get("intelRevealChancePercent") == value, "discovery probability survives native JSON reload")) return false;
foreach (string key, int original : initial)
if (key != "intelRevealChancePercent" && !Check(reloaded.Get(key) == original, "unrelated setting preserved: " + key)) return false;
}
array<string> invalid = {"-1", "101", "1.5", "100junk", "999999"};
foreach (string argument : invalid)
{
adapter.Probe(settings, "intel reveal set " + argument, replies);
ERR_Settings reloaded = new ERR_Settings(path);
if (!Check(replies.Count() == 1 && replies[0].Contains("Invalid") && replies[0].Contains("0-100") && settings.Get("intelRevealChancePercent") == 15 && reloaded.Get("intelRevealChancePercent") == 15, "invalid input preserves live and stored value: " + argument)) return false;
}
adapter.Probe(settings, "intel reveal help", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("second document") && replies[0].Contains("newly recovered") && replies[0].Contains("remain unchanged"), "help explains exact versus rough discovery and apply timing")) return false;
adapter.Probe(settings, "mission radio show", replies);
return Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=" + initial.Get("radioDefenceSeconds").ToString(), "existing radio alias and response remain intact");
}
static bool Run(ERR_GameMode mode)
{
if (!Check(Replication.IsServer() && mode && mode.Admin() && mode.Settings(), "native Core command service and settings ready")) return false;
int original = mode.Settings().Get("intelRevealChancePercent");
array<string> replies = {};
mode.Admin().Execute(mode, false, "intel reveal set 100", replies);
if (!Check(replies.Count() == 1 && replies[0] == "Administrator access required." && mode.Settings().Get("intelRevealChancePercent") == original, "actual unauthorized command cannot mutate discovery")) return false;
mode.Admin().Execute(mode, true, " INTEL REVEAL SHOW ", replies);
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + original.ToString(), "actual Core alias dispatches without Chat")) return false;
mode.Admin().Execute(mode, true, "intel reveal set 101", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("0-100") && mode.Settings().Get("intelRevealChancePercent") == original, "actual dispatch rejects out-of-range value with guidance")) return false;
mode.Admin().Execute(mode, true, "intel reveal help", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("second document"), "actual Core help describes physical document progression")) return false;
string path = "$profile:erri-native-admin-settings-probe.json";
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "disposable fixture filenames are unused")) return false;
bool result = Storage(path);
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
if (!result) return false;
return Check(mode.Settings().Get("intelRevealChancePercent") == original && !FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "fixture removed its files and never changed actual mission settings");
}
}
#endif
@@ -0,0 +1,738 @@
// Explicit local engine fixture, excluded from game/server builds.
#ifdef WORKBENCH
modded class ERR_GameMode
{
protected ref ERR_NativeEnvironmentRestartProbe m_ERRBaseEnvironmentRestartProbe;
protected override string SettingsPath()
{
string enabled, outcome;
if (System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "environment-restart")
{
m_ERRBaseEnvironmentRestartProbe = new ERR_NativeEnvironmentRestartProbe(this);
return ERR_NativeEnvironmentRestartProbe.PATH;
}
return super.SettingsPath();
}
protected ref ERR_NativeCasualtyServiceProbe m_ERRCasualtyServiceProbe;
protected ref ERR_NativePrisonerProbe m_ERRPrisonerProbe;
protected ref ERR_NativeGuardCancelProbe m_ERRGuardCancelProbe;
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
protected ref ERR_NativeCasualtyLoadProbe m_ERRCasualtyLoadProbe;
protected ref ERR_NativeEnvironmentProbe m_ERRBaseEnvironmentProbe;
protected bool m_ERRCatalogChecked;
protected ref ERR_NativePatrolProbe m_ERRPatrolEast;
protected ref ERR_NativePatrolProbe m_ERRPatrolWest;
protected int m_ERRBaseStep;
protected bool m_ERRObjectiveCloseChecked;
protected bool m_ERRStartupFailure;
protected bool m_ERRCacheLoss;
protected string m_ERRExtractionCase;
protected ref ERR_NativeExtractionProbe m_ERRExtractionProbe;
protected ref ERR_NativeTransportProbe m_ERRTransportProbe;
protected ref ERR_NativeLifecycleProbe m_ERRLifecycleProbe;
protected ref ERR_NativeRecoveryProbe m_ERRRecoveryProbe;
protected ref ERR_NativeRadioDefenceProbe m_ERRBaseRadioProbe;
protected string m_ERRActiveCacheId = "everon-supply-1";
protected int m_ERRMedicalDiscoveries;
protected IEntity m_ERRSightBlocker;
protected float m_ERRBaseDeadline;
protected float m_ERRBaseWait;
protected IEntity m_ERRBasePlayer;
protected ERR_LocationComponent m_ERRBaseRadio;
protected ref array<IEntity> m_ERRBaseDocuments = {};
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
ERR_NativeObjectiveProbe.BeginRound();
string outcome;
m_ERRStartupFailure = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "startup-failure";
m_ERRExtractionCase = outcome;
if (outcome == "extraction-startup-failure" && m_Factions) m_Factions.ExtractionPrefab = "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et";
if (outcome == "extraction-boundary-failure") m_EscapeRadius = 0;
m_ERRCacheLoss = outcome == "cache-loss";
if (m_ERRCacheLoss) m_ERRActiveCacheId = "everon-supply-2";
OnLocationDiscovered().Insert(ERR_BaseDiscovered);
OnStop().Insert(ERR_NativeObjectiveProbe.Stop);
OnStop().Insert(ERR_StopRadioProbe);
OnStop().Insert(ERR_StopHealthProbes);
if (m_ERRStartupFailure && m_Factions) m_Factions.EnemyFactionKey = m_Factions.PlayerFactionKey;
m_ERRBaseDeadline = GetWorld().GetWorldTime() + 210000;
if (outcome == "patrols") m_ERRBaseDeadline = GetWorld().GetWorldTime() + 330000;
OnPhaseChanged().Insert(ERR_BaseResult);
GetGame().GetCallqueue().CallLater(ERR_BaseTick, 500, true);
Print("ERR BASE SMOKE: core-only scripted escape; not manual or multiplayer acceptance");
}
protected bool ERR_BaseCheck(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: base " + detail); return true; }
ERR_NativeObjectiveProbe.Stop();
ERR_StopRadioProbe();
ERR_StopHealthProbes();
Print("ERR SMOKE FAIL: base " + detail, LogLevel.ERROR);
GetGame().GetCallqueue().Remove(ERR_BaseTick);
return false;
}
protected void ERR_StopHealthProbes()
{
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
if (m_ERRBaseEnvironmentProbe) m_ERRBaseEnvironmentProbe.Stop();
if (m_ERRPrisonerProbe) m_ERRPrisonerProbe.Stop();
if (m_ERRGuardCancelProbe) m_ERRGuardCancelProbe.Stop();
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
if (m_ERRBaseMedicalProbe) m_ERRBaseMedicalProbe.Stop();
if (m_ERRCasualtyLoadProbe) m_ERRCasualtyLoadProbe.Cleanup();
if (m_ERRRecoveryProbe) m_ERRRecoveryProbe.Stop();
}
protected void ERR_StopRadioProbe() { if (m_ERRBaseRadioProbe) m_ERRBaseRadioProbe.Stop(); }
protected void ERR_BaseMove(vector position)
{
vector transform[4];
m_ERRBasePlayer.GetWorldTransform(transform);
transform[3] = ERR_Actors.Ground(position);
ChimeraCharacter.Cast(m_ERRBasePlayer).Teleport(transform);
}
protected ERR_UnlockPrisonAction ERR_BaseLock(IEntity entity)
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
array<BaseUserAction> actions = {};
manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
if (lock) return lock;
}
}
IEntity child = entity.GetChildren();
while (child)
{
ERR_UnlockPrisonAction lock = ERR_BaseLock(child);
if (lock) return lock;
child = child.GetSibling();
}
return null;
}
protected bool ERR_BaseCover()
{
int count;
foreach (IEntity entity : m_OwnedEntities)
{
if (!entity || !entity.GetPrefabData() || entity.GetPrefabData().GetPrefabName() != "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et") continue;
vector transform[4];
entity.GetWorldTransform(transform);
vector up = transform[1];
if (!ERR_BaseCheck(up[1] > 0.99, "outpost barrier is upright")) return false;
vector low, high;
entity.GetWorldBounds(low, high);
vector center = (low + high) * 0.5;
vector across = "0 0 2";
if (high[0] - low[0] < high[2] - low[2]) across = "2 0 0";
TraceParam trace = new TraceParam();
trace.Start = center + across;
trace.End = center - across;
trace.Flags = TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
float fraction = GetWorld().TraceMove(trace, null);
PrintFormat("ERR COVER GEOMETRY: low=%1 high=%2 trace=%3", low, high, fraction);
if (!ERR_BaseCheck(fraction < 1 && trace.TraceEnt == entity, "outpost barrier has physical collision")) return false;
count++;
}
return ERR_BaseCheck(count == 3, "exactly three authored outpost barriers exist");
}
protected void ERR_BaseDiscovered(ERR_LocationComponent location, int previous, int current)
{
if (location.Id() == "everon-medical-1" && previous == 0 && current == 2) m_ERRMedicalDiscoveries++;
}
protected void ERR_BaseResult(ERR_Phase previous, ERR_Phase current)
{
if (!ERR_MissionRules.Terminal(current)) return;
GetGame().GetCallqueue().Remove(ERR_BaseTick);
if (m_ERRExtractionCase == "early-helicopter-loss")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 201 && Players().HasSurvivor() && Extraction().State() == ERR_ExtractionState.LOST, "lost helicopter ends prison phase despite a living prisoner")) return;
ERR_FailureGameOverScreenInfo result = new ERR_FailureGameOverScreenInfo();
if (!ERR_BaseCheck(EndReason().Contains("helicopter was lost") && result.GetDebriefing(0, null) == EndReason(), "early loss has a specific replicated and debriefing reason")) return;
Print("ERR SMOKE COMPLETE: early extraction vehicle loss");
return;
}
if (m_ERRExtractionCase == "casualty-service")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1402, "casualty service fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native casualty service with helper-only identity adapter; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1302, "radio document-loss fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "guard-cancel")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1502, "guard cancellation fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native guard-cancel; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "prisoner-kit")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 702, "prisoner-kit fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native prisoner-kit; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "medical")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 602, "medical fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native medical; reset requested by fixture");
return;
}
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 802, "casualty-load fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "environment-restart")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1202, "environment restart round finished before explicit test reset")) return;
PrintFormat("ERR SMOKE COMPLETE: environment restart round %1; reset requested by fixture", m_ERRBaseEnvironmentRestartProbe.Round());
return;
}
if (m_ERRExtractionCase == "environment")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1002, "environment fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native environment; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "patrols")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1102, "both patrol routes finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native finite patrol routes and cleanup; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "recovery")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("administrator abandoned"), "explicit authorized abandonment uses normal defeat result")) return;
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
Print("ERR SMOKE COMPLETE: recovery status and explicit administrator abandonment");
return;
}
if (m_ERRExtractionCase == "objective-ui")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 902, "objective UI fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native objective UI close, reopen and deleted-task cleanup; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "lifecycle")
{
if (!ERR_BaseCheck(m_ERRBaseStep == 402, "lifecycle fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native lifecycle admission guards; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "transport-loss")
{
if (!ERR_BaseCheck(m_ERRBaseStep == 302, "transport fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: finite native ground transport; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "extraction-capacity")
{
if (!ERR_BaseCheck(m_ERRBaseStep == 202, "capacity fixture finished before its explicit test reset")) return;
Print("ERR SMOKE COMPLETE: physical capacity and isolated extraction results; reset requested by fixture");
return;
}
if (!ERR_BaseCheck(current == ERR_Phase.VICTORY && m_ERRBaseStep == 5, "escape and restart reached without companion services")) return;
Print("ERR SMOKE COMPLETE: core-only escape");
}
protected void ERR_BaseTick()
{
float now = GetWorld().GetWorldTime();
if (now > m_ERRBaseDeadline) { ERR_BaseCheck(false, "timeout at " + m_ERRBaseStep.ToString()); return; }
if (m_ERRExtractionCase == "extraction-startup-failure" || m_ERRExtractionCase == "extraction-boundary-failure")
{
if (PreparationError().IsEmpty()) return;
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid extraction setup never starts gameplay")) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
if (!ERR_BaseCheck(players.IsEmpty() && Sites() && Sites().Count() == 0 && m_OwnedEntities.IsEmpty() && !ExtractionVehicle(), "late startup failure rolls back all authored stock, scenery, groups and vehicle")) return;
if (!ERR_BaseCheck(PreparationError().Contains("twelve") && !Transition(ERR_Phase.PRISON), "invalid extraction retains actionable diagnosis and blocks progression")) return;
GetGame().GetCallqueue().Remove(ERR_BaseTick);
Print("ERR SMOKE COMPLETE: expected late extraction startup failure");
return;
}
if (m_ERRStartupFailure)
{
if (PreparationError().IsEmpty()) return;
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid faction setup stops services without starting the mission")) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
if (!ERR_BaseCheck(players.IsEmpty() && !Sites() && m_OwnedEntities.IsEmpty(), "failed setup spawns no players, sites or supplies")) return;
array<string> replies = {};
Admin().Execute(this, true, "status", replies);
if (!ERR_BaseCheck(replies.Count() == 1 && replies[0].Contains("configured player and enemy factions"), "administrator receives retained startup failure cause")) return;
if (!ERR_BaseCheck(!Transition(ERR_Phase.PRISON) && !Transition(ERR_Phase.EXTRACTION), "failed preparation cannot be advanced into gameplay")) return;
GetGame().GetCallqueue().Remove(ERR_BaseTick);
Print("ERR SMOKE COMPLETE: expected startup-failure");
return;
}
if (m_ERRBaseStep == 1400)
{
int result = m_ERRCasualtyServiceProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "casualty service fixture failed"); return; }
if (result == 1) { m_ERRBaseStep = 1402; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1300)
{
int result = m_ERRObjectiveLossProbe.Tick(now);
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1302; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1500)
{
int result = m_ERRGuardCancelProbe.Tick(now);
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1502; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 700)
{
int result = m_ERRPrisonerProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 702; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 600)
{
int result = m_ERRBaseMedicalProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 602; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 800)
{
int result = m_ERRCasualtyLoadProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 802; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1200)
{
int result = m_ERRBaseEnvironmentRestartProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "environment restart fixture failed"); return; }
if (result == 1) { m_ERRBaseStep = 1202; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1000)
{
int result = m_ERRBaseEnvironmentProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1002; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1100)
{
int east = m_ERRPatrolEast.Tick(now);
int west = m_ERRPatrolWest.Tick(now);
if (east < 0 || west < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (east == 1 && west == 1)
{
if (!ERR_NativePatrolProbe.StopChecks(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRBaseStep = 1102; Transition(ERR_Phase.DEFEAT);
}
return;
}
if (m_ERRBaseStep == 500)
{
int result = m_ERRRecoveryProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1)
{
m_ERRBaseStep = 502;
array<string> replies = {};
Admin().Execute(this, true, "mission abandon", replies);
ERR_BaseCheck(Phase() == ERR_Phase.DEFEAT && replies.Count() == 1 && replies[0].Contains("Round abandoned") && RecoveryStatus().IsEmpty() && Recovery().Conscious() == 0 && Recovery().Incapacitated() == 0, "administrator command returns accepted outcome after recovery cleanup");
}
return;
}
if (m_ERRBaseStep == 400)
{
int result = m_ERRLifecycleProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 402; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 300)
{
int result = m_ERRTransportProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 302; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 200)
{
int result = m_ERRExtractionProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 202; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 901)
{
int result = ERR_NativeObjectiveProbe.TickCloseReopen(this, now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1)
{
if (m_ERRExtractionCase == "objective-ui")
{
if (!ERR_NativeObjectiveProbe.BeginDeletedTaskClose(this, now)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRBaseStep = 903;
}
else { m_ERRObjectiveCloseChecked = true; m_ERRBaseStep = 0; }
}
return;
}
if (m_ERRBaseStep == 903)
{
int result = ERR_NativeObjectiveProbe.TickDeletedTaskClose(this, now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 902; Transition(ERR_Phase.DEFEAT); }
return;
}
if (now < m_ERRBaseWait) return;
if (m_ERRBaseStep == 0)
{
array<IEntity> players = {};
ERR_Actors.Players(players, true);
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
m_ERRBasePlayer = players[0];
if (!m_ERRCatalogChecked)
{
if (!ERR_NativeFactionCatalogProbe.Run(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRCatalogChecked = true;
}
if (m_ERRExtractionCase == "casualty-service")
{
m_ERRCasualtyServiceProbe = new ERR_NativeCasualtyServiceProbe(this);
m_ERRBaseStep = 1400;
return;
}
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRObjectiveLossProbe = new ERR_NativeObjectiveLossProbe(this, m_ERRBasePlayer, m_ERRExtractionCase == "radio-document-loss-partial");
m_ERRBaseStep = 1300;
return;
}
if (m_ERRExtractionCase == "guard-cancel")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRGuardCancelProbe = new ERR_NativeGuardCancelProbe(this);
m_ERRBaseStep = 1500;
return;
}
if (m_ERRExtractionCase == "prisoner-kit")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPrisonerProbe = new ERR_NativePrisonerProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 700;
return;
}
if (m_ERRExtractionCase == "medical")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRBaseMedicalProbe = new ERR_NativeMedicalProbe(this);
m_ERRBaseStep = 600;
return;
}
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (m_ERRExtractionCase == "casualty-remove") m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyRemoveProbe(this);
else m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyLoadProbe(this, m_ERRExtractionCase != "casualty-load", m_ERRExtractionCase == "casualty-bench");
m_ERRBaseStep = 800;
return;
}
if (m_ERRExtractionCase == "environment-restart")
{
if (!ERR_BaseCheck(m_ERRBaseEnvironmentRestartProbe != null, "environment restart selected isolated settings before mode initialization")) return;
m_ERRBaseStep = 1200;
return;
}
if (m_ERRExtractionCase == "environment")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRBaseEnvironmentProbe = new ERR_NativeEnvironmentProbe(this);
m_ERRBaseStep = 1000;
return;
}
if (m_ERRExtractionCase == "patrols")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPatrolEast = new ERR_NativePatrolProbe(this, "everon-outpost-1");
m_ERRPatrolWest = new ERR_NativePatrolProbe(this, "everon-supply-2");
m_ERRBaseStep = 1100;
return;
}
if (m_ERRExtractionCase == "recovery")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRRecoveryProbe = new ERR_NativeRecoveryProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 500;
return;
}
if (m_ERRExtractionCase == "lifecycle")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRLifecycleProbe = new ERR_NativeLifecycleProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 400;
return;
}
if (m_ERRExtractionCase == "transport-survey")
{
ERR_NativeTransportSurvey.Run(this);
GetGame().GetCallqueue().Remove(ERR_BaseTick);
return;
}
if (m_ERRExtractionCase == "transport-loss")
{
m_ERRTransportProbe = new ERR_NativeTransportProbe(this);
m_ERRBaseStep = 300;
return;
}
if (m_ERRExtractionCase == "early-helicopter-loss")
{
if (!ERR_BaseCheck(Extraction().Ready() && Players().HasSurvivor(), "ready helicopter and living prisoner before deliberate loss")) return;
m_ERRBaseStep = 201;
ERR_Actors.DeleteSpawned(ExtractionVehicle());
return;
}
if (m_ERRExtractionCase == "extraction-capacity")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRExtractionProbe = new ERR_NativeExtractionProbe(this);
m_ERRBaseStep = 200;
return;
}
string survey;
if (System.GetCLIParam("errSmokeOutcome", survey) && survey == "site-survey")
{
ERR_NativeSiteSurvey probe = new ERR_NativeSiteSurvey();
probe.Run();
GetGame().GetCallqueue().Remove(ERR_BaseTick);
return;
}
if (!m_ERRObjectiveCloseChecked)
{
if (!ERR_NativeObjectiveProbe.BeginCloseReopen(this, now)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRBaseStep = 901;
return;
}
if (!ERR_NativeAdminSettingsProbe.Run(this) || !ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
if (!ERR_BaseCheck(Sites().Count() == 7 && cache && cache.Container && vector.DistanceXZ(cache.Container.GetOrigin(), PrisonPosition()) > 100, "authored supply cache is a separate scavenging location")) return;
array<IEntity> stock = {};
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
PrintFormat("ERR BASE STOCK: cache items=%1", stock.Count());
int expectedStock;
foreach (ERR_SiteStock entry : cache.Definition.Stock) expectedStock += entry.Count;
if (!ERR_BaseCheck(stock.Count() >= expectedStock, "cache holds finite physical equipment and objective documents")) return;
if (!ERR_BaseCheck(Command() == null, "no companion HQ or support controller loaded")) return;
if (!ERR_BaseCheck(vector.DistanceXZ(m_ERRBasePlayer.GetOrigin(), PrisonPosition()) < 5, "prisoner spawned in authored prison")) return;
foreach (ERR_LocationComponent location : m_Locations)
if (location.Kind() == ERR_LocationKind.RADIO) m_ERRBaseRadio = location;
ERR_UnlockPrisonAction lock;
foreach (IEntity entity : m_OwnedEntities)
{
if (!entity) continue;
if (!lock) lock = ERR_BaseLock(entity);
}
foreach (IEntity item : stock)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_Resources.ObjectiveDocument()) m_ERRBaseDocuments.Insert(item);
if (m_ERRCacheLoss)
{
ERR_Actors.DeleteSpawned(Sites().Find("everon-supply-1").Container);
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;
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;
if (!ERR_BaseCheck(m_Settings.Set("radioDefenceSeconds", 30), "test defence duration accepted")) return;
SCR_Task prisonTask = SCR_TaskSystem.GetTaskFromTaskID(ERR_Objectives.Id(ERR_Phase.PRISON), false);
if (!ERR_BaseCheck(prisonTask && prisonTask.GetTaskState() == SCR_ETaskState.COMPLETED, "previous native objective completes on phase change")) return;
if (!ERR_BaseCheck(Objectives().Current().GetTaskUIVisibility() == SCR_ETaskUIVisibility.LIST_ONLY, "search objective does not expose a hidden site")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
if (!ERR_BaseCheck(Sites().Find("everon-supply-1").Location.Knowledge() == 2 && Sites().Find("everon-supply-2").Location.Knowledge() == 2, "both named supply routes revealed after escape")) return;
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && Sites().Find("everon-outpost-1").Location.Knowledge() == 0, "optional sites remain hidden until approached")) return;
ERR_BaseMove(medical.Location.Position() + "0 0 8");
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
m_ERRBaseStep = 10;
m_ERRBaseWait = now + 6000;
}
else if (m_ERRBaseStep == 10)
{
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "unconscious player cannot discover optional sites")) return;
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(false);
ERR_BaseMove(medical.Location.Position() + "0 0 65");
m_ERRBaseStep = 11;
m_ERRBaseWait = now + 6000;
}
else if (m_ERRBaseStep == 11)
{
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "distant player cannot discover optional sites")) return;
ERR_BaseMove(medical.Location.Position() + "0 0 8");
m_ERRSightBlocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", ERR_Actors.Ground(medical.Location.Position() + "-1.5 0 4"));
if (!ERR_BaseCheck(m_ERRSightBlocker != null, "isolated visibility obstacle created")) return;
Own(m_ERRSightBlocker);
vector low, high;
m_ERRSightBlocker.GetWorldBounds(low, high);
PrintFormat("ERR VISIBILITY OBSTACLE: low=%1 high=%2 observer=%3 target=%4", low, high, m_ERRBasePlayer.GetOrigin(), medical.Location.Position());
m_ERRBaseStep = 115;
m_ERRBaseWait = now + 6000;
}
else if (m_ERRBaseStep == 115)
{
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "physical wall prevents nearby site discovery")) return;
ERR_Actors.DeleteSpawned(m_ERRSightBlocker);
m_ERRBaseStep = 12;
m_ERRBaseWait = now + 6000;
}
else if (m_ERRBaseStep == 12)
{
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 2 && medical.Location.Label() == "Medical stash" && m_ERRMedicalDiscoveries == 1, "conscious nearby player visually discovers named medical stash once")) return;
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "distant medical-site discovery does not reveal the radio")) return;
m_ERRBaseStep = 1;
}
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
{
ERR_BaseMove(Sites().Find(m_ERRActiveCacheId).Container.GetOrigin() + "0 0 2");
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(Sites().Find(m_ERRActiveCacheId).Container.FindComponent(InventoryStorageManagerComponent));
BaseInventoryStorageComponent destination = inventory.FindStorageForItem(m_ERRBaseDocuments[m_ERRBaseStep - 1]);
if (!ERR_BaseCheck(destination && source.TryMoveItemToStorage(m_ERRBaseDocuments[m_ERRBaseStep - 1], destination), "physical clue recovered")) return;
m_ERRBaseStep++;
m_ERRBaseWait = now + 2500;
}
else if (m_ERRBaseStep == 3)
{
if (!ERR_BaseCheck(Objectives().Current().GetTaskDescription().Contains("The radio station is identified on your map. Reach"), "objective guidance reacts to discovered radio")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
array<IEntity> stock = {};
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
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(m_ERRBaseRadio.Knowledge() == 2, "documents identify radio without Intel addon")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 30;
m_ERRBaseWait = now + 1000;
}
else if (m_ERRBaseStep == 30)
{
m_ERRBaseRadioProbe = new ERR_NativeRadioDefenceProbe(this, m_ERRBasePlayer, m_ERRBaseRadio);
m_ERRBaseStep = 301;
}
else if (m_ERRBaseStep == 301)
{
int result = m_ERRBaseRadioProbe.Tick(now);
if (result < 0) { ERR_StopRadioProbe(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 0) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback remains inactive before tuning")) return;
if (!ERR_BaseCheck(TuneRadio(m_ERRBasePlayer, m_ERRBaseRadio), "identified radio accepts transmission")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.WAITING && ERR_ObjectiveText.Description(this, false).Contains("Waiting for"), "radio feedback publishes waiting before first occupancy sample")) return;
m_ERRBaseStep = 31;
m_ERRBaseWait = now + 5000;
}
else if (m_ERRBaseStep == 31)
{
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
array<IEntity> stock = {};
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
if (!ERR_BaseCheck(stock.IsEmpty() && Sites().Prepare() == ERR_PrepareResult.READY, "depleted stock stays empty when preparation is queried again")) return;
ERR_Actors.DeleteSpawned(cache.Container);
if (!ERR_BaseCheck(DefenceOccupied() && DefenceSecondsRemaining() > 0 && DefenceSecondsRemaining() < 30 && Objectives().Current().GetTaskDescription().Contains("seconds remaining"), "native objective reports advancing radio progress")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide reports transmitting without querying a countdown")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
m_ERRBaseStep = 310;
m_ERRBaseWait = now + 35000;
}
else if (m_ERRBaseStep == 310)
{
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor does not trigger defeat after the full grace period")) return;
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!ERR_BaseCheck(playerController.GetControlledEntity() == m_ERRBasePlayer && playerController.ERR_ObserverState() == ERR_ObserverReason.NONE && !playerController.ERR_HasObserverCamera(), "unconscious player retains their body and camera")) return;
controller.SetUnconscious(false);
m_ERRBaseStep = 311;
m_ERRBaseWait = now + 6000;
}
else if (m_ERRBaseStep == 311)
{
if (!ERR_BaseCheck(ERR_Actors.CanFight(m_ERRBasePlayer) && DefenceOccupied() && DefenceSecondsRemaining() < 30, "regaining consciousness resumes transmission from zero")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide returns to transmitting after recovery")) return;
int playerId = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_ERRBasePlayer);
if (!ERR_BaseCheck(!Players().CanEnter(playerId, Phase()), "regaining consciousness does not grant another life")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "90 0 0");
m_ERRBaseStep = 32;
m_ERRBaseWait = now + 2500;
}
else if (m_ERRBaseStep == 32)
{
if (!ERR_BaseCheck(!Sites().Find(m_ERRActiveCacheId).Container, "lost supply cache is not replaced")) return;
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30 && Objectives().Current().GetTaskDescription().Contains("interrupted"), "objective reports interruption and reset")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Return a conscious survivor"), "radio guide reports interruption after leaving the area")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 4;
m_ERRBaseWait = now + 33000;
}
else if (m_ERRBaseStep == 4)
{
if (!ERR_BaseCheck(Phase() == ERR_Phase.EXTRACTION, "radio defence reveals extraction") || !ERR_NativeTimeProbe.VerifyProgression(this)) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback clears after completion")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRBasePlayer.FindComponent(CompartmentAccessComponent));
BaseCompartmentSlot slot = access.FindFreeCompartment(ExtractionVehicle(), ECompartmentType.PILOT);
if (!ERR_BaseCheck(slot && access.GetInVehicle(ExtractionVehicle(), slot, true, -1, ECloseDoorAfterActions.INVALID, false), "player physically boards extraction helicopter")) return;
m_ERRBaseStep = 5;
m_ERRBaseWait = now + 3000;
}
else if (m_ERRBaseStep == 5)
{
if (!ERR_BaseCheck(CompartmentAccessComponent.GetVehicleIn(m_ERRBasePlayer) == ExtractionVehicle(), "extraction requires real occupancy")) return;
vector transform[4];
ExtractionVehicle().GetWorldTransform(transform);
transform[3] = m_EscapeCenter + Vector(m_EscapeRadius + 100, 150, 0);
ExtractionVehicle().SetWorldTransform(transform);
m_ERRBaseWait = now + 4000;
}
}
}
#endif
@@ -0,0 +1,80 @@
#ifdef WORKBENCH
// Positive check observes production initial placement; the fixture never wakes
// the mission helicopter. Negative transforms
// and velocity are applied only to an independent, disposable empty aircraft.
class ERR_NativeCasualtyGroundProbe
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Negative;
protected float m_Wait;
protected float m_Deadline;
protected int m_Step;
protected bool m_Stopped;
void ERR_NativeCasualtyGroundProbe(ERR_GameMode mode) { m_Mode = mode; }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
ERR_Actors.DeleteSpawned(m_Negative); m_Negative = null;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty ground " + detail); return true; }
Print("ERR SMOKE FAIL: casualty ground " + detail, LogLevel.ERROR); Stop(); return false;
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (m_Deadline > 0 && now > m_Deadline) { Check(false, "bounded ground-support probe deadline"); return -1; }
if (now < m_Wait) return 0;
if (m_Step == 0)
{
Vehicle parked = m_Mode.ExtractionVehicle();
ERR_SiteService sites = m_Mode.Sites();
ERR_ExtractionPlacement placement;
if (sites) placement = sites.ExtractionPlacement();
if (!Check(placement && placement.Ready() && placement.WakeCount() == 1, "production initial placement completed with exactly one wake before player spawn")) return -1;
bool support = ERR_CasualtyGroundSupport.Parked(parked, true);
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(parked.FindComponent(HelicopterControllerComponent));
bool nativeContact = controller && controller.GetSimulation() && controller.GetSimulation().HasAnyGroundContact();
if (!Check((nativeContact || support) && ERR_CasualtyLoadingRules.AircraftReady(parked), "naturally settled mission aircraft has native landing contact or three strict wheel supports")) return -1;
vector raised = parked.GetOrigin() + "0 20 0";
m_Negative = Vehicle.Cast(ERR_Actors.Spawn(m_Mode.Factions().ExtractionPrefab, raised));
if (!Check(m_Negative != null, "separate empty airborne aircraft created for rejection checks")) return -1;
m_Mode.Own(m_Negative);
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Negative.FindComponent(DamageManagerComponent));
if (!Check(damage != null, "negative aircraft damage manager available")) return -1;
damage.EnableDamageHandling(false);
m_Deadline = now + 10000; m_Wait = now + 500; m_Step = 1; return 0;
}
if (m_Step == 1)
{
vector position = m_Negative.GetOrigin();
float height = position[1] - GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
bool support = ERR_CasualtyGroundSupport.Parked(m_Negative, true);
PrintFormat("ERR GROUND NEGATIVE: raisedPosition=%1 terrainClearance=%2", position, height);
if (!Check(ERR_Actors.Alive(m_Negative) && height > 5 && !support && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "living aircraft above terrain is rejected even if stationary or sleeping")) return -1;
vector tilted[4]; Math3D.AnglesToMatrix("0 0 90", tilted); tilted[3] = position;
m_Negative.SetWorldTransform(tilted);
m_Wait = now + 500; m_Step = 2; return 0;
}
if (m_Step == 2)
{
vector actual[4]; m_Negative.GetWorldTransform(actual);
Physics physics = m_Negative.GetPhysics();
if (!Check(physics != null, "negative aircraft has actual native physics")) return -1;
PrintFormat("ERR GROUND NEGATIVE: tiltedUp=%1 position=%2", actual[1][1], actual[3]);
if (!Check(ERR_Actors.Alive(m_Negative) && actual[1][1] < 0.85 && !ERR_CasualtyLoadingRules.AircraftMotion(true, actual[1][1], 0, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual tilted aircraft is rejected independently by upright policy")) return -1;
// Read back an actual native velocity on the disposable clone. No mission
// aircraft activation, simulation state, engine or velocity is changed.
physics.SetVelocity("2 0 0");
float speed = physics.GetVelocity().Length();
PrintFormat("ERR GROUND NEGATIVE: nativeSpeed=%1", speed);
if (!Check(speed > 0.5 && !ERR_CasualtyLoadingRules.AircraftMotion(true, 1, speed, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual native velocity is rejected independently by movement policy")) return -1;
if (!Check(ERR_Actors.Alive(m_Mode.ExtractionVehicle()) && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "mission aircraft remains untouched and eligible after negative checks")) return -1;
Stop(); return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,255 @@
#ifdef WORKBENCH
// Native mechanism experiment, NOT proof of an available player load/drag action.
// Caller must explicitly opt in and tick this independently of medical fixtures.
class ERR_NativeCasualtyLoadProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Caller;
protected IEntity m_Casualty;
protected Vehicle m_Vehicle;
protected SCR_CompartmentAccessComponent m_Access;
protected SCR_CharacterDamageManagerComponent m_Damage;
protected BaseCompartmentSlot m_Seat;
protected vector m_CallerTransform[4];
protected vector m_GroundPosition;
protected bool m_CallerMoved;
protected bool m_Direct;
protected bool m_Bench;
protected float m_DownSettledAt;
protected float m_NextDiagnostic;
protected int m_Entered;
protected int m_Left;
protected int m_Step;
protected float m_Deadline;
protected float m_Wait;
void ERR_NativeCasualtyLoadProbe(ERR_GameMode mode, bool direct = false, bool bench = false) { m_Mode = mode; m_Direct = direct; m_Bench = bench; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty-load " + detail); return true; }
Print("ERR SMOKE FAIL: casualty-load " + detail, LogLevel.ERROR);
Cleanup();
return false;
}
void Cleanup()
{
if (m_Access)
{
if (m_Access.GetOnCompartmentEntered(false)) m_Access.GetOnCompartmentEntered(false).Remove(Entered);
if (m_Access.GetOnCompartmentLeft(false)) m_Access.GetOnCompartmentLeft(false).Remove(Left);
}
// Restore only the same living local body, never a replacement/reconnected actor.
PlayerController controller = GetGame().GetPlayerController();
if (m_CallerMoved && m_Caller && controller && controller.GetControlledEntity() == m_Caller && ERR_Actors.Alive(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller))
ChimeraCharacter.Cast(m_Caller).Teleport(m_CallerTransform);
m_CallerMoved = false;
ERR_Actors.DeleteSpawned(m_Casualty);
m_Casualty = null;
}
protected void Entered(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
{
m_Entered++;
PrintFormat("ERR CASUALTY ENTER EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Entered);
}
protected void Left(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
{
m_Left++;
PrintFormat("ERR CASUALTY EXIT EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Left);
}
protected bool BelongsToAircraft(IEntity entity)
{
while (entity)
{
if (entity == m_Vehicle) return true;
entity = entity.GetParent();
}
return false;
}
protected BaseCompartmentSlot FindTransportBench()
{
array<BaseCompartmentSlot> seats = {};
m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!seat || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) continue;
IEntity owner = seat.GetOwner();
if (!owner || owner == m_Vehicle || !BelongsToAircraft(owner) || !owner.GetPrefabData()) continue;
ResourceName prefab = owner.GetPrefabData().GetPrefabName();
// These installed Mi8 seat-part prefabs own the real transport benches.
// No compartment class, animation, accessibility or health setting is changed.
if (!prefab.Contains("VehPart_MI8_seats")) continue;
if (!seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(m_Casualty) || seat.GetOccupant()) continue;
return seat;
}
return null;
}
protected void Trace(string stage)
{
if (!m_Casualty || !m_Access) return;
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
if (controller && animation)
PrintFormat("ERR CASUALTY STATE: stage=%1 life=%2 ragdoll=%3 falling=%4 physicsLinked=%5 gettingIn=%6 gettingOut=%7", stage, controller.GetLifeState(), animation.IsRagdollActive(), animation.PhysicsIsFalling(), animation.PhysicsIsLinked(), m_Access.IsGettingIn(), m_Access.IsGettingOut());
if (m_Seat)
{
IEntity owner = m_Seat.GetOwner();
string prefab;
if (owner && owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
PrintFormat("ERR CASUALTY SEAT: stage=%1 name=%2 ownerPrefab=%3 rootOwner=%4 hierarchyValid=%5 slotId=%6 managerId=%7", stage, m_Seat.GetCompartmentUniqueName(), prefab, owner == m_Vehicle, BelongsToAircraft(owner), m_Seat.GetCompartmentSlotID(), m_Seat.GetCompartmentMgrID());
PrintFormat("ERR CASUALTY OCCUPANCY: stage=%1 inCompartment=%2 actualSlot=%3 targetOccupant=%4 vehicleRoot=%5 enterEvents=%6 exitEvents=%7", stage, m_Access.IsInCompartment(), m_Access.GetCompartment() == m_Seat, m_Seat.GetOccupant() == m_Casualty, CompartmentAccessComponent.GetVehicleIn(m_Casualty) == m_Vehicle, m_Entered, m_Left);
}
}
protected bool DownOnGround()
{
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
return ERR_Actors.Alive(m_Casualty) && controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED && !CompartmentAccessComponent.GetVehicleIn(m_Casualty);
}
protected bool IncapacitateGroundCasualty()
{
if (!Check(m_Damage && ERR_Actors.Alive(m_Casualty), "living fixture casualty has native damage manager before incapacitation")) return false;
HitZone resilience = m_Damage.GetResilienceHitZone();
if (!Check(resilience != null, "fixture casualty has actual native resilience hit zone")) return false;
float beforeResilience = resilience.GetHealth();
bool damageWasEnabled = m_Damage.IsDamageHandlingEnabled();
// ForceUnconsciousness uses HandleDamage(TRUE), which damage protection can
// suppress. Permit only this synchronous operation, then restore the flag
// before any assertion/cleanup path. Never restore health or life state.
m_Damage.EnableDamageHandling(true);
m_Damage.ForceUnconsciousness();
float afterResilience = resilience.GetHealth();
m_Damage.EnableDamageHandling(damageWasEnabled);
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2 damageOriginallyEnabled=%3 restored=%4", beforeResilience, afterResilience, damageWasEnabled, m_Damage.IsDamageHandlingEnabled() == damageWasEnabled);
return Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Casualty) && m_Damage.IsDamageHandlingEnabled() == damageWasEnabled, "single native incapacitation reduces actual resilience, preserves life and restores damage-protection flag");
}
protected bool BeginExit()
{
// Native unloading is a separate mechanism check, not a user-action claim.
vector exitTransform[4];
m_Casualty.GetWorldTransform(exitTransform);
exitTransform[3] = m_GroundPosition;
return Check(m_Access.GetOutVehicle_NoDoor(exitTransform, true, false), "unconscious cargo occupant accepts native ground exit");
}
protected bool ExitSettled()
{
return ERR_Actors.Alive(m_Casualty) && vector.Distance(m_Casualty.GetOrigin(), m_GroundPosition) < 4 && m_Seat.IsCompartmentAccessible();
}
protected vector ExpectedExit() { return m_GroundPosition; }
protected string ExitResultDetail() { return "unloaded survivor on ground and seat accessible"; }
// now is world milliseconds, matching the other native probes. Returns -1/0/1.
int Tick(float now)
{
if (now < m_Wait) return 0;
if (m_Step == 0)
{
PlayerController controller = GetGame().GetPlayerController();
if (!Check(Replication.IsServer() && controller && m_Mode, "server and local real caller available")) return -1;
m_Caller = controller.GetControlledEntity();
m_Vehicle = m_Mode.ExtractionVehicle();
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && ERR_Actors.Alive(m_Vehicle), "caller on foot and authored helicopter intact")) return -1;
m_GroundPosition = ERR_Actors.Ground(m_Vehicle.GetOrigin() + "6 0 6");
m_Casualty = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_GroundPosition);
if (!Check(m_Casualty != null, "independent casualty spawned on ground")) return -1;
m_Mode.Own(m_Casualty);
m_Access = SCR_CompartmentAccessComponent.Cast(m_Casualty.FindComponent(SCR_CompartmentAccessComponent));
m_Damage = SCR_CharacterDamageManagerComponent.Cast(m_Casualty.FindComponent(SCR_CharacterDamageManagerComponent));
if (!Check(m_Access && m_Damage, "native casualty components present")) return -1;
m_Access.GetOnCompartmentEntered().Insert(Entered);
m_Access.GetOnCompartmentLeft().Insert(Left);
m_Caller.GetWorldTransform(m_CallerTransform);
vector helperTransform[4];
m_Caller.GetWorldTransform(helperTransform);
helperTransform[3] = ERR_Actors.Ground(m_GroundPosition + "2 0 0");
ChimeraCharacter.Cast(m_Caller).Teleport(helperTransform);
m_CallerMoved = true;
m_Step = 1;
m_Wait = now + 1000;
return 0;
}
if (m_Step == 1)
{
if (!IncapacitateGroundCasualty()) return -1;
m_Deadline = now + 8000;
m_Step = 2;
return 0;
}
if (m_Step == 2)
{
if (!DownOnGround())
{
if (now >= m_Deadline) { Check(false, "native health-driven ground incapacitation before boarding deadline"); return -1; }
return 0;
}
if (m_Bench)
{
if (m_DownSettledAt == 0)
{
m_DownSettledAt = now + 5000;
m_Deadline = now + 13000;
Trace("incapacitated; begin five-second bench settling interval");
return 0;
}
if (now < m_DownSettledAt) return 0;
}
PrintFormat("ERR CASUALTY POSITIONS: caller=%1 casualty=%2 helicopter=%3", m_Caller.GetOrigin(), m_Casualty.GetOrigin(), m_Vehicle.GetOrigin());
if (!Check(ERR_Actors.CanFight(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_Casualty.GetOrigin()) < 4, "conscious real caller physically beside ground casualty")) return -1;
if (m_Bench) m_Seat = FindTransportBench();
else m_Seat = m_Access.FindFreeAndAccessibleCompartment(m_Vehicle, ECompartmentType.CARGO);
if (!Check(m_Seat && !SCR_PatientCompartmentSlot.Cast(m_Seat) && !m_Seat.GetOccupant(), "ordinary free cargo seat selected without patient-seat substitution")) return -1;
if (m_Bench) Print("ERR CASUALTY BENCH SOURCE: VehPart_MI8_seats_base.et sha256=1b9a2e55a9a1cb884886488b6e170f437b6c1a615b584449819b96a6c78ad973");
Trace("before selected boarding request");
PrintFormat("ERR CASUALTY LOAD: CanGetInVehicle=%1 state=%2 targetType=%3", m_Access.CanGetInVehicle(m_Seat.GetOwner()), CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent)).GetLifeState(), m_Seat.GetType());
// Exactly the public server-to-owner primitive used by vanilla ambulance loading.
bool accepted;
if (m_Direct) accepted = m_Access.GetInVehicle(m_Seat.GetOwner(), m_Seat, true, -1, ECloseDoorAfterActions.INVALID, false);
else accepted = m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat);
PrintFormat("ERR CASUALTY MECHANISM: direct=%1 bench=%2 accepted=%3", m_Direct, m_Bench, accepted);
Trace("immediately after selected boarding request");
m_NextDiagnostic = now + 1000;
if (!Check(accepted, "selected loading request accepted while already unconscious")) return -1;
m_Deadline = now + 8000;
m_Step = 3;
return 0;
}
if (m_Step == 3)
{
if (now >= m_NextDiagnostic) { Trace("awaiting physical occupancy"); m_NextDiagnostic = now + 1000; }
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) != m_Vehicle || m_Seat.GetOccupant() != m_Casualty)
{
if (now >= m_Deadline) { Check(false, "physical cargo occupancy after selected mechanism before deadline"); return -1; }
return 0;
}
if (!Check(ERR_Actors.Alive(m_Casualty) && !ERR_Actors.CanFight(m_Casualty), "living unconscious ground casualty now physically aboard")) return -1;
if (!Check(!m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat), "occupied target seat rejects repeated request")) return -1;
ERR_PlayerRoster roster = new ERR_PlayerRoster();
UUID account = "00e77200-0000-0000-0000-000000000201";
if (!Check(roster.Bind(10201, account), "isolated casualty identity registered")) return -1;
roster.Spawned(10201, m_Casualty);
ERR_ExtractionService service = new ERR_ExtractionService(roster);
if (!Check(service.Assign(m_Vehicle) && service.Begin(m_Vehicle.GetOrigin() + "100 0 0", 50), "isolated boundary service initialized without moving mission helicopter")) return -1;
service.Tick(ERR_Phase.EXTRACTION);
if (!Check(service.DidEscape(account) && service.EscapedCount() == 1, "loaded unconscious identity satisfies physical extraction rule")) return -1;
if (!BeginExit()) return -1;
m_Deadline = now + 8000;
m_Step = 4;
return 0;
}
if (m_Step == 4)
{
if (now >= m_NextDiagnostic) { Trace("awaiting ground exit"); m_NextDiagnostic = now + 1000; }
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) || m_Seat.GetOccupant())
{
if (now >= m_Deadline) { Check(false, "native exit clears physical vehicle and seat before deadline"); return -1; }
return 0;
}
bool settled = ExitSettled();
if (!settled && now < m_Deadline) return 0;
PrintFormat("ERR CASUALTY EXIT POSITION: actual=%1 requested=%2 alive=%3 accessible=%4", m_Casualty.GetOrigin(), ExpectedExit(), ERR_Actors.Alive(m_Casualty), m_Seat.IsCompartmentAccessible());
if (!Check(settled, ExitResultDetail())) return -1;
Cleanup();
m_Step = 5;
return 1;
}
return 1;
}
}
#endif
@@ -0,0 +1,160 @@
#ifdef WORKBENCH
// Opt-in completion-boundary test of the REAL inherited stock remove action.
// It does not simulate held input, replace the action, or prove remote ownership.
class ERR_NativeCasualtyRemoveProbe : ERR_NativeCasualtyLoadProbe
{
protected SCR_RemoveCasualtyUserAction m_Remove;
protected UserActionContext m_Context;
protected int m_SavedDoor;
protected int m_SelectedDoor;
protected bool m_DoorChanged;
protected bool m_PendingAction;
protected bool m_TestBodyProtected;
protected bool m_RemoveStopped;
protected bool m_RemoveComplete;
protected vector m_AuthoredExit;
protected vector m_SeatedPosition;
protected vector m_HelperPosition;
protected float m_RemoveDeadline;
protected float m_RemoveDiagnostic;
void ERR_NativeCasualtyRemoveProbe(ERR_GameMode mode, bool direct = false, bool bench = false)
{
m_Mode = mode; m_Direct = true; m_Bench = true;
}
override void Cleanup()
{
if (m_Remove && m_DoorChanged) m_Remove.SetCompartmentDoorInfoIndex(m_SavedDoor);
m_DoorChanged = false; m_PendingAction = false; m_RemoveStopped = true;
super.Cleanup();
}
protected void FindAction(IEntity entity)
{
if (!entity || m_Remove) return;
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
SCR_RemoveCasualtyUserAction remove = SCR_RemoveCasualtyUserAction.Cast(action);
if (remove && remove.GetCompartmentSlot() == m_Seat && remove.GetContextIndex("door_l01_door") >= 0)
{
m_Remove = remove; return;
}
}
}
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindAction(child);
}
protected void FindContext(IEntity entity)
{
if (!entity || m_Context) return;
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
UserActionContext context = manager.GetContext("door_l01_door");
if (context)
{
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
if (actions.Contains(m_Remove)) { m_Context = context; return; }
}
}
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindContext(child);
}
protected CompartmentDoorInfo ResolveDoor()
{
BaseCompartmentManagerComponent manager = m_Seat.GetManager();
if (!manager) return null;
CompartmentDoorInfo info = manager.GetDoorInfo(m_SelectedDoor);
if (info) return info;
CompartmentDoorReference doorReference = manager.GetDoorReference(m_SelectedDoor);
if (!doorReference) return null;
BaseCompartmentManagerComponent referenced = doorReference.GetReferencedDoorCompartmentManager();
int referencedIndex = doorReference.GetAnimDoorIndex();
PrintFormat("ERR CASUALTY REMOVE DOOR REFERENCE: selected=%1 referencedIndex=%2 manager=%3", m_SelectedDoor, referencedIndex, referenced != null);
if (!referenced || referencedIndex < 0) return null;
return referenced.GetDoorInfo(referencedIndex);
}
protected override bool BeginExit()
{
FindAction(m_Vehicle);
if (!Check(m_Remove && BelongsToAircraft(m_Remove.GetOwner()) && m_Remove.GetActionDuration() == 1 && !m_Remove.HasLocalEffectOnly(), "stock bench RemoveCasualty action exists, duration one second, nonlocal effect")) return false;
FindContext(m_Vehicle);
if (!Check(m_Context && m_Context.GetContextName() == "door_l01_door", "actual exterior context contains selected stock seat action")) return false;
// Same native point-to-door selection used by SCR_CompartmentUserAction.
// Explicit cached index substitutes only for selecting a UI context, not input.
m_SelectedDoor = m_Seat.PickDoorIndexForPoint(m_Context.GetOrigin());
if (!Check(m_SelectedDoor >= 0, "actual action context resolves a relevant native compartment door")) return false;
CompartmentDoorInfo door = ResolveDoor();
if (!Check(door && door.GetExitTeleportPointInfo(), "selected native door exposes its authored teleport exit")) return false;
vector exitTransform[4]; door.GetExitTeleportPointInfo().GetWorldTransform(exitTransform);
m_AuthoredExit = exitTransform[3]; m_SeatedPosition = m_Casualty.GetOrigin();
if (!Check(vector.Distance(m_AuthoredExit, m_Vehicle.GetOrigin()) < 15, "authored exit resolves beside this physical helicopter")) return false;
m_SavedDoor = m_Remove.GetCompartmentDoorInfoIndex();
m_Remove.SetCompartmentDoorInfoIndex(m_SelectedDoor); m_DoorChanged = true;
vector outside = m_Context.GetOrigin() - m_Vehicle.GetOrigin(); outside[1] = 0;
if (!Check(outside.Length() > 0.1, "exterior context has a distinct outward direction")) return false;
outside.Normalize();
m_HelperPosition = ERR_Actors.Ground(m_Context.GetOrigin() + outside * 0.75);
vector helper[4]; m_Caller.GetWorldTransform(helper); helper[3] = m_HelperPosition;
ChimeraCharacter.Cast(m_Caller).Teleport(helper);
PrintFormat("ERR CASUALTY REMOVE SETUP: action=%1 context=%2 door=%3 contextOrigin=%4 exit=%5 seatedOrigin=%6 helperRequested=%7", m_Remove.GetActionID(), m_Context.GetContextName(), m_SelectedDoor, m_Context.GetOrigin(), m_AuthoredExit, m_SeatedPosition, m_HelperPosition);
m_PendingAction = true;
m_Wait = GetGame().GetWorld().GetWorldTime() + 1000;
return true;
}
static bool SafeExit(float horizontalDistance, float terrainClearance)
{
return horizontalDistance >= 0 && horizontalDistance <= 1.25 && terrainClearance >= -0.25 && terrainClearance <= 1.5;
}
protected override vector ExpectedExit() { return m_AuthoredExit; }
protected override string ExitResultDetail() { return "stock removal leaves a living incapacitated body beside authored door exit, outside compartment, on terrain, and seat accessible"; }
protected override bool ExitSettled()
{
if (!m_Casualty || !m_Access || !m_Seat) return false;
vector actual = m_Casualty.GetOrigin();
float height = actual[1] - GetGame().GetWorld().GetSurfaceY(actual[0], actual[2]);
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
bool safe = DownOnGround() && !m_Access.IsGettingOut() && !m_Access.IsInCompartment() && !m_Seat.GetOccupant() && m_Seat.IsCompartmentAccessible() && animation && !animation.PhysicsIsFalling() && SafeExit(vector.DistanceXZ(actual, m_AuthoredExit), height);
float now = GetGame().GetWorld().GetWorldTime();
if (now >= m_RemoveDiagnostic || safe)
{
PrintFormat("ERR CASUALTY REMOVE POSITION: actual=%1 aiming=%2 authored=%3 seated=%4 horizontalError=%5 terrainClearance=%6 safe=%7", actual, ChimeraCharacter.Cast(m_Casualty).AimingPosition(), m_AuthoredExit, m_SeatedPosition, vector.DistanceXZ(actual, m_AuthoredExit), height, safe);
Trace("stock door removal observation"); m_RemoveDiagnostic = now + 1000;
}
return safe;
}
override int Tick(float now)
{
if (m_RemoveComplete) return 1;
if (m_RemoveStopped) return -1;
if (m_RemoveDeadline == 0)
{
if (!Check(m_Mode && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "production mission helicopter is physically grounded and stable before fixture")) return -1;
m_RemoveDeadline = now + 45000;
}
if (now > m_RemoveDeadline) { Check(false, "bounded 45-second stock removal fixture deadline"); return -1; }
if (m_Step == 4 && m_PendingAction)
{
if (now < m_Wait) return 0;
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_HelperPosition) < 1.5, "real conscious rescuer settled outside selected native door")) return -1;
bool shown = m_Remove.CanBeShown(m_Caller);
bool permitted = m_Remove.CanBePerformed(m_Caller);
PrintFormat("ERR CASUALTY REMOVE ACTION: shown=%1 performedAllowed=%2 reason=%3 cachedDoor=%4 caller=%5", shown, permitted, m_Remove.GetCannotPerformReason(), m_Remove.GetCompartmentDoorInfoIndex(), m_Caller.GetOrigin());
if (!Check(shown && permitted && m_Seat.GetOccupant() == m_Casualty && m_Remove.GetCompartmentDoorInfoIndex() == m_SelectedDoor, "stock native action visibility and permission accept actual rescuer and occupied casualty bench")) return -1;
m_PendingAction = false; // exactly one completion invocation, no native retry
m_Remove.PerformAction(m_Remove.GetOwner(), m_Caller);
Print("ERR CASUALTY REMOVE: invoked existing stock action completion once; held input and network dispatch not exercised");
m_Deadline = now + 8000;
}
int result = super.Tick(now);
if (!m_TestBodyProtected && m_Casualty && m_Damage)
{
// Only the fixture-owned casualty is protected from nearby initial guards.
// ForceUnconsciousness still uses its actual native health/life transition.
m_Damage.EnableDamageHandling(false); m_TestBodyProtected = true;
}
if (result == 1) m_RemoveComplete = true;
return result;
}
}
#endif
@@ -0,0 +1,293 @@
#ifdef WORKBENCH
// Only the helper's membership is synthetic. The casualty keeps its real native
// player controller/account/body and exercises the ordinary owner handshake.
class ERR_NativeCasualtyHelperAdapter : ERR_CasualtyLoadingService
{
protected ref ERR_PlayerLife m_HelperLife;
protected bool m_HelperEnabled;
void ERR_NativeCasualtyHelperAdapter(ERR_GameMode mode) { m_Mode = mode; }
void Helper(IEntity actor)
{
m_HelperLife = new ERR_PlayerLife();
m_HelperLife.Actor = actor; m_HelperLife.Connected = true; m_HelperLife.UsedLife = true;
m_HelperLife.ConnectionId = 15401;
m_HelperLife.AccountId = "00e77200-0000-0000-0000-000000001401";
}
void EnableHelper() { m_HelperEnabled = true; }
bool StrictParticipant(IEntity actor) { return super.CurrentLife(actor) != null; }
bool StoppedForProbe() { return m_Stopped; }
protected override ERR_PlayerLife CurrentLife(IEntity actor)
{
if (m_HelperEnabled && m_HelperLife && actor && actor == m_HelperLife.Actor) return m_HelperLife;
return super.CurrentLife(actor);
}
}
class ERR_NativeCasualtyServiceProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected IEntity m_Helper;
protected IEntity m_Blocker;
protected Vehicle m_Aircraft;
protected ERR_LoadCasualtyAction m_Action;
protected ref ERR_NativeCasualtyHelperAdapter m_Service;
protected ref ERR_NativeCasualtyGroundProbe m_GroundProbe;
protected bool m_GroundChecked;
protected ref array<BaseCompartmentSlot> m_Reserved = {};
protected BaseCompartmentSlot m_LoadedSeat;
protected float m_Deadline;
protected float m_Wait;
protected float m_PositionDeadline;
protected float m_NextDiagnostic;
protected vector m_RequestedPlayer;
protected vector m_RequestedHelper;
protected bool m_PlayerDamageSnapshot;
protected bool m_PlayerDamageOriginallyEnabled;
protected int m_SettleStart;
protected int m_Step;
protected int m_Resolutions;
protected bool m_Loaded;
protected bool m_Observing;
protected bool m_Stopped;
void ERR_NativeCasualtyServiceProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty service " + detail); return true; }
Print("ERR SMOKE FAIL: casualty service " + detail, LogLevel.ERROR);
Stop(); return false;
}
protected void Resolved(IEntity actor, bool loaded)
{
m_Resolutions++;
m_Loaded = actor == m_Player && loaded;
}
protected void ReleaseFixtureReservations()
{
foreach (BaseCompartmentSlot seat : m_Reserved)
if (seat && m_Helper && seat.IsReservedBy(m_Helper)) seat.SetReserved(null);
m_Reserved.Clear();
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_GroundProbe) m_GroundProbe.Stop();
if (m_Service) { m_Service.Stop(); m_Service.OnResolved().Remove(Resolved); }
if (m_Mode) m_Mode.ERR_UseCasualtyFixture(null);
ReleaseFixtureReservations();
// Restore the original damage-handling flag only on this exact still-owned
// living player body. This never restores health or changes another actor.
if (m_PlayerDamageSnapshot && GetGame() && GetGame().GetPlayerController() && GetGame().GetPlayerController().GetControlledEntity() == m_Player && ERR_Actors.Alive(m_Player))
{
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
if (damage) damage.EnableDamageHandling(m_PlayerDamageOriginallyEnabled);
}
m_PlayerDamageSnapshot = false;
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
ERR_Actors.DeleteSpawned(m_Helper); m_Helper = null;
// This opt-in case ends by ordinary round reset. Never restore arbitrary
// health, force recovery, teleport the seated player, or delete its body.
}
protected ERR_LoadCasualtyAction FindAction()
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(m_Player.FindComponent(BaseActionsManagerComponent));
if (!manager) return null;
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
bool medical;
ERR_LoadCasualtyAction load;
foreach (BaseUserAction action : actions)
{
if (SCR_BandageUserAction.Cast(action)) medical = true;
ERR_LoadCasualtyAction candidate = ERR_LoadCasualtyAction.Cast(action);
if (candidate) load = candidate;
}
if (!Check(medical, "successor prisoner retains inherited native medicine actions")) return null;
return load;
}
protected void Diagnose()
{
if (!m_Player || !m_Helper || !m_Aircraft || !m_Service) return;
HelicopterControllerComponent aircraftController = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
Physics physics = m_Aircraft.GetPhysics();
bool groundContact;
bool simulationAvailable;
float speed = -1;
float angularSpeed = -1;
if (aircraftController && aircraftController.GetSimulation())
{
simulationAvailable = true;
groundContact = aircraftController.GetSimulation().HasAnyGroundContact();
}
if (physics) { speed = physics.GetVelocity().Length(); angularSpeed = physics.GetAngularVelocity().Length(); }
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
PrintFormat("ERR CASUALTY AIRCRAFT READINESS: alive=%1 controller=%2 simulation=%3 physics=%4 groundContact=%5 up=%6 speed=%7 angularSpeed=%8 position=%9", ERR_Actors.Alive(m_Aircraft), aircraftController != null, simulationAvailable, physics != null, groundContact, transform[1][1], speed, angularSpeed, m_Aircraft.GetOrigin());
CharacterControllerComponent playerController = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
CharacterControllerComponent helperController = CharacterControllerComponent.Cast(m_Helper.FindComponent(CharacterControllerComponent));
int playerLife = -1;
int helperLife = -1;
if (playerController) playerLife = playerController.GetLifeState();
if (helperController) helperLife = helperController.GetLifeState();
PrintFormat("ERR CASUALTY ACTOR READINESS: playerAlive=%1 playerConscious=%2 playerLife=%3 helperAlive=%4 helperConscious=%5 helperLife=%6 pairRange=%7 playerAircraftRange=%8 helperAircraftRange=%9", ERR_Actors.Alive(m_Player), ERR_Actors.CanFight(m_Player), playerLife, ERR_Actors.Alive(m_Helper), ERR_Actors.CanFight(m_Helper), helperLife, vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()), vector.Distance(m_Player.GetOrigin(), m_Aircraft.GetOrigin()), vector.Distance(m_Helper.GetOrigin(), m_Aircraft.GetOrigin()));
PrintFormat("ERR CASUALTY POSITION READINESS: playerRequested=%1 playerActual=%2 playerError=%3 helperRequested=%4 helperActual=%5 helperError=%6", m_RequestedPlayer, m_Player.GetOrigin(), vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()), m_RequestedHelper, m_Helper.GetOrigin(), vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()));
ERR_CasualtyGroundSupport.Parked(m_Aircraft, true);
PrintFormat("ERR CASUALTY SERVICE STATE: step=%1 player=%2 helper=%3 groundedTarget=%4 aircraftReady=%5 sight=%6 pending=%7 resolutions=%8 loaded=%9", m_Step, m_Player.GetOrigin(), m_Helper.GetOrigin(), ERR_CasualtyLoadingRules.GroundSettled(m_Player), ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft), ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player), m_Service.PendingCount(), m_Resolutions, m_Loaded);
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (m_Deadline > 0 && now > m_Deadline) { if (m_Player && m_Helper && m_Service) Diagnose(); Check(false, "bounded native fixture deadline"); return -1; }
if (m_Service && m_Observing) m_Service.Tick(now);
if (now < m_Wait) return 0;
if (m_Step == 0)
{
PlayerController controller = GetGame().GetPlayerController();
if (!Check(Replication.IsServer() && controller && m_Mode.Phase() == ERR_Phase.PRISON, "explicit local server fixture starts with real prisoner")) return -1;
m_Player = controller.GetControlledEntity(); m_Aircraft = m_Mode.ExtractionVehicle();
if (!Check(ERR_Actors.CanFight(m_Player) && m_Aircraft && !CompartmentAccessComponent.GetVehicleIn(m_Player), "real controlled casualty starts conscious on foot")) return -1;
m_Action = FindAction();
if (!Check(m_Action && m_Action.GetOwner() == m_Player && m_Action.GetActionDuration() == 5 && !m_Action.HasLocalEffectOnly(), "real successor prisoner owns five-second server action")) return -1;
DamageManagerComponent playerDamage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
if (!Check(playerDamage != null, "fixture player damage manager available")) return -1;
m_PlayerDamageOriginallyEnabled = playerDamage.IsDamageHandlingEnabled();
m_PlayerDamageSnapshot = true;
playerDamage.EnableDamageHandling(false);
m_RequestedPlayer = ERR_Actors.Ground(m_Aircraft.GetOrigin() + "6 0 6");
m_RequestedHelper = ERR_Actors.Ground(m_RequestedPlayer + "0 0 2");
m_Helper = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_RequestedHelper);
if (!Check(m_Helper != null, "separate physical helper spawned")) return -1;
m_Mode.Own(m_Helper);
DamageManagerComponent helperDamage = DamageManagerComponent.Cast(m_Helper.FindComponent(DamageManagerComponent));
if (!Check(helperDamage != null, "fixture helper damage manager available")) return -1;
helperDamage.EnableDamageHandling(false);
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = m_RequestedPlayer;
ChimeraCharacter.Cast(m_Player).Teleport(transform);
m_Service = new ERR_NativeCasualtyHelperAdapter(m_Mode); m_Service.Helper(m_Helper);
m_Service.OnResolved().Insert(Resolved);
if (!Check(m_Service.StrictParticipant(m_Player) && !m_Service.StrictParticipant(m_Helper), "actual target passes strict native membership; synthetic helper does not")) return -1;
if (!Check(!m_Mode.CasualtyLoading().Begin(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "production and unadapted sample reject out-of-phase helper request")) return -1;
// Deliberate fixture phase setup, not evidence of a completed radio objective.
if (!Check(m_Mode.Transition(ERR_Phase.RADIO_SEARCH) && m_Mode.Transition(ERR_Phase.RADIO_DEFENCE) && m_Mode.Transition(ERR_Phase.EXTRACTION), "fixture enters extraction through explicit legal phase transitions")) return -1;
m_Deadline = now + 45000;
m_PositionDeadline = now + 10000;
m_NextDiagnostic = now;
m_Wait = now + 500; m_Step = 1; return 0;
}
if (m_Step == 1)
{
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
bool actorsReady = ERR_Actors.CanFight(m_Player) && ERR_Actors.CanFight(m_Helper) && vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()) <= 1.5 && vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()) <= 1.5 && vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()) <= 3;
bool aircraftReady = ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
if ((!actorsReady || !aircraftReady) && now < m_PositionDeadline) return 0;
if (!Check(actorsReady, "actual living conscious actors reached requested nearby ground positions before deadline")) return -1;
if (!Check(aircraftReady, "actual aircraft satisfies unchanged production grounded-motion safety before deadline")) return -1;
if (!m_GroundChecked)
{
if (!m_GroundProbe) m_GroundProbe = new ERR_NativeCasualtyGroundProbe(m_Mode);
int groundResult = m_GroundProbe.Tick(now);
if (groundResult < 0) { Check(false, "native ground-support qualification failed"); return -1; }
if (groundResult == 0) return 0;
m_GroundChecked = true;
}
if (!Check(!m_Service.Begin(m_Helper, m_Player), "conscious target is rejected before native incapacitation")) return -1;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Player.FindComponent(SCR_CharacterDamageManagerComponent));
if (!Check(damage != null, "real casualty has native health manager")) return -1;
HitZone resilience = damage.GetResilienceHitZone();
if (!Check(resilience && damage.GetPermitUnconsciousness(), "native resilience and unconsciousness policy available")) return -1;
float beforeResilience = resilience.GetHealth();
// Protection must not swallow the fixture's own true-damage request. Keep
// this synchronous interval limited to that one native health operation.
damage.EnableDamageHandling(true);
damage.ForceUnconsciousness();
float afterResilience = resilience.GetHealth();
damage.EnableDamageHandling(false);
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2", beforeResilience, afterResilience);
if (!Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Player), "single native resilience damage applied before restoring fixture protection")) return -1;
m_Step = 2; m_Wait = now + 500; return 0;
}
if (m_Step == 2)
{
if (!ERR_CasualtyLoadingRules.GroundSettled(m_Player))
{
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
return 0;
}
if (!Check(ERR_CasualtyLoadingRules.Nearby(m_Mode, m_Helper, m_Player), "actual incapacitated target is settled with clear line of sight")) return -1;
if (!Check(!m_Mode.CasualtyLoading().CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "strict production authorization rejects nonparticipant helper even in valid physical state")) return -1;
m_Service.EnableHelper();
if (!Check(m_Mode.ERR_UseCasualtyFixture(m_Service), "explicit fixture installs helper-only membership adapter")) return -1;
m_Observing = true; m_Service.Tick(now); m_SettleStart = System.GetTickCount();
if (!Check(!m_Service.CanLoad(m_Helper, m_Player), "new observation cannot bypass five-second settling window")) return -1;
m_Step = 3; return 0;
}
if (m_Step == 3)
{
if (System.GetTickCount() - m_SettleStart < 5500) return 0;
Diagnose();
if (!Check(m_Service.CanLoad(m_Helper, m_Player), "settled real casualty is eligible with only helper membership adapted")) return -1;
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) || seat.GetOccupant() || seat.IsReserved()) continue;
seat.SetReserved(m_Helper); m_Reserved.Insert(seat);
}
if (!Check(!m_Reserved.IsEmpty() && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "native external reservations prevent loading when all child benches are reserved")) return -1;
ReleaseFixtureReservations();
vector middle = (ChimeraCharacter.Cast(m_Helper).AimingPosition() + ChimeraCharacter.Cast(m_Player).AimingPosition()) * 0.5;
m_Blocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", middle);
if (!Check(m_Blocker != null, "successor-owned physical line-of-sight obstacle spawned")) return -1;
m_Mode.Own(m_Blocker);
vector low, high; m_Blocker.GetWorldBounds(low, high);
vector wall[4]; m_Blocker.GetWorldTransform(wall); wall[3] = wall[3] + middle - (low + high) * 0.5;
m_Blocker.SetWorldTransform(wall);
m_Step = 4; m_Wait = now + 500; return 0;
}
if (m_Step == 4)
{
if (!Check(!ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player) && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "physical wall blocks completion-time rescue authorization")) return -1;
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
m_Step = 5; m_Wait = now + 500; return 0;
}
if (m_Step == 5)
{
if (!Check(m_Action.CanBePerformedScript(m_Helper) && m_Service.PendingCount() == 0, "real action becomes performable after obstruction removal")) return -1;
// Invoke the real completion boundary. This does not simulate holding input
// for five seconds or claim a second authenticated network player.
m_Action.PerformAction(m_Player, m_Helper);
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "real owner handshake commits one pending physical load")) return -1;
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
if (ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) && seat.IsReservedBy(m_Player)) m_LoadedSeat = seat;
if (!Check(m_LoadedSeat != null, "service reserves the actual selected child bench for the casualty")) return -1;
m_Action.PerformAction(m_Player, m_Helper);
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "duplicate completion cannot issue a second ticket")) return -1;
m_Step = 6; return 0;
}
if (m_Step == 6)
{
if (m_Service.PendingCount() > 0) return 0;
Diagnose();
if (!Check(m_Loaded && m_Resolutions == 1 && ERR_Actors.Alive(m_Player) && ERR_CasualtyLoadingRules.Down(m_Player) && CompartmentAccessComponent.GetVehicleIn(m_Player) == m_Aircraft && m_LoadedSeat.GetOccupant() == m_Player, "production owner handshake physically loads real living incapacitated player exactly once")) return -1;
if (!Check(!m_LoadedSeat.IsReserved() && m_Service.StrictParticipant(m_Player), "successful loading releases reservation and preserves actual player identity")) return -1;
m_Service.Stop(); m_Service.Stop(); m_Service.Tick(now);
if (!Check(m_Service.StoppedForProbe() && m_Service.PendingCount() == 0 && m_Resolutions == 1 && !m_Service.Begin(m_Helper, m_Player), "Stop is latched, idempotent and cannot repeat completion or restart loading")) return -1;
Stop(); return 1;
}
return 0;
}
}
modded class ERR_GameMode
{
protected ref ERR_NativeCasualtyHelperAdapter m_ERRNativeCasualtyAdapter;
bool ERR_UseCasualtyFixture(ERR_NativeCasualtyHelperAdapter service)
{
string enabled, outcome;
if (!Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1" || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "casualty-service") return false;
m_ERRNativeCasualtyAdapter = service; return true;
}
override ERR_CasualtyLoadingService CasualtyLoading()
{
if (m_ERRNativeCasualtyAdapter) return m_ERRNativeCasualtyAdapter;
return super.CasualtyLoading();
}
}
#endif
@@ -0,0 +1,52 @@
#ifdef WORKBENCH
// Exercises native service dispatch, not remote RPC ownership or rendered UI.
class ERR_NativeContractsProbe
{
protected static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: base contracts " + detail); return true; }
Print("ERR SMOKE FAIL: base contracts " + detail, LogLevel.ERROR);
return false;
}
static bool Run(ERR_GameMode mode, IEntity player)
{
if (!Check(mode.Factions() && mode.Factions().WorldReady() && mode.PreparationError().IsEmpty(), "catalog resolves hostile vanilla factions and successful preparation")) return false;
if (!Check(SCR_FactionManager.SGetFaction(player) == mode.PlayerFaction() && player.GetPrefabData().GetPrefabName() == mode.Factions().PrisonerPrefab, "physical prisoner matches injected faction and resource")) return false;
ERR_SiteRuntime site = mode.Sites().Find("everon-supply-1");
if (!Check(site && site.Group && site.Group.GetFaction() == mode.EnemyFaction() && site.Group.GetPrefabData().GetPrefabName() == mode.Factions().GuardGroupPrefab, "physical guard squad comes from injected catalog")) return false;
ERR_FactionCatalog isolated = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"));
if (!isolated) return false;
string enemyKey = isolated.EnemyFactionKey;
isolated.EnemyFactionKey = isolated.PlayerFactionKey;
if (!Check(!isolated.Valid() && !isolated.WorldReady(), "same-side factions rejected")) return false;
isolated.EnemyFactionKey = "ERR_MISSING_FACTION";
if (!Check(!isolated.WorldReady(), "unknown world faction rejected")) return false;
isolated.EnemyFactionKey = enemyKey;
ResourceName prisoner = isolated.PrisonerPrefab;
isolated.PrisonerPrefab = "";
if (!Check(!isolated.Valid(), "missing prisoner resource rejected")) return false;
isolated.PrisonerPrefab = prisoner;
if (!Check(isolated.WorldReady(), "isolated catalog restored without mutating live configuration")) return false;
array<string> replies = {};
int original = mode.Settings().Get("radioDefenceSeconds");
mode.Admin().Execute(mode, false, "mission radio set 60", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies.Count() == 1 && replies[0] == "Administrator access required.", "unauthorized settings request rejected")) return false;
mode.Admin().Execute(mode, true, "mission radio set 60junk", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "partially numeric input rejected without mutation")) return false;
mode.Admin().Execute(mode, true, "mission radio set 0", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "out-of-range duration rejected without mutation")) return false;
mode.Admin().Execute(mode, true, " MISSION RADIO SET 60 ", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == 60 && replies[0] == "radioDefenceSeconds=60 saved", "normalized base command saves validated duration")) return false;
mode.Admin().Execute(mode, true, "mission radio show", replies);
if (!Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=60", "base command reads saved duration")) return false;
mode.Admin().Execute(mode, true, "settings", replies);
if (!Check(replies.Count() == ERR_Settings.Keys().Count(), "base settings listing available")) return false;
mode.Admin().Execute(mode, true, "status", replies);
if (!Check(!replies.IsEmpty() && replies[0] == "Escape the prison", "base mission status available before companions")) return false;
mode.Admin().Execute(mode, true, "mission radio set " + original.ToString(), replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original, "test setting restored")) return false;
return true;
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More