Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
287 lines
11 KiB
Plaintext
287 lines
11 KiB
Plaintext
// 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<SCR_EditorManagerEntity> 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<int> 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<IEntity> entities)
|
|
{
|
|
if (!manager) return;
|
|
map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
|
|
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<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
|
|
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();
|
|
}
|
|
}
|