diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs
index 1f47b2100b3..3394b595c8a 100644
--- a/Robust.Server/GameObjects/ServerEntityManager.cs
+++ b/Robust.Server/GameObjects/ServerEntityManager.cs
@@ -33,6 +33,10 @@ public sealed class ServerEntityManager : EntityManager, IServerEntityManager
"robust_entities_count",
"Amount of alive entities.");
+ private static readonly Gauge EntityMsgQueueSize = Metrics.CreateGauge(
+ "robust_entity_msg_queue_size",
+ "Number of queued MsgEntity messages on the server.");
+
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IServerNetManager _networkManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -145,6 +149,8 @@ internal override void SetLifeStage(MetaDataComponent meta, EntityLifeStage stag
new();
private bool _logLateMsgs;
+ private int _entityMsgQueueLimit;
+ private int _entityMsgMaxFutureTicks;
///
public void SetupNetworking()
@@ -154,6 +160,8 @@ public void SetupNetworking()
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_configurationManager.OnValueChanged(CVars.NetLogLateMsg, b => _logLateMsgs = b, true);
+ _configurationManager.OnValueChanged(CVars.NetEntityMsgQueueLimit, v => _entityMsgQueueLimit = Math.Max(0, v), true);
+ _configurationManager.OnValueChanged(CVars.NetEntityMsgMaxFutureTicks, v => _entityMsgMaxFutureTicks = Math.Max(0, v), true);
}
///
@@ -170,6 +178,7 @@ public override void TickUpdate(float frameTime, bool noPredictions, Histogram?
base.TickUpdate(frameTime, noPredictions, histogram);
EntitiesCount.Set(Entities.Count);
+ EntityMsgQueueSize.Set(_queue.Count);
}
public uint GetLastMessageSequence(ICommonSession? session)
@@ -204,6 +213,33 @@ public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel target
private void HandleEntityNetworkMessage(MsgEntity message)
{
+ if (_entityMsgQueueLimit > 0 && _queue.Count >= _entityMsgQueueLimit)
+ {
+ _netEntSawmill.Warning(
+ "Dropping MsgEntity from {0} for exceeding MsgEntity queue limit ({1}).",
+ message.MsgChannel,
+ _entityMsgQueueLimit);
+ return;
+ }
+
+ if (_entityMsgMaxFutureTicks > 0)
+ {
+ var curTick = _gameTiming.CurTick.Value;
+ var msgTick = message.SourceTick.Value;
+ var maxAllowed = curTick + (uint) _entityMsgMaxFutureTicks;
+
+ if (msgTick > maxAllowed)
+ {
+ _netEntSawmill.Warning(
+ "Dropping MsgEntity from {0} for future tick. cur={1} msg={2} limit={3}",
+ message.MsgChannel,
+ curTick,
+ msgTick,
+ _entityMsgMaxFutureTicks);
+ return;
+ }
+ }
+
if (_logLateMsgs)
{
var msgT = message.SourceTick;
diff --git a/Robust.Server/GameStates/PvsSystem.Ack.cs b/Robust.Server/GameStates/PvsSystem.Ack.cs
index 38f2dace9d0..c035c92b023 100644
--- a/Robust.Server/GameStates/PvsSystem.Ack.cs
+++ b/Robust.Server/GameStates/PvsSystem.Ack.cs
@@ -30,7 +30,10 @@ private void OnClientAck(ICommonSession session, GameTick ackedTick)
return;
sessionData.LastReceivedAck = ackedTick;
- PendingAcks.Add(session);
+ lock (PendingAcks)
+ {
+ PendingAcks.Add(session);
+ }
}
///
@@ -39,18 +42,21 @@ private void OnClientAck(ICommonSession session, GameTick ackedTick)
///
private WaitHandle? ProcessQueuedAcks()
{
- if (PendingAcks.Count == 0)
- return null;
+ lock (PendingAcks)
+ {
+ if (PendingAcks.Count == 0)
+ return null;
- _toAck.Clear();
+ _toAck.Clear();
- foreach (var session in PendingAcks)
- {
- if (session.Status != SessionStatus.Disconnected)
- _toAck.Add(GetOrNewPvsSession(session));
- }
+ foreach (var session in PendingAcks)
+ {
+ if (session.Status != SessionStatus.Disconnected)
+ _toAck.Add(GetOrNewPvsSession(session));
+ }
- PendingAcks.Clear();
+ PendingAcks.Clear();
+ }
if (!_async)
{
diff --git a/Robust.Server/GameStates/PvsSystem.GetStates.cs b/Robust.Server/GameStates/PvsSystem.GetStates.cs
index 87786ceacef..d5a2430ee37 100644
--- a/Robust.Server/GameStates/PvsSystem.GetStates.cs
+++ b/Robust.Server/GameStates/PvsSystem.GetStates.cs
@@ -22,50 +22,64 @@ internal sealed partial class PvsSystem
/// New entity State for the given entity.
private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
{
- var changed = new List();
+ var changed = _componentChangeListPool.Get();
+ changed.Clear();
bool sendCompList = meta.LastComponentRemoved > fromTick;
- HashSet? netComps = sendCompList ? new() : null;
- var stateEv = new ComponentGetState(player, fromTick);
-
- foreach (var (netId, component) in meta.NetComponents)
+ HashSet? netComps = null;
+ if (sendCompList)
+ {
+ netComps = _netComponentSetPool.Get();
+ netComps.Clear();
+ }
+ try
{
- DebugTools.Assert(component.NetSyncEnabled);
+ var stateEv = new ComponentGetState(player, fromTick);
- if (component.Deleted || !component.Initialized)
+ foreach (var (netId, component) in meta.NetComponents)
{
- Log.Error($"Entity manager returned deleted or uninitialized component of type {component.GetType()} on entity {ToPrettyString(entityUid)} while generating entity state data for {player?.Name ?? "replay"}");
- continue;
- }
+ DebugTools.Assert(component.NetSyncEnabled);
- if (component.SendOnlyToOwner && player != null && player.AttachedEntity != entityUid)
- continue;
+ if (component.Deleted || !component.Initialized)
+ {
+ Log.Error($"Entity manager returned deleted or uninitialized component of type {component.GetType()} on entity {ToPrettyString(entityUid)} while generating entity state data for {player?.Name ?? "replay"}");
+ continue;
+ }
- if (component.LastModifiedTick <= fromTick)
- {
- if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(component, player)))
- netComps!.Add(netId);
- continue;
- }
+ if (component.SendOnlyToOwner && player != null && player.AttachedEntity != entityUid)
+ continue;
- if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(component, player))
- continue;
+ if (component.LastModifiedTick <= fromTick)
+ {
+ if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(component, player)))
+ netComps!.Add(netId);
+ continue;
+ }
- var state = ComponentState(entityUid, component, netId, ref stateEv);
- changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
+ if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(component, player))
+ continue;
- if (state != null)
- DebugTools.Assert(fromTick > component.CreationTick || state is not IComponentDeltaState);
+ var state = ComponentState(entityUid, component, netId, ref stateEv);
+ changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
- if (sendCompList)
- netComps!.Add(netId);
- }
+ if (state != null)
+ DebugTools.Assert(fromTick > component.CreationTick || state is not IComponentDeltaState);
- DebugTools.Assert(meta.EntityLastModifiedTick >= meta.LastComponentRemoved);
- DebugTools.Assert(GetEntity(meta.NetEntity) == entityUid);
- var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
+ if (sendCompList)
+ netComps!.Add(netId);
+ }
- return entState;
+ DebugTools.Assert(meta.EntityLastModifiedTick >= meta.LastComponentRemoved);
+ DebugTools.Assert(GetEntity(meta.NetEntity) == entityUid);
+ return new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
+ }
+ catch
+ {
+ _componentChangeListPool.Return(changed);
+ if (netComps != null)
+ _netComponentSetPool.Return(netComps);
+ throw;
+ }
}
private IComponentState? ComponentState(EntityUid uid, IComponent comp, ushort netId, ref ComponentGetState stateEv)
@@ -83,30 +97,39 @@ private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid,
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
{
var bus = EntityManager.EventBusInternal;
- var changed = new List();
+ var changed = _componentChangeListPool.Get();
+ changed.Clear();
var stateEv = new ComponentGetState(player, GameTick.Zero);
- HashSet netComps = new();
+ var netComps = _netComponentSetPool.Get();
+ netComps.Clear();
- foreach (var (netId, component) in meta.NetComponents)
+ try
{
- DebugTools.Assert(component.NetSyncEnabled);
-
- if (component.SendOnlyToOwner && player.AttachedEntity != entityUid)
- continue;
+ foreach (var (netId, component) in meta.NetComponents)
+ {
+ DebugTools.Assert(component.NetSyncEnabled);
- if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player))
- continue;
+ if (component.SendOnlyToOwner && player.AttachedEntity != entityUid)
+ continue;
- var state = ComponentState(entityUid, component, netId, ref stateEv);
- DebugTools.Assert(state is not IComponentDeltaState);
- changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
- netComps.Add(netId);
- }
+ if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player))
+ continue;
- var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
+ var state = ComponentState(entityUid, component, netId, ref stateEv);
+ DebugTools.Assert(state is not IComponentDeltaState);
+ changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
+ netComps.Add(netId);
+ }
- return entState;
+ return new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
+ }
+ catch
+ {
+ _componentChangeListPool.Return(changed);
+ _netComponentSetPool.Return(netComps);
+ throw;
+ }
}
///
diff --git a/Robust.Server/GameStates/PvsSystem.Pooling.cs b/Robust.Server/GameStates/PvsSystem.Pooling.cs
index 871bd8f69a6..458cb4ddb29 100644
--- a/Robust.Server/GameStates/PvsSystem.Pooling.cs
+++ b/Robust.Server/GameStates/PvsSystem.Pooling.cs
@@ -22,6 +22,12 @@ private readonly ObjectPool> _entDataListPool
private readonly ObjectPool> _uidSetPool
= new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize);
+ private readonly ObjectPool> _componentChangeListPool
+ = new DefaultObjectPool>(new ListPolicy(), MaxVisPoolSize);
+
+ private readonly ObjectPool> _netComponentSetPool
+ = new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize);
+
private readonly ObjectPool _chunkPool =
new DefaultObjectPool(new PvsChunkPolicy(), 256);
diff --git a/Robust.Server/GameStates/PvsSystem.Send.cs b/Robust.Server/GameStates/PvsSystem.Send.cs
index 382c4a7bc57..d7552ba2523 100644
--- a/Robust.Server/GameStates/PvsSystem.Send.cs
+++ b/Robust.Server/GameStates/PvsSystem.Send.cs
@@ -45,41 +45,46 @@ private void SendSessionState(PvsSession data, ZStdCompressionContext ctx)
{
DebugTools.AssertEqual(data.State, null);
- // PVS benchmarks use dummy sessions.
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
- if (data.Session.Channel is not DummyChannel)
+ try
{
- DebugTools.AssertNotEqual(data.StateStream, null);
- var msg = new MsgState
+ if (data.Session.Channel is { } channel && channel is not DummyChannel)
{
- StateStream = data.StateStream,
- ForceSendReliably = data.ForceSendReliably,
- CompressionContext = ctx
- };
+ if (!channel.IsConnected)
+ return;
+
+ DebugTools.AssertNotEqual(data.StateStream, null);
+ var msg = new MsgState
+ {
+ StateStream = data.StateStream,
+ ForceSendReliably = data.ForceSendReliably,
+ CompressionContext = ctx
+ };
- _netMan.ServerSendMessage(msg, data.Session.Channel);
- if (msg.ShouldSendReliably())
+ _netMan.ServerSendMessage(msg, channel);
+ if (msg.ShouldSendReliably())
+ {
+ data.RequestedFull = false;
+ data.LastReceivedAck = _gameTiming.CurTick;
+ lock (PendingAcks)
+ {
+ PendingAcks.Add(data.Session);
+ }
+ }
+ }
+ else
{
- data.RequestedFull = false;
data.LastReceivedAck = _gameTiming.CurTick;
+ data.RequestedFull = false;
lock (PendingAcks)
{
PendingAcks.Add(data.Session);
}
}
}
- else
+ finally
{
- // Always "ack" dummy sessions.
- data.LastReceivedAck = _gameTiming.CurTick;
- data.RequestedFull = false;
- lock (PendingAcks)
- {
- PendingAcks.Add(data.Session);
- }
+ data.StateStream?.Dispose();
+ data.StateStream = null;
}
-
- data.StateStream?.Dispose();
- data.StateStream = null;
}
}
diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs
index d4df28dea09..a98c5218161 100644
--- a/Robust.Server/GameStates/PvsSystem.Serialize.cs
+++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs
@@ -1,5 +1,3 @@
-using System;
-using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -9,6 +7,9 @@
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
namespace Robust.Server.GameStates;
@@ -59,18 +60,43 @@ private void SerializeState(int i)
///
private void SerializeSessionState(PvsSession data)
{
- ComputeSessionState(data);
- InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
- DebugTools.AssertEqual(data.StateStream, null);
+ try
+ {
+ if (!TryComputeSessionState(data))
+ return;
+ InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
+ DebugTools.AssertEqual(data.StateStream, null);
- // PVS benchmarks use dummy sessions.
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
- if (data.Session.Channel is not DummyChannel)
+ // PVS benchmarks use dummy sessions.
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (data.Session.Channel is not DummyChannel)
+ {
+ data.StateStream = RobustMemoryManager.GetMemoryStream();
+ _serializer.SerializeDirect(data.StateStream, data.State);
+ }
+ }
+ finally
{
- data.StateStream = RobustMemoryManager.GetMemoryStream();
- _serializer.SerializeDirect(data.StateStream, data.State);
+ ReleasePooledStateData(data);
+ data.ClearState();
}
+ }
+
+ private void ReleasePooledStateData(PvsSession data)
+ {
+ var states = data.States;
+ for (var i = 0; i < states.Count; i++)
+ {
+ var state = states[i];
+ var changes = state.ComponentChanges.Value;
+ if (changes is List list)
+ _componentChangeListPool.Return(list);
- data.ClearState();
+ if (state.NetComponents != null)
+ {
+ _netComponentSetPool.Return(state.NetComponents);
+ state.NetComponents = null;
+ }
+ }
}
}
diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs
index 9e3a87dee40..6d47a08093a 100644
--- a/Robust.Server/GameStates/PvsSystem.Session.cs
+++ b/Robust.Server/GameStates/PvsSystem.Session.cs
@@ -35,8 +35,38 @@ private PvsSession GetOrNewPvsSession(ICommonSession session)
return pvsSession;
}
-
internal void ComputeSessionState(PvsSession session)
+ {
+ if (session.Session is null)
+ {
+ // Minimal session update for replay.
+ session.FromTick = session.RequestedFull ? GameTick.Zero : session.LastReceivedAck;
+ session.LastInput = 0;
+ session.LastMessage = 0;
+ session.VisMask = EyeComponent.DefaultVisibilityMask;
+
+ if (CullingEnabled && !session.DisableCulling)
+ GetEntityStates(session);
+ else
+ GetAllEntityStates(session);
+
+ DebugTools.AssertNull(session.State);
+ session.State = new GameState(
+ session.FromTick,
+ _gameTiming.CurTick,
+ 0,
+ session.States,
+ session.PlayerStates,
+ _deletedEntities);
+
+ session.ForceSendReliably = false;
+ return;
+ }
+
+ TryComputeSessionState(session);
+ }
+
+ internal bool TryComputeSessionState(PvsSession session)
{
UpdateSession(session);
@@ -49,6 +79,17 @@ internal void ComputeSessionState(PvsSession session)
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
+ if (_maxEntityStates > 0 && session.States.Count > _maxEntityStates)
+ {
+ Log.Warning(
+ "Skipping PVS state for {0} due to exceeding net.pvs_max_entity_states. Count={1} Limit={2}",
+ session.Session,
+ session.States.Count,
+ _maxEntityStates);
+
+ return false;
+ }
+
DebugTools.Assert(session.States.Select(x=> x.NetEntity).ToHashSet().Count == session.States.Count);
DebugTools.AssertNull(session.State);
session.State = new GameState(
@@ -61,6 +102,7 @@ internal void ComputeSessionState(PvsSession session)
session.ForceSendReliably = session.RequestedFull
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
+ return true;
}
private void UpdateSession(PvsSession session)
diff --git a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
index 99e3e86b5c0..524e2244107 100644
--- a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
+++ b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
@@ -83,6 +83,9 @@ private void AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref Pvs
return;
}
+ if (!Exists(ent.Uid))
+ return;
+
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
if (budgetExceeded)
@@ -158,6 +161,9 @@ private bool AddEntity(PvsSession session, Entity entity, Gam
return false;
}
+ if (!Exists(uid))
+ return false;
+
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(entity.Comp.PvsData);
diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs
index 26ca25493be..8cf14722d05 100644
--- a/Robust.Server/GameStates/PvsSystem.cs
+++ b/Robust.Server/GameStates/PvsSystem.cs
@@ -72,6 +72,7 @@ internal sealed partial class PvsSystem : EntitySystem
///
private readonly List _toAck = new();
internal readonly HashSet PendingAcks = new();
+ private int _maxEntityStates;
private PvsAckJob _ackJob;
private PvsChunkJob _chunkJob;
@@ -115,6 +116,22 @@ internal sealed partial class PvsSystem : EntitySystem
Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25)
});
+ private static readonly Gauge PvsSessionsGauge = Metrics.CreateGauge(
+ "robust_pvs_sessions",
+ "Number of PVS sessions currently tracked.");
+
+ private static readonly Gauge PvsChunksGauge = Metrics.CreateGauge(
+ "robust_pvs_chunks",
+ "Number of PVS chunks currently cached.");
+
+ private static readonly Gauge PvsPendingAcksGauge = Metrics.CreateGauge(
+ "robust_pvs_pending_acks",
+ "Number of sessions pending PVS ack processing.");
+
+ private static readonly Gauge PvsMetadataCurrentSizeGauge = Metrics.CreateGauge(
+ "robust_pvs_metadata_current_size",
+ "Current allocated size of PVS metadata memory region.");
+
public override void Initialize()
{
base.Initialize();
@@ -148,6 +165,7 @@ public override void Initialize()
Subs.CVar(_configManager, CVars.NetForceAckThreshold, OnForceAckChanged, true);
Subs.CVar(_configManager, CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPvsCompressLevel, ResetParallelism, true);
+ Subs.CVar(_configManager, CVars.NetPvsMaxEntityStates, v => _maxEntityStates = Math.Max(0, v), true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
@@ -190,6 +208,14 @@ internal void SendGameStates(ICommonSession[] players)
{
_getStateHandlers ??= EntityManager.EventBusInternal.GetNetCompEventHandlers();
+ PvsSessionsGauge.Set(PlayerData.Count);
+ PvsChunksGauge.Set(_chunks.Count);
+ PvsMetadataCurrentSizeGauge.Set(_metadataMemory.CurrentSize);
+ lock (PendingAcks)
+ {
+ PvsPendingAcksGauge.Set(PendingAcks.Count);
+ }
+
// Wait for pending jobs and process disconnected players
ProcessDisconnections();
@@ -437,14 +463,22 @@ internal void ProcessDisconnections()
_leaveTask?.WaitOne();
_leaveTask = null;
- foreach (var session in _disconnected)
+ lock (PendingAcks)
{
- if (PlayerData.Remove(session, out var pvsSession))
+ foreach (var session in _disconnected)
{
- ClearSendHistory(pvsSession);
- FreeSessionDataMemory(pvsSession);
+ PendingAcks.Remove(session);
+ _seenAllEnts.Remove(session);
+
+ if (PlayerData.Remove(session, out var pvsSession))
+ {
+ ClearSendHistory(pvsSession);
+ FreeSessionDataMemory(pvsSession);
+ }
}
}
+
+ _disconnected.Clear();
}
internal void CacheSessionData(ICommonSession[] players)
diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs
index 63cec1e2d3a..451af8a24c4 100644
--- a/Robust.Server/Physics/GridFixtureSystem.cs
+++ b/Robust.Server/Physics/GridFixtureSystem.cs
@@ -1,11 +1,9 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Numerics;
using Robust.Server.Console;
+using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -17,6 +15,10 @@
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Numerics;
namespace Robust.Server.Physics
{
@@ -30,6 +32,7 @@ public sealed partial class GridFixtureSystem : SharedGridFixtureSystem
[Dependency] private readonly IConGroupController _conGroup = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
@@ -65,6 +68,7 @@ public override void Initialize()
SubscribeNetworkEvent(OnDebugStopRequest);
Subs.CVar(_cfg, CVars.GridSplitting, SetSplitAllowed, true);
+ _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
private void SetSplitAllowed(bool value) => SplitAllowed = value;
@@ -72,9 +76,16 @@ public override void Initialize()
public override void Shutdown()
{
base.Shutdown();
+ _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_subscribedSessions.Clear();
}
+ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
+ {
+ if (args.NewStatus == SessionStatus.Disconnected)
+ _subscribedSessions.Remove(args.Session);
+ }
+
///
/// Due to how MapLoader works need to ensure grid exists in dictionary before it's initialised.
///
diff --git a/Robust.Server/ViewVariables/ServerViewVariablesManager.cs b/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
index d03881e6464..fe4c4e446d6 100644
--- a/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
+++ b/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
@@ -57,11 +57,9 @@ private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
if (!_users.TryGetValue(e.Session.UserId, out var vvSessions))
return;
-
- foreach (var id in vvSessions)
- {
- _closeSession(id, false);
- }
+
+ while (vvSessions.Count > 0)
+ _closeSession(vvSessions[^1], false);
}
private void _msgCloseSession(MsgViewVariablesCloseSession message)
@@ -272,6 +270,14 @@ private void _closeSession(uint sessionId, bool sendMsg)
}
_sessions.Remove(sessionId);
+
+ if (_users.TryGetValue(session.PlayerUser, out var userSessions))
+ {
+ userSessions.Remove(sessionId);
+ if (userSessions.Count == 0)
+ _users.Remove(session.PlayerUser);
+ }
+
if (!sendMsg || !_playerManager.TryGetSessionById(session.PlayerUser, out var player) ||
player.Status == SessionStatus.Disconnected)
{
diff --git a/Robust.Server/ViewVariables/Traits/ViewVariablesTraitEnumerable.cs b/Robust.Server/ViewVariables/Traits/ViewVariablesTraitEnumerable.cs
index 09b11e8efa6..1f3a67bae88 100644
--- a/Robust.Server/ViewVariables/Traits/ViewVariablesTraitEnumerable.cs
+++ b/Robust.Server/ViewVariables/Traits/ViewVariablesTraitEnumerable.cs
@@ -7,6 +7,9 @@ namespace Robust.Server.ViewVariables.Traits
{
internal sealed class ViewVariablesTraitEnumerable : ViewVariablesTrait
{
+
+ private const int MaxCacheSize = 5_000;
+
private readonly List