Build independent Everon successor foundation and native regressions
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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 Eligible(ERR_GameMode mode)
|
||||
{
|
||||
if (!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 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; }
|
||||
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());
|
||||
}
|
||||
|
||||
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; }
|
||||
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_FactionCatalog.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: %2", unit.Id, order.Description(now)), order.MessageId);
|
||||
}
|
||||
|
||||
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_Journal.Flush();
|
||||
}
|
||||
|
||||
override void Stop()
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units) unit.Release();
|
||||
m_Units.Clear();
|
||||
m_Contacts.Clear();
|
||||
m_Network.Clear();
|
||||
m_Journal.Write("STOP", "HQ stopped; tactical orders released");
|
||||
m_Journal.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected override ERR_CommandService CreateCommandService() { return new ERR_HQ(this); }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Server-owned audit stream. The file contains every event; chat reads a bounded
|
||||
// recent-history window with explicit gap notices for clients that fall behind.
|
||||
class ERR_HQJournal
|
||||
{
|
||||
protected ref array<string> m_Recent = {};
|
||||
protected ref array<string> m_Pending = {};
|
||||
protected int m_Sequence;
|
||||
protected int m_Message;
|
||||
protected static int s_Part;
|
||||
protected static int s_Session;
|
||||
protected string m_Session;
|
||||
|
||||
void ERR_HQJournal()
|
||||
{
|
||||
s_Session++;
|
||||
m_Session = string.Format("%1-%2", System.GetUnixTime(), s_Session);
|
||||
}
|
||||
protected float m_RetryAt;
|
||||
protected int m_Lost;
|
||||
|
||||
string Path()
|
||||
{
|
||||
if (s_Part == 0)
|
||||
return "$logs:rereintroduced-hq.log";
|
||||
return "$logs:rereintroduced-hq-" + s_Part.ToString() + ".log";
|
||||
}
|
||||
|
||||
int Message()
|
||||
{
|
||||
return ++m_Message;
|
||||
}
|
||||
|
||||
int Sequence()
|
||||
{
|
||||
return m_Sequence;
|
||||
}
|
||||
|
||||
void Write(string kind, string message, int messageId = 0)
|
||||
{
|
||||
if (!Replication.IsServer())
|
||||
return;
|
||||
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
|
||||
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
|
||||
if (m_Recent.Count() >= 300)
|
||||
m_Recent.Remove(0);
|
||||
m_Recent.Insert(line);
|
||||
if (m_Pending.Count() < 4096)
|
||||
m_Pending.Insert(line);
|
||||
else
|
||||
m_Lost++;
|
||||
}
|
||||
|
||||
void Read(int after, out array<string> lines, out int cursor, int limit = 8)
|
||||
{
|
||||
lines.Clear();
|
||||
cursor = Math.Clamp(after, 0, m_Sequence);
|
||||
int first = m_Sequence - m_Recent.Count();
|
||||
if (cursor < first)
|
||||
{
|
||||
lines.Insert(string.Format("HQ log: %1 older entries are outside chat history; see %2", first - cursor, Path()));
|
||||
cursor = first;
|
||||
}
|
||||
int characters;
|
||||
for (int i = cursor - first; i < m_Recent.Count() && lines.Count() < Math.Clamp(limit, 1, 40); i++)
|
||||
{
|
||||
// Bound each menu RPC without skipping unread records. Chat still
|
||||
// requests eight; the live menu can drain a busy journal faster.
|
||||
if (characters + m_Recent[i].Length() > 16000 && !lines.IsEmpty())
|
||||
break;
|
||||
lines.Insert(m_Recent[i]);
|
||||
characters += m_Recent[i].Length();
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (m_Pending.IsEmpty() || !Replication.IsServer())
|
||||
return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now < m_RetryAt)
|
||||
return;
|
||||
FileHandle file = FileIO.OpenFile(Path(), FileMode.APPEND);
|
||||
if (!file)
|
||||
{
|
||||
m_RetryAt = now + 30000;
|
||||
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
foreach (string line : m_Pending)
|
||||
file.WriteLine(line);
|
||||
if (m_Lost > 0)
|
||||
file.WriteLine(string.Format("HQ log: %1 entries lost while file was unavailable and buffer full", m_Lost));
|
||||
bool rotate = file.GetLength() >= 5242880;
|
||||
file.Close();
|
||||
m_Pending.Clear();
|
||||
m_Lost = 0;
|
||||
if (rotate)
|
||||
s_Part++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Radio propagation is simulated using actual living carriers and inventory.
|
||||
// A manpack is the HQ uplink; short-range hops are limited to 500 m and three
|
||||
// retransmissions. No virtual relay is fabricated when the network is broken.
|
||||
class ERR_HQRadioNode
|
||||
{
|
||||
IEntity Carrier;
|
||||
SCR_AIGroup Group;
|
||||
int UnitId;
|
||||
int Capability;
|
||||
protected float m_CheckedAt = -1;
|
||||
protected int m_LiveCapability;
|
||||
|
||||
bool Valid(bool manpack = false)
|
||||
{
|
||||
ERR_GameMode factions = ERR_GameMode.Get();
|
||||
if (!Carrier || !Group || Group.GetPlayerCount(true) > 0 || !factions || Group.GetFaction() != factions.EnemyFaction() || ERR_Actors.Group(Carrier) != Group)
|
||||
return false;
|
||||
int required = 1;
|
||||
if (manpack)
|
||||
required = 2;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_CheckedAt < 0 || now < m_CheckedAt || now - m_CheckedAt >= 1000)
|
||||
{
|
||||
m_CheckedAt = now;
|
||||
m_LiveCapability = ERR_HQRadio.Capability(Carrier);
|
||||
}
|
||||
return m_LiveCapability >= required;
|
||||
}
|
||||
|
||||
string Label()
|
||||
{
|
||||
return "unit " + UnitId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQRoute
|
||||
{
|
||||
ref array<ref ERR_HQRadioNode> Nodes = {};
|
||||
bool Downlink;
|
||||
int Cursor;
|
||||
bool Sending;
|
||||
float Due;
|
||||
vector SentFrom;
|
||||
float StartedAt = -1;
|
||||
int MessageId;
|
||||
string Payload;
|
||||
bool Failed;
|
||||
|
||||
// -1 failed, 0 travelling, 1 delivered. Each hop really gates delivery.
|
||||
int Advance(float now, ERR_HQJournal journal)
|
||||
{
|
||||
if (Failed)
|
||||
return -1;
|
||||
if (StartedAt < 0)
|
||||
{
|
||||
StartedAt = now;
|
||||
if (Downlink)
|
||||
Cursor = -1; // first leg is HQ to the manpack
|
||||
}
|
||||
if (now - StartedAt > 30000)
|
||||
return Fail(journal, "route timed out");
|
||||
int target = Cursor + 1;
|
||||
if (Sending)
|
||||
{
|
||||
if (now < Due)
|
||||
return 0;
|
||||
if (target < Nodes.Count())
|
||||
{
|
||||
if (!Nodes[target].Valid(Downlink && target == 0))
|
||||
return Fail(journal, "receiving carrier lost radio, died or changed ownership");
|
||||
if (Cursor >= 0 && vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin()) > 500)
|
||||
return Fail(journal, "receiver moved beyond the 500m link");
|
||||
journal.Write("RECEIVE", Nodes[target].Label() + " received from " + SenderLabel(), MessageId);
|
||||
}
|
||||
else
|
||||
journal.Write("RECEIVE", "HQ received from " + SenderLabel(), MessageId);
|
||||
Cursor++;
|
||||
Sending = false;
|
||||
if ((!Downlink && Cursor >= Nodes.Count()) || (Downlink && Cursor == Nodes.Count() - 1))
|
||||
return 1;
|
||||
target = Cursor + 1;
|
||||
}
|
||||
if (Nodes.IsEmpty())
|
||||
return Fail(journal, "no physical radio route");
|
||||
if (Cursor >= 0 && !Nodes[Cursor].Valid(!Downlink && Cursor == Nodes.Count() - 1))
|
||||
return Fail(journal, "transmitting carrier lost radio, died or changed ownership");
|
||||
string recipient = "HQ";
|
||||
string link = "long-range";
|
||||
if (target < Nodes.Count())
|
||||
{
|
||||
if (!Nodes[target].Valid(Downlink && target == 0))
|
||||
return Fail(journal, "next relay unavailable");
|
||||
recipient = Nodes[target].Label();
|
||||
if (Cursor >= 0)
|
||||
{
|
||||
SentFrom = Nodes[Cursor].Carrier.GetOrigin();
|
||||
float distance = vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin());
|
||||
if (distance > 500)
|
||||
return Fail(journal, "next relay beyond 500m");
|
||||
link = Math.Round(distance).ToString() + "m short-range";
|
||||
}
|
||||
}
|
||||
journal.Write("TRANSMIT", SenderLabel() + " -> " + recipient + " (" + link + ")", MessageId);
|
||||
Sending = true;
|
||||
Due = now + 2000;
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected string SenderLabel()
|
||||
{
|
||||
if (Cursor < 0)
|
||||
return "HQ";
|
||||
return Nodes[Cursor].Label();
|
||||
}
|
||||
|
||||
protected int Fail(ERR_HQJournal journal, string reason)
|
||||
{
|
||||
Failed = true;
|
||||
journal.Write("LOST", reason + "; " + Payload, MessageId);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQNetwork
|
||||
{
|
||||
protected ref array<ref ERR_HQRadioNode> m_Nodes = {};
|
||||
protected ref array<int> m_Parents = {};
|
||||
protected ref array<int> m_Depths = {};
|
||||
protected ref map<IEntity, int> m_Indices = new map<IEntity, int>();
|
||||
protected ref map<string, ref array<int>> m_Cells = new map<string, ref array<int>>();
|
||||
bool Overflow;
|
||||
|
||||
void Clear()
|
||||
{
|
||||
m_Nodes.Clear();
|
||||
m_Parents.Clear();
|
||||
m_Depths.Clear();
|
||||
m_Indices.Clear();
|
||||
m_Cells.Clear();
|
||||
Overflow = false;
|
||||
}
|
||||
|
||||
protected string Cell(int x, int z)
|
||||
{
|
||||
return string.Format("%1:%2", x, z);
|
||||
}
|
||||
|
||||
void Add(IEntity carrier, SCR_AIGroup group, int unitId, int capability)
|
||||
{
|
||||
if (m_Indices.Contains(carrier))
|
||||
return;
|
||||
if (m_Nodes.Count() >= 2048)
|
||||
{
|
||||
Overflow = true;
|
||||
return;
|
||||
}
|
||||
ERR_HQRadioNode node = new ERR_HQRadioNode();
|
||||
node.Carrier = carrier;
|
||||
node.Group = group;
|
||||
node.UnitId = unitId;
|
||||
node.Capability = capability;
|
||||
int index = m_Nodes.Count();
|
||||
m_Nodes.Insert(node);
|
||||
m_Indices.Insert(carrier, index);
|
||||
m_Parents.Insert(-2);
|
||||
m_Depths.Insert(99);
|
||||
vector position = carrier.GetOrigin();
|
||||
string key = Cell(Math.Floor(position[0] / 500), Math.Floor(position[2] / 500));
|
||||
array<int> cell = m_Cells.Get(key);
|
||||
if (!cell)
|
||||
{
|
||||
cell = {};
|
||||
m_Cells.Insert(key, cell);
|
||||
}
|
||||
cell.Insert(index);
|
||||
}
|
||||
|
||||
void Build()
|
||||
{
|
||||
array<int> queue = {};
|
||||
int checks;
|
||||
for (int root = 0; root < m_Nodes.Count(); root++)
|
||||
{
|
||||
if (m_Nodes[root].Capability == 2)
|
||||
{
|
||||
m_Parents[root] = -1;
|
||||
m_Depths[root] = 0;
|
||||
queue.Insert(root);
|
||||
}
|
||||
}
|
||||
for (int head = 0; head < queue.Count(); head++)
|
||||
{
|
||||
int from = queue[head];
|
||||
if (m_Depths[from] >= 3)
|
||||
continue;
|
||||
vector position = m_Nodes[from].Carrier.GetOrigin();
|
||||
int x = Math.Floor(position[0] / 500);
|
||||
int z = Math.Floor(position[2] / 500);
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
for (int dz = -1; dz <= 1; dz++)
|
||||
{
|
||||
array<int> neighbors = m_Cells.Get(Cell(x + dx, z + dz));
|
||||
if (!neighbors)
|
||||
continue;
|
||||
foreach (int candidate : neighbors)
|
||||
{
|
||||
if (++checks > 100000)
|
||||
{
|
||||
Overflow = true;
|
||||
return;
|
||||
}
|
||||
if (m_Parents[candidate] != -2 || vector.DistanceXZ(position, m_Nodes[candidate].Carrier.GetOrigin()) > 500)
|
||||
continue;
|
||||
m_Parents[candidate] = from;
|
||||
m_Depths[candidate] = m_Depths[from] + 1;
|
||||
queue.Insert(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ERR_HQRoute Route(IEntity caller, bool downlink = false)
|
||||
{
|
||||
int index;
|
||||
if (!m_Indices.Find(caller, index) || m_Parents[index] == -2)
|
||||
return null;
|
||||
ERR_HQRoute route = new ERR_HQRoute();
|
||||
route.Downlink = downlink;
|
||||
while (index >= 0)
|
||||
{
|
||||
if (downlink)
|
||||
route.Nodes.InsertAt(m_Nodes[index], 0);
|
||||
else
|
||||
route.Nodes.Insert(m_Nodes[index]);
|
||||
index = m_Parents[index];
|
||||
}
|
||||
return route;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// These samples exist only at intake. HQ never revisits their entity loadouts.
|
||||
class ERR_HQVisualContact
|
||||
{
|
||||
IEntity Entity;
|
||||
vector Position;
|
||||
float ObservedAt;
|
||||
bool Escapist;
|
||||
string Travel = "unknown";
|
||||
string Weapon = "unknown";
|
||||
}
|
||||
|
||||
class ERR_HQObservation
|
||||
{
|
||||
// Require current native line of sight before reading visible equipment.
|
||||
static bool IsClearSighting(float age, float traceFraction, float exposure)
|
||||
{
|
||||
return age >= 0 && age <= 0.5 && traceFraction >= 1 && exposure > 0;
|
||||
}
|
||||
|
||||
static bool CanIdentifyWeapon(float distance, float exposure)
|
||||
{
|
||||
return distance >= 0 && distance <= 150 && exposure >= 0.5;
|
||||
}
|
||||
|
||||
static string WeaponCategory(EWeaponType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case EWeaponType.WT_HANDGUN: return "pistol";
|
||||
case EWeaponType.WT_RIFLE: return "rifle";
|
||||
case EWeaponType.WT_SNIPERRIFLE: return "sniper rifle";
|
||||
case EWeaponType.WT_MACHINEGUN: return "machine gun";
|
||||
case EWeaponType.WT_ROCKETLAUNCHER: return "rocket launcher";
|
||||
case EWeaponType.WT_GRENADELAUNCHER: return "grenade launcher";
|
||||
case EWeaponType.WT_FRAGGRENADE: return "grenade";
|
||||
case EWeaponType.WT_SMOKEGRENADE: return "smoke grenade";
|
||||
}
|
||||
return "unidentified weapon";
|
||||
}
|
||||
|
||||
static ERR_HQVisualContact Capture(BaseTarget target, float now)
|
||||
{
|
||||
if (!target || target.GetTargetCategory() != ETargetCategory.ENEMY || !IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
||||
return null;
|
||||
DamageManagerComponent damage = target.GetDamageManagerComponent();
|
||||
if (damage && damage.IsDestroyed())
|
||||
return null;
|
||||
IEntity entity = target.GetTargetEntity();
|
||||
if (!entity)
|
||||
return null;
|
||||
ERR_HQVisualContact seen = new ERR_HQVisualContact();
|
||||
seen.Entity = entity;
|
||||
seen.Position = target.GetLastSeenPosition();
|
||||
seen.ObservedAt = now - target.GetTimeSinceSeen() * 1000;
|
||||
SCR_ChimeraCharacter character = SCR_ChimeraCharacter.Cast(entity);
|
||||
if (character)
|
||||
{
|
||||
if (GetGame().GetPlayerManager())
|
||||
seen.Escapist = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(entity) > 0;
|
||||
seen.Travel = "on foot";
|
||||
if (CompartmentAccessComponent.GetVehicleIn(entity))
|
||||
seen.Travel = "in vehicle";
|
||||
// Read only the drawn weapon; never enumerate weapons or inventory.
|
||||
if (CanIdentifyWeapon(target.GetDistance(), target.GetExposure()))
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(entity.FindComponent(CharacterControllerComponent));
|
||||
if (controller)
|
||||
{
|
||||
IEntity heldItem = controller.GetCurrentItemInHands();
|
||||
BaseWeaponComponent weapon;
|
||||
if (heldItem)
|
||||
weapon = BaseWeaponComponent.Cast(heldItem.FindComponent(BaseWeaponComponent));
|
||||
seen.Weapon = "none drawn";
|
||||
if (weapon)
|
||||
seen.Weapon = WeaponCategory(weapon.GetWeaponType());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Vehicle.Cast(entity))
|
||||
{
|
||||
seen.Travel = "vehicle";
|
||||
if (target.GetUnitType() == EAIUnitType.UnitType_Aircraft)
|
||||
seen.Travel = "aircraft";
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
static void Gather(SCR_AIGroupUtilityComponent utility, float now, out array<ref ERR_HQVisualContact> samples)
|
||||
{
|
||||
samples.Clear();
|
||||
if (!utility)
|
||||
return;
|
||||
array<BaseTarget> visible = {};
|
||||
utility.ERR_HQGetVisualTargets(visible);
|
||||
foreach (BaseTarget target : visible)
|
||||
{
|
||||
ERR_HQVisualContact seen = Capture(target, now);
|
||||
if (!seen)
|
||||
continue;
|
||||
ERR_HQVisualContact existing;
|
||||
foreach (ERR_HQVisualContact sample : samples)
|
||||
{
|
||||
if (sample.Entity == seen.Entity)
|
||||
{
|
||||
existing = sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!existing)
|
||||
{
|
||||
if (samples.Count() < 64)
|
||||
samples.Insert(seen);
|
||||
}
|
||||
else if (seen.ObservedAt > existing.ObservedAt || (seen.ObservedAt == existing.ObservedAt && existing.Weapon == "unknown" && seen.Weapon != "unknown"))
|
||||
{
|
||||
existing.Position = seen.Position;
|
||||
existing.ObservedAt = seen.ObservedAt;
|
||||
existing.Travel = seen.Travel;
|
||||
existing.Weapon = seen.Weapon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void Describe(ERR_HQReport report, array<ref ERR_HQVisualContact> samples, float now)
|
||||
{
|
||||
// New observations must not silently inherit old equipment/travel details.
|
||||
report.SeenEscapists = -1;
|
||||
report.TravelMode = "unknown";
|
||||
report.SeenWeapons = "unknown";
|
||||
report.DetailsObservedAt = -1;
|
||||
if (report.Distress)
|
||||
return;
|
||||
ERR_HQVisualContact anchor;
|
||||
foreach (ERR_HQVisualContact sample : samples)
|
||||
{
|
||||
if (sample.Entity == report.Target && now - sample.ObservedAt >= 0 && now - sample.ObservedAt <= 500)
|
||||
{
|
||||
anchor = sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anchor)
|
||||
return;
|
||||
report.DetailsObservedAt = anchor.ObservedAt;
|
||||
report.TravelMode = anchor.Travel;
|
||||
report.SeenWeapons = anchor.Weapon;
|
||||
array<IEntity> counted = {};
|
||||
array<string> weapons = {};
|
||||
string travel;
|
||||
foreach (ERR_HQVisualContact sighting : samples)
|
||||
{
|
||||
if (!sighting.Escapist || !sighting.Entity || counted.Contains(sighting.Entity) || now - sighting.ObservedAt < 0 || now - sighting.ObservedAt > 500 || vector.DistanceXZ(anchor.Position, sighting.Position) > 100)
|
||||
continue;
|
||||
counted.Insert(sighting.Entity);
|
||||
if (travel.IsEmpty())
|
||||
travel = sighting.Travel;
|
||||
else if (travel != sighting.Travel)
|
||||
travel = "mixed travel";
|
||||
if (sighting.Weapon != "unknown" && !weapons.Contains(sighting.Weapon) && weapons.Count() < 4)
|
||||
weapons.Insert(sighting.Weapon);
|
||||
}
|
||||
if (counted.IsEmpty())
|
||||
return; // A visible vehicle does not reveal its hidden occupants.
|
||||
report.SeenEscapists = counted.Count();
|
||||
// A vehicle anchor remains a vehicle report, even with people near it.
|
||||
if (anchor.Escapist)
|
||||
report.TravelMode = travel;
|
||||
report.SeenWeapons = "unknown";
|
||||
if (!weapons.IsEmpty())
|
||||
{
|
||||
report.SeenWeapons = weapons[0];
|
||||
for (int i = 1; i < weapons.Count(); i++)
|
||||
report.SeenWeapons += ", " + weapons[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIGroupUtilityComponent
|
||||
{
|
||||
void ERR_HQGetVisualTargets(out array<BaseTarget> targets)
|
||||
{
|
||||
targets.Clear();
|
||||
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
|
||||
{
|
||||
if (!info || !info.m_Perception)
|
||||
continue;
|
||||
AIAgent observer = AIAgent.Cast(info.GetOwner());
|
||||
if (!observer || !ERR_Actors.CanFight(observer.GetControlledEntity()))
|
||||
continue;
|
||||
array<BaseTarget> own = {};
|
||||
info.m_Perception.GetTargetsList(own, ETargetCategory.ENEMY);
|
||||
foreach (BaseTarget target : own)
|
||||
{
|
||||
if (target && ERR_HQObservation.IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
||||
targets.Insert(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// A radio's category is protected in the stock gadget component.
|
||||
modded class SCR_RadioComponent
|
||||
{
|
||||
bool ERR_IsHQManpack()
|
||||
{
|
||||
return m_iRadioCategory == ERadioCategory.MANPACK;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HQRadio
|
||||
{
|
||||
// 0: no radio, 1: basic radio, 2: manpack. Inventory is checked live.
|
||||
static int Capability(IEntity character)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(character))
|
||||
return 0;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory)
|
||||
return 0;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
int result;
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent))
|
||||
continue;
|
||||
result = 1;
|
||||
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (gadget && gadget.ERR_IsHQManpack())
|
||||
return 2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// HQ uses entity identity only for matching observations, never for its origin.
|
||||
// Visible details are captured at intake and copied as scalar snapshots.
|
||||
// Reports and orders contain immutable observed coordinates and timestamps.
|
||||
class ERR_HQReport
|
||||
{
|
||||
int MessageId;
|
||||
IEntity Target;
|
||||
SCR_AIGroup Source;
|
||||
vector Position;
|
||||
vector Velocity;
|
||||
float ObservedAt = -1;
|
||||
float LastSent = -60000;
|
||||
float PerceptionStamp = -1;
|
||||
bool HasVelocity;
|
||||
bool Engaged;
|
||||
bool Distress;
|
||||
int SeenEscapists = -1;
|
||||
string TravelMode = "unknown";
|
||||
string SeenWeapons = "unknown";
|
||||
float DetailsObservedAt = -1;
|
||||
|
||||
ERR_HQReport Snapshot()
|
||||
{
|
||||
ERR_HQReport copy = new ERR_HQReport();
|
||||
copy.MessageId = MessageId;
|
||||
copy.Target = Target;
|
||||
copy.Source = Source;
|
||||
copy.Position = Position;
|
||||
copy.Velocity = Velocity;
|
||||
copy.ObservedAt = ObservedAt;
|
||||
copy.Engaged = Engaged;
|
||||
copy.Distress = Distress;
|
||||
copy.HasVelocity = HasVelocity;
|
||||
copy.SeenEscapists = SeenEscapists;
|
||||
copy.TravelMode = TravelMode;
|
||||
copy.SeenWeapons = SeenWeapons;
|
||||
copy.DetailsObservedAt = DetailsObservedAt;
|
||||
return copy;
|
||||
}
|
||||
|
||||
string TravelDirection()
|
||||
{
|
||||
if (!HasVelocity)
|
||||
return "unknown";
|
||||
if (Velocity.Length() < 0.5)
|
||||
return "stationary";
|
||||
float x = Math.AbsFloat(Velocity[0]);
|
||||
float z = Math.AbsFloat(Velocity[2]);
|
||||
string northSouth = "N";
|
||||
string eastWest = "E";
|
||||
if (Velocity[2] < 0)
|
||||
northSouth = "S";
|
||||
if (Velocity[0] < 0)
|
||||
eastWest = "W";
|
||||
if (x < z * 0.414214)
|
||||
return northSouth;
|
||||
if (z < x * 0.414214)
|
||||
return eastWest;
|
||||
return northSouth + eastWest;
|
||||
}
|
||||
|
||||
string Description(float now, bool brief = false)
|
||||
{
|
||||
if (Distress)
|
||||
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
string count = "count unknown";
|
||||
if (SeenEscapists >= 0)
|
||||
count = "at least " + SeenEscapists.ToString() + " spotted nearby";
|
||||
string motion = "speed/direction unknown";
|
||||
if (HasVelocity)
|
||||
{
|
||||
float speed = Math.Round(Velocity.Length() * 10) / 10.0;
|
||||
float azimuth = Math.Atan2(Velocity[0], Velocity[2]) * Math.RAD2DEG;
|
||||
if (azimuth < 0)
|
||||
azimuth += 360;
|
||||
int heading = Math.Round(azimuth);
|
||||
motion = string.Format("%1 km/h, azimuth %2 degrees (%3)", Math.Round(speed * 3.6), heading % 360, TravelDirection());
|
||||
}
|
||||
string details = count + "; " + TravelMode + "; " + motion + "; seen weapons: " + SeenWeapons;
|
||||
if (brief)
|
||||
return details;
|
||||
string velocity = "unknown";
|
||||
if (HasVelocity)
|
||||
velocity = string.Format("[%1,%2] m/s", Math.Round(Velocity[0] * 10) / 10.0, Math.Round(Velocity[2] * 10) / 10.0);
|
||||
return string.Format("X=%1 Z=%2: %3; vector X/Z=%4 (%5s ago)", Math.Round(Position[0]), Math.Round(Position[2]), details, velocity, Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
}
|
||||
|
||||
static vector Predict(vector position, vector velocity, float seconds)
|
||||
{
|
||||
vector offset = velocity * Math.Clamp(seconds, 0, 20);
|
||||
offset[1] = 0;
|
||||
if (offset.Length() > 300)
|
||||
offset = offset.Normalized() * 300;
|
||||
return position + offset;
|
||||
}
|
||||
|
||||
int Priority(float now)
|
||||
{
|
||||
if (Distress)
|
||||
return 0;
|
||||
if (now - ObservedAt <= 15000)
|
||||
{
|
||||
if (Engaged)
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// An idle rotor or zero speed alone is normal during takeoff, landing and reload.
|
||||
// Confirm prolonged grounding, but recognize actual loss of flight capability.
|
||||
class ERR_HelicopterAvailability
|
||||
{
|
||||
protected float m_fGroundedSince = -1;
|
||||
|
||||
string Check(float now, float spawnedAt, bool destroyed, bool disabled, bool livingPilot, bool playerControlled, bool servicing, float height, float speed)
|
||||
{
|
||||
if (destroyed) return "destroyed";
|
||||
if (now - spawnedAt < 20000)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (disabled) return "engine, rotor or airframe disabled";
|
||||
if (playerControlled)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (!livingPilot) return "no living pilot";
|
||||
if (servicing || height > 5 || speed > 2)
|
||||
{
|
||||
m_fGroundedSince = -1;
|
||||
return "";
|
||||
}
|
||||
if (m_fGroundedSince < 0) m_fGroundedSince = now;
|
||||
if (now - m_fGroundedSince >= 60000) return "grounded for 60 seconds";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// All target coordinates come from delivered HQ observations, never a player's
|
||||
// current transform. Road projection stops at an unknown junction/road end.
|
||||
class ERR_MortarPrediction
|
||||
{
|
||||
// Read immediately after a failed synchronous prediction; no extra world scan.
|
||||
static string FailureReason;
|
||||
// Includes observation uplink, HQ downlink and native aiming. All stages
|
||||
// retain the original observation time; radio delivery never renews it.
|
||||
static const float MAX_REPORT_AGE_MS = 60000;
|
||||
|
||||
static bool Fresh(ERR_HQReport report, float now)
|
||||
{
|
||||
return report && report.Target && !report.Distress && now >= report.ObservedAt && now - report.ObservedAt <= MAX_REPORT_AGE_MS && report.TravelMode != "aircraft";
|
||||
}
|
||||
|
||||
static bool Reject(string reason)
|
||||
{
|
||||
FailureReason = reason;
|
||||
return false;
|
||||
}
|
||||
|
||||
static vector ClosestOnSegment(vector point, vector start, vector end)
|
||||
{
|
||||
vector segment = end - start;
|
||||
segment[1] = 0;
|
||||
vector offset = point - start;
|
||||
offset[1] = 0;
|
||||
float lengthSq = segment.LengthSq();
|
||||
if (lengthSq < 0.01)
|
||||
return start;
|
||||
return vector.Lerp(start, end, Math.Clamp(vector.Dot(offset, segment) / lengthSq, 0, 1));
|
||||
}
|
||||
|
||||
static bool FollowRoad(array<vector> points, vector observed, vector velocity, float seconds, out vector impact)
|
||||
{
|
||||
if (!points || points.Count() < 2)
|
||||
return Reject("road polyline has fewer than two points");
|
||||
int segment = -1;
|
||||
float closest = 20;
|
||||
vector projected;
|
||||
for (int i = 0; i < points.Count() - 1; i++)
|
||||
{
|
||||
vector candidate = ClosestOnSegment(observed, points[i], points[i + 1]);
|
||||
float distance = vector.DistanceXZ(candidate, observed);
|
||||
if (distance < closest)
|
||||
{
|
||||
closest = distance;
|
||||
segment = i;
|
||||
projected = candidate;
|
||||
}
|
||||
}
|
||||
if (segment < 0)
|
||||
return Reject("observed vehicle is not within 20m of a road segment");
|
||||
vector forward = points[segment + 1] - points[segment];
|
||||
forward[1] = 0;
|
||||
forward.Normalize();
|
||||
vector travel = velocity;
|
||||
travel[1] = 0;
|
||||
float speed = travel.Length();
|
||||
travel.Normalize();
|
||||
float alignment = vector.Dot(forward, travel);
|
||||
if (Math.AbsFloat(alignment) < 0.7)
|
||||
return Reject("vehicle heading crosses road; alignment below 0.7");
|
||||
int step = 1;
|
||||
int next = segment + 1;
|
||||
if (alignment < 0)
|
||||
{
|
||||
step = -1;
|
||||
next = segment;
|
||||
}
|
||||
float remaining = speed * seconds;
|
||||
vector position = projected;
|
||||
while (next >= 0 && next < points.Count())
|
||||
{
|
||||
float length = vector.DistanceXZ(position, points[next]);
|
||||
if (length >= remaining && length > 0.01)
|
||||
{
|
||||
impact = vector.Lerp(position, points[next], remaining / length);
|
||||
return true;
|
||||
}
|
||||
remaining -= length;
|
||||
position = points[next];
|
||||
next += step;
|
||||
}
|
||||
return Reject("prediction reaches unknown road end/junction");
|
||||
}
|
||||
|
||||
static bool Predict(ERR_HQReport report, float now, float flightSeconds, out vector impact)
|
||||
{
|
||||
FailureReason = "";
|
||||
if (!report) return Reject("target report missing");
|
||||
if (report.Distress) return Reject("distress report cannot authorize fire");
|
||||
if (now < report.ObservedAt) return Reject("observation timestamp is in the future");
|
||||
if (now - report.ObservedAt > MAX_REPORT_AGE_MS) return Reject("observation older than 60s");
|
||||
float seconds = (now - report.ObservedAt) / 1000 + flightSeconds;
|
||||
if (seconds < 0 || seconds > 90)
|
||||
return Reject("observation age plus flight outside 0..90s");
|
||||
if (report.TravelMode == "aircraft") return Reject("aircraft target unsupported");
|
||||
if (report.TravelMode == "mixed travel") return Reject("mixed travel cannot be predicted");
|
||||
impact = report.Position;
|
||||
if (!report.HasVelocity || report.Velocity.Length() < 0.5)
|
||||
return true;
|
||||
if (report.Velocity.Length() * seconds > 1800)
|
||||
return Reject("predicted travel exceeds 1800m");
|
||||
bool vehicle = report.TravelMode == "vehicle" || report.TravelMode == "in vehicle";
|
||||
if (vehicle)
|
||||
{
|
||||
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world || !world.GetRoadNetworkManager())
|
||||
return Reject("road network unavailable");
|
||||
BaseRoad road;
|
||||
float roadDistance;
|
||||
world.GetRoadNetworkManager().GetClosestRoad(report.Position, road, roadDistance, skipNavlinks: true);
|
||||
if (!road) return Reject("no road near reported vehicle position");
|
||||
if (roadDistance > 20) return Reject("reported vehicle farther than 20m from road");
|
||||
array<vector> points = {};
|
||||
road.GetPoints(points);
|
||||
return FollowRoad(points, report.Position, report.Velocity, seconds, impact);
|
||||
}
|
||||
impact += report.Velocity * seconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ThreatensFriendly(vector impact, vector position, vector velocity, float flightSeconds)
|
||||
{
|
||||
velocity[1] = 0;
|
||||
vector future = position + velocity * Math.Clamp(flightSeconds, 0, 90);
|
||||
// Protect the entire predicted path, not only where a soldier is now.
|
||||
return vector.DistanceXZ(impact, ClosestOnSegment(impact, position, future)) <= 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// One delivered HQ order drives a continuous native loading/firing cycle.
|
||||
class ERR_MortarSalvo
|
||||
{
|
||||
static const int ROUNDS = 8;
|
||||
static const float RADIUS_METRES = 40;
|
||||
static const float TIMEOUT_MS = 120000;
|
||||
|
||||
static vector Offset(int site, int order, int shot)
|
||||
{
|
||||
// Stable throughout one ballistic solve; successive shells cover the disk
|
||||
// instead of clustering on the same point or changing every iteration.
|
||||
float radius = RADIUS_METRES * Math.Sqrt((shot + 1.0) / ROUNDS);
|
||||
float angle = (site * 53 + order * 97 + shot * 137.5) * Math.DEG2RAD;
|
||||
return Vector(Math.Sin(angle) * radius, 0, Math.Cos(angle) * radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
GameProject {
|
||||
ID "ERR_Ai"
|
||||
GUID "5192138CB9D5B911"
|
||||
TITLE "Escapists Rereintroduced - Ai"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
"2528466DE220D91C"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
SCR_MissionHeader {
|
||||
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
||||
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
||||
m_sName "Rereintroduced: Everon (development)"
|
||||
m_sAuthor "Rereintroduced contributors"
|
||||
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress."
|
||||
m_sGameMode "Escape"
|
||||
m_iPlayerCount 12
|
||||
m_eSaveTypes 0
|
||||
m_bShowInScenarioMenu 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{C1E20667DCE324ED}Missions/ERR_Everon.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
class ERR_CommandArguments
|
||||
{
|
||||
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;
|
||||
}
|
||||
static 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";
|
||||
if (option == "reveal") return "intelRevealChancePercent";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected ChatCommandInvoker m_ERRCommand;
|
||||
protected float m_ERRLastRequest = -1000;
|
||||
protected int m_ERRRegisterAttempts;
|
||||
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
if (this == GetGame().GetPlayerController() && ERR_GameMode.Get()) ERR_Register();
|
||||
}
|
||||
|
||||
protected void ERR_Register()
|
||||
{
|
||||
if (m_ERRCommand || m_ERRRegisterAttempts++ > 40) return;
|
||||
SCR_ChatPanelManager manager = SCR_ChatPanelManager.GetInstance();
|
||||
if (!manager) { GetGame().GetCallqueue().CallLater(ERR_Register, 250, false); return; }
|
||||
m_ERRCommand = manager.GetCommandInvoker("esc");
|
||||
if (m_ERRCommand) m_ERRCommand.Insert(ERR_OnCommand);
|
||||
}
|
||||
protected override void ERR_OnObserverMonitorStarted()
|
||||
{
|
||||
super.ERR_OnObserverMonitorStarted();
|
||||
ERR_Register();
|
||||
}
|
||||
protected void ERR_OnCommand(SCR_ChatPanel panel, string arguments)
|
||||
{
|
||||
string localCommand = arguments.Trim(); localCommand.ToLower();
|
||||
if (localCommand == "spectate next" || localCommand == "spectate previous")
|
||||
{
|
||||
int direction = 1;
|
||||
if (localCommand == "spectate previous") direction = -1;
|
||||
ERR_CycleObserver(direction);
|
||||
return;
|
||||
}
|
||||
Rpc(ERR_Request, arguments);
|
||||
}
|
||||
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_Request(string arguments)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || !SCR_Global.IsAdmin(GetPlayerId())) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (arguments.Length() > 160 || now - m_ERRLastRequest < 250) return;
|
||||
m_ERRLastRequest = now;
|
||||
arguments = arguments.Trim(); arguments.ToLower();
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (arguments == "hq show" || arguments == "status")
|
||||
{
|
||||
string status = "HQ unavailable";
|
||||
if (hq) status = hq.Status();
|
||||
ERR_Reply(string.Format("Mission phase=%1; %2", mode.Phase(), status)); return;
|
||||
}
|
||||
if (arguments == "hq log")
|
||||
{
|
||||
if (!hq) { ERR_Reply("HQ unavailable"); return; }
|
||||
array<string> lines = {}; int cursor;
|
||||
hq.Journal().Read(Math.Max(0, hq.Journal().Sequence() - 8), lines, cursor);
|
||||
foreach (string line : lines) ERR_Reply(line);
|
||||
return;
|
||||
}
|
||||
array<string> parts = {};
|
||||
arguments.Split(" ", parts, true);
|
||||
if (parts.Count() == 3 || parts.Count() == 4)
|
||||
{
|
||||
string key = ERR_CommandArguments.Setting(parts[0], parts[1]);
|
||||
if (!key.IsEmpty())
|
||||
{
|
||||
if (parts.Count() == 3 && parts[2] == "show") { ERR_Reply(key + "=" + mode.Settings().Get(key).ToString()); return; }
|
||||
int value;
|
||||
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && mode.Settings().Set(key, value))
|
||||
ERR_Reply(key + "=" + value.ToString() + " saved");
|
||||
else ERR_Reply("Invalid value or settings could not be saved; previous setting retained.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
ERR_Reply("/esc status | hq show | hq log | hq interval|squads|radius show|set N | intel carrying|reveal show|set N");
|
||||
}
|
||||
protected void ERR_Reply(string text) { Rpc(ERR_Response, text); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_Response(string text) { SCR_ChatComponent.RadioProtocolMessage(text); }
|
||||
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_Register);
|
||||
if (m_ERRCommand) m_ERRCommand.Remove(ERR_OnCommand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Opt-in engine fixture: physical radios and production delivery routes only.
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_HQ
|
||||
{
|
||||
vector ERR_ProbeResponseOrigin()
|
||||
{
|
||||
if (m_Units.IsEmpty() || !m_Units[0].Group) return vector.Zero;
|
||||
return m_Units[0].Group.GetOrigin() + "0 0 20";
|
||||
}
|
||||
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 m_Units.Count() == 3 && 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()
|
||||
{
|
||||
if (m_Units.Count() != 3) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
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 true;
|
||||
}
|
||||
}
|
||||
|
||||
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: three physical six-person squads each have a manpack operator");
|
||||
m_Route = hq.ERR_ProbeRoute();
|
||||
if (!m_Route || m_Route.Nodes.IsEmpty()) return -1;
|
||||
m_Carrier = m_Route.Nodes[0].Carrier;
|
||||
m_Route.MessageId = hq.Journal().Message();
|
||||
m_Step = 1;
|
||||
}
|
||||
int result = m_Route.Advance(now, hq.Journal());
|
||||
if (result == 0) return 0;
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (result >= 0) return -1;
|
||||
Print("ERR SMOKE PASS: removing receiving manpack interrupts an in-flight HQ order");
|
||||
return 1;
|
||||
}
|
||||
if (result < 0) return -1;
|
||||
if (m_Step == 1) Print("ERR SMOKE PASS: physical manpack uplink delivers to HQ");
|
||||
if (m_Step == 2) Print("ERR SMOKE PASS: HQ downlink delivers to a physical receiver");
|
||||
m_Route = hq.ERR_ProbeRoute(m_Carrier, true);
|
||||
if (!m_Route || m_Route.Nodes.IsEmpty()) return -1;
|
||||
m_Route.MessageId = hq.Journal().Message();
|
||||
m_Step++;
|
||||
if (m_Step == 3)
|
||||
{
|
||||
// Start HQ transmission, then remove the actual receiving equipment.
|
||||
if (m_Route.Advance(now, hq.Journal()) != 0) return -1;
|
||||
IEntity receiver = m_Route.Nodes[0].Carrier;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(receiver.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return -1;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
if (item && item.FindComponent(BaseRadioComponent)) SCR_EntityHelper.DeleteEntityAndChildren(item);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,302 @@
|
||||
// Explicit, local Workbench diagnostic only. Never compiled into game/server builds.
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
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 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_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
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; }
|
||||
Print("ERR SMOKE FAIL: " + name, LogLevel.ERROR);
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
return false;
|
||||
}
|
||||
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_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 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 == 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()) 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(m_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;
|
||||
foreach (IEntity tracked : m_OwnedEntities)
|
||||
{
|
||||
if (tracked && tracked.GetPrefabData() && tracked.GetPrefabData().GetPrefabName() == ERR_FactionCatalog.IntelFolder()) 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;
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!ERR_Check(inventory && inventory.TryInsertItem(m_ERRCacheFolders[m_ERRSmokeStep - 1]), "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(m_DefenceSince >= 0, "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(m_DefenceSince < 0 && 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(m_ExtractionVehicle);
|
||||
return;
|
||||
}
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRSmokePlayer.FindComponent(CompartmentAccessComponent));
|
||||
BaseCompartmentSlot slot = access.FindFreeCompartment(m_ExtractionVehicle, ECompartmentType.PILOT);
|
||||
if (!ERR_Check(slot && access.GetInVehicle(m_ExtractionVehicle, slot, true, -1, ECloseDoorAfterActions.INVALID, false), "boarding extraction vehicle")) return;
|
||||
m_ERRSmokeStep = 7;
|
||||
m_ERRSmokeWait = now + 3000;
|
||||
}
|
||||
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) == m_ExtractionVehicle, "physical extraction occupancy")) return;
|
||||
// Controlled transport tests the boundary detector, not piloting skill.
|
||||
m_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Diagnose inherited teardown failures without changing their behavior.
|
||||
modded class SCR_BaseItemSupportStationComponent
|
||||
{
|
||||
override void OnDelete(IEntity owner)
|
||||
{
|
||||
if (owner && !m_EntityCatalogManager)
|
||||
PrintFormat("ERR TEARDOWN: uninitialized item support station on %1", owner);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
[WorkbenchPluginAttribute(name: "Validate Rereintroduced scenario", wbModules: {"WorldEditor"})]
|
||||
class ERR_ValidateWorldPlugin : WorldEditorPlugin
|
||||
{
|
||||
protected bool m_Obstructed;
|
||||
protected bool CheckClearance(IEntity entity)
|
||||
{
|
||||
if (entity && entity.GetVObject()) m_Obstructed = true;
|
||||
return !m_Obstructed;
|
||||
}
|
||||
override void Run() { Validate(); }
|
||||
override void RunCommandline() { Validate(); }
|
||||
protected void Validate()
|
||||
{
|
||||
WorldEditor editor = Workbench.GetModule(WorldEditor);
|
||||
if (!editor) { Print("ERR VALIDATION: World Editor missing", LogLevel.ERROR); return; }
|
||||
WorldEditorAPI api = editor.GetApi();
|
||||
if (!api) { Print("ERR VALIDATION: world API missing", LogLevel.ERROR); return; }
|
||||
PrintFormat("ERR VALIDATION: prison terrain=%1, radio terrain=%2, extraction terrain=%3", api.GetTerrainSurfaceY(6090,7160), api.GetTerrainSurfaceY(6500,7200), api.GetTerrainSurfaceY(7000,7500));
|
||||
for (int dx = -6; dx <= 6; dx += 3)
|
||||
for (int dz = -6; dz <= 6; dz += 3)
|
||||
PrintFormat("ERR TERRAIN: x=%1 z=%2 y=%3", 6090 + dx, 7160 + dz, api.GetTerrainSurfaceY(6090 + dx, 7160 + dz));
|
||||
float bestSlope = 1000;
|
||||
vector bestPosition;
|
||||
for (int x = 5800; x <= 6200; x += 10)
|
||||
{
|
||||
for (int z = 6800; z <= 7200; z += 10)
|
||||
{
|
||||
float low = 10000;
|
||||
float high = -10000;
|
||||
for (int sx = -9; sx <= 9; sx += 3)
|
||||
for (int sz = -15; sz <= 9; sz += 3)
|
||||
{
|
||||
float height = api.GetTerrainSurfaceY(x + sx, z + sz);
|
||||
low = Math.Min(low, height);
|
||||
high = Math.Max(high, height);
|
||||
}
|
||||
if (low > 5 && high - low < bestSlope)
|
||||
{
|
||||
m_Obstructed = false;
|
||||
api.GetWorld().QueryEntitiesByAABB(Vector(x - 10, low + 0.1, z - 16), Vector(x + 10, high + 5, z + 10), CheckClearance);
|
||||
if (m_Obstructed) continue;
|
||||
bestSlope = high - low;
|
||||
bestPosition = Vector(x, api.GetTerrainSurfaceY(x, z), z);
|
||||
}
|
||||
}
|
||||
}
|
||||
PrintFormat("ERR SITE CANDIDATE: position=%1 elevationRange=%2", bestPosition, bestSlope);
|
||||
string play;
|
||||
if (System.GetCLIParam("errPlay", play) && play == "1") editor.SwitchToGameMode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
GameProject {
|
||||
ID "ERR_Chat"
|
||||
GUID "FA804F411BDDF2C6"
|
||||
TITLE "Escapists Rereintroduced - Chat"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
"2528466DE220D91C"
|
||||
"5192138CB9D5B911"
|
||||
"D31039C7C53E348A"
|
||||
"9AAEE9041D79809E"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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"
|
||||
}
|
||||
}
|
||||
SCR_BaseGameOverScreenInfo "{E66F29A7A8AD7438}" {
|
||||
m_GameOverScreenId LOOSE
|
||||
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
|
||||
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
|
||||
m_sTitle "Escape failed"
|
||||
m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost."
|
||||
m_sAudioOneShot "SOUND_ONDEFEAT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Id "everon-extraction-1" m_Kind 2 }
|
||||
RplComponent "{0BF5BAAA5FBFEECE}" { Streamable Disabled }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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_Prison "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
|
||||
m_Radio "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
|
||||
m_Extraction "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
}
|
||||
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
|
||||
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
|
||||
}
|
||||
}
|
||||
m_bUseSpawnPreload 0
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
SCR_AIGroup : "{E552DABF3636C2AD}Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et" {
|
||||
m_aUnitPrefabSlots {
|
||||
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"
|
||||
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
|
||||
"{23ADBBC31B6A3DC6}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et"
|
||||
"{1C78331E156A3D65}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et"
|
||||
"{333DA6244C7DA34C}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et"
|
||||
"{BF643BE4ADBDFDD3}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
Hierarchy "{30ADADE77D1EC3B8}" {}
|
||||
RplComponent "{7FA5168C6FBD0C22}" {}
|
||||
}
|
||||
{
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "2879356FFFBE36E0"
|
||||
coords -6 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "C99EF9AFCEBA863D"
|
||||
coords -3 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "91048277F04ACA61"
|
||||
coords 0 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "8F2200A922FB0A6E"
|
||||
coords 3 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "3C442429BBCF5D2F"
|
||||
coords -6 0 -6
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "9E855CC802C592CA"
|
||||
coords 6 0 -6
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "778DAC42C9208F67"
|
||||
coords -6 0 -3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "7E350EB9053673C4"
|
||||
coords 6 0 -3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "AD17884D5EA37D37"
|
||||
coords -6 0 0
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "1A157E23E1A995D8"
|
||||
coords 6 0 0
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "3A78A6F3ACB7ACB1"
|
||||
coords -6 0 3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "275B472B565FFF6C"
|
||||
coords 6 0 3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "1B20E11B3FC7B59A"
|
||||
coords -6 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "2735B52246DD6213"
|
||||
coords -3 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "28101A0E33470B38"
|
||||
coords 3 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et" {
|
||||
ID "54D8F96ECC743335"
|
||||
scale 1.5
|
||||
coords 0 -0.1 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
GenericEntity : "{34B08027DBDE322A}Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et" {
|
||||
components {
|
||||
DoorComponent "{221C667A9F7D4B69}" {
|
||||
DoorAction ERR_PrisonDoorAction "{50888D3646A61640}" {
|
||||
ParentContextList { "SCR_DoorUserAction" }
|
||||
}
|
||||
}
|
||||
ActionsManagerComponent "{F32B667C3C823800}" {
|
||||
ActionContexts { UserActionContext "{50888D37F529F720}" { ContextName "SCR_DoorUserAction" Radius 2 } }
|
||||
additionalActions {
|
||||
ERR_UnlockPrisonAction "{CA7918FEA177BFB8}" {
|
||||
ParentContextList { "SCR_DoorUserAction" }
|
||||
UIInfo SCR_ActionUIInfo "{F734C81E11AA6BEF}" { Name "Pick prison lock" }
|
||||
Duration 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
Hierarchy "{4F20A90EDBD625A4}" {}
|
||||
MeshObject "{4F20A90EDBD625A1}" { Object "{0B96D139FEBCA006}Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob" }
|
||||
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
|
||||
RplComponent "{4F20A90EDBD625A3}" {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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_Id "everon-radio-1" m_Kind 1 }
|
||||
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
|
||||
ActionsManagerComponent "{B081EC0768636EAB}" {
|
||||
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
|
||||
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
|
||||
ParentContextList { "ERR_Radio" }
|
||||
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
|
||||
Duration 8
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
SCR_ManualCamera {
|
||||
m_bInputEnabled 0
|
||||
m_bStartAtEntity 1
|
||||
m_aComponents {
|
||||
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
class ERR_UnlockPrisonAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.UnlockPrison(pUserEntity)) return;
|
||||
BaseDoorComponent door = BaseDoorComponent.Cast(pOwnerEntity.FindComponent(BaseDoorComponent));
|
||||
if (door) door.UseDoorAction(pUserEntity);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PrisonDoorAction : SCR_DoorUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() > ERR_Phase.PRISON && super.CanBePerformedScript(user);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_TuneRadioAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_LocationComponent location = ERR_LocationComponent.Cast(GetOwner().FindComponent(ERR_LocationComponent));
|
||||
return mode && mode.Phase() == ERR_Phase.RADIO_SEARCH && location && location.Knowledge() == 2 && ERR_Actors.CanFight(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.TuneRadio(pUserEntity, ERR_LocationComponent.Cast(pOwnerEntity.FindComponent(ERR_LocationComponent)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
class ERR_Actors
|
||||
{
|
||||
static bool Alive(IEntity actor)
|
||||
{
|
||||
if (!actor) return false;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
return damage && !damage.IsDestroyed();
|
||||
}
|
||||
|
||||
static bool CanFight(IEntity actor)
|
||||
{
|
||||
if (!Alive(actor)) return false;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
return controller && !controller.IsUnconscious();
|
||||
}
|
||||
|
||||
static SCR_AIGroup Group(IEntity actor)
|
||||
{
|
||||
if (!actor) return null;
|
||||
AIControlComponent control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
|
||||
if (!control || !control.GetAIAgent()) return null;
|
||||
return SCR_AIGroup.Cast(control.GetAIAgent().GetParentGroup());
|
||||
}
|
||||
|
||||
static bool Player(IEntity actor)
|
||||
{
|
||||
return actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor) > 0;
|
||||
}
|
||||
|
||||
static IEntity Spawn(ResourceName prefab, vector position, vector angles = "0 0 0")
|
||||
{
|
||||
if (!Replication.IsServer() || prefab.IsEmpty()) return null;
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return null;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
Math3D.AnglesToMatrix(angles, params.Transform);
|
||||
params.Transform[3] = position;
|
||||
return GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
|
||||
}
|
||||
|
||||
static void DeleteSpawned(IEntity entity)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || Player(entity)) return;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(entity);
|
||||
if (group)
|
||||
{
|
||||
// Never remove a group that has acquired a player since mission creation.
|
||||
if (group.GetPlayerCount(true) > 0) return;
|
||||
group.DespawnMembers();
|
||||
}
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(entity);
|
||||
}
|
||||
|
||||
static vector Ground(vector position)
|
||||
{
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.1;
|
||||
return position;
|
||||
}
|
||||
|
||||
static void Players(out array<IEntity> players, bool conscious = false)
|
||||
{
|
||||
players.Clear();
|
||||
array<int> ids = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (Alive(actor) && (!conscious || CanFight(actor))) players.Insert(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One server-owned implementation is installed by the AI addon.
|
||||
class ERR_CommandService
|
||||
{
|
||||
void Tick(float now) {}
|
||||
void Stop() {}
|
||||
void RegisterGroup(SCR_AIGroup group) {}
|
||||
void UnregisterGroup(SCR_AIGroup group) {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[BaseContainerProps()]
|
||||
class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
override string GetTitle(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
string title = "The helicopter escaped";
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId)) 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)
|
||||
{
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId))
|
||||
return "You crossed the extraction boundary aboard the helicopter.";
|
||||
return "You were not aboard when the helicopter crossed the extraction boundary.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Vanilla-only catalog. Optional factions will supply separate catalogs.
|
||||
class ERR_FactionCatalog
|
||||
{
|
||||
static ResourceName Prisoner() { return "{2F912ED6E399FF47}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et"; }
|
||||
static ResourceName Patrol() { return "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"; }
|
||||
static ResourceName Helicopter() { return "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"; }
|
||||
static ResourceName SearchWaypoint() { return "{B3E7B8DC2BAB8ACC}Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et"; }
|
||||
static ResourceName DefendWaypoint() { return "{93291E72AC23930F}Prefabs/AI/Waypoints/AIWaypoint_Defend.et"; }
|
||||
static ResourceName IntelFolder() { return "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et"; }
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
class ERR_GameModeClass : SCR_BaseGameModeClass {}
|
||||
class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
[Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; validated by scenario author")]
|
||||
protected vector m_PrisonPosition;
|
||||
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
|
||||
protected vector m_EscapeCenter;
|
||||
[Attribute("9000", UIWidgets.EditBox, "Helicopter must cross this radius to escape")]
|
||||
protected float m_EscapeRadius;
|
||||
[RplProp()]
|
||||
protected ERR_Phase m_Phase;
|
||||
[RplProp()]
|
||||
protected float m_DefenceSince = -1;
|
||||
protected ref ERR_Settings m_Settings;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
protected ref ERR_CommandService m_Command;
|
||||
protected ref array<ERR_LocationComponent> m_Locations = {};
|
||||
protected ref array<IEntity> m_OwnedEntities = {};
|
||||
protected ref set<int> m_Participants = new set<int>();
|
||||
protected ref set<int> m_Escaped = new set<int>();
|
||||
protected ERR_LocationComponent m_Radio;
|
||||
protected Vehicle m_ExtractionVehicle;
|
||||
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_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_Settings Settings() { return m_Settings; }
|
||||
ERR_CommandService Command() { return m_Command; }
|
||||
ScriptInvoker OnPhaseChanged() { return m_OnPhaseChanged; }
|
||||
ScriptInvoker OnTick() { return m_OnTick; }
|
||||
ScriptInvoker OnStop() { return m_OnStop; }
|
||||
ScriptInvoker OnGroup() { return m_OnGroup; }
|
||||
Faction EnemyFaction() { return GetGame().GetFactionManager().GetFactionByKey("USSR"); }
|
||||
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 RegisterLocation(ERR_LocationComponent location)
|
||||
{
|
||||
if (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);
|
||||
}
|
||||
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); }
|
||||
void SetExtractionVehicle(Vehicle vehicle) { if (Replication.IsServer()) m_ExtractionVehicle = vehicle; }
|
||||
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() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
|
||||
ERR_Phase previous = m_Phase;
|
||||
m_Phase = phase;
|
||||
Replication.BumpMe();
|
||||
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
|
||||
m_OnPhaseChanged.Invoke(previous, phase);
|
||||
if (ERR_MissionRules.Terminal(phase))
|
||||
{
|
||||
StopServices();
|
||||
array<int> winners = {};
|
||||
foreach (int playerId : m_Escaped) winners.Insert(playerId);
|
||||
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)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_SEARCH || !location || !m_Locations.Contains(location)) return false;
|
||||
if (location.Kind() != ERR_LocationKind.RADIO || location.Knowledge() < 2) return false;
|
||||
if (!ERR_Actors.Player(player) || !ERR_Actors.CanFight(player) || vector.DistanceXZ(player.GetOrigin(), location.Position()) > 5) return false;
|
||||
m_Radio = location;
|
||||
m_DefenceSince = -1;
|
||||
return Transition(ERR_Phase.RADIO_DEFENCE);
|
||||
}
|
||||
|
||||
void PlayerSpawned(int id, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
||||
m_Roster.Spawned(id, actor);
|
||||
m_Participants.Insert(id);
|
||||
m_StartedPlayers = true;
|
||||
m_NoSurvivorsSince = -1;
|
||||
}
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode()) return;
|
||||
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Settings = new ERR_Settings();
|
||||
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) return;
|
||||
// Scenario content is created through a separate authoring component.
|
||||
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
|
||||
if (!scenario || !scenario.Prepare())
|
||||
{
|
||||
Print("Rereintroduced: scenario preparation failed; spawning remains disabled", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
Transition(ERR_Phase.PRISON);
|
||||
}
|
||||
|
||||
protected void ServerTick()
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
|
||||
{
|
||||
m_NoSurvivorsSince = -1;
|
||||
m_DefenceSince = -1;
|
||||
return;
|
||||
}
|
||||
if (m_StartedPlayers && !m_Roster.HasSurvivor())
|
||||
{
|
||||
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
|
||||
if (now - m_NoSurvivorsSince >= 30000) { Transition(ERR_Phase.DEFEAT); return; }
|
||||
}
|
||||
else m_NoSurvivorsSince = -1;
|
||||
if (m_Phase == ERR_Phase.RADIO_DEFENCE)
|
||||
{
|
||||
bool occupied;
|
||||
foreach (IEntity actor : players)
|
||||
if (m_Radio && ERR_Actors.CanFight(actor) && vector.DistanceXZ(actor.GetOrigin(), m_Radio.Position()) < 60) occupied = true;
|
||||
if (!occupied) m_DefenceSince = -1;
|
||||
else if (m_DefenceSince < 0) m_DefenceSince = now;
|
||||
if (ERR_MissionRules.DefenceComplete(now, m_DefenceSince, m_Settings.Get("radioDefenceSeconds") * 1000, occupied))
|
||||
{
|
||||
foreach (ERR_LocationComponent location : m_Locations)
|
||||
if (location.Kind() == ERR_LocationKind.EXTRACTION) location.Discover(0, 100);
|
||||
Transition(ERR_Phase.EXTRACTION);
|
||||
}
|
||||
}
|
||||
if (m_Phase == ERR_Phase.EXTRACTION)
|
||||
{
|
||||
if (!ERR_Actors.Alive(m_ExtractionVehicle)) { Transition(ERR_Phase.DEFEAT); return; }
|
||||
foreach (IEntity actor : players)
|
||||
{
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != m_ExtractionVehicle) continue;
|
||||
if (vector.DistanceXZ(actor.GetOrigin(), m_EscapeCenter) <= m_EscapeRadius) continue;
|
||||
m_Escaped.Insert(GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor));
|
||||
}
|
||||
if (!m_Escaped.IsEmpty()) { Transition(ERR_Phase.VICTORY); return; }
|
||||
}
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_UpdateObserverServer();
|
||||
}
|
||||
if (m_Command) m_Command.Tick(now);
|
||||
m_OnTick.Invoke(now);
|
||||
}
|
||||
|
||||
protected void StopServices()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
GetGame().GetCallqueue().Remove(ServerTick);
|
||||
if (m_Command) m_Command.Stop();
|
||||
m_OnStop.Invoke();
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
}
|
||||
foreach (IEntity entity : m_OwnedEntities)
|
||||
if (SCR_AIGroup.Cast(entity)) ERR_Actors.DeleteSpawned(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ~ERR_GameMode()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(StartScenario);
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
StopServices();
|
||||
}
|
||||
m_Locations.Clear();
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
{
|
||||
[Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
|
||||
protected string m_Id;
|
||||
[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; }
|
||||
ERR_LocationKind Kind() { return m_Kind; }
|
||||
int Knowledge() { return m_Knowledge; }
|
||||
vector Position() { return GetOwner().GetOrigin(); }
|
||||
|
||||
bool Discover(int roll, int chance)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
|
||||
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
||||
Replication.BumpMe();
|
||||
return true;
|
||||
}
|
||||
|
||||
override void OnPostInit(IEntity owner)
|
||||
{
|
||||
super.OnPostInit(owner);
|
||||
if (!SCR_Global.IsEditMode()) SetEventMask(owner, EntityEvent.INIT);
|
||||
}
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.RegisterLocation(this);
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.UnregisterLocation(this);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
enum ERR_Phase
|
||||
{
|
||||
PREPARING,
|
||||
PRISON,
|
||||
RADIO_SEARCH,
|
||||
RADIO_DEFENCE,
|
||||
EXTRACTION,
|
||||
VICTORY,
|
||||
DEFEAT
|
||||
}
|
||||
|
||||
// Pure mission policy. Entity queries and authority checks belong to the owner.
|
||||
class ERR_MissionRules
|
||||
{
|
||||
static bool Terminal(int phase)
|
||||
{
|
||||
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
|
||||
}
|
||||
|
||||
static bool CanTransition(int from, int to)
|
||||
{
|
||||
if (Terminal(from) || from == to) return false;
|
||||
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
|
||||
if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
|
||||
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
|
||||
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
|
||||
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
|
||||
return from == ERR_Phase.EXTRACTION && to == ERR_Phase.VICTORY;
|
||||
}
|
||||
|
||||
static bool DefenceComplete(float now, float since, float duration, bool occupied)
|
||||
{
|
||||
return occupied && since >= 0 && duration > 0 && now >= since && now - since >= duration;
|
||||
}
|
||||
|
||||
static bool ContactFresh(float observed, float now)
|
||||
{
|
||||
return observed >= 0 && now >= observed && now - observed <= 120000;
|
||||
}
|
||||
|
||||
static int Discovery(int knowledge, int roll, int identifyPercent)
|
||||
{
|
||||
if (knowledge >= 1 || roll < identifyPercent) return 2;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
enum ERR_ObserverReason
|
||||
{
|
||||
NONE,
|
||||
SPENT_LIFE,
|
||||
LATE_EXTRACTION,
|
||||
SPAWN_UNAVAILABLE
|
||||
}
|
||||
|
||||
// A local follow camera. It never possesses its target or offers free flight.
|
||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
{
|
||||
override bool EOnCameraInit() { return true; }
|
||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
IEntity target = controller.ERR_ObservedEntity();
|
||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||
vector position = aim + "0 4 -6";
|
||||
IEntity exclude = target;
|
||||
if (target)
|
||||
{
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
vector transform[4];
|
||||
if (vehicle)
|
||||
{
|
||||
vehicle.GetWorldTransform(transform);
|
||||
vector lower, upper;
|
||||
vehicle.GetWorldBounds(lower, upper);
|
||||
aim = (lower + upper) * 0.5;
|
||||
position = aim + "0 5 0" - transform[2] * 10;
|
||||
exclude = vehicle;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.GetWorldTransform(transform);
|
||||
aim = transform[3] + "0 1.5 0";
|
||||
position = aim + "0 1.5 0" - transform[2] * 4;
|
||||
}
|
||||
}
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = aim;
|
||||
trace.End = position;
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = exclude;
|
||||
float fraction = GetCameraEntity().GetWorld().TraceMove(trace, null);
|
||||
if (fraction < 1) position = vector.Lerp(aim, position, Math.Max(0.05, fraction - 0.05));
|
||||
Math3D.DirectionAndUpMatrix(vector.Direction(position, aim), "0 1 0", param.transform);
|
||||
param.transform[3] = position;
|
||||
param.isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
[RplProp()]
|
||||
protected ERR_ObserverReason m_ERRObserverReason;
|
||||
[RplProp()]
|
||||
protected RplId m_ERRObservedId = RplId.Invalid();
|
||||
[RplProp()]
|
||||
protected vector m_ERRObserverAnchor;
|
||||
protected IEntity m_ERRServerObserved;
|
||||
protected IEntity m_ERRServerObservedVehicle;
|
||||
protected SCR_ManualCamera m_ERRObserverCamera;
|
||||
protected bool m_ERRObserverMonitor;
|
||||
protected bool m_ERRObserverReady;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
|
||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||
IEntity ERR_ObservedEntity()
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRObservedId));
|
||||
if (!component) return null;
|
||||
return component.GetEntity();
|
||||
}
|
||||
bool ERR_HasObserverCamera() { return m_ERRObserverCamera != null; }
|
||||
|
||||
void ERR_SetObserver(ERR_ObserverReason reason)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity())) return;
|
||||
m_ERRObserverReason = reason;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
||||
if (reason == ERR_ObserverReason.NONE) ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
ERR_UpdateObserverServer();
|
||||
}
|
||||
|
||||
protected void ERR_StreamObserved(IEntity entity, bool normalStreaming)
|
||||
{
|
||||
RplIdentity identity = GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local() || !entity) return;
|
||||
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
if (component) component.EnableStreamingConNode(identity, normalStreaming);
|
||||
}
|
||||
protected void ERR_ReleaseObserved()
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObserved, true);
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObserved = null;
|
||||
m_ERRServerObservedVehicle = null;
|
||||
m_ERRObservedId = RplId.Invalid();
|
||||
}
|
||||
void ERR_UpdateObserverServer(int direction = 0)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active() || ERR_Actors.Alive(GetControlledEntity()))
|
||||
{
|
||||
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
||||
ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
return;
|
||||
}
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors);
|
||||
Faction friendly = GetGame().GetFactionManager().GetFactionByKey("US");
|
||||
for (int i = survivors.Count() - 1; i >= 0; i--)
|
||||
if (SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||
int index = survivors.Find(m_ERRServerObserved);
|
||||
if (index < 0) index = 0;
|
||||
else index += direction;
|
||||
if (index < 0) index = survivors.Count() - 1;
|
||||
if (index >= survivors.Count()) index = 0;
|
||||
IEntity target;
|
||||
if (!survivors.IsEmpty()) target = survivors[index];
|
||||
if (target != m_ERRServerObserved)
|
||||
{
|
||||
ERR_ReleaseObserved();
|
||||
m_ERRServerObserved = target;
|
||||
if (target)
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
||||
if (component) m_ERRObservedId = component.Id();
|
||||
ERR_StreamObserved(target, false);
|
||||
}
|
||||
}
|
||||
if (target)
|
||||
{
|
||||
m_ERRObserverAnchor = target.GetOrigin();
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
if (vehicle != m_ERRServerObservedVehicle)
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObservedVehicle = vehicle;
|
||||
ERR_StreamObserved(vehicle, false);
|
||||
}
|
||||
}
|
||||
Replication.BumpMe();
|
||||
}
|
||||
void ERR_CycleObserver(int direction)
|
||||
{
|
||||
if (this == GetGame().GetPlayerController()) Rpc(ERR_RequestObserverCycle, direction);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_RequestObserverCycle(int direction)
|
||||
{
|
||||
if (!Replication.IsServer() || (direction != -1 && direction != 1)) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now - m_ERRLastCycle < 250) return;
|
||||
m_ERRLastCycle = now;
|
||||
ERR_UpdateObserverServer(direction);
|
||||
}
|
||||
|
||||
void ERR_StartObserverMonitor()
|
||||
{
|
||||
if (this != GetGame().GetPlayerController() || System.IsConsoleApp() || m_ERRObserverMonitor) return;
|
||||
m_ERRObserverMonitor = true;
|
||||
ERR_OnObserverMonitorStarted();
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateObserverClient, 250, true);
|
||||
}
|
||||
protected void ERR_OnObserverMonitorStarted() {}
|
||||
protected void ERR_UpdateObserverClient()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
bool observe = mode && mode.Active() && m_ERRObserverReason != ERR_ObserverReason.NONE && !ERR_Actors.Alive(GetControlledEntity());
|
||||
if (!observe)
|
||||
{
|
||||
ERR_CloseObserverCamera();
|
||||
return;
|
||||
}
|
||||
if (!m_ERRObserverCamera)
|
||||
{
|
||||
Resource resource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
|
||||
if (!resource || !resource.IsValid()) return;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
params.Transform[3] = m_ERRObserverAnchor + "0 5 -6";
|
||||
m_ERRObserverCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params));
|
||||
if (!m_ERRObserverCamera) return;
|
||||
}
|
||||
CameraManager manager = GetGame().GetCameraManager();
|
||||
if (!manager || manager.CurrentCamera() != m_ERRObserverCamera) return;
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) respawn.DestroyLoadingPlaceholder();
|
||||
if (!m_ERRObserverReady)
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (hints) hints.ShowCustom("You are spectating the surviving prisoners until the next round.", "Spectating", 12);
|
||||
m_ERRObserverReady = true;
|
||||
}
|
||||
}
|
||||
protected void ERR_CloseObserverCamera()
|
||||
{
|
||||
m_ERRObserverReady = false;
|
||||
if (!m_ERRObserverCamera) return;
|
||||
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
||||
m_ERRObserverCamera.Terminate();
|
||||
delete m_ERRObserverCamera;
|
||||
m_ERRObserverCamera = null;
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
|
||||
ERR_CloseObserverCamera();
|
||||
if (Replication.IsServer()) ERR_ReleaseObserved();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Local presentation reads replicated state; it cannot advance objectives.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected int m_ERRGuidePhase = -1;
|
||||
protected bool m_ERRGuideStarted;
|
||||
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get() || m_ERRGuideStarted) return;
|
||||
m_ERRGuideStarted = true;
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateGuide, 1000, true);
|
||||
}
|
||||
protected void ERR_UpdateGuide()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !GetControlledEntity() || m_ERRGuidePhase == mode.Phase()) return;
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints) return;
|
||||
string message;
|
||||
if (mode.Phase() == ERR_Phase.PRISON)
|
||||
message = "Find the supplies and field map inside the prison. Pick the gate lock to begin your escape.";
|
||||
else if (mode.Phase() == ERR_Phase.RADIO_SEARCH)
|
||||
message = "Recover the two documents on the guard's table just outside the gate. They identify the radio station on your map. Enemy soldiers may carry more intel.";
|
||||
else if (mode.Phase() == ERR_Phase.RADIO_DEFENCE)
|
||||
message = "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
|
||||
else if (mode.Phase() == ERR_Phase.EXTRACTION)
|
||||
message = "The extraction helicopter is marked on your map. Board it and fly at least 9 kilometres from the prison. Everyone aboard escapes; players left behind do not.";
|
||||
if (!message.IsEmpty()) hints.ShowCustom(message, "Escape Everon", 15);
|
||||
m_ERRGuidePhase = mode.Phase();
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateGuide);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Round-owned identities survive controller IDs, death and reconnect attempts.
|
||||
class ERR_PlayerLifeRules
|
||||
{
|
||||
static bool CanEnter(bool usedLife, int phase)
|
||||
{
|
||||
return !usedLife && phase >= ERR_Phase.PRISON && phase <= ERR_Phase.RADIO_DEFENCE;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PlayerLife
|
||||
{
|
||||
UUID AccountId;
|
||||
int ConnectionId;
|
||||
bool Connected;
|
||||
bool UsedLife;
|
||||
IEntity Actor;
|
||||
}
|
||||
|
||||
class ERR_PlayerRoster
|
||||
{
|
||||
protected ref map<UUID, ref ERR_PlayerLife> m_Lives = new map<UUID, ref ERR_PlayerLife>();
|
||||
protected ref map<int, UUID> m_Connections = new map<int, UUID>();
|
||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
|
||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||
bool Bind(int playerId, UUID identity)
|
||||
{
|
||||
if (!Replication.IsServer() || playerId <= 0 || identity.IsNull()) return false;
|
||||
ERR_PlayerLife life;
|
||||
if (!m_Lives.Find(identity, life))
|
||||
{
|
||||
life = new ERR_PlayerLife();
|
||||
life.AccountId = identity;
|
||||
m_Lives.Insert(identity, life);
|
||||
}
|
||||
if (life.Connected && life.ConnectionId != playerId) return false;
|
||||
// A recycled transient ID cannot inherit the previous connection's record.
|
||||
UUID previous;
|
||||
if (m_Connections.Find(playerId, previous) && previous != identity) Disconnect(playerId);
|
||||
life.ConnectionId = playerId;
|
||||
life.Connected = true;
|
||||
m_Connections.Set(playerId, identity);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
return true;
|
||||
}
|
||||
ERR_PlayerLife Get(int playerId)
|
||||
{
|
||||
UUID identity;
|
||||
if (!m_Connections.Find(playerId, identity)) return null;
|
||||
return m_Lives.Get(identity);
|
||||
}
|
||||
bool CanEnter(int playerId, int phase)
|
||||
{
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
||||
}
|
||||
void Spawned(int playerId, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (!life) return;
|
||||
life.Actor = actor;
|
||||
life.UsedLife = true;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Disconnect(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (life) life.Connected = false;
|
||||
m_Connections.Remove(playerId);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
bool HasSurvivor()
|
||||
{
|
||||
// Native reconnect reservations retain the original actor until expiry.
|
||||
// A reserved survivor must not lose the round while their client reconnects.
|
||||
foreach (UUID identity, ERR_PlayerLife life : m_Lives)
|
||||
if (life.UsedLife && ERR_Actors.Alive(life.Actor)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_ReconnectComponent
|
||||
{
|
||||
override bool HandlePlayerDisconnect(int playerId, KickCauseCode cause)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
if (identity.IsNull()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
// Voluntary and involuntary departures use the same vanilla reservation,
|
||||
// timeout, character handoff and expiry cleanup. No replacement life is issued.
|
||||
SCR_ReconnectData data = StoreData(playerId);
|
||||
if (!IsDataRelevant(data)) return false;
|
||||
m_mReconnectData.Set(identity, data);
|
||||
UpdateExpieryCheck();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
[ComponentEditorProps(category: "Rereintroduced", description: "Everon escape composition and population")]
|
||||
class ERR_ScenarioComponentClass : ScriptComponentClass {}
|
||||
class ERR_ScenarioComponent : ScriptComponent
|
||||
{
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Prison composition", "et")]
|
||||
protected ResourceName m_Prison;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Radio station", "et")]
|
||||
protected ResourceName m_Radio;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Extraction marker", "et")]
|
||||
protected ResourceName m_Extraction;
|
||||
[Attribute("6500 0 7200", UIWidgets.Coords, "Radio site")]
|
||||
protected vector m_RadioPosition;
|
||||
[Attribute("7000 0 7500", UIWidgets.Coords, "Extraction helicopter site")]
|
||||
protected vector m_ExtractionPosition;
|
||||
protected ref array<IEntity> m_Spawned = {};
|
||||
|
||||
bool Prepare()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || !m_Spawned.IsEmpty()) return false;
|
||||
vector start = mode.PrisonPosition();
|
||||
vector radioPosition = ERR_Actors.Ground(m_RadioPosition);
|
||||
vector extractionPosition = ERR_Actors.Ground(m_ExtractionPosition);
|
||||
if (start[1] < 1 || radioPosition[1] < 1 || extractionPosition[1] < 1) return false;
|
||||
IEntity prison = Spawn(m_Prison, start);
|
||||
IEntity radio = Spawn(m_Radio, radioPosition);
|
||||
IEntity extraction = Spawn(m_Extraction, extractionPosition);
|
||||
Vehicle helicopter = Vehicle.Cast(Spawn(ERR_FactionCatalog.Helicopter(), ERR_Actors.Ground(extractionPosition + "12 0 0")));
|
||||
if (!prison || !radio || !extraction || !helicopter) { Rollback(); return false; }
|
||||
mode.SetExtractionVehicle(helicopter);
|
||||
// Keep equipment above vegetation and visible to newly spawned prisoners.
|
||||
IEntity supplyTable = Spawn(ERR_Supplies.IntelTable(), ERR_Actors.Ground(start + "-4 0 0"));
|
||||
if (!supplyTable) { Rollback(); return false; }
|
||||
vector supplyLow, supplyHigh;
|
||||
supplyTable.GetWorldBounds(supplyLow, supplyHigh);
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (!Spawn(ERR_Supplies.Pistol(), TablePoint(supplyLow, supplyHigh, (i + 1) / 6.0, 0.3))) { Rollback(); return false; }
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (!Spawn(ERR_Supplies.Magazine(), TablePoint(supplyLow, supplyHigh, (i + 1) / 9.0, 0.65))) { Rollback(); return false; }
|
||||
if (!Spawn(FieldMap(), TablePoint(supplyLow, supplyHigh, 0.5, 0.9))) { Rollback(); return false; }
|
||||
// Objective documents are guaranteed; random carried intel remains optional.
|
||||
vector cachePosition = ERR_Actors.Ground(start + "0 0 -12");
|
||||
IEntity table = Spawn(ERR_Supplies.IntelTable(), cachePosition);
|
||||
if (!table) { Rollback(); return false; }
|
||||
vector lower, upper;
|
||||
table.GetWorldBounds(lower, upper);
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
vector documentPosition = cachePosition + Vector(-0.2 + i * 0.4, 0, 0);
|
||||
documentPosition[1] = upper[1] + 0.05;
|
||||
if (!Spawn(ERR_FactionCatalog.IntelFolder(), documentPosition)) { Rollback(); return false; }
|
||||
}
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
vector centre = start + "35 0 0";
|
||||
if (i == 1) centre = radioPosition + "30 0 0";
|
||||
if (i == 2) centre = extractionPosition + "30 0 0";
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(Spawn(ERR_FactionCatalog.Patrol(), ERR_Actors.Ground(centre)));
|
||||
if (!group) { Rollback(); return false; }
|
||||
AIWaypoint defend = AIWaypoint.Cast(Spawn(ERR_FactionCatalog.DefendWaypoint(), ERR_Actors.Ground(centre)));
|
||||
if (!defend) { Rollback(); return false; }
|
||||
group.AddWaypoint(defend);
|
||||
GetGame().GetCallqueue().CallLater(RegisterGroup, 2000, false, group);
|
||||
}
|
||||
foreach (IEntity entity : m_Spawned) mode.Own(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected vector TablePoint(vector lower, vector upper, float along, float across)
|
||||
{
|
||||
float x = along;
|
||||
float z = across;
|
||||
if (upper[2] - lower[2] > upper[0] - lower[0]) { x = across; z = along; }
|
||||
return Vector(lower[0] + (upper[0] - lower[0]) * x, upper[1] + 0.05, lower[2] + (upper[2] - lower[2]) * z);
|
||||
}
|
||||
|
||||
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
|
||||
|
||||
protected IEntity Spawn(ResourceName resource, vector position)
|
||||
{
|
||||
IEntity entity = ERR_Actors.Spawn(resource, position);
|
||||
if (entity) m_Spawned.Insert(entity);
|
||||
return entity;
|
||||
}
|
||||
protected void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.Active() && group) mode.RegisterGroup(group);
|
||||
}
|
||||
protected void Rollback()
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(RegisterGroup);
|
||||
foreach (IEntity entity : m_Spawned) ERR_Actors.DeleteSpawned(entity);
|
||||
m_Spawned.Clear();
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(RegisterGroup);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// One configuration owner; never mutate runtime state after a failed save.
|
||||
class ERR_Settings
|
||||
{
|
||||
static const string PATH = "$profile:rereintroduced.json";
|
||||
protected ref map<string, int> m_Values = new map<string, int>();
|
||||
protected bool m_ReadOnly;
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
{
|
||||
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
||||
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
|
||||
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
|
||||
if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") return value >= 0 && value <= 100;
|
||||
if (key == "radioDefenceSeconds") return value >= 30 && value <= 1800;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ERR_Settings()
|
||||
{
|
||||
m_Values.Set("reportIntervalSeconds", 10);
|
||||
m_Values.Set("supportSquadLimit", 4);
|
||||
m_Values.Set("responseRadiusMetres", 2000);
|
||||
m_Values.Set("aiIntelChancePercent", 60);
|
||||
m_Values.Set("intelRevealChancePercent", 15);
|
||||
m_Values.Set("radioDefenceSeconds", 180);
|
||||
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||
map<string, int> loaded;
|
||||
if (Read(PATH, loaded) || Read(PATH + ".bak", loaded)) m_Values = loaded;
|
||||
else if (FileIO.FileExists(PATH) || FileIO.FileExists(PATH + ".bak"))
|
||||
{
|
||||
m_ReadOnly = true;
|
||||
Print("Rereintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
int Get(string key) { return m_Values.Get(key); }
|
||||
|
||||
protected bool Read(string path, out map<string, int> values)
|
||||
{
|
||||
JsonLoadContext json = new JsonLoadContext();
|
||||
if (!json.LoadFromFile(path)) return false;
|
||||
values = new map<string, int>();
|
||||
foreach (string key : Keys())
|
||||
{
|
||||
float number;
|
||||
if (!json.ReadValue(key, number)) return false;
|
||||
int value = Math.Round(number);
|
||||
if (number != value || !Valid(key, value)) return false;
|
||||
values.Set(key, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Set(string key, int value)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !Valid(key, value)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int saved = Get(option);
|
||||
if (option == key) saved = value;
|
||||
json.WriteValue(option, saved);
|
||||
}
|
||||
map<string, int> checked;
|
||||
if (!json.SaveToFile(PATH + ".tmp") || !Read(PATH + ".tmp", checked)) return false;
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int expected = Get(option);
|
||||
if (option == key) expected = value;
|
||||
if (checked.Get(option) != expected) return false;
|
||||
}
|
||||
map<string, int> previous;
|
||||
if (Read(PATH, previous) && !FileIO.CopyFile(PATH, PATH + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(PATH + ".tmp", PATH) || !Read(PATH, checked))
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
return false;
|
||||
}
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int expected = Get(option);
|
||||
if (option == key) expected = value;
|
||||
if (checked.Get(option) != expected)
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_Values = checked;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
[BaseContainerProps(category: "Respawn")]
|
||||
class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
protected ref map<int, int> m_Attempts = new map<int, int>();
|
||||
protected ref map<int, UUID> m_Identities = new map<int, UUID>();
|
||||
protected ref map<int, UUID> m_Retries = new map<int, UUID>();
|
||||
protected ref map<int, vector> m_AuthorizedPositions = new map<int, vector>();
|
||||
protected ref set<int> m_Pending = new set<int>();
|
||||
|
||||
override void OnPlayerAuditSuccess_S(int playerId)
|
||||
{
|
||||
super.OnPlayerAuditSuccess_S(playerId);
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
#ifdef WORKBENCH
|
||||
if (identity.IsNull() && RplSession.Mode() != RplMode.Dedicated)
|
||||
{
|
||||
string name = GetGame().GetPlayerManager().GetPlayerName(playerId);
|
||||
string suffix = Math.AbsInt(name.Hash()).ToString(12, true);
|
||||
identity = "00e77100-0000-0000-0000-" + suffix;
|
||||
}
|
||||
#endif
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Players().Bind(playerId, identity))
|
||||
{
|
||||
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
m_Identities.Set(playerId, identity);
|
||||
if (ResolveReconnection(playerId))
|
||||
{
|
||||
mode.PlayerSpawned(playerId, GetGame().GetPlayerManager().GetPlayerControlledEntity(playerId));
|
||||
SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId)).ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
return;
|
||||
}
|
||||
m_Attempts.Set(playerId, 0);
|
||||
DoSpawn_S(playerId);
|
||||
}
|
||||
|
||||
protected override void DoSpawn_S(int playerId)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (!controller || !mode || !m_Identities.Contains(playerId) || !m_Attempts.Contains(playerId) || m_Pending.Contains(playerId)) return;
|
||||
if (controller.GetControlledEntity()) return;
|
||||
if (ERR_MissionRules.Terminal(mode.Phase())) return;
|
||||
int attempts = m_Attempts.Get(playerId);
|
||||
if (attempts >= 30) { Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE); return; }
|
||||
m_Attempts.Set(playerId, attempts + 1);
|
||||
if (!mode.Active()) { RetryLater(playerId); return; }
|
||||
if (!mode.Players().CanEnter(playerId, mode.Phase()))
|
||||
{
|
||||
ERR_ObserverReason reason = ERR_ObserverReason.SPENT_LIFE;
|
||||
ERR_PlayerLife life = mode.Players().Get(playerId);
|
||||
if (life && !life.UsedLife) reason = ERR_ObserverReason.LATE_EXTRACTION;
|
||||
Observe(playerId, reason);
|
||||
return;
|
||||
}
|
||||
SCR_PlayerFactionAffiliationComponent affiliation = GetPlayerFactionComponent_S(playerId);
|
||||
SCR_RespawnComponent respawn = GetPlayerRespawnComponent_S(playerId);
|
||||
Faction faction = GetGame().GetFactionManager().GetFactionByKey("US");
|
||||
if (!affiliation || !respawn || !faction) { RetryLater(playerId); return; }
|
||||
affiliation.RequestFaction(faction);
|
||||
vector position = SpawnPosition(mode, playerId);
|
||||
SCR_FreeSpawnData request = new SCR_FreeSpawnData(ERR_FactionCatalog.Prisoner(), position);
|
||||
if (!respawn.CanSpawn(request)) { RetryLater(playerId); return; }
|
||||
m_AuthorizedPositions.Set(playerId, position);
|
||||
m_Pending.Insert(playerId);
|
||||
if (!respawn.RequestSpawn(request)) OnPlayerSpawnFailed_S(playerId);
|
||||
}
|
||||
|
||||
protected vector SpawnPosition(ERR_GameMode mode, int playerId)
|
||||
{
|
||||
int column = playerId % 3;
|
||||
int row = playerId % 4;
|
||||
vector offset = Vector(column - 1, 0, row - 1);
|
||||
vector prison = ERR_Actors.Ground(mode.PrisonPosition() + offset);
|
||||
if (mode.Phase() == ERR_Phase.PRISON) return prison;
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors, true);
|
||||
foreach (IEntity survivor : survivors)
|
||||
{
|
||||
if (CompartmentAccessComponent.GetVehicleIn(survivor)) continue;
|
||||
vector anchor = survivor.GetOrigin();
|
||||
vector ground = ERR_Actors.Ground(anchor);
|
||||
if (Math.AbsFloat(anchor[1] - ground[1]) > 1.5) continue;
|
||||
vector position = ERR_Actors.Ground(anchor + "3 0 0" + offset);
|
||||
if (position[1] < 1 || Math.AbsFloat(position[1] - anchor[1]) > 1.5) continue;
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = anchor + "0 1 0";
|
||||
trace.End = position + "0 1 0";
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = survivor;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
|
||||
trace.Start = position + "0 0.15 0";
|
||||
trace.End = position + "0 2 0";
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
|
||||
return position;
|
||||
}
|
||||
return prison;
|
||||
}
|
||||
|
||||
protected void ClearPending(int playerId)
|
||||
{
|
||||
int index = m_Pending.Find(playerId);
|
||||
if (index >= 0) m_Pending.Remove(index);
|
||||
}
|
||||
protected void RetryLater(int playerId)
|
||||
{
|
||||
UUID identity;
|
||||
if (!m_Identities.Find(playerId, identity) || !m_Attempts.Contains(playerId)) return;
|
||||
if (m_Retries.Contains(playerId) && m_Retries.Get(playerId) == identity) return;
|
||||
m_Retries.Set(playerId, identity);
|
||||
GetGame().GetCallqueue().CallLater(RetrySpawn, 1000, false, playerId, identity);
|
||||
}
|
||||
protected void RetrySpawn(int playerId, UUID identity)
|
||||
{
|
||||
if (!m_Identities.Contains(playerId) || m_Identities.Get(playerId) != identity || m_Retries.Get(playerId) != identity) return;
|
||||
m_Retries.Remove(playerId);
|
||||
DoSpawn_S(playerId);
|
||||
}
|
||||
protected override void OnPlayerSpawnFailed_S(int playerId)
|
||||
{
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
RetryLater(playerId);
|
||||
}
|
||||
protected void Observe(int playerId, ERR_ObserverReason reason)
|
||||
{
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (controller) controller.ERR_SetObserver(reason);
|
||||
}
|
||||
protected override void OnPlayerEntityLost_S(int playerId)
|
||||
{
|
||||
super.OnPlayerEntityLost_S(playerId);
|
||||
if (!m_Identities.Contains(playerId)) return;
|
||||
GetGame().GetCallqueue().CallLater(ObserveAfterLoss, 100, false, playerId, m_Identities.Get(playerId));
|
||||
}
|
||||
protected void ObserveAfterLoss(int playerId, UUID identity)
|
||||
{
|
||||
if (!m_Identities.Contains(playerId) || m_Identities.Get(playerId) != identity) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_PlayerLife life;
|
||||
if (mode) life = mode.Players().Get(playerId);
|
||||
if (life && life.UsedLife) Observe(playerId, ERR_ObserverReason.SPENT_LIFE);
|
||||
}
|
||||
override void OnPlayerSpawned_S(int playerId, IEntity entity)
|
||||
{
|
||||
super.OnPlayerSpawned_S(playerId, entity);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.PlayerSpawned(playerId, entity);
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
PrintFormat("Rereintroduced [spawn]: player %1 at %2", playerId, entity.GetOrigin());
|
||||
}
|
||||
override void OnPlayerDisconnected_S(int playerId, KickCauseCode cause, int timeout)
|
||||
{
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
m_Identities.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.Players().Disconnect(playerId);
|
||||
if (GetPlayerRespawnComponent_S(playerId)) super.OnPlayerDisconnected_S(playerId, cause, timeout);
|
||||
}
|
||||
bool Authorizes(int playerId, SCR_SpawnData data)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || !data || !mode.Players().CanEnter(playerId, mode.Phase())) return false;
|
||||
if (!m_Pending.Contains(playerId) || !m_AuthorizedPositions.Contains(playerId) || data.GetPrefab() != ERR_FactionCatalog.Prisoner()) return false;
|
||||
return vector.Distance(data.GetPosition(), m_AuthorizedPositions.Get(playerId)) < 0.1;
|
||||
}
|
||||
void ~ERR_SpawnLogic()
|
||||
{
|
||||
if (!GetGame()) return;
|
||||
GetGame().GetCallqueue().Remove(RetrySpawn);
|
||||
GetGame().GetCallqueue().Remove(ObserveAfterLoss);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_RespawnSystemComponent
|
||||
{
|
||||
override bool CanRequestSpawn_S(SCR_SpawnRequestComponent requestComponent, SCR_SpawnHandlerComponent handlerComponent, SCR_SpawnData data, out SCR_ESpawnResult result = SCR_ESpawnResult.SPAWN_NOT_ALLOWED)
|
||||
{
|
||||
if (ERR_GameMode.Get())
|
||||
{
|
||||
ERR_SpawnLogic logic = ERR_SpawnLogic.Cast(GetSpawnLogic());
|
||||
if (!requestComponent || !logic || !logic.Authorizes(requestComponent.GetPlayerId(), data))
|
||||
{
|
||||
result = SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.CanRequestSpawn_S(requestComponent, handlerComponent, data, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
class ERR_Supplies
|
||||
{
|
||||
static ResourceName Pistol() { return "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et"; }
|
||||
static ResourceName Magazine() { return "{8B853CDD11BA916E}Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et"; }
|
||||
static ResourceName Map() { return "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et"; }
|
||||
static ResourceName IntelTable() { return "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et"; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
SubScene {
|
||||
Parent "{853E92315D1D9EFE}worlds/Eden/Eden.ent"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
||||
Configurations {
|
||||
WorldResourceClass PC {
|
||||
}
|
||||
WorldResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
ERR_GameMode Rereintroduced : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
|
||||
}
|
||||
SCR_AIWorld AIWorld : "{E0A05C76552E7F58}Prefabs/AI/SCR_AIWorld.et" {
|
||||
components {
|
||||
NavmeshWorldComponent "{5584F30E67F617AD}" {
|
||||
NavmeshSettings NavmeshWorld "{50FC63BEBE3973C5}" {
|
||||
NavmeshFilesConfig BaseNavmeshFilesConfig "{60CA9BE5536BF701}" : "{8BBC73472092902E}Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf" {}
|
||||
}
|
||||
}
|
||||
NavmeshWorldComponent "{5584F30EEFEE1223}" {
|
||||
NavmeshSettings ChimeraNavmeshWorld "{50FC63BEBE3973C5}" {
|
||||
NavmeshFilesConfig BaseNavmeshFilesConfig "{60CA9BE5536BF701}" : "{0228A79846FD8A97}Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf" {}
|
||||
}
|
||||
}
|
||||
NavmeshWorldComponent "{5C8C9B750D124A63}" {
|
||||
NavmeshSettings NavmeshWorld "{5C8C9B750B60C6E2}" {
|
||||
NavmeshFilesConfig BaseNavmeshFilesConfig "{5C90BD0EC793647D}" {
|
||||
NavmeshFile "{5E19E60F26327356}worlds/MP/Navmeshes/LowResEden.nmn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
|
||||
}
|
||||
SCR_FactionManager Factions : "{589540B35EBA8CDC}Prefabs/MP/Managers/Factions/FactionManager_USxUSSR.et" {
|
||||
}
|
||||
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
|
||||
}
|
||||
|
||||
RadioManagerEntity RadioManager : "{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et" {
|
||||
}
|
||||
|
||||
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
GameProject {
|
||||
ID "ERR_Core"
|
||||
GUID "2528466DE220D91C"
|
||||
TITLE "Escapists Rereintroduced - Core"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
class ERR_IntelRequest
|
||||
{
|
||||
IEntity Actor;
|
||||
int Attempts;
|
||||
bool Accepted;
|
||||
}
|
||||
|
||||
// Mission-owned inventory service. Physical folder identities are consumed once,
|
||||
// even when transferred between players; owned inventory entities are not deleted.
|
||||
class ERR_IntelService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_IntelRequest> m_Requests = {};
|
||||
protected ref set<IEntity> m_Rolled = new set<IEntity>();
|
||||
protected ref set<IEntity> m_Spent = new set<IEntity>();
|
||||
|
||||
void ERR_IntelService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
array<AIAgent> members = {};
|
||||
group.GetAgents(members);
|
||||
foreach (AIAgent member : members)
|
||||
{
|
||||
if (!member) continue;
|
||||
IEntity actor = member.GetControlledEntity();
|
||||
if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || m_Rolled.Contains(actor)) continue;
|
||||
m_Rolled.Insert(actor);
|
||||
if (Math.RandomInt(0, 100) >= m_Mode.Settings().Get("aiIntelChancePercent")) continue;
|
||||
if (m_Requests.Count() >= 256) continue;
|
||||
ERR_IntelRequest request = new ERR_IntelRequest();
|
||||
request.Actor = actor;
|
||||
m_Requests.Insert(request);
|
||||
}
|
||||
}
|
||||
|
||||
protected void Folders(InventoryStorageManagerComponent inventory, out array<IEntity> folders)
|
||||
{
|
||||
folders.Clear();
|
||||
SCR_PrefabNamePredicate predicate = new SCR_PrefabNamePredicate();
|
||||
predicate.prefabName = ERR_FactionCatalog.IntelFolder();
|
||||
inventory.FindItems(folders, predicate, EStoragePurpose.PURPOSE_ANY);
|
||||
}
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
int budget = 8;
|
||||
for (int i = m_Requests.Count() - 1; i >= 0 && budget > 0; i--)
|
||||
{
|
||||
budget--;
|
||||
ERR_IntelRequest request = m_Requests[i];
|
||||
if (!ERR_Actors.Alive(request.Actor) || ERR_Actors.Player(request.Actor)) { m_Requests.Remove(i); continue; }
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(request.Actor.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
array<IEntity> folders = {};
|
||||
if (inventory) Folders(inventory, folders);
|
||||
if (!folders.IsEmpty()) { m_Requests.Remove(i); continue; }
|
||||
request.Attempts++;
|
||||
if (request.Attempts > 30)
|
||||
{
|
||||
Print("Rereintroduced [intel]: bounded assignment failed", LogLevel.WARNING);
|
||||
m_Requests.Remove(i); continue;
|
||||
}
|
||||
if (inventory && !request.Accepted)
|
||||
request.Accepted = inventory.TrySpawnPrefabToStorage(ERR_FactionCatalog.IntelFolder(), purpose: EStoragePurpose.PURPOSE_ANY);
|
||||
}
|
||||
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 = {};
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
if (location && location.Kind() != ERR_LocationKind.PRISON && location.Kind() != ERR_LocationKind.EXTRACTION && location.Knowledge() < 2) 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_Requests.Clear(); m_Rolled.Clear(); m_Spent.Clear(); }
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_IntelService m_Intel;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer()) return;
|
||||
m_Intel = new ERR_IntelService(this);
|
||||
OnGroup().Insert(m_Intel.RegisterGroup);
|
||||
OnTick().Insert(m_Intel.Tick);
|
||||
OnStop().Insert(m_Intel.Stop);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
GameProject {
|
||||
ID "ERR_Intel"
|
||||
GUID "D31039C7C53E348A"
|
||||
TITLE "Escapists Rereintroduced - Intel"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
"2528466DE220D91C"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MatPBRBasic {
|
||||
Color 0.882 0.847 0.792 1
|
||||
AlphaTest 0.5
|
||||
AllowUserAlphaBias 1
|
||||
BCRMap "$rendertarget"
|
||||
OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 )
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
MetaFileClass {
|
||||
Name "{03D6DE777C8E411C}Materials/ERRM_MapRenderTarget.emat"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass XBOX_ONE : PC {
|
||||
}
|
||||
ResourceClass XBOX_SERIES : PC {
|
||||
}
|
||||
ResourceClass PS4 : PC {
|
||||
}
|
||||
ResourceClass PS5 : PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
|
||||
ID "6FB475409DE44320"
|
||||
components {
|
||||
InventoryItemComponent "{5222EB4D0C73006B}" {
|
||||
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
|
||||
ItemDisplayName UIInfo "{7236A6DB18BD574C}" {
|
||||
Name "Shared Field Map"
|
||||
Description "A field map whose visible surface can be read by nearby players."
|
||||
}
|
||||
ItemPhysAttributes ItemPhysicalAttributes PhysicalAttributes {
|
||||
Weight 0.1
|
||||
SizeSetupStrategy Manual
|
||||
ItemDimensions 5 5 5
|
||||
ItemVolume 200
|
||||
DimensionScaler 1
|
||||
RestingAdditiveRotationLS 0 0 0
|
||||
RestingAdditiveOffsetLS 0 0 0
|
||||
}
|
||||
ItemAnimationAttributes ItemAnimationAttributes "{4AC999738D39BA9A}" {
|
||||
AnimationAttachments {
|
||||
AnimationAttachmentInfo "{1D66E5B861D2F00A}" {
|
||||
AnimGraph "{83675D0B0AB1B2C3}Assets/Items/Equipment/workspaces/paper_map.agr"
|
||||
AnimInstance "{56794534F6B8C3D7}Assets/Items/Equipment/workspaces/player_map.asi"
|
||||
BindingName "Gadget"
|
||||
}
|
||||
}
|
||||
AllowInspection 1
|
||||
}
|
||||
CustomAttributes {
|
||||
PreviewRenderAttributes "{D12878E6C065B83D}" {
|
||||
CameraPreset Top
|
||||
CameraOrbitAngles -25 35 -25
|
||||
CameraDistanceToItem 2
|
||||
CameraOffset -0.01 0 0
|
||||
ItemRenderRotation 0 0 180
|
||||
FOV 10.5
|
||||
AspectRatio 1
|
||||
LodModel 1
|
||||
}
|
||||
CharacterModifierAttributes "{78A32EF06ED3CB42}" {
|
||||
ADSSpeedLimit 1
|
||||
}
|
||||
}
|
||||
m_eQuickSlotSize SLOT_1x1
|
||||
m_SlotType SLOT_GADGETS_STORAGE
|
||||
}
|
||||
}
|
||||
MeshObject "{E66421D681D64AB7}" {
|
||||
Object "{E443663E5241622A}Assets/Items/Equipment/Maps/Map_Paper_01/Map_Paper_01_folded.xob"
|
||||
Materials {
|
||||
MaterialAssignClass "{667CE655A40B0E50}" {
|
||||
SourceMaterial "01__Default"
|
||||
AssignedMaterial "{03D6DE777C8E411C}Materials/ERRM_MapRenderTarget.emat"
|
||||
}
|
||||
}
|
||||
InheritVisibility 1
|
||||
}
|
||||
RigidBody "{B29D8B091D657415}" {
|
||||
Enabled 1
|
||||
LinearDamping 0.5
|
||||
AngularDamping 0.5
|
||||
AngularSleepingThreashold 1
|
||||
ModelGeometry 1
|
||||
}
|
||||
ERRM_SharedMapComponent "{B576912A7A69396F}" {
|
||||
m_sLayoutName "{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout"
|
||||
m_eAnimVariable MAP
|
||||
}
|
||||
SCR_SoundDataComponent "{B6F06259A1F25D94}" {
|
||||
m_aAudioSourceConfiguration {
|
||||
SCR_AudioSourceConfiguration "{1F4628334AB3CE95}" {
|
||||
m_sSoundProject "{13F8D0DE67678118}Sounds/Items/_SharedData/PickUp/Items_PickUp_Map.acp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coords 0 0 0
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
MetaFileClass {
|
||||
Name "{A46500E4A30B653B}Prefabs/Items/Equipment/ERRM_SharedFieldMap.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass XBOX_ONE : PC {
|
||||
}
|
||||
EntityTemplateResourceClass XBOX_SERIES : PC {
|
||||
}
|
||||
EntityTemplateResourceClass PS4 : PC {
|
||||
}
|
||||
EntityTemplateResourceClass PS5 : PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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 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) 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 (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
|
||||
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Extraction helicopter";
|
||||
else label = Id();
|
||||
}
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
m_ERRShown = Knowledge();
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateMarker);
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (manager && m_ERRMarker) manager.RemoveStaticMarker(m_ERRMarker);
|
||||
m_ERRMarker = null;
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
modded class ERR_ScenarioComponent
|
||||
{
|
||||
protected override ResourceName FieldMap()
|
||||
{
|
||||
return "{A46500E4A30B653B}Prefabs/Items/Equipment/ERRM_SharedFieldMap.et";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
[ComponentEditorProps(category: "GameScripted/Gadgets", description: "Vanilla map gadget with a world-visible rendered surface")]
|
||||
class ERRM_SharedMapComponentClass : SCR_MapGadgetComponentClass
|
||||
{
|
||||
[Attribute("{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout", UIWidgets.ResourcePickerThumbnail, "Off-screen map render target", "layout")]
|
||||
protected ResourceName m_sLayoutName;
|
||||
|
||||
[Attribute("0.12", UIWidgets.EditBox, "Initial map zoom in pixels per metre", "0.01 20", category: "Shared Map")]
|
||||
protected float m_fInitialZoom;
|
||||
|
||||
ResourceName GetSharedMapLayout()
|
||||
{
|
||||
return m_sLayoutName;
|
||||
}
|
||||
|
||||
float GetInitialZoom()
|
||||
{
|
||||
return m_fInitialZoom;
|
||||
}
|
||||
}
|
||||
|
||||
// This remains a real SCR_MapGadgetComponent, so the normal M-key map,
|
||||
// animations, ruler and faction-specific protractor settings keep working.
|
||||
// The extra off-screen widget supplies the texture used by the held model.
|
||||
class ERRM_SharedMapComponent : SCR_MapGadgetComponent
|
||||
{
|
||||
protected const ResourceName MAP_CONFIG = "{A786DD4868598F15}Configs/Map/MapPlain.conf";
|
||||
protected const int MAP_SETUP_DELAY_MS = 100;
|
||||
protected const int MAP_CAPTURE_DELAY_MS = 500;
|
||||
protected const int MAP_RETRY_DELAY_MS = 250;
|
||||
|
||||
protected Widget m_ESMRootWidget;
|
||||
protected RTTextureWidget m_ESMRenderTarget;
|
||||
protected SCR_MapEntity m_ESMMapEntity;
|
||||
protected IEntity m_ESMCharacterOwner;
|
||||
protected bool m_bESMInHand;
|
||||
protected bool m_bESMOwnsMapRenderer;
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
|
||||
ERRM_SharedMapComponentClass data = ERRM_SharedMapComponentClass.Cast(GetComponentData(owner));
|
||||
WorkspaceWidget workspace = GetGame().GetWorkspace();
|
||||
if (!data || !workspace || data.GetSharedMapLayout().IsEmpty())
|
||||
return;
|
||||
|
||||
m_ESMRootWidget = workspace.CreateWidgets(data.GetSharedMapLayout());
|
||||
if (!m_ESMRootWidget)
|
||||
return;
|
||||
|
||||
m_ESMRenderTarget = RTTextureWidget.Cast(m_ESMRootWidget.FindAnyWidget("RTTexture0"));
|
||||
if (!m_ESMRenderTarget)
|
||||
return;
|
||||
|
||||
m_ESMRenderTarget.SetRenderTarget(owner);
|
||||
m_ESMRenderTarget.SetEnabled(false);
|
||||
}
|
||||
|
||||
protected override void ModeSwitch(EGadgetMode mode, IEntity charOwner)
|
||||
{
|
||||
if (mode != EGadgetMode.IN_HAND || !m_ESMRootWidget || !m_ESMRenderTarget)
|
||||
{
|
||||
super.ModeSwitch(mode, charOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
m_bESMInHand = true;
|
||||
m_ESMCharacterOwner = charOwner;
|
||||
TryCaptureMapSurface();
|
||||
}
|
||||
|
||||
protected override void ModeClear(EGadgetMode mode)
|
||||
{
|
||||
if (mode == EGadgetMode.IN_HAND)
|
||||
StopMapCapture();
|
||||
|
||||
super.ModeClear(mode);
|
||||
}
|
||||
|
||||
protected void TryCaptureMapSurface()
|
||||
{
|
||||
if (!m_bESMInHand)
|
||||
return;
|
||||
|
||||
if (!m_ESMRootWidget || !m_ESMRenderTarget)
|
||||
{
|
||||
CompleteMapModeSwitch();
|
||||
return;
|
||||
}
|
||||
|
||||
m_ESMMapEntity = SCR_MapEntity.GetMapInstance();
|
||||
if (!m_ESMMapEntity)
|
||||
{
|
||||
CompleteMapModeSwitch();
|
||||
return;
|
||||
}
|
||||
|
||||
// The owner's normal map has priority. Capture only after it closes;
|
||||
// otherwise this auxiliary render would replace or close the M-key UI.
|
||||
if (m_ESMMapEntity.IsOpen())
|
||||
{
|
||||
GetGame().GetCallqueue().CallLater(TryCaptureMapSurface, MAP_RETRY_DELAY_MS, false);
|
||||
return;
|
||||
}
|
||||
|
||||
m_ESMRenderTarget.SetEnabled(true);
|
||||
MapConfiguration mapConfig = m_ESMMapEntity.SetupMapConfig(EMapEntityMode.PLAIN, MAP_CONFIG, m_ESMRootWidget);
|
||||
if (!mapConfig)
|
||||
{
|
||||
CompleteMapModeSwitch();
|
||||
return;
|
||||
}
|
||||
|
||||
m_ESMMapEntity.OpenMap(mapConfig);
|
||||
m_bESMOwnsMapRenderer = true;
|
||||
RestoreCharacterCamera();
|
||||
|
||||
GetGame().GetCallqueue().CallLater(CenterMapOnHolder, MAP_SETUP_DELAY_MS, false);
|
||||
GetGame().GetCallqueue().CallLater(FinishMapCapture, MAP_CAPTURE_DELAY_MS, false);
|
||||
}
|
||||
|
||||
protected void CenterMapOnHolder()
|
||||
{
|
||||
if (!m_bESMOwnsMapRenderer || !m_ESMMapEntity || !m_ESMCharacterOwner)
|
||||
return;
|
||||
|
||||
ERRM_SharedMapComponentClass data = ERRM_SharedMapComponentClass.Cast(GetComponentData(GetOwner()));
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
vector holderPosition = m_ESMCharacterOwner.GetOrigin();
|
||||
m_ESMMapEntity.ZoomPanSmooth(data.GetInitialZoom(), holderPosition[0], holderPosition[2], 0.1);
|
||||
}
|
||||
|
||||
protected void FinishMapCapture()
|
||||
{
|
||||
if (!m_bESMOwnsMapRenderer)
|
||||
return;
|
||||
|
||||
// Freeze the last rendered frame before closing the auxiliary map.
|
||||
// Leaving the widget enabled would redraw the closed map as an empty
|
||||
// texture and make the physical map appear to vanish.
|
||||
if (m_ESMRenderTarget)
|
||||
m_ESMRenderTarget.SetEnabled(false);
|
||||
|
||||
if (m_ESMMapEntity && m_ESMMapEntity.IsOpen())
|
||||
m_ESMMapEntity.CloseMap();
|
||||
|
||||
m_bESMOwnsMapRenderer = false;
|
||||
RestoreCharacterCamera();
|
||||
CompleteMapModeSwitch();
|
||||
}
|
||||
|
||||
protected void CompleteMapModeSwitch()
|
||||
{
|
||||
if (!m_bESMInHand || !m_ESMCharacterOwner)
|
||||
return;
|
||||
|
||||
// Start the stock map only after the world-visible surface owns a frame.
|
||||
super.ModeSwitch(EGadgetMode.IN_HAND, m_ESMCharacterOwner);
|
||||
}
|
||||
|
||||
protected void StopMapCapture()
|
||||
{
|
||||
m_bESMInHand = false;
|
||||
GetGame().GetCallqueue().Remove(TryCaptureMapSurface);
|
||||
GetGame().GetCallqueue().Remove(CenterMapOnHolder);
|
||||
GetGame().GetCallqueue().Remove(FinishMapCapture);
|
||||
|
||||
if (m_bESMOwnsMapRenderer && m_ESMMapEntity && m_ESMMapEntity.IsOpen())
|
||||
m_ESMMapEntity.CloseMap();
|
||||
|
||||
m_bESMOwnsMapRenderer = false;
|
||||
m_ESMCharacterOwner = null;
|
||||
RestoreCharacterCamera();
|
||||
}
|
||||
|
||||
protected void RestoreCharacterCamera()
|
||||
{
|
||||
if (!GetGame()) return;
|
||||
PlayerController playerController = GetGame().GetPlayerController();
|
||||
if (playerController)
|
||||
playerController.SetCharacterCameraRenderActive(true);
|
||||
}
|
||||
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
StopMapCapture();
|
||||
|
||||
// The entity may already be destroyed when this callback runs during a
|
||||
// world restart. Destroying the widget releases its render binding without
|
||||
// passing the deleted entity back into the native renderer.
|
||||
if (m_ESMRootWidget)
|
||||
m_ESMRootWidget.RemoveFromHierarchy();
|
||||
|
||||
m_ESMRenderTarget = null;
|
||||
m_ESMRootWidget = null;
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
FrameWidgetClass {
|
||||
Name "rootFrame"
|
||||
Slot FrameWidgetSlot {
|
||||
SizeX 1024
|
||||
OffsetRight -1024
|
||||
SizeY 1024
|
||||
OffsetBottom -1024
|
||||
}
|
||||
{
|
||||
RTTextureWidgetClass {
|
||||
Name "RTTexture0"
|
||||
Slot FrameWidgetSlot {
|
||||
Anchor 0 0 1 1
|
||||
SizeX -1024
|
||||
OffsetRight 0
|
||||
SizeY -1024
|
||||
OffsetBottom 0
|
||||
}
|
||||
{
|
||||
MapWidgetClass {
|
||||
Name "MapWidget"
|
||||
Slot FrameWidgetSlot {
|
||||
Anchor 0 0 1 1
|
||||
SizeX -1024
|
||||
OffsetRight 0
|
||||
SizeY -1024
|
||||
OffsetBottom 0
|
||||
}
|
||||
Clipping False
|
||||
Color 1 1 1 1
|
||||
SizeInUnits 1024 1024
|
||||
OffsetPixels 0 0
|
||||
Clear 1
|
||||
ClearColor 0.78 0.74 0.62 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
MetaFileClass {
|
||||
Name "{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout"
|
||||
Configurations {
|
||||
LayoutResourceClass PC {
|
||||
}
|
||||
LayoutResourceClass XBOX_ONE : PC {
|
||||
}
|
||||
LayoutResourceClass XBOX_SERIES : PC {
|
||||
}
|
||||
LayoutResourceClass PS4 : PC {
|
||||
}
|
||||
LayoutResourceClass PS5 : PC {
|
||||
}
|
||||
LayoutResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
GameProject {
|
||||
ID "ERR_Map"
|
||||
GUID "9AAEE9041D79809E"
|
||||
TITLE "Escapists Rereintroduced - Map"
|
||||
Dependencies {
|
||||
"58D0FB3206B6F859"
|
||||
"2528466DE220D91C"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user