Preserve native chat during terrain takeover with fallback for missing chat

This commit is contained in:
2026-10-14 14:39:55 +02:00
parent e4ca37dfc1
commit 01994ed344
9 changed files with 170 additions and 15 deletions
@@ -11,6 +11,11 @@ enum ERR_TerrainTakeoverStage
class ERR_TerrainTakeoverRules
{
static ResourceName Bootstrap(bool hasNativeChat)
{
if (hasNativeChat) return "{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et";
return "{DA8E364BC095712F}Prefabs/ERR/TerrainTakeoverWithChat.et";
}
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 empty && !TimedOut(elapsed); }
@@ -68,6 +73,7 @@ modded class ArmaReforgerScripted
protected ERR_TerrainTakeoverStage m_ERRTerrainStage;
protected float m_ERRTerrainElapsed;
protected RadioManagerEntity m_ERRTerrainRadio;
protected BaseChatEntity m_ERRTerrainChat;
protected EntityID m_ERRTerrainOldLoadoutId;
protected BaseWorld m_ERRTerrainWorld;
protected SCR_BaseGameMode m_ERRTerrainOriginal;
@@ -110,6 +116,7 @@ modded class ArmaReforgerScripted
m_ERRTerrainStage = ERR_TerrainTakeoverStage.INACTIVE;
m_ERRTerrainElapsed = 0;
m_ERRTerrainRadio = null;
m_ERRTerrainChat = null;
m_ERRTerrainOldLoadoutId = EntityID.INVALID;
m_ERRTerrainWorld = null;
m_ERRTerrainOriginal = null;
@@ -130,7 +137,7 @@ modded class ArmaReforgerScripted
ERR_FailTerrainTakeover("Select a plain scenario with SCR_BaseGameMode and a Soldiers navmesh; original scenario retained.");
return false;
}
Resource prefab = Resource.Load("{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et");
Resource prefab = Resource.Load(ERR_TerrainTakeoverRules.Bootstrap(GetChat() != null));
if (!prefab || !prefab.IsValid())
{
ERR_FailTerrainTakeover("Escape bootstrap prefab is missing; original scenario retained.");
@@ -210,8 +217,13 @@ modded class ArmaReforgerScripted
{
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
if (editorCore) editorCore.ERR_SetTerrainModeEvents(m_ERRTerrainOriginal, false);
array<IEntity> managers = {m_ERRTerrainOriginal, GetLoadoutManager(), GetFactionManager(), GetPerceptionManager(), GetChat()};
array<IEntity> managers = {m_ERRTerrainOriginal, GetLoadoutManager(), GetFactionManager(), GetPerceptionManager()};
ERR_TerrainTakeoverReplication.CollectDelegates(SCR_DelegateFactionManagerComponent.GetInstance(), managers);
// Native chat unregister can clear a newer registration after replication
// delivers the replacement. Keep the terrain's chat on both peers instead.
m_ERRTerrainChat = GetChat();
if (m_ERRTerrainChat && !ERR_TerrainTakeoverReplication.RetainManager(m_ERRTerrainChat, managers))
{ ERR_FailTerrainTakeover("Could not retain the native chat service; original scenario retained."); return; }
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
if (chimera) m_ERRTerrainRadio = chimera.GetRadioManager();
// Native radio teardown clears the world pointer even after a replacement
@@ -230,7 +242,7 @@ modded class ArmaReforgerScripted
ancestor = ancestor.GetParent();
}
}
Print("Reintroduced [terrain takeover]: stopping original scenario; preserving radio manager");
Print("Reintroduced [terrain takeover]: stopping original scenario; preserving radio and chat managers");
m_ERRTerrainOriginal.OnGameEnd();
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
@@ -258,7 +270,7 @@ modded class ArmaReforgerScripted
protected bool ERR_TerrainRegistrationsEmpty()
{
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
return !GetGameMode() && !GetLoadoutManager() && !GetFactionManager() && !GetPerceptionManager() && !GetChat() && (!chimera || chimera.GetRadioManager() == m_ERRTerrainRadio) && !SCR_EntityCatalogManagerComponent.GetInstance() && !m_DataCollectorComponent;
return !GetGameMode() && !GetLoadoutManager() && !GetFactionManager() && !GetPerceptionManager() && GetChat() == m_ERRTerrainChat && (!chimera || chimera.GetRadioManager() == m_ERRTerrainRadio) && !SCR_EntityCatalogManagerComponent.GetInstance() && !m_DataCollectorComponent;
}
protected void ERR_CreateTerrainMode()
{
@@ -277,9 +289,9 @@ modded class ArmaReforgerScripted
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("{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et"), GetWorld()));
ERR_GameMode mode = ERR_GameMode.Cast(SpawnEntityPrefab(Resource.Load(ERR_TerrainTakeoverRules.Bootstrap(m_ERRTerrainChat != null)), GetWorld()));
if (!mode || GetGameMode() != mode) { ERR_FailTerrainTakeover("Escape game mode could not register; restart the scenario."); return; }
if (!ERR_TerrainTakeoverReplication.Link(mode)) { ERR_FailTerrainTakeover("Escape replication hierarchy could not register; restart the scenario."); return; }
if (!ERR_TerrainTakeoverReplication.Link(mode, !m_ERRTerrainChat)) { ERR_FailTerrainTakeover("Escape replication hierarchy could not register; restart the scenario."); return; }
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
if (editorCore) editorCore.ERR_SetTerrainModeEvents(mode, true);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.RUNNING;
@@ -57,7 +57,6 @@ modded class ArmaReforgerScripted
m_ERRClientOldManagers.Insert(GetLoadoutManager());
m_ERRClientOldManagers.Insert(GetFactionManager());
m_ERRClientOldManagers.Insert(GetPerceptionManager());
m_ERRClientOldManagers.Insert(GetChat());
}
override void UnregisterLoadoutManager(SCR_LoadoutManager instance)
{
@@ -89,6 +88,13 @@ modded class ArmaReforgerScripted
void ERR_BeginClientTerrainTakeover(ERR_TerrainTakeoverMode incoming)
{
if (m_ERRClientOldManagers.IsEmpty()) return;
// Match authority retention before deleting any original parent. No second
// chat entity is created when the terrain already supplies one.
if (GetChat() && !ERR_TerrainTakeoverReplication.RetainManager(GetChat(), m_ERRClientOldManagers))
{
Print("Reintroduced [terrain client]: could not retain native chat; original managers retained", LogLevel.ERROR);
return;
}
m_ERRClientTerrainTakeover = true;
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
PrintFormat("Reintroduced [terrain client]: drain retained mode=%1 incoming=%2", mode, incoming);
@@ -256,6 +256,25 @@ class ERR_TerrainTakeoverReplication
if (delegate && delegate.GetOwner() && !entities.Contains(delegate.GetOwner())) entities.Insert(delegate.GetOwner());
}
}
// Shared world services must survive removal of an old game-mode hierarchy.
static bool RetainManager(IEntity entity, array<IEntity> removed)
{
if (!entity || !removed || removed.Contains(entity)) return false;
IEntity ancestor = entity.GetParent();
while (ancestor)
{
if (removed.Contains(ancestor))
{
entity.GetParent().RemoveChild(entity, true);
if (entity.GetParent()) return false;
RplComponent rpl = SCR_EntityHelper.GetEntityRplComponent(entity);
if (rpl && rpl.GetNode()) rpl.GetNode().SetParent(null);
return true;
}
ancestor = ancestor.GetParent();
}
return true;
}
protected static bool Parent(IEntity entity, IEntity parent)
{
RplComponent childRpl = SCR_EntityHelper.GetEntityRplComponent(entity);
@@ -267,11 +286,14 @@ class ERR_TerrainTakeoverReplication
if (childNode.GetParent() != parentNode) childNode.SetParent(parentNode);
return childNode.GetParent() == parentNode;
}
static bool Link(ERR_GameMode mode)
static bool Link(ERR_GameMode mode, bool ownsChat)
{
IEntity factions = GetGame().GetFactionManager();
if (!factions || !Parent(factions, mode)) return false;
if (!Parent(GetGame().GetLoadoutManager(), mode) || !Parent(GetGame().GetChat(), mode)) return false;
if (!Parent(GetGame().GetLoadoutManager(), mode)) return false;
// Retained native chat stays world-owned. Only a newly supplied fallback
// belongs to this replacement mode's replication node.
if (ownsChat && !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>();