Advance standalone admission, finite artillery and native validation
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
+7
@@ -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); }
|
||||
}
|
||||
+101
@@ -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++;
|
||||
}
|
||||
}
|
||||
+241
@@ -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;
|
||||
}
|
||||
}
|
||||
+199
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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 "";
|
||||
}
|
||||
}
|
||||
+461
@@ -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)));
|
||||
}
|
||||
}
|
||||
+231
@@ -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;
|
||||
}
|
||||
}
|
||||
+131
@@ -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;
|
||||
}
|
||||
}
|
||||
+177
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+108
@@ -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;
|
||||
}
|
||||
}
|
||||
+244
@@ -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();
|
||||
}
|
||||
}
|
||||
+241
@@ -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
|
||||
+87
@@ -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
|
||||
+63
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{C1E20667DCE324ED}Missions/ERR_Everon.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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";
|
||||
}
|
||||
}
|
||||
+10
@@ -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
|
||||
+114
@@ -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
|
||||
+434
@@ -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
|
||||
+16
@@ -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
|
||||
+51
@@ -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"
|
||||
}
|
||||
}
|
||||
+136
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+277
@@ -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" {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{5C67A1DDE0044806}Configs/ERR/Factions/US.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+276
@@ -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" {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{D7A5CFA62A36480D}Configs/ERR/Factions/USSR.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+21
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
+9
@@ -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 }
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+13
@@ -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}" {}
|
||||
}
|
||||
}
|
||||
+7
@@ -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
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
GenericEntity : "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et" {
|
||||
components {
|
||||
ERR_DocumentComponent "{E4598D0BE2C251A3}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -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"
|
||||
}
|
||||
}
|
||||
+7
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -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}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
SCR_ManualCamera {
|
||||
m_bInputEnabled 0
|
||||
m_bStartAtEntity 1
|
||||
m_aComponents {
|
||||
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -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 }
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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)));
|
||||
}
|
||||
}
|
||||
+81
@@ -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) {}
|
||||
}
|
||||
+111
@@ -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());
|
||||
}
|
||||
}
|
||||
+82
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+305
@@ -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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+62
@@ -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(); }
|
||||
}
|
||||
+8
@@ -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; }
|
||||
}
|
||||
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+72
@@ -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.";
|
||||
}
|
||||
}
|
||||
+112
@@ -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;
|
||||
}
|
||||
}
|
||||
+101
@@ -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; }
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
+419
@@ -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();
|
||||
}
|
||||
}
|
||||
+12
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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);
|
||||
}
|
||||
}
|
||||
+107
@@ -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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
+738
@@ -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
|
||||
+80
@@ -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
|
||||
+255
@@ -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
|
||||
+160
@@ -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
|
||||
+293
@@ -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
|
||||
+52
@@ -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
Reference in New Issue
Block a user