// The editor core survives a game-mode replacement, but vanilla attaches its // player lifecycle/role listeners only once, during OnGameStart. Move those // listeners without restarting the core or replacing per-player editors. [BaseContainerProps(configRoot: true)] modded class SCR_EditorManagerCore { protected ERR_GameMode m_ERREditorRetryMode; protected bool m_ERREditorRetryQueued; protected bool m_ERREditorRetryNeeded; protected int m_ERREditorRetryAttempts; // A retained controller may still carry the server's temporary identity while // a connected restart restores its network owner. Never give it an editor yet. protected bool ERR_EditorConnectionReady(int playerID) { PlayerManager players = GetGame().GetPlayerManager(); PlayerController controller = players.GetPlayerController(playerID); if (!players.IsPlayerConnected(playerID) || !controller) return false; RplIdentity identity = controller.GetRplIdentity(); if (identity == RplIdentity.Invalid()) return false; if (RplSession.Mode() == RplMode.Dedicated && identity == RplIdentity.Local()) return false; array editors = {}; GetEditorEntities(editors); foreach (SCR_EditorManagerEntity editor : editors) { int otherID = editor.GetPlayerID(); if (otherID == playerID) continue; PlayerController other = players.GetPlayerController(otherID); if (other && other.GetRplIdentity() == identity) return false; } return true; } protected override SCR_EditorManagerEntity CreateEditorManager(int playerID) { // Keep vanilla behavior outside the replaced terrain mode. ERR_TerrainTakeoverMode mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (!Replication.IsServer() || !mode) return super.CreateEditorManager(playerID); SCR_EditorManagerEntity existing = GetEditorManager(playerID); if (existing) return existing; if (!ERR_EditorConnectionReady(playerID)) { if (!m_ERREditorRetryQueued && m_ERREditorRetryAttempts == 0) PrintFormat("Reintroduced [terrain editor]: waiting for distinct network ownership; player=%1", playerID); ERR_QueueTerrainEditorRetry(mode); return null; } return super.CreateEditorManager(playerID); } protected void ERR_QueueTerrainEditorRetry(ERR_GameMode mode) { m_ERREditorRetryNeeded = true; if (m_ERREditorRetryQueued || m_ERREditorRetryAttempts >= 80) return; m_ERREditorRetryMode = mode; m_ERREditorRetryQueued = true; GetGame().GetCallqueue().CallLater(ERR_RetryTerrainEditors, 250, false); } protected void ERR_RetryTerrainEditors() { m_ERREditorRetryQueued = false; if (!m_ERREditorRetryMode || m_ERREditorRetryMode != ERR_GameMode.Get()) { ERR_StopTerrainEditorRetry(); return; } m_ERREditorRetryAttempts++; m_ERREditorRetryNeeded = false; ERR_ReconcileTerrainEditors(m_ERREditorRetryMode, true); if (m_ERREditorRetryNeeded && m_ERREditorRetryAttempts == 80) Print("Reintroduced [terrain editor]: ownership repair window ended; future player registration can still create its editor", LogLevel.WARNING); if (!m_ERREditorRetryNeeded || m_ERREditorRetryAttempts >= 80) ERR_StopTerrainEditorRetry(); } protected void ERR_StopTerrainEditorRetry() { if (GetGame()) GetGame().GetCallqueue().Remove(ERR_RetryTerrainEditors); m_ERREditorRetryMode = null; m_ERREditorRetryNeeded = false; m_ERREditorRetryQueued = false; m_ERREditorRetryAttempts = 0; } override void OnGameEnd() { ERR_StopTerrainEditorRetry(); super.OnGameEnd(); } // Registered-player events can precede takeover (notably on #restart). // Restore only missing managers, and let vanilla derive modes from actual roles. void ERR_ReconcileTerrainEditors(ERR_GameMode mode, bool missingOnly = false) { if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.ERR_TerrainTakenOver()) return; PlayerManager players = GetGame().GetPlayerManager(); array ids = {}; players.GetPlayers(ids); foreach (int id : ids) { if (!players.IsPlayerConnected(id) || !players.GetPlayerController(id)) continue; SCR_EditorManagerEntity editor = GetEditorManager(id); if (editor && missingOnly) continue; if (!editor) { OnPlayerRegistered(id); editor = GetEditorManager(id); } if (!editor) continue; // Ownership is still settling; the bounded retry owns repair. editor.RecreateEditorModes(); IEntity body = players.GetPlayerControlledEntity(id); if (body && ERR_Actors.Alive(body)) OnPlayerSpawn(id, body); PrintFormat("Reintroduced [terrain editor]: player=%1 admin=%2 limited=%3 canOpen=%4", id, SCR_Global.IsAdmin(id), editor.IsLimited(), editor.CanOpen()); } } void ERR_SetTerrainModeEvents(SCR_BaseGameMode mode, bool attach) { if (!mode || !Replication.IsServer()) return; ERR_StopTerrainEditorRetry(); mode.GetOnPlayerRegistered().Remove(OnPlayerRegistered); mode.GetOnPostCompPlayerDisconnected().Remove(OnPlayerDisconnected); mode.GetOnPlayerSpawned().Remove(OnPlayerSpawn); mode.GetOnPlayerKilled().Remove(OnPlayerKilled); mode.GetOnPlayerDeleted().Remove(OnPlayerDeleted); mode.GetOnPlayerRoleChange().Remove(OnPlayerRoleChange); if (!attach) return; mode.GetOnPlayerRegistered().Insert(OnPlayerRegistered); mode.GetOnPostCompPlayerDisconnected().Insert(OnPlayerDisconnected); mode.GetOnPlayerSpawned().Insert(OnPlayerSpawn); mode.GetOnPlayerKilled().Insert(OnPlayerKilled); mode.GetOnPlayerDeleted().Insert(OnPlayerDeleted); mode.GetOnPlayerRoleChange().Insert(OnPlayerRoleChange); Print("Reintroduced [terrain takeover]: Game Master player and admin events attached to Escape"); } } // Vanilla caches are tied to the original game mode. Release its exact // components before takeover. Some vanilla destructors unconditionally clear // their static cache later; resolve those getters from the current takeover // owner so delayed destruction cannot invalidate the replacement. modded class SCR_ArsenalManagerComponent { static void ERR_ReleaseTerrainSingleton(SCR_ArsenalManagerComponent expected) { if (s_Instance == expected) s_Instance = null; } } modded class SCR_GroupsManagerComponent { static void ERR_ReleaseTerrainSingleton(SCR_GroupsManagerComponent expected) { if (s_Instance == expected) s_Instance = null; } } modded class SCR_SupportStationManagerComponent { static void ERR_ReleaseTerrainSingleton(SCR_SupportStationManagerComponent expected) { if (s_Instance == expected) s_Instance = null; } } modded class SCR_TaskFinishHistoryManagerComponent { static void ERR_ReleaseTerrainSingleton(SCR_TaskFinishHistoryManagerComponent expected) { if (s_Instance == expected) s_Instance = null; } override static SCR_TaskFinishHistoryManagerComponent GetInstance() { ERR_TerrainTakeoverMode mode; if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (mode) return SCR_TaskFinishHistoryManagerComponent.Cast(mode.FindComponent(SCR_TaskFinishHistoryManagerComponent)); return super.GetInstance(); } } modded class SCR_TaskManagerUIComponent { static void ERR_ReleaseTerrainSingleton(SCR_TaskManagerUIComponent expected) { if (s_Instance == expected) s_Instance = null; } override static SCR_TaskManagerUIComponent GetInstance() { ERR_TerrainTakeoverMode mode; if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (mode) return SCR_TaskManagerUIComponent.Cast(mode.FindComponent(SCR_TaskManagerUIComponent)); return super.GetInstance(); } } modded class SCR_TextsTaskManagerComponent { static void ERR_ReleaseTerrainSingleton(SCR_TextsTaskManagerComponent expected) { if (s_Instance == expected) s_Instance = null; } override static SCR_TextsTaskManagerComponent GetInstance() { ERR_TerrainTakeoverMode mode; if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (mode) return SCR_TextsTaskManagerComponent.Cast(mode.FindComponent(SCR_TextsTaskManagerComponent)); return super.GetInstance(); } } modded class SCR_MapMarkerManagerComponent { override static SCR_MapMarkerManagerComponent GetInstance() { ERR_TerrainTakeoverMode mode; if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (mode) return SCR_MapMarkerManagerComponent.Cast(mode.FindComponent(SCR_MapMarkerManagerComponent)); return super.GetInstance(); } } modded class SCR_RespawnSystemComponent { override static SCR_RespawnSystemComponent GetInstance() { ERR_TerrainTakeoverMode mode; if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode()); if (mode) return SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent)); return super.GetInstance(); } } // World systems survive mode replacement too. Vanilla caches this component // once in OnInit; rebinding also wakes a system disabled by the original mode. modded class DeployMenuSystem { void ERR_BindEscapeRespawn(SCR_RespawnSystemComponent respawn) { ERR_GameMode mode = ERR_GameMode.Get(); if (!mode || !respawn || respawn != SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent))) return; if (m_RespawnSystem != respawn) { m_RespawnSystem = respawn; m_bReady = false; Print("Reintroduced [loading]: respawn animation and preparation display attached to Escape"); } Enable(true); } } // Standalone global nodes otherwise have no delivery-order relationship. During // a connected restart a faction delegate can arrive before its new mode and // faction manager. Express that dependency to replication before publishing the // newly spawned hierarchy, rather than relying on an arbitrary frame delay. class ERR_TerrainTakeoverReplication { static void CollectDelegates(SCR_DelegateFactionManagerComponent manager, array entities) { if (!manager) return; map delegates = new map(); manager.GetFactionDelegates(delegates); for (int i = 0; i < delegates.Count(); i++) { SCR_EditableFactionComponent delegate = delegates.GetElement(i); if (delegate && delegate.GetOwner() && !entities.Contains(delegate.GetOwner())) entities.Insert(delegate.GetOwner()); } } protected static bool Parent(IEntity entity, IEntity parent) { RplComponent childRpl = SCR_EntityHelper.GetEntityRplComponent(entity); RplComponent parentRpl = SCR_EntityHelper.GetEntityRplComponent(parent); if (!childRpl || !parentRpl) return false; RplNode childNode = childRpl.GetNode(); RplNode parentNode = parentRpl.GetNode(); if (!childNode || !parentNode || childNode == parentNode) return false; if (childNode.GetParent() != parentNode) childNode.SetParent(parentNode); return childNode.GetParent() == parentNode; } static bool Link(ERR_GameMode mode) { IEntity factions = GetGame().GetFactionManager(); if (!factions || !Parent(factions, mode)) return false; if (!Parent(GetGame().GetLoadoutManager(), mode) || !Parent(GetGame().GetChat(), mode)) return false; SCR_DelegateFactionManagerComponent manager = SCR_DelegateFactionManagerComponent.GetInstance(); if (!manager) return false; map delegates = new map(); manager.GetFactionDelegates(delegates); for (int i = 0; i < delegates.Count(); i++) { SCR_EditableFactionComponent delegate = delegates.GetElement(i); if (!delegate || !Parent(delegate.GetOwner(), factions)) return false; } return !delegates.IsEmpty(); } }