using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Players; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Server.GameStates; public interface IPVSCollection { /// /// Processes all previous additions, removals and updates of indices. /// public void Process(); public void AddPlayer(ICommonSession session); public void AddGrid(GridId gridId); public void AddMap(MapId mapId); public void RemovePlayer(ICommonSession session); public void RemoveGrid(GridId gridId); public void RemoveMap(MapId mapId); /// /// Remove all deletions up to a . /// /// The before which all deletions should be removed. public void CullDeletionHistoryUntil(GameTick tick); public bool IsDirty(IChunkIndexLocation location); public bool MarkDirty(IChunkIndexLocation location); public void ClearDirty(); } public sealed class PVSCollection : IPVSCollection where TIndex : IComparable, IEquatable { [Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2i GetChunkIndices(Vector2 coordinates) { return (coordinates / PVSSystem.ChunkSize).Floored(); } /// /// Index of which are contained in which mapchunk, indexed by . /// private readonly Dictionary>> _mapChunkContents = new(); /// /// Index of which are contained in which gridchunk, indexed by . /// private readonly Dictionary>> _gridChunkContents = new(); /// /// List of that should always get sent. /// private readonly HashSet _globalOverrides = new(); /// /// List of that should always get sent. /// public HashSet.Enumerator GlobalOverridesEnumerator => _globalOverrides.GetEnumerator(); /// /// List of that should always get sent to a certain . /// private readonly Dictionary> _localOverrides = new(); /// /// Which where last seen/sent to a certain . /// private readonly Dictionary> _lastSeen = new(); /// /// History of deletion-tuples, containing the of the deletion, as well as the of the object which was deleted. /// private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new(); /// /// An index containing the s of all . /// private readonly Dictionary _indexLocations = new(); /// /// Buffer of all locationchanges since the last process call /// private readonly Dictionary _locationChangeBuffer = new(); /// /// Buffer of all indexremovals since the last process call /// private readonly Dictionary _removalBuffer = new(); /// /// To avoid re-allocating the hashset every tick we'll just store it. /// private HashSet _changedIndices = new(); /// /// A set of all chunks changed last tick /// private HashSet _dirtyChunks = new(); public PVSCollection() { IoCManager.InjectDependencies(this); } public void Process() { _changedIndices.EnsureCapacity(_locationChangeBuffer.Count); foreach (var (key, loc) in _locationChangeBuffer) { _changedIndices.Add(key); } foreach (var (index, tick) in _removalBuffer) { //changes dont need to be computed if we are removing the index anyways if (_changedIndices.Remove(index) && !_indexLocations.ContainsKey(index)) { //this index wasnt added yet, so we can safely just skip the deletion continue; } var location = RemoveIndexInternal(index); if(location is GridChunkLocation or MapChunkLocation) _dirtyChunks.Add((IChunkIndexLocation) location); _deletionHistory.Add((tick, index)); } // remove empty chunk-subsets foreach (var chunkLocation in _dirtyChunks) { switch (chunkLocation) { case GridChunkLocation gridChunkLocation: if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunks)) continue; if(!gridChunks.TryGetValue(gridChunkLocation.ChunkIndices, out var chunk)) continue; if(chunk.Count == 0) gridChunks.Remove(gridChunkLocation.ChunkIndices); break; case MapChunkLocation mapChunkLocation: if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunks)) continue; if(!mapChunks.TryGetValue(mapChunkLocation.ChunkIndices, out chunk)) continue; if(chunk.Count == 0) mapChunks.Remove(mapChunkLocation.ChunkIndices); break; } } foreach (var index in _changedIndices) { var oldLoc = RemoveIndexInternal(index); if(oldLoc is GridChunkLocation or MapChunkLocation) _dirtyChunks.Add((IChunkIndexLocation) oldLoc); AddIndexInternal(index, _locationChangeBuffer[index], _dirtyChunks); } _changedIndices.Clear(); _locationChangeBuffer.Clear(); _removalBuffer.Clear(); } public bool IsDirty(IChunkIndexLocation location) => _dirtyChunks.Contains(location); public bool MarkDirty(IChunkIndexLocation location) => _dirtyChunks.Add(location); public void ClearDirty() => _dirtyChunks.Clear(); public bool TryGetChunk(MapId mapId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet? indices) => _mapChunkContents[mapId].TryGetValue(chunkIndices, out indices); public bool TryGetChunk(GridId gridId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet? indices) => _gridChunkContents[gridId].TryGetValue(chunkIndices, out indices); public HashSet.Enumerator GetElementsForSession(ICommonSession session) => _localOverrides[session].GetEnumerator(); private void AddIndexInternal(TIndex index, IIndexLocation location, HashSet dirtyChunks) { switch (location) { case GlobalOverride _: _globalOverrides.Add(index); break; case GridChunkLocation gridChunkLocation: // might be gone due to grid-deletions if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunk)) return; var gridLoc = gridChunk.GetOrNew(gridChunkLocation.ChunkIndices); gridLoc.Add(index); dirtyChunks.Add(gridChunkLocation); break; case LocalOverride localOverride: // might be gone due to disconnects if(!_localOverrides.ContainsKey(localOverride.Session)) return; _localOverrides[localOverride.Session].Add(index); break; case MapChunkLocation mapChunkLocation: // might be gone due to map-deletions if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunk)) return; var mapLoc = mapChunk.GetOrNew(mapChunkLocation.ChunkIndices); mapLoc.Add(index); dirtyChunks.Add(mapChunkLocation); break; } // we want this to throw if there is already an entry because if that happens we fucked up somewhere _indexLocations.Add(index, location); } private IIndexLocation? RemoveIndexInternal(TIndex index) { // the index might be gone due to disconnects/grid-/map-deletions if (!_indexLocations.TryGetValue(index, out var location)) return null; // since we can find the index, we can assume the dicts will be there too & dont need to do any checks. gaming. switch (location) { case GlobalOverride _: _globalOverrides.Remove(index); break; case GridChunkLocation gridChunkLocation: _gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index); break; case LocalOverride localOverride: _localOverrides[localOverride.Session].Remove(index); break; case MapChunkLocation mapChunkLocation: _mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index); break; } _indexLocations.Remove(index); return location; } #region Init Functions /// public void AddPlayer(ICommonSession session) { _localOverrides[session] = new(); _lastSeen[session] = new(); } /// public void AddGrid(GridId gridId) => _gridChunkContents[gridId] = new(); /// public void AddMap(MapId mapId) => _mapChunkContents[mapId] = new(); #endregion #region ShutdownFunctions /// public void RemovePlayer(ICommonSession session) { foreach (var index in _localOverrides[session]) { _indexLocations.Remove(index); } _localOverrides.Remove(session); _lastSeen.Remove(session); } /// public void RemoveGrid(GridId gridId) { foreach (var (_, indices) in _gridChunkContents[gridId]) { foreach (var index in indices) { _indexLocations.Remove(index); } } _gridChunkContents.Remove(gridId); } /// public void RemoveMap(MapId mapId) { foreach (var (_, indices) in _mapChunkContents[mapId]) { foreach (var index in indices) { _indexLocations.Remove(index); } } _mapChunkContents.Remove(mapId); } #endregion #region DeletionHistory & RemoveIndex /// /// Registers a deletion of an on a . WARNING: this also clears the index out of the internal cache!!! /// /// The at which the deletion took place. /// The of the removed object. public void RemoveIndex(GameTick tick, TIndex index) { _removalBuffer[index] = tick; } /// public void CullDeletionHistoryUntil(GameTick tick) => _deletionHistory.RemoveAll(hist => hist.tick < tick); public List GetDeletedIndices(GameTick fromTick) { var list = new List(); foreach (var (tick, id) in _deletionHistory) { if (tick >= fromTick) list.Add(id); } return list; } #endregion #region UpdateIndex private bool IsOverride(TIndex index) { if (_locationChangeBuffer.TryGetValue(index, out var change) && change is GlobalOverride or LocalOverride) return true; if (_indexLocations.TryGetValue(index, out var indexLoc) && indexLoc is GlobalOverride or LocalOverride) return true; return false; } /// /// Updates an to be sent to all players at all times. /// /// The to update. /// An index at an override position will not be updated unless you set this flag. public void UpdateIndex(TIndex index, bool removeFromOverride = false) { if(!removeFromOverride && IsOverride(index)) return; if (_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation is GlobalOverride) return; RegisterUpdate(index, new GlobalOverride()); } /// /// Updates an to be sent to a specific at all times. /// /// The to update. /// The receiving the object. /// An index at an override position will not be updated unless you set this flag. public void UpdateIndex(TIndex index, ICommonSession session, bool removeFromOverride = false) { if(!removeFromOverride && IsOverride(index)) return; if (_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation is LocalOverride local && local.Session == session) return; RegisterUpdate(index, new LocalOverride(session)); } /// /// Updates an with the location based on the provided . /// /// The to update. /// The to use when adding the to the internal cache. /// An index at an override position will not be updated unless you set this flag. public void UpdateIndex(TIndex index, EntityCoordinates coordinates, bool removeFromOverride = false) { if(!removeFromOverride && IsOverride(index)) return; var gridId = coordinates.GetGridId(_entityManager); if (gridId != GridId.Invalid) { var gridIndices = GetChunkIndices(coordinates.Position); UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups) return; } var mapCoordinates = coordinates.ToMap(_entityManager); var mapIndices = GetChunkIndices(coordinates.Position); UpdateIndex(index, mapCoordinates.MapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups) } public IChunkIndexLocation GetChunkIndex(EntityCoordinates coordinates) { var gridId = coordinates.GetGridId(_entityManager); if (gridId != GridId.Invalid) { var gridIndices = GetChunkIndices(coordinates.Position); return new GridChunkLocation(gridId, gridIndices); } var mapCoordinates = coordinates.ToMap(_entityManager); var mapIndices = GetChunkIndices(coordinates.Position); return new MapChunkLocation(mapCoordinates.MapId, mapIndices); } /// /// Updates an using the provided and . /// /// The to update. /// The id of the grid. /// The indices of the chunk. /// An index at an override position will not be updated unless you set this flag. public void UpdateIndex(TIndex index, GridId gridId, Vector2i chunkIndices, bool removeFromOverride = false) { if(!removeFromOverride && IsOverride(index)) return; if (_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation is GridChunkLocation oldGrid && oldGrid.ChunkIndices == chunkIndices && oldGrid.GridId == gridId) return; RegisterUpdate(index, new GridChunkLocation(gridId, chunkIndices)); } /// /// Updates an using the provided and . /// /// The to update. /// The id of the map. /// The indices of the mapchunk. /// An index at an override position will not be updated unless you set this flag. public void UpdateIndex(TIndex index, MapId mapId, Vector2i chunkIndices, bool removeFromOverride = false) { if(!removeFromOverride && IsOverride(index)) return; if (_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation is MapChunkLocation oldMap && oldMap.ChunkIndices == chunkIndices && oldMap.MapId == mapId) return; RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices)); } private void RegisterUpdate(TIndex index, IIndexLocation location) { _locationChangeBuffer[index] = location; } #endregion } #region IndexLocations public interface IIndexLocation {}; public interface IChunkIndexLocation{ }; public struct MapChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable { public MapChunkLocation(MapId mapId, Vector2i chunkIndices) { MapId = mapId; ChunkIndices = chunkIndices; } public MapId MapId { get; init; } public Vector2i ChunkIndices { get; init; } public bool Equals(MapChunkLocation other) { return MapId.Equals(other.MapId) && ChunkIndices.Equals(other.ChunkIndices); } public override bool Equals(object? obj) { return obj is MapChunkLocation other && Equals(other); } public override int GetHashCode() { return HashCode.Combine(MapId, ChunkIndices); } } public struct GridChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable { public GridChunkLocation(GridId gridId, Vector2i chunkIndices) { GridId = gridId; ChunkIndices = chunkIndices; } public GridId GridId { get; init; } public Vector2i ChunkIndices { get; init; } public bool Equals(GridChunkLocation other) { return GridId.Equals(other.GridId) && ChunkIndices.Equals(other.ChunkIndices); } public override bool Equals(object? obj) { return obj is GridChunkLocation other && Equals(other); } public override int GetHashCode() { return HashCode.Combine(GridId, ChunkIndices); } } public struct GlobalOverride : IIndexLocation { } public struct LocalOverride : IIndexLocation { public LocalOverride(ICommonSession session) { Session = session; } public ICommonSession Session { get; init; } } #endregion