Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
250 lines
11 KiB
C
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();
|
|
}
|
|
}
|