Retain spawn replication through admission and nearby crate use

This commit is contained in:
2026-09-20 18:00:34 +02:00
parent e8d472c6ff
commit 173faae6d4
8 changed files with 193 additions and 24 deletions
@@ -14,8 +14,9 @@ modded class SCR_SpawnRequestComponent
protected bool m_ERRScenePreloadRequired;
protected bool m_ERRSceneObserver;
protected vector m_ERRScenePosition;
protected bool m_ERRSceneReleased;
protected bool m_ERRSceneFinalRequested;
protected bool m_ERRSceneVerified;
protected ref ERR_SpawnSceneLease m_ERRSceneLease;
protected ref array<IEntity> m_ERRSceneRoots = {};
protected ref array<IEntity> m_ERRSceneStockOwners = {};
protected ref array<IEntity> m_ERRSceneStockItems = {};
@@ -31,17 +32,14 @@ modded class SCR_SpawnRequestComponent
protected void ERR_StreamScene(IEntity entity, bool normalStreaming)
{
if (!entity || !GetPlayerController()) return;
RplIdentity identity = GetPlayerController().GetRplIdentity();
if (!identity.IsValid() || identity == RplIdentity.Local()) return;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component) component.EnableStreamingConNode(identity, normalStreaming);
ERR_SpawnSceneLease.Stream(GetPlayerController(), entity, normalStreaming);
}
void ERR_BeginSpawnScene(array<IEntity> entities, bool valid, vector position)
{
if (!Replication.IsServer()) return;
ERR_EndSpawnScene();
if (m_ERRSceneLease) m_ERRSceneLease.Stop();
m_ERRScenePosition = position;
// Native StartSpawnPreload normally installs this observer. Keep streaming
// the requested position while we defer asset preload until inventory arrives.
@@ -87,7 +85,7 @@ modded class SCR_SpawnRequestComponent
m_ERRSceneEntities.Clear();
m_ERRSceneToken++;
m_ERRSceneAcknowledged = false;
m_ERRSceneReleased = false;
m_ERRSceneFinalRequested = false;
m_ERRSceneVerified = false;
m_ERRScenePreloadRequired = true;
m_ERRSceneInventoryPoll = GetGame().GetWorld().GetWorldTime() + 500;
@@ -141,24 +139,32 @@ modded class SCR_SpawnRequestComponent
{
return ERR_SpawnSceneReady() && m_ERRSceneVerified;
}
// Called only after native preload completes. Recheck under normal streaming,
// with the observer still at the spawn point, before handing over control.
bool ERR_VerifySceneRelease()
// Called only after native preload completes. Retain the scene through the
// final check and possession; normal streaming cannot yet keep every node.
bool ERR_VerifyLoadedScene()
{
if (m_ERRSceneTimedOut) return true;
if (!ERR_SpawnSceneReady() || m_ERRScenePreloadRequired || IsPreloading()) return false;
if (!m_ERRSceneReleased)
if (!m_ERRSceneFinalRequested)
{
ObserversSystem observers = GetObserversSystem();
if (observers && GetPlayerController())
observers.InsertObserverMP(GetPlayerController().GetRplIdentity(), m_ERRScenePosition[0], m_ERRScenePosition[2], null);
foreach (IEntity entity : m_ERRSceneEntities) ERR_StreamScene(entity, true);
m_ERRSceneReleased = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 preload finished; verifying storage after stream release", GetPlayerId(), m_ERRSceneToken);
m_ERRSceneFinalRequested = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 preload finished; verifying held storage", GetPlayerId(), m_ERRSceneToken);
Rpc(ERR_ConfirmClientSpawnScene, m_ERRSceneToken);
}
return m_ERRSceneVerified;
}
void ERR_RetainSpawnScene(IEntity actor)
{
if (!Replication.IsServer() || !ERR_SpawnSceneVerified() || !actor || !GetPlayerController()) return;
if (!m_ERRSceneLease) m_ERRSceneLease = new ERR_SpawnSceneLease();
m_ERRSceneLease.Start(GetPlayerController(), actor, m_ERRScenePosition, m_ERRSceneEntities);
PrintFormat("Reintroduced [spawn scene]: player=%1 retaining %2 objects until leaving the spawn area", GetPlayerId(), m_ERRSceneEntities.Count());
// Transfer ownership of the streaming hold; request cleanup must not undo it.
m_ERRSceneEntities.Clear();
}
bool ERR_SpawnSceneMayFinish()
{
if (!m_ERRScenePending) return true;
@@ -187,7 +193,7 @@ modded class SCR_SpawnRequestComponent
m_ERRSceneAcknowledged = false;
m_ERRSceneTimedOut = false;
m_ERRScenePreloadRequired = false;
m_ERRSceneReleased = false;
m_ERRSceneFinalRequested = false;
m_ERRSceneVerified = false;
}
@@ -256,7 +262,12 @@ modded class SCR_SpawnRequestComponent
foreach (RplId id : m_ERRClientSceneNodes)
{
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component || !component.GetEntity()) { m_ERRClientSceneStableSince = -1; return; }
if (!component || !component.GetEntity())
{
ERR_ReportStockWait(id, RplId.Invalid(), -1);
m_ERRClientSceneStableSince = -1;
return;
}
}
if (!ERR_ClientStockReady()) { m_ERRClientSceneStableSince = -1; return; }
if (m_ERRClientSceneFinal)
@@ -277,7 +288,7 @@ modded class SCR_SpawnRequestComponent
if (!Replication.IsServer() || !m_ERRScenePending || m_ERRSceneTimedOut || token != m_ERRSceneToken) return;
if (finalCheck)
{
if (!m_ERRSceneReleased || !m_ERRSceneAcknowledged) return;
if (!m_ERRSceneFinalRequested || !m_ERRSceneAcknowledged) return;
m_ERRSceneVerified = true;
}
else m_ERRSceneAcknowledged = true;
@@ -314,5 +325,6 @@ modded class SCR_SpawnRequestComponent
if (!GetGame()) return;
ERR_ClearClientSpawnScene();
ERR_EndSpawnScene();
if (m_ERRSceneLease) m_ERRSceneLease.Stop();
}
}
@@ -57,7 +57,7 @@ modded class SCR_FreeSpawnHandlerComponent
if (!requestComponent.ERR_SpawnSceneMayFinish()) return false;
if (requestComponent.ERR_TakeScenePreload()) super.OnFinalizeBegin_S(requestComponent, data, entity);
if (!super.CanFinalize_S(requestComponent, data, entity)) return false;
return requestComponent.ERR_VerifySceneRelease();
return requestComponent.ERR_VerifyLoadedScene();
}
return super.CanFinalize_S(requestComponent, data, entity);
}
@@ -104,6 +104,7 @@ modded class SCR_FreeSpawnHandlerComponent
return SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
}
SCR_ESpawnResult result = super.FinalizeRequest_S(requestComponent, data, entity);
if (result == SCR_ESpawnResult.OK) requestComponent.ERR_RetainSpawnScene(entity);
requestComponent.ERR_EndSpawnScene();
if (result != SCR_ESpawnResult.OK) ERR_RejectOwnedActor(requestComponent, entity, data);
return result;
@@ -0,0 +1,46 @@
// Keep the bounded spawn snapshot relevant through possession and nearby crate
// use. Releasing it before possession can stream out runtime inventory nodes.
class ERR_SpawnSceneLease
{
protected SCR_PlayerController m_Controller;
protected IEntity m_Actor;
protected vector m_Position;
protected ref array<IEntity> m_Entities = {};
static void Stream(SCR_PlayerController controller, IEntity entity, bool normalStreaming)
{
if (!controller || !entity) return;
RplIdentity identity = controller.GetRplIdentity();
if (!identity.IsValid() || identity == RplIdentity.Local()) return;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component) component.EnableStreamingConNode(identity, normalStreaming);
}
void Start(SCR_PlayerController controller, IEntity actor, vector position, array<IEntity> entities)
{
Stop();
m_Controller = controller;
m_Actor = actor;
m_Position = position;
m_Entities.Copy(entities);
GetGame().GetCallqueue().CallLater(Check, 500, true);
}
protected void Check()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !mode.Active() || !m_Controller || !m_Actor || m_Controller.GetControlledEntity() != m_Actor || vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) > 120)
Stop();
}
void Stop()
{
if (GetGame()) GetGame().GetCallqueue().Remove(Check);
foreach (IEntity entity : m_Entities) Stream(m_Controller, entity, true);
m_Entities.Clear();
m_Controller = null;
m_Actor = null;
}
void ~ERR_SpawnSceneLease() { Stop(); }
}