Wait for client inventory readiness before player admission

This commit is contained in:
2026-09-20 17:31:33 +02:00
parent cab998fb64
commit 1201f9666f
8 changed files with 362 additions and 25 deletions
@@ -1,6 +1,7 @@
// A native resource preload can finish before runtime replication delivers the
// prison. Keep the existing admission pending until the owner resolves every
// required replication node. No client can choose the server's spawn contents.
// required replication node and usable inventory item, then preloads its assets.
// No client can choose the server's spawn contents.
modded class SCR_SpawnRequestComponent
{
protected int m_ERRSceneToken;
@@ -8,10 +9,20 @@ modded class SCR_SpawnRequestComponent
protected bool m_ERRSceneAcknowledged;
protected bool m_ERRSceneTimedOut;
protected float m_ERRSceneDeadline;
protected float m_ERRSceneStartedAt;
protected float m_ERRSceneInventoryPoll;
protected bool m_ERRScenePreloadRequired;
protected bool m_ERRSceneObserver;
protected ref array<IEntity> m_ERRSceneRoots = {};
protected ref array<IEntity> m_ERRSceneStockOwners = {};
protected ref array<IEntity> m_ERRSceneStockItems = {};
protected ref array<IEntity> m_ERRSceneEntities = {};
protected int m_ERRClientSceneToken;
protected bool m_ERRClientScenePending;
protected ref array<RplId> m_ERRClientSceneNodes = {};
protected ref array<RplId> m_ERRClientStockOwners = {};
protected ref array<RplId> m_ERRClientStockItems = {};
protected float m_ERRClientStockReportAt;
protected void ERR_StreamScene(IEntity entity, bool normalStreaming)
{
@@ -22,23 +33,67 @@ modded class SCR_SpawnRequestComponent
if (component) component.EnableStreamingConNode(identity, normalStreaming);
}
void ERR_BeginSpawnScene(array<IEntity> entities, bool valid)
void ERR_BeginSpawnScene(array<IEntity> entities, bool valid, vector position)
{
if (!Replication.IsServer()) return;
ERR_EndSpawnScene();
m_ERRSceneToken++;
// Native StartSpawnPreload normally installs this observer. Keep streaming
// the requested position while we defer asset preload until inventory arrives.
ObserversSystem observers = GetObserversSystem();
if (observers && GetPlayerController())
{
observers.InsertObserverMP(GetPlayerController().GetRplIdentity(), position[0], position[2], null);
m_ERRSceneObserver = true;
}
m_ERRScenePending = true;
m_ERRSceneDeadline = GetGame().GetWorld().GetWorldTime() + 60000;
array<RplId> nodes = {};
m_ERRSceneStartedAt = GetGame().GetWorld().GetWorldTime();
m_ERRSceneDeadline = m_ERRSceneStartedAt + 60000;
foreach (IEntity entity : entities)
{
RplComponent component;
if (entity) component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component || component.Id() == RplId.Invalid()) { valid = false; break; }
if (nodes.Contains(component.Id())) continue;
nodes.Insert(component.Id());
m_ERRSceneEntities.Insert(entity);
ERR_StreamScene(entity, false);
if (!entity) { valid = false; continue; }
// Inventory contents are captured through their manager below. They may
// have local/world origins unrelated to the container and may be consumed.
InventoryItemComponent item = InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent));
if (item && item.GetParentSlot()) continue;
if (!m_ERRSceneRoots.Contains(entity)) m_ERRSceneRoots.Insert(entity);
}
array<IEntity> owners, items;
if (!ERR_SpawnSceneInventory.Gather(m_ERRSceneRoots, owners, items)) valid = false;
ERR_PublishSpawnScene(owners, items, valid);
}
protected bool ERR_AddSceneNode(IEntity entity, array<RplId> nodes)
{
RplComponent component;
if (entity) component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component || component.Id() == RplId.Invalid()) return false;
if (nodes.Contains(component.Id())) return true;
if (nodes.Count() >= 256) return false;
nodes.Insert(component.Id());
m_ERRSceneEntities.Insert(entity);
ERR_StreamScene(entity, false);
return true;
}
protected void ERR_PublishSpawnScene(array<IEntity> owners, array<IEntity> items, bool valid)
{
foreach (IEntity previous : m_ERRSceneEntities) ERR_StreamScene(previous, true);
m_ERRSceneEntities.Clear();
m_ERRSceneToken++;
m_ERRSceneAcknowledged = false;
m_ERRScenePreloadRequired = true;
m_ERRSceneInventoryPoll = GetGame().GetWorld().GetWorldTime() + 500;
m_ERRSceneStockOwners.Copy(owners);
m_ERRSceneStockItems.Copy(items);
array<RplId> nodes = {};
array<RplId> stockOwners = {}, stockItems = {};
foreach (IEntity root : m_ERRSceneRoots)
if (!ERR_AddSceneNode(root, nodes)) valid = false;
for (int i = 0; i < items.Count(); i++)
{
if (!ERR_AddSceneNode(owners[i], nodes) || !ERR_AddSceneNode(items[i], nodes)) { valid = false; break; }
stockOwners.Insert(RplComponent.Cast(owners[i].FindComponent(RplComponent)).Id());
stockItems.Insert(RplComponent.Cast(items[i].FindComponent(RplComponent)).Id());
}
if (!valid)
{
@@ -46,8 +101,28 @@ modded class SCR_SpawnRequestComponent
Print("Reintroduced [spawn scene]: invalid server scene snapshot; admission withheld", LogLevel.ERROR);
return;
}
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 waiting for %3 replicated objects", GetPlayerId(), m_ERRSceneToken, nodes.Count());
Rpc(ERR_ReceiveSpawnScene, m_ERRSceneToken, nodes);
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 waiting for %3 replicated objects and %4 inventory items", GetPlayerId(), m_ERRSceneToken, nodes.Count(), stockItems.Count());
Rpc(ERR_ReceiveSpawnScene, m_ERRSceneToken, nodes, stockOwners, stockItems);
}
protected void ERR_RefreshSceneInventory()
{
if (!Replication.IsServer() || !m_ERRScenePending || m_ERRSceneTimedOut || GetGame().GetWorld().GetWorldTime() < m_ERRSceneInventoryPoll) return;
m_ERRSceneInventoryPoll = GetGame().GetWorld().GetWorldTime() + 500;
array<IEntity> owners, items;
bool valid = ERR_SpawnSceneInventory.Gather(m_ERRSceneRoots, owners, items);
if (!valid || !ERR_SpawnSceneInventory.Same(owners, items, m_ERRSceneStockOwners, m_ERRSceneStockItems))
ERR_PublishSpawnScene(owners, items, valid);
}
// Start native resource preload only after the client has the actual scene
// and usable inventory links. A refreshed manifest requires a fresh pass.
bool ERR_TakeScenePreload()
{
if (!ERR_SpawnSceneReady() || !m_ERRScenePreloadRequired || IsPreloading()) return false;
m_ERRScenePreloadRequired = false;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 scene ready after %3ms; starting asset preload", GetPlayerId(), m_ERRSceneToken, GetGame().GetWorld().GetWorldTime() - m_ERRSceneStartedAt);
return true;
}
bool ERR_SpawnSceneReady()
@@ -57,6 +132,7 @@ modded class SCR_SpawnRequestComponent
bool ERR_SpawnSceneMayFinish()
{
if (!m_ERRScenePending) return true;
ERR_RefreshSceneInventory();
if (!m_ERRSceneAcknowledged && !m_ERRSceneTimedOut && GetGame().GetWorld().GetWorldTime() >= m_ERRSceneDeadline)
{
m_ERRSceneTimedOut = true;
@@ -71,24 +147,66 @@ modded class SCR_SpawnRequestComponent
if (!Replication.IsServer()) return;
foreach (IEntity entity : m_ERRSceneEntities) ERR_StreamScene(entity, true);
m_ERRSceneEntities.Clear();
m_ERRSceneRoots.Clear();
m_ERRSceneStockOwners.Clear();
m_ERRSceneStockItems.Clear();
if (m_ERRSceneObserver && GetPlayerController()) UpdateObserverMP_S(GetPlayerController().GetControlledEntity());
m_ERRSceneObserver = false;
if (m_ERRScenePending) Rpc(ERR_CancelClientSpawnScene, m_ERRSceneToken);
m_ERRScenePending = false;
m_ERRSceneAcknowledged = false;
m_ERRSceneTimedOut = false;
m_ERRScenePreloadRequired = false;
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_ReceiveSpawnScene(int token, array<RplId> nodes)
protected void ERR_ReceiveSpawnScene(int token, array<RplId> nodes, array<RplId> stockOwners, array<RplId> stockItems)
{
ERR_ClearClientSpawnScene();
m_ERRClientSceneToken = token;
m_ERRClientScenePending = true;
m_ERRClientSceneNodes.Copy(nodes);
m_ERRClientStockOwners.Copy(stockOwners);
m_ERRClientStockItems.Copy(stockItems);
m_ERRClientStockReportAt = 0;
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
if (respawn) respawn.ERR_ShowSpawnSceneLoading();
PrintFormat("Reintroduced [spawn scene]: client token=%1 waiting for %2 replicated objects", token, nodes.Count());
PrintFormat("Reintroduced [spawn scene]: client token=%1 waiting for %2 replicated objects and %3 inventory items", token, nodes.Count(), stockItems.Count());
GetGame().GetCallqueue().CallLater(ERR_CheckClientSpawnScene, 100, true);
}
protected bool ERR_ClientStockReady()
{
if (m_ERRClientStockOwners.Count() != m_ERRClientStockItems.Count()) return false;
RplId previousOwner = RplId.Invalid();
array<IEntity> contents = {};
for (int i = 0; i < m_ERRClientStockItems.Count(); i++)
{
RplId ownerId = m_ERRClientStockOwners[i];
if (ownerId != previousOwner)
{
RplComponent owner = RplComponent.Cast(Replication.FindItem(ownerId));
if (!owner || !owner.GetEntity()) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.GetEntity().FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
ERR_Inventory.Items(inventory, contents);
previousOwner = ownerId;
}
RplComponent item = RplComponent.Cast(Replication.FindItem(m_ERRClientStockItems[i]));
if (!item || !item.GetEntity() || !contents.Contains(item.GetEntity()))
{
ERR_ReportStockWait(ownerId, m_ERRClientStockItems[i], contents.Count());
return false;
}
}
return true;
}
protected void ERR_ReportStockWait(RplId owner, RplId item, int visible)
{
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);
}
protected void ERR_CheckClientSpawnScene()
{
if (!m_ERRClientScenePending || !ERR_GameMode.Get()) { ERR_ClearClientSpawnScene(); return; }
@@ -97,8 +215,9 @@ modded class SCR_SpawnRequestComponent
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component || !component.GetEntity()) return;
}
if (!ERR_ClientStockReady()) return;
int token = m_ERRClientSceneToken;
PrintFormat("Reintroduced [spawn scene]: client token=%1 all %2 replicated objects present", token, m_ERRClientSceneNodes.Count());
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);
}
@@ -107,12 +226,15 @@ modded class SCR_SpawnRequestComponent
{
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);
}
protected void ERR_ClearClientSpawnScene()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene);
m_ERRClientScenePending = false;
m_ERRClientSceneNodes.Clear();
m_ERRClientStockOwners.Clear();
m_ERRClientStockItems.Clear();
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_CancelClientSpawnScene(int token)
@@ -45,13 +45,18 @@ modded class SCR_FreeSpawnHandlerComponent
array<IEntity> scene = {};
bool valid = mode.Sites() && mode.Sites().SpawnScene(data.GetPosition(), scene);
scene.Insert(entity);
requestComponent.ERR_BeginSpawnScene(scene, valid);
requestComponent.ERR_BeginSpawnScene(scene, valid, data.GetPosition());
return;
}
super.OnFinalizeBegin_S(requestComponent, data, entity);
}
override bool CanFinalize_S(SCR_SpawnRequestComponent requestComponent, SCR_SpawnData data, IEntity entity)
{
if (ERR_GameMode.Get() && requestComponent && !requestComponent.ERR_SpawnSceneMayFinish()) return false;
if (ERR_GameMode.Get() && requestComponent)
{
if (!requestComponent.ERR_SpawnSceneMayFinish()) return false;
if (requestComponent.ERR_TakeScenePreload()) super.OnFinalizeBegin_S(requestComponent, data, entity);
}
return super.CanFinalize_S(requestComponent, data, entity);
}
protected ERR_SpawnLogic ERR_AdmissionLogic()
@@ -0,0 +1,35 @@
// Read current physical contents, never the authored starting quantity: another
// prisoner may already have taken loot when a late arrival joins the round.
class ERR_SpawnSceneInventory
{
static bool Gather(array<IEntity> roots, out array<IEntity> owners, out array<IEntity> items)
{
owners = {}; items = {};
foreach (IEntity root : roots)
{
if (!root) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(root.FindComponent(InventoryStorageManagerComponent));
if (!inventory) continue;
array<IEntity> contents = {};
ERR_Inventory.Items(inventory, contents);
foreach (IEntity item : contents)
{
if (!item || items.Contains(item)) continue;
if (items.Count() >= 256) return false;
owners.Insert(root); items.Insert(item);
}
}
return true;
}
static bool Same(array<IEntity> owners, array<IEntity> items, array<IEntity> previousOwners, array<IEntity> previousItems)
{
if (items.Count() != previousItems.Count() || owners.Count() != items.Count() || previousOwners.Count() != previousItems.Count()) return false;
for (int i = 0; i < items.Count(); i++)
{
int previous = previousItems.Find(items[i]);
if (previous < 0 || previousOwners[previous] != owners[i]) return false;
}
return true;
}
}