diff --git a/Robust.Server/GameStates/EntityViewCulling.cs b/Robust.Server/GameStates/PVSSystem.cs similarity index 86% rename from Robust.Server/GameStates/EntityViewCulling.cs rename to Robust.Server/GameStates/PVSSystem.cs index c862626ea..dfd803911 100644 --- a/Robust.Server/GameStates/EntityViewCulling.cs +++ b/Robust.Server/GameStates/PVSSystem.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Composition; using System.Runtime.CompilerServices; using Microsoft.Extensions.ObjectPool; using Robust.Server.GameObjects; using Robust.Server.Player; +using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -15,7 +18,7 @@ using Robust.Shared.Utility; namespace Robust.Server.GameStates { - internal sealed class EntityViewCulling + internal sealed class PVSSystem : EntitySystem { private const int ViewSetCapacity = 256; // starting number of entities that are in view private const int PlayerSetSize = 64; // Starting number of players @@ -23,12 +26,15 @@ namespace Robust.Server.GameStates private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN); - private readonly IServerEntityManager _entMan; - private readonly IMapManager _mapManager; - private readonly IEntityLookup _lookup; + [Shared.IoC.Dependency] private readonly IServerEntityManager _entMan = default!; + [Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; + [Shared.IoC.Dependency] private readonly IEntityLookup _lookup = default!; + [Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!; + [Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!; + [Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!; private readonly Dictionary> _playerVisibleSets = new(PlayerSetSize); - internal readonly Dictionary> PlayerChunks = new(PlayerSetSize); + private readonly Dictionary> _playerChunks = new(PlayerSetSize); private readonly Dictionary _streamingChunks = new(); @@ -81,25 +87,11 @@ namespace Robust.Server.GameStates } } - public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager, IEntityLookup lookup) - { - _entMan = entMan; - _mapManager = mapManager; - _lookup = lookup; - } - public void SetTransformNetId(ushort value) { _transformNetId = value; } - // Not thread safe - public void EntityDeleted(EntityUid e) - { - // Not aware of prediction - _deletionHistory.Add((_entMan.CurrentTick, e)); - } - // Not thread safe public void CullDeletionHistory(GameTick oldestAck) { @@ -117,54 +109,86 @@ namespace Robust.Server.GameStates return list; } - // Not thread safe + public override void Initialize() + { + base.Initialize(); + EntityManager.EntityDeleted += OnEntityDelete; + _playerManager.PlayerStatusChanged += OnPlayerStatusChange; + _mapManager.OnGridRemoved += OnGridRemoved; + + // If you want to make this modifiable at runtime you need to subscribe to tickrate updates and streaming updates + // plus invalidate any chunks currently being streamed as well. + StreamingTilesPerTick = (int) (_configManager.GetCVar(CVars.StreamedTilesPerSecond) / _gameTiming.TickRate); + _configManager.OnValueChanged(CVars.StreamedTileRange, SetStreamRange, true); + } + + private void SetStreamRange(float value) + { + StreamRange = value; + } + + public override void Shutdown() + { + base.Shutdown(); + EntityManager.EntityDeleted -= OnEntityDelete; + _playerManager.PlayerStatusChanged -= OnPlayerStatusChange; + _mapManager.OnGridRemoved -= OnGridRemoved; + _configManager.UnsubValueChanged(CVars.StreamedTileRange, SetStreamRange); + } + + private void OnGridRemoved(MapId mapid, GridId gridid) + { + // Remove any sort of tracking for when a chunk was sent. + foreach (var (_, chunks) in _playerChunks) + { + foreach (var (chunk, _) in chunks.ToArray()) + { + if (chunk is not MapChunk mapChunk || + mapChunk.GridId == gridid) + { + chunks.Remove(chunk); + } + } + } + } + + #region Player Status + + private void OnPlayerStatusChange(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.InGame) + { + AddPlayer(e.Session); + } + else if (e.OldStatus == SessionStatus.InGame) + { + RemovePlayer(e.Session); + } + } + public void AddPlayer(ICommonSession session) { var visSet = _visSetPool.Get(); _playerVisibleSets.Add(session, visSet); - PlayerChunks.Add(session, new Dictionary(32)); + _playerChunks.Add(session, new Dictionary(32)); _streamingChunks.Add(session, new ChunkStreamingData()); } - // Not thread safe public void RemovePlayer(ICommonSession session) { _playerVisibleSets.Remove(session); - PlayerChunks.Remove(session); + _playerChunks.Remove(session); _playerLastFullMap.Remove(session, out _); _streamingChunks.Remove(session); } - // thread safe - public bool IsPointVisible(ICommonSession session, in MapCoordinates position) + #endregion + + private void OnEntityDelete(object? sender, EntityUid e) { - var viewables = GetSessionViewers(session); - - bool CheckInView(MapCoordinates mapCoordinates, HashSet entityUids) - { - foreach (var euid in entityUids) - { - var (viewBox, mapId) = CalcViewBounds(in euid); - - if (mapId != mapCoordinates.MapId) - continue; - - if (!CullingEnabled) - return true; - - if (viewBox.Contains(mapCoordinates.Position)) - return true; - } - - return false; - } - - bool result = CheckInView(position, viewables); - - viewables.Clear(); - _viewerEntsPool.Return(viewables); - return result; + // Not aware of prediction + _deletionHistory.Add((EntityManager.CurrentTick, e)); } private HashSet GetSessionViewers(ICommonSession session) @@ -214,7 +238,7 @@ namespace Robust.Server.GameStates if (session.AttachedEntityUid is not null) { var viewers = GetSessionViewers(session); - var chunksSeen = PlayerChunks[session]; + var chunksSeen = _playerChunks[session]; foreach (var eyeEuid in viewers) { @@ -449,10 +473,10 @@ namespace Robust.Server.GameStates //TODO: HACK: somehow an entity left the view, transform does not exist (deleted?), but was not in the // deleted list. This seems to happen with the map entity on round restart. - if (!_entMan.EntityExists(entityUid)) + if (!EntityManager.EntityExists(entityUid)) continue; - var xform = _entMan.GetComponent(entityUid); + var xform = EntityManager.GetComponent(entityUid); // Anchored entities don't ever leave if (xform.Anchored) continue; @@ -477,14 +501,14 @@ namespace Robust.Server.GameStates { // skip sending anchored entities (walls) - DebugTools.Assert(!_entMan.GetComponent(entityUid).Anchored); + DebugTools.Assert(!EntityManager.GetComponent(entityUid).Anchored); if (previousSet.Contains(entityUid)) { //Still Visible // Nothing new to send - if(_entMan.GetEntity(entityUid).LastModifiedTick < fromTick) + if (EntityManager.GetEntity(entityUid).LastModifiedTick < fromTick) continue; // only send new changes @@ -537,13 +561,13 @@ namespace Robust.Server.GameStates return true; // if we are invisible, we are not going into the visSet, so don't worry about parents, and children are not going in - if (_entMan.TryGetComponent(uid, out var visComp)) + if (EntityManager.TryGetComponent(uid, out var visComp)) { if ((visMask & visComp.Layer) == 0) return false; } - var xform = _entMan.GetComponent(uid); + var xform = EntityManager.GetComponent(uid); var parentUid = xform.ParentUid; @@ -606,7 +630,7 @@ namespace Robust.Server.GameStates // Read Safe private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid) { - var xform = _entMan.GetComponent(euid); + var xform = _entMan.GetComponent(euid); var view = Box2.UnitCentered.Scale(ViewSize).Translated(xform.WorldPosition); var map = xform.MapID; diff --git a/Robust.Server/GameStates/ServerGameStateManager.cs b/Robust.Server/GameStates/ServerGameStateManager.cs index 4e4ac4065..63ad822d3 100644 --- a/Robust.Server/GameStates/ServerGameStateManager.cs +++ b/Robust.Server/GameStates/ServerGameStateManager.cs @@ -13,7 +13,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Players; @@ -30,10 +29,9 @@ namespace Robust.Server.GameStates private readonly Dictionary _ackedStates = new(); private GameTick _lastOldestAck = GameTick.Zero; - private EntityViewCulling _entityView = null!; + private PVSSystem _pvs = default!; [Dependency] private readonly IServerEntityManager _entityManager = default!; - [Dependency] private readonly IEntityLookup _lookup = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IServerNetManager _networkManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -58,13 +56,12 @@ namespace Robust.Server.GameStates public void SetTransformNetId(ushort netId) { - _entityView.SetTransformNetId(netId); + _pvs.SetTransformNetId(netId); } public void PostInject() { _logger = Logger.GetSawmill("PVS"); - _entityView = new EntityViewCulling(_entityManager, _mapManager, _lookup); } /// @@ -76,49 +73,7 @@ namespace Robust.Server.GameStates _networkManager.Connected += HandleClientConnected; _networkManager.Disconnect += HandleClientDisconnect; - _playerManager.PlayerStatusChanged += HandlePlayerStatusChanged; - - _entityManager.EntityDeleted += HandleEntityDeleted; - - _mapManager.OnGridRemoved += HandleGridRemove; - - // If you want to make this modifiable at runtime you need to subscribe to tickrate updates and streaming updates - // plus invalidate any chunks currently being streamed as well. - _entityView.StreamingTilesPerTick = (int) (_configurationManager.GetCVar(CVars.StreamedTilesPerSecond) / _gameTiming.TickRate); - _configurationManager.OnValueChanged(CVars.StreamedTileRange, value => _entityView.StreamRange = value, true); - } - - private void HandleGridRemove(MapId mapid, GridId gridid) - { - // Remove any sort of tracking for when a chunk was sent. - foreach (var (_, chunks) in _entityView.PlayerChunks) - { - foreach (var (chunk, _) in chunks.ToArray()) - { - if (chunk is not MapChunk mapChunk || - mapChunk.GridId == gridid) - { - chunks.Remove(chunk); - } - } - } - } - - private void HandleEntityDeleted(object? sender, EntityUid e) - { - _entityView.EntityDeleted(e); - } - - private void HandlePlayerStatusChanged(object? sender, SessionStatusEventArgs e) - { - if (e.NewStatus == SessionStatus.InGame) - { - _entityView.AddPlayer(e.Session); - } - else if(e.OldStatus == SessionStatus.InGame) - { - _entityView.RemovePlayer(e.Session); - } + _pvs = EntitySystem.Get(); } private void HandleClientConnected(object? sender, NetChannelArgs e) @@ -166,14 +121,14 @@ namespace Robust.Server.GameStates { DebugTools.Assert(_networkManager.IsServer); - _entityView.ViewSize = PvsRange * 2; - _entityView.CullingEnabled = PvsEnabled; + _pvs.ViewSize = PvsRange * 2; + _pvs.CullingEnabled = PvsEnabled; if (!_networkManager.IsConnected) { // Prevent deletions piling up if we have no clients. _entityManager.CullDeletionHistory(GameTick.MaxValue); - _entityView.CullDeletionHistory(GameTick.MaxValue); + _pvs.CullDeletionHistory(GameTick.MaxValue); _mapManager.CullDeletionHistory(GameTick.MaxValue); return; } @@ -202,7 +157,7 @@ namespace Robust.Server.GameStates DebugTools.Assert("Why does this channel not have an entry?"); } - var (entStates, deletions) = _entityView.CalculateEntityStates(session, lastAck, _gameTiming.CurTick); + var (entStates, deletions) = _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick); var playerStates = _playerManager.GetPlayerStates(lastAck); var mapData = _mapManager.GetStateData(lastAck); @@ -254,7 +209,7 @@ namespace Robust.Server.GameStates { _lastOldestAck = oldestAck; _entityManager.CullDeletionHistory(oldestAck); - _entityView.CullDeletionHistory(oldestAck); + _pvs.CullDeletionHistory(oldestAck); _mapManager.CullDeletionHistory(oldestAck); } }