Build independent Everon successor foundation and native regressions

This commit is contained in:
2026-09-19 01:37:41 +02:00
commit 2550bc0423
155 changed files with 15098 additions and 0 deletions
+260
View File
@@ -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); }
}
+101
View File
@@ -0,0 +1,101 @@
// Server-owned audit stream. The file contains every event; chat reads a bounded
// recent-history window with explicit gap notices for clients that fall behind.
class ERR_HQJournal
{
protected ref array<string> m_Recent = {};
protected ref array<string> m_Pending = {};
protected int m_Sequence;
protected int m_Message;
protected static int s_Part;
protected static int s_Session;
protected string m_Session;
void ERR_HQJournal()
{
s_Session++;
m_Session = string.Format("%1-%2", System.GetUnixTime(), s_Session);
}
protected float m_RetryAt;
protected int m_Lost;
string Path()
{
if (s_Part == 0)
return "$logs:rereintroduced-hq.log";
return "$logs:rereintroduced-hq-" + s_Part.ToString() + ".log";
}
int Message()
{
return ++m_Message;
}
int Sequence()
{
return m_Sequence;
}
void Write(string kind, string message, int messageId = 0)
{
if (!Replication.IsServer())
return;
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
if (m_Recent.Count() >= 300)
m_Recent.Remove(0);
m_Recent.Insert(line);
if (m_Pending.Count() < 4096)
m_Pending.Insert(line);
else
m_Lost++;
}
void Read(int after, out array<string> lines, out int cursor, int limit = 8)
{
lines.Clear();
cursor = Math.Clamp(after, 0, m_Sequence);
int first = m_Sequence - m_Recent.Count();
if (cursor < first)
{
lines.Insert(string.Format("HQ log: %1 older entries are outside chat history; see %2", first - cursor, Path()));
cursor = first;
}
int characters;
for (int i = cursor - first; i < m_Recent.Count() && lines.Count() < Math.Clamp(limit, 1, 40); i++)
{
// Bound each menu RPC without skipping unread records. Chat still
// requests eight; the live menu can drain a busy journal faster.
if (characters + m_Recent[i].Length() > 16000 && !lines.IsEmpty())
break;
lines.Insert(m_Recent[i]);
characters += m_Recent[i].Length();
cursor++;
}
}
void Flush()
{
if (m_Pending.IsEmpty() || !Replication.IsServer())
return;
float now = GetGame().GetWorld().GetWorldTime();
if (now < m_RetryAt)
return;
FileHandle file = FileIO.OpenFile(Path(), FileMode.APPEND);
if (!file)
{
m_RetryAt = now + 30000;
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
return;
}
foreach (string line : m_Pending)
file.WriteLine(line);
if (m_Lost > 0)
file.WriteLine(string.Format("HQ log: %1 entries lost while file was unavailable and buffer full", m_Lost));
bool rotate = file.GetLength() >= 5242880;
file.Close();
m_Pending.Clear();
m_Lost = 0;
if (rotate)
s_Part++;
}
}
+241
View File
@@ -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);
}
}
}
}
+144
View File
@@ -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);
}
}
+9
View File
@@ -0,0 +1,9 @@
GameProject {
ID "ERR_Ai"
GUID "5192138CB9D5B911"
TITLE "Escapists Rereintroduced - Ai"
Dependencies {
"58D0FB3206B6F859"
"2528466DE220D91C"
}
}
+11
View File
@@ -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();
}
}
+12
View File
@@ -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 {
}
}
}
+6
View File
@@ -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 {
}
}
}
+16
View File
@@ -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
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+10
View File
@@ -0,0 +1,10 @@
SCR_AIGroup : "{E552DABF3636C2AD}Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et" {
m_aUnitPrefabSlots {
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
"{23ADBBC31B6A3DC6}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et"
"{1C78331E156A3D65}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et"
"{333DA6244C7DA34C}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et"
"{BF643BE4ADBDFDD3}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et"
}
}
+7
View File
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
+89
View File
@@ -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
}
}
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+19
View File
@@ -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 {
}
}
}
+8
View File
@@ -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 {
}
}
}
+16
View File
@@ -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
} }
}
}
}
+9
View File
@@ -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)));
}
}
+81
View File
@@ -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"; }
}
+239
View File
@@ -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;
}
}
+225
View File
@@ -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"; }
}
+3
View File
@@ -0,0 +1,3 @@
SubScene {
Parent "{853E92315D1D9EFE}worlds/Eden/Eden.ent"
}
+9
View File
@@ -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" {
}
+8
View File
@@ -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);
}
}
+9
View File
@@ -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 {
}
}
}
+9
View File
@@ -0,0 +1,9 @@
GameProject {
ID "ERR_Map"
GUID "9AAEE9041D79809E"
TITLE "Escapists Rereintroduced - Map"
Dependencies {
"58D0FB3206B6F859"
"2528466DE220D91C"
}
}