Preserve native chat during terrain takeover with fallback for missing chat
This commit is contained in:
@@ -16,9 +16,5 @@ ERR_TerrainTakeoverMode : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
|
|||||||
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
|
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
|
||||||
ID "588AD55F7655278B"
|
ID "588AD55F7655278B"
|
||||||
}
|
}
|
||||||
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
|
|
||||||
ID "85ED17B0836DB4A2"
|
|
||||||
components { RplComponent "{CA456708D69D73BA}" { Streamable Disabled } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ERR_TerrainTakeoverMode : "{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et" {
|
||||||
|
ID "CE902374A658BDF1"
|
||||||
|
{
|
||||||
|
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
|
||||||
|
ID "85ED17B0836DB4A2"
|
||||||
|
components { RplComponent "{CA456708D69D73BA}" { Streamable Disabled } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
MetaFileClass {
|
||||||
|
Name "{DA8E364BC095712F}Prefabs/ERR/TerrainTakeoverWithChat.et"
|
||||||
|
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} }
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@ enum ERR_TerrainTakeoverStage
|
|||||||
|
|
||||||
class ERR_TerrainTakeoverRules
|
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 TimedOut(float elapsed) { return elapsed >= 10; }
|
||||||
static bool MayDrain(float elapsed, bool sameMode) { return elapsed >= 0.5 && sameMode; }
|
static bool MayDrain(float elapsed, bool sameMode) { return elapsed >= 0.5 && sameMode; }
|
||||||
static bool MayCreate(float elapsed, bool empty) { return empty && !TimedOut(elapsed); }
|
static bool MayCreate(float elapsed, bool empty) { return empty && !TimedOut(elapsed); }
|
||||||
@@ -68,6 +73,7 @@ modded class ArmaReforgerScripted
|
|||||||
protected ERR_TerrainTakeoverStage m_ERRTerrainStage;
|
protected ERR_TerrainTakeoverStage m_ERRTerrainStage;
|
||||||
protected float m_ERRTerrainElapsed;
|
protected float m_ERRTerrainElapsed;
|
||||||
protected RadioManagerEntity m_ERRTerrainRadio;
|
protected RadioManagerEntity m_ERRTerrainRadio;
|
||||||
|
protected BaseChatEntity m_ERRTerrainChat;
|
||||||
protected EntityID m_ERRTerrainOldLoadoutId;
|
protected EntityID m_ERRTerrainOldLoadoutId;
|
||||||
protected BaseWorld m_ERRTerrainWorld;
|
protected BaseWorld m_ERRTerrainWorld;
|
||||||
protected SCR_BaseGameMode m_ERRTerrainOriginal;
|
protected SCR_BaseGameMode m_ERRTerrainOriginal;
|
||||||
@@ -110,6 +116,7 @@ modded class ArmaReforgerScripted
|
|||||||
m_ERRTerrainStage = ERR_TerrainTakeoverStage.INACTIVE;
|
m_ERRTerrainStage = ERR_TerrainTakeoverStage.INACTIVE;
|
||||||
m_ERRTerrainElapsed = 0;
|
m_ERRTerrainElapsed = 0;
|
||||||
m_ERRTerrainRadio = null;
|
m_ERRTerrainRadio = null;
|
||||||
|
m_ERRTerrainChat = null;
|
||||||
m_ERRTerrainOldLoadoutId = EntityID.INVALID;
|
m_ERRTerrainOldLoadoutId = EntityID.INVALID;
|
||||||
m_ERRTerrainWorld = null;
|
m_ERRTerrainWorld = null;
|
||||||
m_ERRTerrainOriginal = 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.");
|
ERR_FailTerrainTakeover("Select a plain scenario with SCR_BaseGameMode and a Soldiers navmesh; original scenario retained.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Resource prefab = Resource.Load("{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et");
|
Resource prefab = Resource.Load(ERR_TerrainTakeoverRules.Bootstrap(GetChat() != null));
|
||||||
if (!prefab || !prefab.IsValid())
|
if (!prefab || !prefab.IsValid())
|
||||||
{
|
{
|
||||||
ERR_FailTerrainTakeover("Escape bootstrap prefab is missing; original scenario retained.");
|
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));
|
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
|
||||||
if (editorCore) editorCore.ERR_SetTerrainModeEvents(m_ERRTerrainOriginal, false);
|
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);
|
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());
|
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
|
||||||
if (chimera) m_ERRTerrainRadio = chimera.GetRadioManager();
|
if (chimera) m_ERRTerrainRadio = chimera.GetRadioManager();
|
||||||
// Native radio teardown clears the world pointer even after a replacement
|
// Native radio teardown clears the world pointer even after a replacement
|
||||||
@@ -230,7 +242,7 @@ modded class ArmaReforgerScripted
|
|||||||
ancestor = ancestor.GetParent();
|
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();
|
m_ERRTerrainOriginal.OnGameEnd();
|
||||||
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
|
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
|
||||||
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
|
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
|
||||||
@@ -258,7 +270,7 @@ modded class ArmaReforgerScripted
|
|||||||
protected bool ERR_TerrainRegistrationsEmpty()
|
protected bool ERR_TerrainRegistrationsEmpty()
|
||||||
{
|
{
|
||||||
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
|
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()
|
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; }
|
if (!marker) { ERR_FailTerrainTakeover("Could not create configured airport marker; restart the scenario."); return; }
|
||||||
marker.SetName("ERR_Airfield_" + m_ERRTerrainMarkerSlots[i].ToString());
|
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 (!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));
|
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
|
||||||
if (editorCore) editorCore.ERR_SetTerrainModeEvents(mode, true);
|
if (editorCore) editorCore.ERR_SetTerrainModeEvents(mode, true);
|
||||||
m_ERRTerrainStage = ERR_TerrainTakeoverStage.RUNNING;
|
m_ERRTerrainStage = ERR_TerrainTakeoverStage.RUNNING;
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ modded class ArmaReforgerScripted
|
|||||||
m_ERRClientOldManagers.Insert(GetLoadoutManager());
|
m_ERRClientOldManagers.Insert(GetLoadoutManager());
|
||||||
m_ERRClientOldManagers.Insert(GetFactionManager());
|
m_ERRClientOldManagers.Insert(GetFactionManager());
|
||||||
m_ERRClientOldManagers.Insert(GetPerceptionManager());
|
m_ERRClientOldManagers.Insert(GetPerceptionManager());
|
||||||
m_ERRClientOldManagers.Insert(GetChat());
|
|
||||||
}
|
}
|
||||||
override void UnregisterLoadoutManager(SCR_LoadoutManager instance)
|
override void UnregisterLoadoutManager(SCR_LoadoutManager instance)
|
||||||
{
|
{
|
||||||
@@ -89,6 +88,13 @@ modded class ArmaReforgerScripted
|
|||||||
void ERR_BeginClientTerrainTakeover(ERR_TerrainTakeoverMode incoming)
|
void ERR_BeginClientTerrainTakeover(ERR_TerrainTakeoverMode incoming)
|
||||||
{
|
{
|
||||||
if (m_ERRClientOldManagers.IsEmpty()) return;
|
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;
|
m_ERRClientTerrainTakeover = true;
|
||||||
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
|
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
|
||||||
PrintFormat("Reintroduced [terrain client]: drain retained mode=%1 incoming=%2", mode, incoming);
|
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());
|
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)
|
protected static bool Parent(IEntity entity, IEntity parent)
|
||||||
{
|
{
|
||||||
RplComponent childRpl = SCR_EntityHelper.GetEntityRplComponent(entity);
|
RplComponent childRpl = SCR_EntityHelper.GetEntityRplComponent(entity);
|
||||||
@@ -267,11 +286,14 @@ class ERR_TerrainTakeoverReplication
|
|||||||
if (childNode.GetParent() != parentNode) childNode.SetParent(parentNode);
|
if (childNode.GetParent() != parentNode) childNode.SetParent(parentNode);
|
||||||
return childNode.GetParent() == parentNode;
|
return childNode.GetParent() == parentNode;
|
||||||
}
|
}
|
||||||
static bool Link(ERR_GameMode mode)
|
static bool Link(ERR_GameMode mode, bool ownsChat)
|
||||||
{
|
{
|
||||||
IEntity factions = GetGame().GetFactionManager();
|
IEntity factions = GetGame().GetFactionManager();
|
||||||
if (!factions || !Parent(factions, mode)) return false;
|
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();
|
SCR_DelegateFactionManagerComponent manager = SCR_DelegateFactionManagerComponent.GetInstance();
|
||||||
if (!manager) return false;
|
if (!manager) return false;
|
||||||
map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
|
map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
|
||||||
|
|||||||
@@ -240,8 +240,12 @@ account admission. Publication and live-server changes remain with the owner.
|
|||||||
- The private `Prefabs/ERR/TerrainTakeover.et` triggers client cleanup. It releases
|
- The private `Prefabs/ERR/TerrainTakeover.et` triggers client cleanup. It releases
|
||||||
only the captured original singleton components, waits for the new factions
|
only the captured original singleton components, waits for the new factions
|
||||||
before catalog/identity initialization, and receives the server's faction
|
before catalog/identity initialization, and receives the server's faction
|
||||||
delegates. Chat is replicated to every client. The ordinary authored connector
|
delegates. The terrain's native chat service is retained on both peers, detached
|
||||||
remains separate from this bootstrap.
|
from any removed entity/replication parent, and never replaced or attached to
|
||||||
|
the new mode. The ordinary authored connector retains its own chat service.
|
||||||
|
If the terrain has no chat service, `TerrainTakeoverWithChat.et` supplies a
|
||||||
|
non-streaming replicated chat child instead. Selection depends on service
|
||||||
|
presence, not terrain name; both variants share the same takeover mode.
|
||||||
- Arsenal, group, support-station and task managers release the original mode's
|
- Arsenal, group, support-station and task managers release the original mode's
|
||||||
cached instances on both peers. Task, map-marker and respawn getters resolve
|
cached instances on both peers. Task, map-marker and respawn getters resolve
|
||||||
the current takeover mode even if an old destructor clears a shared cache.
|
the current takeover mode even if an old destructor clears a shared cache.
|
||||||
@@ -260,6 +264,18 @@ account admission. Publication and live-server changes remain with the owner.
|
|||||||
a separate connected Windows client. The connected test recreates the player's
|
a separate connected Windows client. The connected test recreates the player's
|
||||||
controller and verifies the replacement managers in both rounds.
|
controller and verifies the replacement managers in both rounds.
|
||||||
|
|
||||||
|
On 14 October, the original Everon client log showed delayed removal of the
|
||||||
|
original `ScriptedChatEntity` after the replacement registered, followed by
|
||||||
|
`Cannot send message - ChatEntity is missing.` This blocked native chat commands,
|
||||||
|
including administrator login, before authentication. `test_terrain_chat_retention.py`
|
||||||
|
checks shared-service retention, nested parent detachment and fallback selection.
|
||||||
|
The revised scripts compiled in Workbench; the authored-world native restart
|
||||||
|
smoke completed two rounds and 21 assertions with no script/runtime failures.
|
||||||
|
That run still reported existing engine/dependency errors, and the full repository
|
||||||
|
check stopped on the existing missing root ID in `ERRM_AtlasLeaf.et`.
|
||||||
|
Live remote login and takeover of both chat variants remain separate verification
|
||||||
|
steps; the authored-world smoke does not exercise those paths.
|
||||||
|
|
||||||
The earlier isolated prototype passed a Nogova Workbench prisoner-spawn test with
|
The earlier isolated prototype passed a Nogova Workbench prisoner-spawn test with
|
||||||
16 generated sites and zero script errors. See
|
16 generated sites and zero script errors. See
|
||||||
[prototype evidence](evidence/scenario-takeover-2026-10-01/report.json). The current
|
[prototype evidence](evidence/scenario-takeover-2026-10-01/report.json). The current
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Native chat stays world-owned across server/client terrain handover."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
|
||||||
|
core = ROOT / 'addons/core/Scripts/Game/ERR/00_Core'
|
||||||
|
server = core / 'ERR_TerrainTakeover.c'
|
||||||
|
client = core / 'ERR_TerrainTakeoverClient.c'
|
||||||
|
managers = core / 'ERR_TerrainTakeoverManagers.c'
|
||||||
|
run('#include <cassert>\n#include <string>\nusing ResourceName=std::string;\n' +
|
||||||
|
body(server, 'static ResourceName Bootstrap(') + r'''
|
||||||
|
int main(){
|
||||||
|
assert(Bootstrap(true)=="{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et");
|
||||||
|
assert(Bootstrap(false)=="{DA8E364BC095712F}Prefabs/ERR/TerrainTakeoverWithChat.et");
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
retain = body(managers, 'static bool RetainManager(')
|
||||||
|
retain = (retain.replace('IEntity entity', 'IEntity* entity')
|
||||||
|
.replace('IEntity ancestor', 'IEntity* ancestor')
|
||||||
|
.replace('array<IEntity> removed', 'Entities* removed')
|
||||||
|
.replace('RplComponent rpl', 'RplComponent* rpl')
|
||||||
|
.replace('SCR_EntityHelper.', 'SCR_EntityHelper::')
|
||||||
|
.replace('null', 'nullptr'))
|
||||||
|
for value in ('entity', 'ancestor', 'removed', 'rpl', 'GetParent()', 'GetNode()'):
|
||||||
|
retain = retain.replace(value + '.', value + '->')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
struct Node {Node* parent=nullptr;int changes=0;void SetParent(Node* p){parent=p;changes++;}};
|
||||||
|
struct RplComponent {Node* node=nullptr;Node* GetNode(){return node;}};
|
||||||
|
struct IEntity {
|
||||||
|
IEntity* parent=nullptr;RplComponent* rpl=nullptr;bool refuseDetach=false;int removals=0;
|
||||||
|
IEntity* GetParent(){return parent;}
|
||||||
|
void RemoveChild(IEntity* child,bool keepWorld){assert(keepWorld&&child->parent==this);removals++;if(!refuseDetach)child->parent=nullptr;}
|
||||||
|
};
|
||||||
|
struct Entities:std::vector<IEntity*> {
|
||||||
|
using std::vector<IEntity*>::vector;
|
||||||
|
bool Contains(IEntity* entity){return std::find(begin(),end(),entity)!=end();}
|
||||||
|
};
|
||||||
|
namespace SCR_EntityHelper {RplComponent* GetEntityRplComponent(IEntity* e){return e->rpl;}}
|
||||||
|
''' + retain + r'''
|
||||||
|
int main(){
|
||||||
|
IEntity oldMode,chat,intermediate,unrelated;Entities removed{&oldMode,nullptr};
|
||||||
|
Node oldNode,chatNode;chatNode.parent=&oldNode;RplComponent chatRpl{&chatNode};chat.rpl=&chatRpl;
|
||||||
|
// Ordinary terrain chat is standalone: no unregister, replacement or reparent.
|
||||||
|
assert(RetainManager(&chat,&removed)&&chat.parent==nullptr&&chatNode.changes==0);
|
||||||
|
// Nested chat is detached before old hierarchy deletion; replication cannot
|
||||||
|
// later remove the retained service with the discarded mode either.
|
||||||
|
intermediate.parent=&oldMode;chat.parent=&intermediate;
|
||||||
|
assert(RetainManager(&chat,&removed)&&chat.parent==nullptr&&intermediate.removals==1);
|
||||||
|
assert(chatNode.parent==nullptr&&chatNode.changes==1);
|
||||||
|
assert(RetainManager(&chat,&removed)&&chatNode.changes==1);
|
||||||
|
chat.parent=&unrelated;assert(RetainManager(&chat,&removed)&&chat.parent==&unrelated&&unrelated.removals==0);
|
||||||
|
assert(!RetainManager(nullptr,&removed)&&!RetainManager(&chat,nullptr));
|
||||||
|
removed.push_back(&chat);assert(!RetainManager(&chat,&removed)&&chat.parent==&unrelated);removed.pop_back();
|
||||||
|
oldMode.refuseDetach=true;chat.parent=&oldMode;int changes=chatNode.changes;
|
||||||
|
assert(!RetainManager(&chat,&removed)&&chat.parent==&oldMode&&chatNode.changes==changes);
|
||||||
|
oldMode.refuseDetach=false;chat.rpl=nullptr;assert(RetainManager(&chat,&removed)&&chat.parent==nullptr);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
|
||||||
|
drain = body(server, 'protected void ERR_DrainTerrainMode(')
|
||||||
|
capture = body(client, 'void ERR_ClientCaptureOriginal(')
|
||||||
|
handover = body(client, 'void ERR_BeginClientTerrainTakeover(')
|
||||||
|
assert 'GetChat()' not in drain.split('array<IEntity> managers =', 1)[1].split(';', 1)[0]
|
||||||
|
assert 'm_ERRClientOldManagers.Insert(GetChat())' not in capture
|
||||||
|
assert drain.index('RetainManager(m_ERRTerrainChat, managers)') < drain.index('m_ERRTerrainOriginal.OnGameEnd()')
|
||||||
|
assert handover.index('RetainManager(GetChat(), m_ERRClientOldManagers)') < handover.index('mode.OnGameEnd()')
|
||||||
|
assert 'GetChat() == m_ERRTerrainChat' in body(server, 'protected bool ERR_TerrainRegistrationsEmpty(')
|
||||||
|
assert 'm_ERRTerrainChat = null' in body(server, 'protected void ERR_ResetTerrainTakeover(')
|
||||||
|
preflight = body(server, 'protected bool ERR_PreflightTerrainTakeover(')
|
||||||
|
create = body(server, 'protected void ERR_CreateTerrainMode(')
|
||||||
|
assert 'Bootstrap(GetChat() != null)' in preflight
|
||||||
|
assert 'if (!GetChat())' not in preflight
|
||||||
|
assert 'Bootstrap(m_ERRTerrainChat != null)' in create
|
||||||
|
assert 'Link(mode, !m_ERRTerrainChat)' in create
|
||||||
|
assert 'if (m_ERRTerrainChat && !ERR_TerrainTakeoverReplication.RetainManager' in drain
|
||||||
|
assert 'if (GetChat() && !ERR_TerrainTakeoverReplication.RetainManager' in handover
|
||||||
|
assert '&& m_ERRTerrainChat &&' not in body(server, 'protected bool ERR_TerrainRegistrationsEmpty(')
|
||||||
|
assert 'if (ownsChat && !Parent(GetGame().GetChat(), mode)) return false' in body(managers, 'static bool Link(')
|
||||||
|
prefabs = ROOT / 'addons/core/Prefabs/ERR'
|
||||||
|
assert 'ScriptedChatEntity' not in (prefabs / 'TerrainTakeover.et').read_text()
|
||||||
|
fallback = (prefabs / 'TerrainTakeoverWithChat.et').read_text()
|
||||||
|
assert '"{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et"' in fallback
|
||||||
|
assert fallback.count('ScriptedChatEntity Chat') == 1
|
||||||
|
assert 'Streamable Disabled' in fallback
|
||||||
|
assert '{DA8E364BC095712F}Prefabs/ERR/TerrainTakeoverWithChat.et' in (prefabs / 'TerrainTakeoverWithChat.et.meta').read_text()
|
||||||
|
assert 'ScriptedChatEntity' in (prefabs / 'TerrainConnector.et').read_text()
|
||||||
|
assert 'ScriptedChatEntity' in (ROOT / 'addons/core/Worlds/ERR_Everon_Layers/default.layer').read_text()
|
||||||
|
print('PASS: native chat retained on both peers, nested ownership detached, absent chat supplied by replicated fallback, authored mission chat preserved')
|
||||||
@@ -8,6 +8,7 @@ from prefab_identity import check_prefabs
|
|||||||
ROOT=Path(__file__).resolve().parents[1]
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
BASE='58D0FB3206B6F859'
|
BASE='58D0FB3206B6F859'
|
||||||
def check():
|
def check():
|
||||||
|
subprocess.run([sys.executable,str(ROOT/'tests/test_terrain_chat_retention.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_empty_server_restart.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_empty_server_restart.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_factions.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_factions.py')],check=True)
|
||||||
subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_landing.py')],check=True)
|
subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_landing.py')],check=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user