Files
EscapistsReintroduced/docs/evidence/terrain-takeover-feature-2026-10-01/offline-spawn/ERR_TerrainTakeover.c
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

250 lines
11 KiB
C

// Experimental opt-in for an existing island scenario. Native multiplayer
// replication must be qualified before recommending this route on live servers.
enum ERR_TerrainTakeoverStage
{
INACTIVE,
SETTLING,
DRAINING,
RUNNING,
FAILED
}
class ERR_TerrainTakeoverRules
{
static bool TimedOut(float elapsed) { return elapsed >= 10; }
static bool MayDrain(float elapsed, bool sameMode) { return elapsed >= 0.5 && sameMode; }
static bool MayCreate(float elapsed, bool empty) { return elapsed >= 0.5 && empty && !TimedOut(elapsed); }
static bool SameAdmission(bool connected, bool sameController, bool identityWasPresent, bool sameIdentity)
{
return connected && sameController && (!identityWasPresent || sameIdentity);
}
}
class ERR_TerrainTakeoverAdmission
{
int Connection;
SCR_PlayerController Controller;
UUID AccountId;
}
modded class SCR_EntityCatalogManagerComponent
{
// Vanilla's singleton is not explicitly reset when its owner is removed.
static void ERR_ReleaseTerrainCatalog(IEntity owner)
{
if (s_Instance && s_Instance.GetOwner() == owner) s_Instance = null;
}
}
modded class SCR_BaseGameMode
{
override void OnPlayerAuditSuccess(int iPlayerID)
{
if (GetGame().ERR_QueueTerrainAdmission(this, iPlayerID)) return;
super.OnPlayerAuditSuccess(iPlayerID);
}
protected override void OnPlayerDisconnected(int playerId, KickCauseCode cause, int timeout)
{
GetGame().ERR_ForgetTerrainAdmission(playerId);
super.OnPlayerDisconnected(playerId, cause, timeout);
}
}
modded class ERR_GameMode
{
[RplProp()] protected bool m_ERRTerrainTakenOver;
bool ERR_TerrainTakenOver() { return m_ERRTerrainTakenOver; }
void ERR_StartTerrainTakeover(World world)
{
m_ERRTerrainTakenOver = true;
Replication.BumpMe();
OnWorldPostProcess(world);
OnGameStart();
}
}
modded class ArmaReforgerScripted
{
protected ERR_TerrainTakeoverStage m_ERRTerrainStage;
protected float m_ERRTerrainElapsed;
protected BaseWorld m_ERRTerrainWorld;
protected SCR_BaseGameMode m_ERRTerrainOriginal;
protected ref array<ref ERR_TerrainTakeoverAdmission> m_ERRTerrainAdmissions = {};
protected ref array<vector> m_ERRTerrainAirfields = {};
protected ref array<int> m_ERRTerrainMarkerSlots = {};
void ERR_ForgetTerrainAdmission(int connection)
{
for (int i = m_ERRTerrainAdmissions.Count() - 1; i >= 0; i--)
if (m_ERRTerrainAdmissions[i].Connection == connection) m_ERRTerrainAdmissions.Remove(i);
}
bool ERR_QueueTerrainAdmission(SCR_BaseGameMode mode, int connection)
{
if (m_ERRTerrainStage != ERR_TerrainTakeoverStage.SETTLING && m_ERRTerrainStage != ERR_TerrainTakeoverStage.DRAINING) return false;
if (mode != m_ERRTerrainOriginal || ERR_GameMode.Cast(mode) || !Replication.IsServer()) return false;
ERR_ForgetTerrainAdmission(connection);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetPlayerManager().GetPlayerController(connection));
if (!controller || !GetPlayerManager().IsPlayerConnected(connection)) return true;
ERR_TerrainTakeoverAdmission admission = new ERR_TerrainTakeoverAdmission();
admission.Connection = connection;
admission.Controller = controller;
admission.AccountId = SCR_PlayerIdentityUtils.GetPlayerIdentityId(connection);
m_ERRTerrainAdmissions.Insert(admission);
PrintFormat("Reintroduced [terrain takeover]: held native admission connection=%1", connection);
return true;
}
protected void ERR_ResetTerrainTakeover()
{
m_ERRTerrainStage = ERR_TerrainTakeoverStage.INACTIVE;
m_ERRTerrainElapsed = 0;
m_ERRTerrainWorld = null;
m_ERRTerrainOriginal = null;
m_ERRTerrainAdmissions.Clear();
m_ERRTerrainAirfields.Clear();
m_ERRTerrainMarkerSlots.Clear();
}
protected void ERR_FailTerrainTakeover(string reason)
{
m_ERRTerrainStage = ERR_TerrainTakeoverStage.FAILED;
m_ERRTerrainAdmissions.Clear();
PrintFormat("Reintroduced [terrain takeover failed]: %1", reason, LogLevel.ERROR);
}
protected bool ERR_PreflightTerrainTakeover()
{
if (!m_ERRTerrainOriginal || !GetWorldEntity() || !GetAIWorld() || !GetAIWorld().GetNavmeshWorldComponent("Soldiers"))
{
ERR_FailTerrainTakeover("Select a plain scenario with SCR_BaseGameMode and a Soldiers navmesh; original scenario retained.");
return false;
}
Resource prefab = Resource.Load("{8FB1559AE3F29722}Prefabs/ERR/TerrainConnector.et");
if (!prefab || !prefab.IsValid())
{
ERR_FailTerrainTakeover("Escape bootstrap prefab is missing; original scenario retained.");
return false;
}
vector low, high;
GetWorldEntity().GetWorldBounds(low, high);
if (!ERR_TerrainSites.Bounds(low, high) || !ERR_TerrainTakeoverSettings.ParseAirfields(ERR_TerrainTakeoverSettings.Airfields(), m_ERRTerrainAirfields))
{
ERR_FailTerrainTakeover("Invalid terrain size or m_sERR_Airfields; use up to 16 X Z pairs separated by semicolons. Original scenario retained.");
return false;
}
foreach (vector centre : m_ERRTerrainAirfields)
{
if (centre[0] < low[0] || centre[0] > high[0] || centre[2] < low[2] || centre[2] > high[2] || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetWorld(), centre))
{
ERR_FailTerrainTakeover("An airport centre is outside this terrain or over water; original scenario retained.");
return false;
}
}
for (int slot = 0; slot < 16; slot++)
if (!GetWorld().FindEntityByName("ERR_Airfield_" + slot.ToString())) m_ERRTerrainMarkerSlots.Insert(slot);
if (m_ERRTerrainMarkerSlots.Count() < m_ERRTerrainAirfields.Count())
{
ERR_FailTerrainTakeover("Not enough unused ERR_Airfield marker names for configured centres; original scenario retained.");
return false;
}
return true;
}
protected override void OnWorldPostProcess(World world)
{
// Do not opt an existing authored Escape mission into replacement.
if (ERR_TerrainTakeoverSettings.Enabled() && !ERR_GameMode.Get() && Replication.IsServer())
{
ERR_ResetTerrainTakeover();
m_ERRTerrainWorld = world;
m_ERRTerrainOriginal = SCR_BaseGameMode.Cast(GetGameMode());
if (ERR_PreflightTerrainTakeover())
{
m_ERRTerrainStage = ERR_TerrainTakeoverStage.SETTLING;
PrintFormat("Reintroduced [terrain takeover]: enabled; configured airports=%1", m_ERRTerrainAirfields.Count());
}
}
super.OnWorldPostProcess(world);
}
override void OnUpdate(BaseWorld world, float timeslice)
{
super.OnUpdate(world, timeslice);
if (m_ERRTerrainStage != ERR_TerrainTakeoverStage.SETTLING && m_ERRTerrainStage != ERR_TerrainTakeoverStage.DRAINING) return;
if (world != m_ERRTerrainWorld || GetWorld() != m_ERRTerrainWorld || !Replication.IsServer()) { ERR_ResetTerrainTakeover(); return; }
m_ERRTerrainElapsed += timeslice;
if (ERR_TerrainTakeoverRules.TimedOut(m_ERRTerrainElapsed)) { ERR_FailTerrainTakeover("Native manager registrations did not settle within 10 seconds; restart the scenario."); return; }
if (m_ERRTerrainStage == ERR_TerrainTakeoverStage.SETTLING)
{
if (!m_ERRTerrainOriginal || GetGameMode() != m_ERRTerrainOriginal) { ERR_FailTerrainTakeover("Original mode changed during startup; takeover cancelled."); return; }
if (ERR_TerrainTakeoverRules.MayDrain(m_ERRTerrainElapsed, true)) ERR_DrainTerrainMode();
}
else if (ERR_TerrainTakeoverRules.MayCreate(m_ERRTerrainElapsed, ERR_TerrainRegistrationsEmpty())) ERR_CreateTerrainMode();
}
protected void ERR_DrainTerrainMode()
{
Print("Reintroduced [terrain takeover]: stopping original scenario");
m_ERRTerrainOriginal.OnGameEnd();
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
// Capture first, delete after: a manager may be a child of the old mode.
array<IEntity> managers = {m_ERRTerrainOriginal, GetLoadoutManager(), GetFactionManager(), GetPerceptionManager(), GetChat()};
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
if (chimera) managers.Insert(chimera.GetRadioManager());
foreach (IEntity manager : managers) if (manager) SCR_EntityHelper.DeleteEntityAndChildren(manager);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.DRAINING;
m_ERRTerrainElapsed = 0;
}
protected bool ERR_TerrainRegistrationsEmpty()
{
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
return !GetGameMode() && !GetLoadoutManager() && !GetFactionManager() && !GetPerceptionManager() && !GetChat() && (!chimera || !chimera.GetRadioManager()) && !SCR_EntityCatalogManagerComponent.GetInstance() && !m_DataCollectorComponent;
}
protected void ERR_CreateTerrainMode()
{
Print("Reintroduced [terrain takeover]: native registrations cleared");
for (int i = 0; i < m_ERRTerrainAirfields.Count(); i++)
{
EntitySpawnParams parameters = new EntitySpawnParams();
parameters.TransformMode = ETransformMode.WORLD;
parameters.Transform[3] = m_ERRTerrainAirfields[i];
IEntity marker = SpawnEntity(GenericEntity, GetWorld(), parameters);
if (!marker) { ERR_FailTerrainTakeover("Could not create configured airport marker; restart the scenario."); return; }
marker.SetName("ERR_Airfield_" + m_ERRTerrainMarkerSlots[i].ToString());
}
ERR_GameMode mode = ERR_GameMode.Cast(SpawnEntityPrefab(Resource.Load("{8FB1559AE3F29722}Prefabs/ERR/TerrainConnector.et"), GetWorld()));
if (!mode || GetGameMode() != mode) { ERR_FailTerrainTakeover("Escape game mode could not register; restart the scenario."); return; }
m_ERRTerrainStage = ERR_TerrainTakeoverStage.RUNNING;
mode.ERR_StartTerrainTakeover(GetWorld());
foreach (ERR_TerrainTakeoverAdmission admission : m_ERRTerrainAdmissions)
{
int connection = admission.Connection;
bool connected = GetPlayerManager().IsPlayerConnected(connection);
bool sameController = admission.Controller && GetPlayerManager().GetPlayerController(connection) == admission.Controller;
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(connection);
if (ERR_TerrainTakeoverRules.SameAdmission(connected, sameController, !admission.AccountId.IsNull(), identity == admission.AccountId)) mode.OnPlayerAuditSuccess(connection);
else PrintFormat("Reintroduced [terrain takeover]: discarded stale admission connection=%1", connection);
}
m_ERRTerrainAdmissions.Clear();
m_ERRTerrainOriginal = null;
Print("Reintroduced [terrain takeover]: Escape started on selected scenario");
}
override protected void OnBeforeWorldCleanup()
{
ERR_ResetTerrainTakeover();
super.OnBeforeWorldCleanup();
}
}
modded class SCR_SpawnRequestComponent
{
override SCR_SpawnHandlerComponent GetHandlerComponent()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.ERR_TerrainTakenOver())
{
// Controllers that arrived before handover retain the removed handler.
// Resolve directly from the current owner, avoiding any old singleton.
SCR_RespawnSystemComponent system = SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent));
if (system) return SCR_SpawnHandlerComponent.Cast(system.FindComponent(GetHandlerType()));
return null;
}
return super.GetHandlerComponent();
}
}