Files
RobustToolbox/Robust.Server/GameStates/PVSSystem.cs
Leon Friedrich 557de01532 Dirty PVS chunk when visibility mask changes (#3045)
* Dirty PVS chunk when visibility mask changes

* fix tests?

* aaaaaa

* maybe sorta finally fix tests

* directly return vis mask,

Also defaults to 1 only if visibility component does not exist.

* make sure everything has first bit set
2022-07-17 06:23:35 +10:00

1047 lines
39 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.ObjectPool;
using NetSerializer;
using Robust.Server.GameObjects;
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.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PVSSystem : EntitySystem
{
[Shared.IoC.Dependency] private readonly IMapManagerInternal _mapManager = default!;
[Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
[Shared.IoC.Dependency] private readonly IServerEntityManager _serverEntManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _stateManager = default!;
[Shared.IoC.Dependency] private readonly SharedTransformSystem _transform = default!;
[Shared.IoC.Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
public const float ChunkSize = 8;
public const int TickBuffer = 10;
private static TransformComponentState _transformCullState =
new(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false);
/// <summary>
/// Maximum number of pooled objects
/// </summary>
private const int MaxVisPoolSize = 1024;
/// <summary>
/// Is view culling enabled, or will we send the whole map?
/// </summary>
public bool CullingEnabled { get; private set; }
/// <summary>
/// Size of the side of the view bounds square.
/// </summary>
private float _viewSize;
/// <summary>
/// If PVS disabled then we'll track if we've dumped all entities on the player.
/// This way any future ticks can be orders of magnitude faster as we only send what changes.
/// </summary>
public HashSet<ICommonSession> SeenAllEnts = new();
/// <summary>
/// All <see cref="Robust.Shared.GameObjects.EntityUid"/>s a <see cref="ICommonSession"/> saw last iteration.
/// </summary>
private readonly Dictionary<ICommonSession, OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>> _playerVisibleSets = new();
private PVSCollection<EntityUid> _entityPvsCollection = default!;
public PVSCollection<EntityUid> EntityPVSCollection => _entityPvsCollection;
private readonly List<IPVSCollection> _pvsCollections = new();
private readonly ObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>> _visSetPool
= new DefaultObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>>(
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize*TickBuffer);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<EntityUid, MetaDataComponent>> _chunkCachePool =
new DefaultObjectPool<Dictionary<EntityUid, MetaDataComponent>>(
new DictPolicy<EntityUid, MetaDataComponent>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<int>> _playerChunkPool =
new DefaultObjectPool<HashSet<int>>(new SetPolicy<int>(), MaxVisPoolSize);
private readonly ObjectPool<RobustTree<EntityUid>> _treePool =
new DefaultObjectPool<RobustTree<EntityUid>>(new TreePolicy<EntityUid>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<MapChunkLocation, int>> _mapChunkPool =
new DefaultObjectPool<Dictionary<MapChunkLocation, int>>(
new ChunkPoolPolicy<MapChunkLocation>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<GridChunkLocation, int>> _gridChunkPool =
new DefaultObjectPool<Dictionary<GridChunkLocation, int>>(
new ChunkPoolPolicy<GridChunkLocation>(), MaxVisPoolSize);
private readonly Dictionary<uint, Dictionary<MapChunkLocation, int>> _mapIndices = new(4);
private readonly Dictionary<uint, Dictionary<GridChunkLocation, int>> _gridIndices = new(4);
private readonly List<(uint, IChunkIndexLocation)> _chunkList = new(64);
private readonly List<MapGrid> _gridsPool = new(8);
public override void Initialize()
{
base.Initialize();
_entityPvsCollection = RegisterPVSCollection<EntityUid>();
SubscribeLocalEvent<MapChangedEvent>(ev =>
{
if (ev.Created)
OnMapCreated(ev);
else
OnMapDestroyed(ev);
});
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MoveEvent>(OnEntityMove);
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
EntityManager.EntityDeleted += OnEntityDeleted;
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
InitializeDirty();
}
private void OnParentChange(ref EntParentChangedMessage ev)
{
if (ev.Transform.GridUid == ev.Entity || _mapManager.IsMap(ev.Entity)) return;
// If parent changes then the RobustTree for that chunk will no longer be valid and we need to force it as dirty.
// Should still be at its old location as moveevent is called after.
var coordinates = _transform.GetMoverCoordinates(ev.Transform);
var index = _entityPvsCollection.GetChunkIndex(coordinates);
_entityPvsCollection.MarkDirty(index);
}
/// <summary>
/// Marks an entity's current chunk as drity.
/// </summary>
internal void MarkDirty(EntityUid uid)
{
var xform = Transform(uid);
var coordinates = _transform.GetMoverCoordinates(xform);
_entityPvsCollection.MarkDirty(_entityPvsCollection.GetChunkIndex(coordinates));
}
public override void Shutdown()
{
base.Shutdown();
UnregisterPVSCollection(_entityPvsCollection);
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
EntityManager.EntityDeleted -= OnEntityDeleted;
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
ShutdownDirty();
}
private void OnViewsizeChanged(float obj)
{
_viewSize = obj * 2;
}
private void SetPvs(bool value)
{
CullingEnabled = value;
}
public void ProcessCollections()
{
foreach (var collection in _pvsCollections)
{
collection.Process();
}
}
public void Cleanup(IEnumerable<IPlayerSession> sessions)
{
var playerSessions = sessions.ToArray();
if (!CullingEnabled)
{
foreach (var player in playerSessions)
{
SeenAllEnts.Add(player);
}
}
else
{
SeenAllEnts.Clear();
}
CleanupDirty(playerSessions);
foreach (var collection in _pvsCollections)
{
collection.ClearDirty();
}
}
public void CullDeletionHistory(GameTick oldestAck)
{
_entityPvsCollection.CullDeletionHistoryUntil(oldestAck);
}
#region PVSCollection methods to maybe make public someday:tm:
private PVSCollection<TIndex> RegisterPVSCollection<TIndex>() where TIndex : IComparable<TIndex>, IEquatable<TIndex>
{
var collection = new PVSCollection<TIndex>();
_pvsCollections.Add(collection);
return collection;
}
private bool UnregisterPVSCollection<TIndex>(PVSCollection<TIndex> pvsCollection) where TIndex : IComparable<TIndex>, IEquatable<TIndex> =>
_pvsCollections.Remove(pvsCollection);
#endregion
#region PVSCollection Event Updates
private void OnEntityDeleted(EntityUid e)
{
_entityPvsCollection.RemoveIndex(EntityManager.CurrentTick, e);
}
private void OnEntityMove(ref MoveEvent ev)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(ev.Component);
UpdateEntityRecursive(ev.Sender, ev.Component, coordinates, xformQuery, false);
}
private void OnTransformStartup(EntityUid uid, TransformComponent component, ref TransformStartupEvent args)
{
// use Startup because GridId is not set during the eventbus init yet!
var xformQuery = GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(component);
UpdateEntityRecursive(uid, component, coordinates, xformQuery, false);
}
private void UpdateEntityRecursive(EntityUid uid, TransformComponent xform, EntityCoordinates coordinates, EntityQuery<TransformComponent> xformQuery, bool mover)
{
if (mover && !xform.LocalPosition.Equals(Vector2.Zero))
{
coordinates = _transform.GetMoverCoordinates(xform);
}
_entityPvsCollection.UpdateIndex(uid, coordinates);
// since elements are cached grid-/map-relative, we dont need to update a given grids/maps children
if(_mapManager.IsGrid(uid) || _mapManager.IsMap(uid)) return;
var children = xform.ChildEnumerator;
while (children.MoveNext(out var child))
{
UpdateEntityRecursive(child.Value, xformQuery.GetComponent(child.Value), coordinates, xformQuery, true);
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.InGame)
{
_playerVisibleSets.Add(e.Session, new OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>(TickBuffer, _visSetPool.Return));
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.AddPlayer(e.Session);
}
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
var overflowDict = _playerVisibleSets[e.Session];
_playerVisibleSets.Remove(e.Session);
foreach (var (_, playerVisSet) in overflowDict)
{
_visSetPool.Return(playerVisSet);
}
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.RemovePlayer(e.Session);
}
}
}
private void OnGridRemoved(GridRemovalEvent ev)
{
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.RemoveGrid(ev.GridId);
}
}
private void OnGridCreated(GridInitializeEvent ev)
{
var gridId = ev.GridId;
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.AddGrid(gridId);
}
var euid = _mapManager.GetGridEuid(gridId);
_entityPvsCollection.UpdateIndex(euid);
}
private void OnMapDestroyed(MapChangedEvent e)
{
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.RemoveMap(e.Map);
}
}
private void OnMapCreated(MapChangedEvent e)
{
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.AddMap(e.Map);
}
if(e.Map == MapId.Nullspace) return;
var uid = _mapManager.GetMapEntityId(e.Map);
_entityPvsCollection.UpdateIndex(uid);
}
#endregion
public (List<(uint, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(IPlayerSession[] sessions)
{
var playerChunks = new HashSet<int>[sessions.Length];
var eyeQuery = EntityManager.GetEntityQuery<EyeComponent>();
var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var viewerEntities = new EntityUid[sessions.Length][];
_chunkList.Clear();
// Keep track of the index of each chunk we use for a faster index lookup.
// Pool it because this will allocate a lot across ticks as we scale in players.
foreach (var (_, chunks) in _mapIndices)
_mapChunkPool.Return(chunks);
foreach (var (_, chunks) in _gridIndices)
_gridChunkPool.Return(chunks);
_mapIndices.Clear();
_gridIndices.Clear();
var xformQuery = GetEntityQuery<TransformComponent>();
var physicsQuery = GetEntityQuery<PhysicsComponent>();
for (int i = 0; i < sessions.Length; i++)
{
var session = sessions[i];
playerChunks[i] = _playerChunkPool.Get();
var viewers = GetSessionViewers(session);
viewerEntities[i] = viewers;
foreach (var eyeEuid in viewers)
{
var (viewPos, range, mapId) = CalcViewBounds(in eyeEuid, transformQuery);
if(mapId == MapId.Nullspace) continue;
uint visMask = EyeComponent.DefaultVisibilityMask;
if (eyeQuery.TryGetComponent(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
// Get the nyoom dictionary for index lookups.
if (!_mapIndices.TryGetValue(visMask, out var mapDict))
{
mapDict = _mapChunkPool.Get();
_mapIndices[visMask] = mapDict;
}
var mapChunkEnumerator = new ChunkIndicesEnumerator(viewPos, range, ChunkSize);
while (mapChunkEnumerator.MoveNext(out var chunkIndices))
{
var chunkLocation = new MapChunkLocation(mapId, chunkIndices.Value);
var entry = (visMask, chunkLocation);
if (mapDict.TryGetValue(chunkLocation, out var indexOf))
{
playerChunks[i].Add(indexOf);
}
else
{
playerChunks[i].Add(_chunkList.Count);
mapDict.Add(chunkLocation, _chunkList.Count);
_chunkList.Add(entry);
}
}
// Get the nyoom dictionary for index lookups.
if (!_gridIndices.TryGetValue(visMask, out var gridDict))
{
gridDict = _gridChunkPool.Get();
_gridIndices[visMask] = gridDict;
}
_gridsPool.Clear();
foreach (var mapGrid in _mapManager.FindGridsIntersecting(
mapId,
new Box2(viewPos - range, viewPos + range),
_gridsPool,
xformQuery,
physicsQuery,
true))
{
var localPos = transformQuery.GetComponent(mapGrid.GridEntityId).InvWorldMatrix.Transform(viewPos);
var gridChunkEnumerator =
new ChunkIndicesEnumerator(localPos, range, ChunkSize);
while (gridChunkEnumerator.MoveNext(out var gridChunkIndices))
{
var chunkLocation = new GridChunkLocation(mapGrid.Index, gridChunkIndices.Value);
var entry = (visMask, chunkLocation);
if (gridDict.TryGetValue(chunkLocation, out var indexOf))
{
playerChunks[i].Add(indexOf);
}
else
{
playerChunks[i].Add(_chunkList.Count);
gridDict.Add(chunkLocation, _chunkList.Count);
_chunkList.Add(entry);
}
}
}
}
}
return (_chunkList, playerChunks, viewerEntities);
}
private Dictionary<(uint visMask, IChunkIndexLocation location), (Dictionary<EntityUid, MetaDataComponent> metadata,
RobustTree<EntityUid> tree)?> _previousTrees = new();
private HashSet<(uint visMask, IChunkIndexLocation location)> _reusedTrees = new();
public void RegisterNewPreviousChunkTrees(
List<(uint, IChunkIndexLocation)> chunks,
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] trees,
bool[] reuse)
{
// For any chunks able to re-used we'll chuck them in a dictionary for faster lookup.
for (var i = 0; i < chunks.Count; i++)
{
var canReuse = reuse[i];
if (!canReuse) continue;
_reusedTrees.Add(chunks[i]);
}
var previousIndices = _previousTrees.Keys.ToArray();
foreach (var index in previousIndices)
{
// ReSharper disable once InconsistentlySynchronizedField
if(_reusedTrees.Contains(index)) continue;
var chunk = _previousTrees[index];
if (chunk.HasValue)
{
_chunkCachePool.Return(chunk.Value.metadata);
_treePool.Return(chunk.Value.tree);
}
if (!chunks.Contains(index))
{
_previousTrees.Remove(index);
}
}
_previousTrees.EnsureCapacity(chunks.Count);
for (int i = 0; i < chunks.Count; i++)
{
//this is a redundant assign if the tree has been reused. the assumption is that this is cheaper than a .Contains call
_previousTrees[chunks[i]] = trees[i];
}
// ReSharper disable once InconsistentlySynchronizedField
_reusedTrees.Clear();
}
public bool TryCalculateChunk(
IChunkIndexLocation chunkLocation,
uint visMask,
EntityQuery<TransformComponent> transform,
EntityQuery<MetaDataComponent> metadata,
out (Dictionary<EntityUid, MetaDataComponent> mData, RobustTree<EntityUid> tree)? result)
{
if (!_entityPvsCollection.IsDirty(chunkLocation) && _previousTrees.TryGetValue((visMask, chunkLocation), out var previousTree))
{
result = previousTree;
return true;
}
var chunk = chunkLocation switch
{
GridChunkLocation gridChunkLocation => _entityPvsCollection.TryGetChunk(gridChunkLocation.GridId,
gridChunkLocation.ChunkIndices, out var gridChunk)
? gridChunk
: null,
MapChunkLocation mapChunkLocation => _entityPvsCollection.TryGetChunk(mapChunkLocation.MapId,
mapChunkLocation.ChunkIndices, out var mapChunk)
? mapChunk
: null,
_ => null
};
if (chunk == null)
{
result = null;
return false;
}
var chunkSet = _chunkCachePool.Get();
var tree = _treePool.Get();
foreach (var uid in chunk)
{
AddToChunkSetRecursively(in uid, visMask, tree, chunkSet, transform, metadata);
}
result = (chunkSet, tree);
return false;
}
public void ReturnToPool(HashSet<int>[] playerChunks)
{
foreach (var playerChunk in playerChunks)
{
_playerChunkPool.Return(playerChunk);
}
}
private bool AddToChunkSetRecursively(in EntityUid uid, uint visMask, RobustTree<EntityUid> tree, Dictionary<EntityUid, MetaDataComponent> set, EntityQuery<TransformComponent> transform,
EntityQuery<MetaDataComponent> metadata)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid()) return false;
if (set.ContainsKey(uid)) return true;
var mComp = metadata.GetComponent(uid);
// TODO: Don't need to know about parents so no longer need to use bool for this method.
// If the eye is missing ANY layer this entity or any of its parents belongs to, it is considered invisible.
if ((visMask & mComp.VisibilityMask) != mComp.VisibilityMask)
return false;
var parent = transform.GetComponent(uid).ParentUid;
if (parent.IsValid() && //is it not a worldentity?
!set.ContainsKey(parent) && //was the parent not yet added to toSend?
!AddToChunkSetRecursively(in parent, visMask, tree, set, transform, metadata)) //did we just fail to add the parent?
return false; //we failed? suppose we dont get added either
//i want it to crash here if it gets added double bc that shouldnt happen and will add alot of unneeded cycles
tree.Set(uid, parent);
set.Add(uid, mComp);
return true;
}
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(IPlayerSession session,
GameTick fromTick, GameTick toTick,
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache,
HashSet<int> chunkIndices, EntityQuery<MetaDataComponent> mQuery, EntityQuery<TransformComponent> tQuery,
EntityUid[] viewerEntities)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
var entitiesSent = 0;
_playerVisibleSets[session].TryGetValue(fromTick, out var playerVisibleSet);
var visibleEnts = _visSetPool.Get();
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
foreach (var i in chunkIndices)
{
var cache = chunkCache[i];
if(!cache.HasValue) continue;
foreach (var rootNode in cache.Value.tree.RootNodes)
{
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, cache.Value.metadata, in enteredEntityBudget);
}
}
var globalEnumerator = _entityPvsCollection.GlobalOverridesEnumerator;
while (globalEnumerator.MoveNext())
{
var uid = globalEnumerator.Current;
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
globalEnumerator.Dispose();
var localEnumerator = _entityPvsCollection.GetElementsForSession(session);
while (localEnumerator.MoveNext())
{
var uid = localEnumerator.Current;
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
localEnumerator.Dispose();
foreach (var viewerEntity in viewerEntities)
{
RecursivelyAddOverride(in viewerEntity, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
var expandEvent = new ExpandPvsEvent(session, new List<EntityUid>());
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
RecursivelyAddOverride(in entityUid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
var entityStates = new List<EntityState>();
foreach (var (entityUid, visiblity) in visibleEnts)
{
if (visiblity == PVSEntityVisiblity.StayedUnchanged)
continue;
var @new = visiblity == PVSEntityVisiblity.Entered;
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick, mQuery.GetComponent(entityUid).Flags);
//this entity is not new & nothing changed
if(!@new && state.Empty) continue;
entityStates.Add(state);
}
if(playerVisibleSet != null)
{
foreach (var (entityUid, _) in playerVisibleSet)
{
// it was deleted, so we dont need to exit pvs
if (deletions.Contains(entityUid)) continue;
//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 (!EntityManager.EntityExists(entityUid))
continue;
entityStates.Add(new EntityState(entityUid, new NetListAsArray<ComponentChange>(new[]
{
ComponentChange.Changed(_stateManager.TransformNetId, _transformCullState),
}), true));
}
}
_playerVisibleSets[session].Add(toTick, visibleEnts);
if (deletions.Count == 0) deletions = default;
if (entityStates.Count == 0) entityStates = default;
return (entityStates, deletions);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void RecursivelyAddTreeNode(in EntityUid nodeIndex,
RobustTree<EntityUid> tree,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
Dictionary<EntityUid, MetaDataComponent> metaDataCache,
in int enteredEntityBudget)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
// As every map is parented to uid 0 in the tree we still need to get their children, plus because we go top-down
// we may find duplicate parents with children we haven't encountered before
// on different chunks (this is especially common with direct grid children)
if (nodeIndex.IsValid() && !toSend.ContainsKey(nodeIndex))
{
//are we new?
var (entered, budgetFail) = ProcessEntry(in nodeIndex, previousVisibleEnts,
ref totalEnteredEntities, in enteredEntityBudget);
if (budgetFail) return;
AddToSendSet(in nodeIndex, metaDataCache[nodeIndex], toSend, fromTick, entered);
}
var node = tree[nodeIndex];
//our children are important regardless! iterate them!
if(node.Children != null)
{
foreach (var child in node.Children)
{
RecursivelyAddTreeNode(in child, tree, previousVisibleEnts, toSend, fromTick,
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget);
}
}
}
public bool RecursivelyAddOverride(
in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
EntityQuery<MetaDataComponent> metaQuery,
EntityQuery<TransformComponent> transQuery,
in int enteredEntityBudget)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid()) return false;
//did we already get added?
if (toSend.ContainsKey(uid)) return true;
var parent = transQuery.GetComponent(uid).ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, previousVisibleEnts, toSend, fromTick,
ref totalEnteredEntities, metaQuery, transQuery, in enteredEntityBudget))
return false;
var (entered, _) = ProcessEntry(in uid, previousVisibleEnts,
ref totalEnteredEntities, in enteredEntityBudget);
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, entered);
return true;
}
private (bool entered, bool budgetFail) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
ref int totalEnteredEntities, in int enteredEntityBudget)
{
var entered = previousVisibleEnts?.Remove(uid) == false;
if (entered)
{
if (totalEnteredEntities >= enteredEntityBudget)
return (entered, true);
totalEnteredEntities++;
}
return (entered, false);
}
private void AddToSendSet(in EntityUid uid, MetaDataComponent metaDataComponent, Dictionary<EntityUid, PVSEntityVisiblity> toSend, GameTick fromTick, bool entered)
{
if (entered)
{
toSend.Add(uid, PVSEntityVisiblity.Entered);
return;
}
if (metaDataComponent.EntityLastModifiedTick < fromTick)
{
//entity has been sent before and hasnt been updated since
toSend.Add(uid, PVSEntityVisiblity.StayedUnchanged);
return;
}
//add us
toSend.Add(uid, PVSEntityVisiblity.StayedChanged);
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
public (List<EntityState>? updates, List<EntityUid>? deletions) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
{
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
// no point sending an empty collection
if (deletions.Count == 0) deletions = default;
var stateEntities = new List<EntityState>();
var seenEnts = new HashSet<EntityUid>();
var slowPath = false;
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
if (!SeenAllEnts.Contains(player))
{
// Give them E V E R Y T H I N G
stateEntities = new List<EntityState>(EntityManager.EntityCount);
// This is the same as iterating every existing entity.
foreach (var md in EntityManager.EntityQuery<MetaDataComponent>(true))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md.Flags));
}
return (stateEntities.Count == 0 ? default : stateEntities, deletions);
}
// Just get the relevant entities that have been dirtied
// This should be extremely fast.
if (!slowPath)
{
for (var i = fromTick.Value; i <= toTick.Value; i++)
{
// Fallback to dumping every entity on them.
var tick = new GameTick(i);
if (!TryGetTick(tick, out var add, out var dirty))
{
slowPath = true;
break;
}
foreach (var uid in add)
{
if (!seenEnts.Add(uid)) continue;
// This is essentially the same as IEntityManager.EntityExists, but returning MetaDataComponent.
if (!metadataQuery.TryGetComponent(uid, out var md)) continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md.Flags));
}
foreach (var uid in dirty)
{
DebugTools.Assert(!add.Contains(uid));
if (!seenEnts.Add(uid)) continue;
if (!metadataQuery.TryGetComponent(uid, out var md)) continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md.Flags));
}
}
}
if (!slowPath)
{
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
}
stateEntities = new List<EntityState>(EntityManager.EntityCount);
// This is the same as iterating every existing entity.
foreach (var md in EntityManager.EntityQuery<MetaDataComponent>(true))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md.Flags));
}
// no point sending an empty collection
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="player">The player to generate this state for.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <param name="flags">Any applicable metadata flags</param>
/// <returns>New entity State for the given entity.</returns>
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataFlags flags)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
// Whether this entity has any component states that are only for a specific session.
// TODO: This GetComp is probably expensive, less expensive than before, but ideally we'd cache it somewhere or something from a previous getcomp
// Probably still needs tweaking but checking for add / changed states up front should do most of the work.
var specificStates = (flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
DebugTools.Assert(component.Initialized);
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
// i.e. when the client spawns the entity and loads the entity prototype,
// the data it deserializes from the prototype SHOULD be equal
// to what the component state / ComponentChange would send.
// As such, we can avoid sending this data in this case since the client "already has it".
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
var addState = false;
var changeState = false;
// We'll check the properties first; if we ever have specific states then doing the struct event is expensive.
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
addState = true;
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
changeState = true;
if (!addState && !changeState)
continue;
if (specificStates && !EntityManager.CanGetComponentState(bus, component, player))
continue;
if (addState)
{
ComponentState? state = null;
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
component.LastModifiedTick >= fromTick)
state = EntityManager.GetComponentState(bus, component);
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Added(netId, state));
}
else
{
DebugTools.Assert(changeState);
changed.Add(ComponentChange.Changed(netId, EntityManager.GetComponentState(bus, component)));
}
}
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, fromTick))
{
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray());
}
private EntityUid[] GetSessionViewers(ICommonSession session)
{
if (session.Status != SessionStatus.InGame)
return Array.Empty<EntityUid>();
var viewers = _uidSetPool.Get();
if (session.AttachedEntity != null)
{
// Fast path
if (session is IPlayerSession { ViewSubscriptionCount: 0 })
{
_uidSetPool.Return(viewers);
return new[] { session.AttachedEntity.Value };
}
viewers.Add(session.AttachedEntity.Value);
}
// This is awful, but we're not gonna add the list of view subscriptions to common session.
if (session is IPlayerSession playerSession)
{
foreach (var uid in playerSession.ViewSubscriptions)
{
viewers.Add(uid);
}
}
var viewerArray = viewers.ToArray();
_uidSetPool.Return(viewers);
return viewerArray;
}
// Read Safe
private (Vector2 worldPos, float range, MapId mapId) CalcViewBounds(in EntityUid euid, EntityQuery<TransformComponent> transformQuery)
{
var xform = transformQuery.GetComponent(euid);
return (xform.WorldPosition, _viewSize / 2f, xform.MapID);
}
public sealed class SetPolicy<T> : PooledObjectPolicy<HashSet<T>>
{
public override HashSet<T> Create()
{
return new HashSet<T>();
}
public override bool Return(HashSet<T> obj)
{
obj.Clear();
return true;
}
}
public sealed class DictPolicy<T1, T2> : PooledObjectPolicy<Dictionary<T1, T2>> where T1 : notnull
{
public override Dictionary<T1, T2> Create()
{
return new Dictionary<T1, T2>();
}
public override bool Return(Dictionary<T1, T2> obj)
{
obj.Clear();
return true;
}
}
public sealed class TreePolicy<T> : PooledObjectPolicy<RobustTree<T>> where T : notnull
{
public override RobustTree<T> Create()
{
var pool = new DefaultObjectPool<HashSet<T>>(new SetPolicy<T>(), MaxVisPoolSize);
return new RobustTree<T>(pool);
}
public override bool Return(RobustTree<T> obj)
{
obj.Clear();
return true;
}
}
private sealed class ChunkPoolPolicy<T> : PooledObjectPolicy<Dictionary<T, int>> where T : notnull
{
public override Dictionary<T, int> Create()
{
return new Dictionary<T, int>(32);
}
public override bool Return(Dictionary<T, int> obj)
{
obj.Clear();
return true;
}
}
}
[ByRefEvent]
public readonly struct ExpandPvsEvent
{
public readonly IPlayerSession Session;
public readonly List<EntityUid> Entities;
public ExpandPvsEvent(IPlayerSession session, List<EntityUid> entities)
{
Session = session;
Entities = entities;
}
}