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 _cache = new(); private IEnumerator? _enumerator; private readonly IEnumerable _enumerable; @@ -72,9 +75,15 @@ private void _cacheTo(int index) return; } + if (index >= MaxCacheSize) + return; + DebugTools.AssertNotNull(_enumerator); while (_cache.Count <= index) { + if (_cache.Count >= MaxCacheSize) + break; + if (!_enumerator!.MoveNext()) { _enumerator = null; diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index ad0c8a4ee41..491fb23a09e 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -267,6 +267,9 @@ protected CVars() public static readonly CVarDef NetPVSEntityEnterBudget = CVarDef.Create("net.pvs_enter_budget", 200, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT); + public static readonly CVarDef NetPvsMaxEntityStates = + CVarDef.Create("net.pvs_max_entity_states", 50000, CVar.SERVERONLY); + /// /// The amount of pvs-exiting entities that a client will process in a single tick. /// @@ -285,6 +288,12 @@ protected CVars() public static readonly CVarDef NetLogLateMsg = CVarDef.Create("net.log_late_msg", true); + + public static readonly CVarDef NetEntityMsgQueueLimit = + CVarDef.Create("net.entity_msg_queue_limit", 8192, CVar.SERVERONLY); + public static readonly CVarDef NetEntityMsgMaxFutureTicks = + CVarDef.Create("net.entity_msg_max_future_ticks", 10, CVar.SERVERONLY); + /// /// Ticks per second on the server. /// This influences both how frequently game code processes, and how frequently updates are sent to clients. diff --git a/Robust.Shared/GameObjects/EntityManager.Network.cs b/Robust.Shared/GameObjects/EntityManager.Network.cs index 370d43269a0..8a8765bb667 100644 --- a/Robust.Shared/GameObjects/EntityManager.Network.cs +++ b/Robust.Shared/GameObjects/EntityManager.Network.cs @@ -183,7 +183,7 @@ public NetEntity GetNetEntity(EntityUid uid, MetaDataComponent? metadata = null) if (uid == EntityUid.Invalid) return NetEntity.Invalid; - if (!MetaQuery.Resolve(uid, ref metadata)) + if (!MetaQuery.Resolve(uid, ref metadata, false)) return NetEntity.Invalid; return metadata.NetEntity; diff --git a/Robust.Shared/Network/Transfer/BaseTransferImpl.cs b/Robust.Shared/Network/Transfer/BaseTransferImpl.cs index 35e0cb1f80e..172d135b1e6 100644 --- a/Robust.Shared/Network/Transfer/BaseTransferImpl.cs +++ b/Robust.Shared/Network/Transfer/BaseTransferImpl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; @@ -34,7 +34,7 @@ internal abstract class BaseTransferImpl(ISawmill sawmill, BaseTransferManager p protected readonly ISawmill Sawmill = sawmill; protected long OutgoingIdCounter; - public int MaxChannelCount = int.MaxValue; + public int MaxChannelCount = 10; private readonly Dictionary>> _receivingChannels = []; @@ -418,6 +418,8 @@ public virtual void Dispose() { channel.Complete(); } + + _receivingChannels.Clear(); } protected enum Opcode : byte diff --git a/Robust.Shared/Physics/Dynamics/Joints/WeldJoint.cs b/Robust.Shared/Physics/Dynamics/Joints/WeldJoint.cs index 027f9b8e112..b05e209e496 100644 --- a/Robust.Shared/Physics/Dynamics/Joints/WeldJoint.cs +++ b/Robust.Shared/Physics/Dynamics/Joints/WeldJoint.cs @@ -24,6 +24,13 @@ public override Joint GetJoint(IEntityManager entManager, EntityUid owner) public sealed partial class WeldJoint : Joint, IEquatable { + private static float WrapAnglePi(float angle) // Lua patch + { + angle = (angle + MathHelper.Pi) % MathHelper.TwoPi; + if (angle < 0f) angle += MathHelper.TwoPi; + return angle - MathHelper.Pi; + } + // Shared private float _gamma; private Vector3 _impulse; @@ -162,7 +169,7 @@ internal override void InitVelocityConstraints( float invM = iA + iB; - float C = aB - aA - ReferenceAngle; + float C = WrapAnglePi(aB - aA - ReferenceAngle); // Lua patch // Damping coefficient float d = Damping; @@ -328,7 +335,7 @@ internal override bool SolvePositionConstraints( else { var C1 = cB + rB - cA - rA; - float C2 = aB - aA - ReferenceAngle; + float C2 = WrapAnglePi(aB - aA - ReferenceAngle); // Lua patch positionError = C1.Length(); angularError = Math.Abs(C2); diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index dbd1eff5677..ffa9e0e7487 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -188,7 +188,10 @@ internal void FindNewContacts() // EZ if (moveBuffer.Count == 0) + { + movedGrids.Clear(); return; + } _contactJob.MoveBuffer.Clear(); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs index ffa33ad2a80..50f320ac097 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs @@ -96,6 +96,23 @@ public abstract partial class SharedPhysicsSystem private readonly LinkedList _activeContacts = new(); + private Contact[] _contactsScratch = Array.Empty(); + private ContactStatus[] _contactStatusScratch = Array.Empty(); + private FixedArray4[] _contactWorldPointsScratch = Array.Empty>(); + private bool[] _contactWakeScratch = Array.Empty(); + + private static int GrowScratch(int current, int required) + { + if (current == 0) + return required; + + var grown = current; + while (grown < required) + grown *= 2; + + return grown; + } + private sealed class ContactPoolPolicy : IPooledObjectPolicy { private readonly SharedDebugPhysicsSystem _debugPhysicsSystem; @@ -398,7 +415,11 @@ internal void CollideContacts() { // Due to the fact some contacts may be removed (and we need to update this array as we iterate). // the length may not match the actual contact count, hence we track the index. - var contacts = ArrayPool.Shared.Rent(ContactCount); + var contactCount = ContactCount; + if (_contactsScratch.Length < contactCount) + Array.Resize(ref _contactsScratch, GrowScratch(_contactsScratch.Length, contactCount)); + + var contacts = _contactsScratch; var index = 0; // Can be changed while enumerating @@ -556,11 +577,21 @@ internal void CollideContacts() contacts[index++] = contact; } - var status = ArrayPool.Shared.Rent(index); - var worldPoints = ArrayPool>.Shared.Rent(index); + if (_contactStatusScratch.Length < index) + Array.Resize(ref _contactStatusScratch, GrowScratch(_contactStatusScratch.Length, index)); + + if (_contactWorldPointsScratch.Length < index) + Array.Resize(ref _contactWorldPointsScratch, GrowScratch(_contactWorldPointsScratch.Length, index)); + + if (_contactWakeScratch.Length < index) + Array.Resize(ref _contactWakeScratch, GrowScratch(_contactWakeScratch.Length, index)); + + var status = _contactStatusScratch; + var worldPoints = _contactWorldPointsScratch; + var wake = _contactWakeScratch; // Update contacts all at once. - BuildManifolds(contacts, index, status, worldPoints); + BuildManifolds(contacts, index, status, worldPoints, wake); // Single-threaded so content doesn't need to worry about race conditions. for (var i = 0; i < index; i++) @@ -581,10 +612,6 @@ internal void CollideContacts() RunContactEvents(status[i], contact, worldPoints[i]); } - - ArrayPool.Shared.Return(contacts); - ArrayPool.Shared.Return(status); - ArrayPool>.Shared.Return(worldPoints); } internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray4 worldPoint) @@ -592,48 +619,48 @@ internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray switch (status) { case ContactStatus.StartTouching: - { - if (!contact.IsTouching) return; - - var fixtureA = contact.FixtureA!; - var fixtureB = contact.FixtureB!; - var bodyA = contact.BodyA!; - var bodyB = contact.BodyB!; - var uidA = contact.EntityA; - var uidB = contact.EntityB; - var points = new FixedArray2(worldPoint._00, worldPoint._01); - var worldNormal = worldPoint._02; - - var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal); - var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal); - - RaiseLocalEvent(uidA, ref ev1, true); - RaiseLocalEvent(uidB, ref ev2, true); - break; - } + { + if (!contact.IsTouching) return; + + var fixtureA = contact.FixtureA!; + var fixtureB = contact.FixtureB!; + var bodyA = contact.BodyA!; + var bodyB = contact.BodyB!; + var uidA = contact.EntityA; + var uidB = contact.EntityB; + var points = new FixedArray2(worldPoint._00, worldPoint._01); + var worldNormal = worldPoint._02; + + var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal); + var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal); + + RaiseLocalEvent(uidA, ref ev1, true); + RaiseLocalEvent(uidB, ref ev2, true); + break; + } case ContactStatus.Touching: break; case ContactStatus.EndTouching: - { - var fixtureA = contact.FixtureA; - var fixtureB = contact.FixtureB; + { + var fixtureA = contact.FixtureA; + var fixtureB = contact.FixtureB; - // If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted) - // then we'll just skip the EndCollide. - if (fixtureA == null || fixtureB == null) return; + // If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted) + // then we'll just skip the EndCollide. + if (fixtureA == null || fixtureB == null) return; - var bodyA = contact.BodyA!; - var bodyB = contact.BodyB!; - var uidA = contact.EntityA; - var uidB = contact.EntityB; + var bodyA = contact.BodyA!; + var bodyB = contact.BodyB!; + var uidA = contact.EntityA; + var uidB = contact.EntityB; - var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB); - var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA); + var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB); + var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA); - RaiseLocalEvent(uidA, ref ev1); - RaiseLocalEvent(uidB, ref ev2); - break; - } + RaiseLocalEvent(uidA, ref ev1); + RaiseLocalEvent(uidB, ref ev2); + break; + } case ContactStatus.NoContact: break; default: @@ -641,13 +668,11 @@ internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray } } - private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4[] worldPoints) + private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4[] worldPoints, bool[] wake) { if (count == 0) return; - var wake = ArrayPool.Shared.Rent(count); - _parallel.ProcessNow(new ManifoldsJob() { Physics = this, @@ -672,8 +697,6 @@ private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] statu SetAwake((aUid, bodyA), true); SetAwake((bUid, bodyB), true); } - - ArrayPool.Shared.Return(wake); } private record struct ManifoldsJob : IParallelRobustJob diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs index 6bf52122f80..f5b6f3970ec 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs @@ -177,6 +177,8 @@ internal record struct IslandData( private readonly HashSet> _islandSet = new(64); private readonly Stack> _bodyStack = new(64); private readonly List> _awakeBodyList = new(256); + private readonly List _islandsBuffer = new(); + private readonly List<(Joint Original, Joint Joint)> _islandJointsBuffer = new(); // Config private bool _warmStarting; @@ -319,8 +321,8 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) _islandJointPool.Get(), new List<(Joint Joint, float Error)>()); - var islands = new List(); - var islandJoints = new List<(Joint Original, Joint Joint)>(); + _islandsBuffer.Clear(); + _islandJointsBuffer.Clear(); // Build the relevant islands / graphs for all bodies. foreach (var ent in _awakeBodyList) @@ -441,7 +443,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) } var copy = joint.Clone(uidA, uidB); - islandJoints.Add((joint, copy)); + _islandJointsBuffer.Add((joint, copy)); joint.IslandFlag = true; } } @@ -471,12 +473,12 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) } var copy = joint.Clone(uidA, uidB); - islandJoints.Add((joint, copy)); + _islandJointsBuffer.Add((joint, copy)); joint.IslandFlag = true; } } - foreach (var (original, joint) in islandJoints) + foreach (var (original, joint) in _islandJointsBuffer) { // TODO: Same here store physicscomp + transform on the joint, the savings are worth it. var bodyA = PhysicsQuery.GetComponent(joint.BodyAUid); @@ -500,7 +502,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) } } - islandJoints.Clear(); + _islandJointsBuffer.Clear(); } int idx; @@ -519,7 +521,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) { MapUid = mapUid.Value }; - islands.Add(data); + _islandsBuffer.Add(data); idx = data.Index; } @@ -541,20 +543,21 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) // If we didn't use lone island just return it. if (loneIsland.Bodies.Count > 0) { - islands.Add(loneIsland); + _islandsBuffer.Add(loneIsland); } else { ReturnIsland(loneIsland); } - SolveIslands(islands, frameTime, dtRatio, invDt, prediction); + SolveIslands(_islandsBuffer, frameTime, dtRatio, invDt, prediction); - foreach (var island in islands) + foreach (var island in _islandsBuffer) { ReturnIsland(island); } + _islandsBuffer.Clear(); Cleanup(frameTime); } @@ -602,7 +605,6 @@ protected virtual void Cleanup(float frameTime) // So Box2D would update broadphase here buutttt we'll just wait until MoveEvent queue is used. } - _islandSet.Clear(); _islandSet.Clear(); _awakeBodyList.Clear(); } @@ -916,7 +918,7 @@ static void ProcessParallelInternal( float[] solvedAngles) { const int FinaliseBodies = 32; - var batches = (int)MathF.Ceiling((float) bodyCount / FinaliseBodies); + var batches = (int)MathF.Ceiling((float)bodyCount / FinaliseBodies); Parallel.For(0, batches, options, i => { @@ -1045,7 +1047,7 @@ private void FinalisePositions(int start, int end, int offset, ListShould only predicted entities be considered in this simulation step? protected void SimulateWorld(float deltaTime, bool prediction) { + if (MetricsEnabled) + { + PhysicsSubstepsGauge.Set(_substeps); + PhysicsAwakeBodiesGauge.Set(AwakeBodies.Count); + + var awakeDynamic = 0; + foreach (var ent in AwakeBodies) + { + if (ent.Comp1.BodyType == BodyType.Dynamic) + awakeDynamic++; + } + + PhysicsAwakeDynamicBodiesGauge.Set(awakeDynamic); + PhysicsContactCountGauge.Set(ContactCount); + PhysicsMoveBufferCountGauge.Set(MoveBuffer.Count); + PhysicsMovedGridsGauge.Set(MovedGrids.Count); + } + var frameTime = deltaTime / _substeps; EffectiveCurTime = _gameTiming.CurTime;