Wait for client inventory readiness before player admission
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user