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