Recheck storage slots after spawn preload and streaming handoff

This commit is contained in:
2026-09-20 17:46:45 +02:00
parent 1201f9666f
commit e8d472c6ff
7 changed files with 200 additions and 32 deletions
@@ -13,6 +13,9 @@ modded class SCR_SpawnRequestComponent
protected float m_ERRSceneInventoryPoll;
protected bool m_ERRScenePreloadRequired;
protected bool m_ERRSceneObserver;
protected vector m_ERRScenePosition;
protected bool m_ERRSceneReleased;
protected bool m_ERRSceneVerified;
protected ref array<IEntity> m_ERRSceneRoots = {};
protected ref array<IEntity> m_ERRSceneStockOwners = {};
protected ref array<IEntity> m_ERRSceneStockItems = {};
@@ -23,6 +26,8 @@ modded class SCR_SpawnRequestComponent
protected ref array<RplId> m_ERRClientStockOwners = {};
protected ref array<RplId> m_ERRClientStockItems = {};
protected float m_ERRClientStockReportAt;
protected bool m_ERRClientSceneFinal;
protected float m_ERRClientSceneStableSince = -1;
protected void ERR_StreamScene(IEntity entity, bool normalStreaming)
{
@@ -37,6 +42,7 @@ modded class SCR_SpawnRequestComponent
{
if (!Replication.IsServer()) return;
ERR_EndSpawnScene();
m_ERRScenePosition = position;
// Native StartSpawnPreload normally installs this observer. Keep streaming
// the requested position while we defer asset preload until inventory arrives.
ObserversSystem observers = GetObserversSystem();
@@ -81,6 +87,8 @@ modded class SCR_SpawnRequestComponent
m_ERRSceneEntities.Clear();
m_ERRSceneToken++;
m_ERRSceneAcknowledged = false;
m_ERRSceneReleased = false;
m_ERRSceneVerified = false;
m_ERRScenePreloadRequired = true;
m_ERRSceneInventoryPoll = GetGame().GetWorld().GetWorldTime() + 500;
m_ERRSceneStockOwners.Copy(owners);
@@ -129,11 +137,33 @@ modded class SCR_SpawnRequestComponent
{
return m_ERRScenePending && m_ERRSceneAcknowledged && !m_ERRSceneTimedOut;
}
bool ERR_SpawnSceneVerified()
{
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()
{
if (m_ERRSceneTimedOut) return true;
if (!ERR_SpawnSceneReady() || m_ERRScenePreloadRequired || IsPreloading()) return false;
if (!m_ERRSceneReleased)
{
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);
Rpc(ERR_ConfirmClientSpawnScene, m_ERRSceneToken);
}
return m_ERRSceneVerified;
}
bool ERR_SpawnSceneMayFinish()
{
if (!m_ERRScenePending) return true;
ERR_RefreshSceneInventory();
if (!m_ERRSceneAcknowledged && !m_ERRSceneTimedOut && GetGame().GetWorld().GetWorldTime() >= m_ERRSceneDeadline)
if (!m_ERRSceneVerified && !m_ERRSceneTimedOut && GetGame().GetWorld().GetWorldTime() >= m_ERRSceneDeadline)
{
m_ERRSceneTimedOut = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 timed out; refusing assignment and retrying", GetPlayerId(), m_ERRSceneToken);
@@ -157,6 +187,8 @@ modded class SCR_SpawnRequestComponent
m_ERRSceneAcknowledged = false;
m_ERRSceneTimedOut = false;
m_ERRScenePreloadRequired = false;
m_ERRSceneReleased = false;
m_ERRSceneVerified = false;
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
@@ -192,7 +224,7 @@ modded class SCR_SpawnRequestComponent
previousOwner = ownerId;
}
RplComponent item = RplComponent.Cast(Replication.FindItem(m_ERRClientStockItems[i]));
if (!item || !item.GetEntity() || !contents.Contains(item.GetEntity()))
if (!item || !item.GetEntity() || !contents.Contains(item.GetEntity()) || !ERR_SpawnSceneInventory.InStorage(item.GetEntity()))
{
ERR_ReportStockWait(ownerId, m_ERRClientStockItems[i], contents.Count());
return false;
@@ -205,33 +237,58 @@ modded class SCR_SpawnRequestComponent
float now = GetGame().GetWorld().GetWorldTime();
if (now < m_ERRClientStockReportAt) return;
m_ERRClientStockReportAt = now + 5000;
PrintFormat("Reintroduced [spawn scene]: client token=%1 waiting for inventory owner=%2 item=%3 visible=%4", m_ERRClientSceneToken, owner, item, visible);
PrintFormat("Reintroduced [spawn scene]: client token=%1 final=%2 waiting for storage owner=%3 item=%4 managerItems=%5", m_ERRClientSceneToken, m_ERRClientSceneFinal, owner, item, visible);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_ConfirmClientSpawnScene(int token)
{
if (token != m_ERRClientSceneToken || m_ERRClientSceneNodes.IsEmpty()) return;
m_ERRClientSceneFinal = true;
m_ERRClientScenePending = true;
m_ERRClientSceneStableSince = -1;
GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene);
GetGame().GetCallqueue().CallLater(ERR_CheckClientSpawnScene, 100, true);
}
protected void ERR_CheckClientSpawnScene()
{
if (!m_ERRClientScenePending || !ERR_GameMode.Get()) { ERR_ClearClientSpawnScene(); return; }
if (!ERR_GameMode.Get()) { ERR_ClearClientSpawnScene(); return; }
if (!m_ERRClientScenePending) return;
foreach (RplId id : m_ERRClientSceneNodes)
{
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component || !component.GetEntity()) return;
if (!component || !component.GetEntity()) { m_ERRClientSceneStableSince = -1; return; }
}
if (!ERR_ClientStockReady()) { m_ERRClientSceneStableSince = -1; return; }
if (m_ERRClientSceneFinal)
{
float now = GetGame().GetWorld().GetWorldTime();
if (m_ERRClientSceneStableSince < 0) m_ERRClientSceneStableSince = now;
if (now - m_ERRClientSceneStableSince < 1000) return;
}
if (!ERR_ClientStockReady()) return;
int token = m_ERRClientSceneToken;
PrintFormat("Reintroduced [spawn scene]: client token=%1 all %2 objects and %3 inventory items ready", token, m_ERRClientSceneNodes.Count(), m_ERRClientStockItems.Count());
ERR_ClearClientSpawnScene();
Rpc(ERR_AcknowledgeSpawnScene, token);
PrintFormat("Reintroduced [spawn scene]: client token=%1 final=%2 all %3 objects and %4 storage items ready", token, m_ERRClientSceneFinal, m_ERRClientSceneNodes.Count(), m_ERRClientStockItems.Count());
GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene);
m_ERRClientScenePending = false;
Rpc(ERR_AcknowledgeSpawnScene, token, m_ERRClientSceneFinal);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_AcknowledgeSpawnScene(int token)
protected void ERR_AcknowledgeSpawnScene(int token, bool finalCheck)
{
if (!Replication.IsServer() || !m_ERRScenePending || m_ERRSceneTimedOut || token != m_ERRSceneToken) return;
m_ERRSceneAcknowledged = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 inventory acknowledged after %3ms", GetPlayerId(), token, GetGame().GetWorld().GetWorldTime() - m_ERRSceneStartedAt);
if (finalCheck)
{
if (!m_ERRSceneReleased || !m_ERRSceneAcknowledged) return;
m_ERRSceneVerified = true;
}
else m_ERRSceneAcknowledged = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 final=%3 storage acknowledged after %4ms", GetPlayerId(), token, finalCheck, GetGame().GetWorld().GetWorldTime() - m_ERRSceneStartedAt);
}
protected void ERR_ClearClientSpawnScene()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene);
m_ERRClientScenePending = false;
m_ERRClientSceneFinal = false;
m_ERRClientSceneStableSince = -1;
m_ERRClientSceneNodes.Clear();
m_ERRClientStockOwners.Clear();
m_ERRClientStockItems.Clear();
@@ -56,6 +56,8 @@ 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 super.CanFinalize_S(requestComponent, data, entity);
}
@@ -95,7 +97,7 @@ modded class SCR_FreeSpawnHandlerComponent
if (logic) ticket = logic.TakeSpawnTicket(requestComponent, data, entity);
// No ticket means no ownership proof: reject without deleting arbitrary input.
if (!ticket) return SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
if (!requestComponent.ERR_SpawnSceneReady() || !logic.MayFinalizeSpawn(ticket))
if (!requestComponent.ERR_SpawnSceneVerified() || !logic.MayFinalizeSpawn(ticket))
{
requestComponent.ERR_EndSpawnScene();
ERR_RejectOwnedActor(requestComponent, entity, data);
@@ -2,6 +2,22 @@
// prisoner may already have taken loot when a late arrival joins the round.
class ERR_SpawnSceneInventory
{
// Manager membership alone does not establish that the storage UI can read
// the item. Check both directions of the replicated slot and the UI's GetAll.
static bool InStorage(IEntity entity)
{
if (!entity) return false;
InventoryItemComponent item = InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent));
if (!item) return false;
InventoryStorageSlot slot = item.GetParentSlot();
if (!slot) return false;
BaseInventoryStorageComponent storage = slot.GetStorage();
if (!storage || storage.FindItemSlot(entity) != slot || storage.Get(slot.GetID()) != entity) return false;
array<IEntity> visible = {};
storage.GetAll(visible);
return visible.Contains(entity);
}
static bool Gather(array<IEntity> roots, out array<IEntity> owners, out array<IEntity> items)
{
owners = {}; items = {};