Network View Bubble (#1629)

* Adds barbones culling.

* Visibility culling and recursive parent ent additions.
DebugEntityNetView improvements.
Visibility moved from session to eyecomponent.

* Multiple viewport support.

* Perf improvements.

* Removed old netbubble system from ServerEntityManager.
Supports old NaN system for entities leaving view.
Supports old SendFullMap optimization for anchored, non-updating Entities.

* Fixes size of netView box.

* Remove empty EntityManager.Update method.
Switching ViewCulling back to PLINQ.
This commit is contained in:
Acruid
2021-03-29 16:17:34 -07:00
committed by GitHub
parent 91f61bb9de
commit e16732eb7b
18 changed files with 725 additions and 957 deletions

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
@@ -9,6 +10,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -28,7 +30,7 @@ namespace Robust.Client.GameStates
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
private readonly Font _font;
private readonly int _lineHeight;
@@ -95,9 +97,9 @@ namespace Robust.Client.GameStates
}
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize*2, pvsSize*2));
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
int timeout = _gameTiming.TickRate * 3;
for (int i = 0; i < _netEnts.Count; i++)
@@ -130,16 +132,52 @@ namespace Robust.Client.GameStates
if (!_netManager.IsConnected)
return;
switch (currentSpace)
{
case OverlaySpace.ScreenSpace:
DrawScreen(handle);
break;
case OverlaySpace.WorldSpace:
DrawWorld(handle);
break;
}
}
private void DrawWorld(DrawingHandleBase handle)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
var worldHandle = (DrawingHandleWorld)handle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(DrawingHandleBase handle)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = (DrawingHandleScreen)handle;
var screenHandle = (DrawingHandleScreen) handle;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if (!_entityManager.TryGetEntity(netEnt.Id, out var ent))
{
_netEnts.RemoveSwap(i);
i--;
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetEntity(netEnt.Id).Prototype?.ID}";
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
var color = CalcTextColor(ref netEnt);
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
@@ -225,7 +263,7 @@ namespace Robust.Client.GameStates
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
@@ -36,6 +38,10 @@ namespace Robust.Client.GameStates
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
private int _totalHistoryPayload; // sum of all data point sizes in bytes
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
@@ -60,7 +66,73 @@ namespace Robust.Client.GameStates
// calc interp info
var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize;
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, interpBuff));
// not watching an ent
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
return;
string? entStateString = null;
string? entDelString = null;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates is not null)
{
var sb = new StringBuilder();
foreach (var entState in entStates)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges is not null)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges)
{
var del = compChange.Deleted ? 'D' : 'C';
sb.Append($"\n [{del}]{compChange.NetID}:{compChange.ComponentName}");
}
}
if (entState.ComponentStates is not null)
{
sb.Append($"\n States:");
foreach (var compState in entState.ComponentStates)
{
sb.Append($"\n {compState.NetID}:{compState.GetType().Name}");
}
}
}
}
entStateString = sb.ToString();
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes is not null)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
{
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
if (!string.IsNullOrWhiteSpace(entStateString))
fullString += entStateString;
if (!string.IsNullOrWhiteSpace(entDelString))
fullString += entDelString;
conShell.WriteLine(fullString + "\n");
}
}
/// <inheritdoc />
@@ -69,10 +141,16 @@ namespace Robust.Client.GameStates
base.FrameUpdate(args);
var over = _history.Count - HistorySize;
if (over > 0)
if (over <= 0)
return;
for (int i = 0; i < over; i++)
{
_history.RemoveRange(0, over);
var point = _history[i];
_totalHistoryPayload -= point.Payload;
}
_history.RemoveRange(0, over);
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
@@ -82,6 +160,7 @@ namespace Robust.Client.GameStates
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
// bottom payload line
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
@@ -101,6 +180,12 @@ namespace Robust.Client.GameStates
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
// Draw size if above average
if (drawSizeThreshold * 1.5 < state.Payload)
{
DrawString((DrawingHandleScreen) handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
}
// second tick marks
if (state.Tick.Value % _gameTiming.TickRate == 0)
{
@@ -125,6 +210,10 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f));
}
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
@@ -197,5 +286,36 @@ namespace Robust.Client.GameStates
}
}
}
private class NetWatchEntCommand : IConsoleCommand
{
public string Command => "net_watchent";
public string Help => "net_watchent <0|EntityUid>";
public string Description => "Dumps all network updates for an EntityId to the console.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
}
if (!EntityUid.TryParse(args[0], out var eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
netOverlay.WatchEntId = eValue;
}
}
}
}
}

View File

@@ -87,11 +87,11 @@ namespace Robust.Client.Graphics
public bool HasOverlay(Type overlayClass) {
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
Logger.Error("HasOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return _overlays.Remove(overlayClass);
return _overlays.ContainsKey(overlayClass);
}
public bool HasOverlay<T>() where T : Overlay {
return _overlays.Remove(typeof(T));
return _overlays.ContainsKey(typeof(T));
}
}

View File

@@ -1,41 +1,9 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
namespace Robust.Server.GameObjects
{
public interface IServerEntityManager : IEntityManager
{
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player);
/// <summary>
/// Gets all entity states within an AABB that have been modified after and including the provided tick.
/// </summary>
List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range);
// Keep track of deleted entities so we can sync deletions with the client.
/// <summary>
/// Gets a list of all entity UIDs that were deleted between <paramref name="fromTick" /> and now.
/// </summary>
List<EntityUid>? GetDeletedEntities(GameTick fromTick);
/// <summary>
/// Remove deletion history.
/// </summary>
/// <param name="toTick">The last tick to delete the history for. Inclusive.</param>
void CullDeletionHistory(GameTick toTick);
/// <summary>
/// Removes entity state persistence information from the entity manager for a player.
/// </summary>
/// <param name="player"></param>
void DropPlayerState(IPlayerSession player);
float MaxUpdateRange { get; }
}
/// <summary>
/// Server side version of the <see cref="IEntityManager"/>.
/// </summary>
public interface IServerEntityManager : IEntityManager { }
}

View File

@@ -1,55 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Shared.GameObjects.TransformComponent;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
[UsedImplicitly] // DI Container
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
{
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
"robust_entities_count",
"Amount of alive entities.");
private const float MinimumMotionForMovers = 1 / 128f;
#region IEntityManager Members
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[Shared.IoC.Dependency] private readonly IPauseManager _pauseManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configurationManager = default!;
private float? _maxUpdateRangeCache;
public float MaxUpdateRange => _maxUpdateRangeCache
??= _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
private int _nextServerEntityUid = (int) EntityUid.FirstUid;
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
public override void Update()
{
base.Update();
_maxUpdateRangeCache = null;
}
/// <inheritdoc />
public override IEntity CreateEntityUninitialized(string? prototypeName)
{
@@ -79,33 +53,6 @@ namespace Robust.Server.GameObjects
return newEntity;
}
private Entity CreateEntityServer(string? prototypeName)
{
var entity = CreateEntity(prototypeName);
if (prototypeName != null)
{
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
if (prototype.Components.ContainsKey(component.Name))
{
((Component) component).ClearTicks();
}
}
}
return entity;
}
/// <inheritdoc />
public override IEntity SpawnEntity(string? protoName, EntityCoordinates coordinates)
{
@@ -116,10 +63,7 @@ namespace Robust.Server.GameObjects
InitializeAndStartEntity((Entity) entity);
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this)))
{
entity.RunMapInit();
}
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this))) entity.RunMapInit();
return entity;
}
@@ -133,723 +77,26 @@ namespace Robust.Server.GameObjects
}
/// <inheritdoc />
public List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player)
public override void Startup()
{
var stateEntities = new List<EntityState>();
foreach (var entity in AllEntities)
{
if (entity.Deleted)
{
continue;
}
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick <= fromTick)
continue;
stateEntities.Add(GetEntityState(ComponentManager, entity.Uid, fromTick, player));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
base.Startup();
EntitySystemManager.Initialize();
Started = true;
}
private readonly Dictionary<IPlayerSession, SortedSet<EntityUid>> _seenMovers
= new();
// Is thread safe.
private SortedSet<EntityUid> GetSeenMovers(IPlayerSession player)
{
lock (_seenMovers)
{
return GetSeenMoversUnlocked(player);
}
}
private SortedSet<EntityUid> GetSeenMoversUnlocked(IPlayerSession player)
{
if (!_seenMovers.TryGetValue(player, out var movers))
{
movers = new SortedSet<EntityUid>();
_seenMovers.Add(player, movers);
}
return movers;
}
private void AddToSeenMovers(IPlayerSession player, EntityUid entityUid)
{
var movers = GetSeenMoversUnlocked(player);
movers.Add(entityUid);
}
private readonly Dictionary<IPlayerSession, Dictionary<EntityUid, GameTick>> _playerLastSeen
= new();
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
private Dictionary<EntityUid, GameTick> GetLastSeen(IPlayerSession player)
{
lock (_playerLastSeen)
{
if (!_playerLastSeen.TryGetValue(player, out var lastSeen))
{
lastSeen = new Dictionary<EntityUid, GameTick>();
_playerLastSeen.Add(player, lastSeen);
}
return lastSeen;
}
}
private static GameTick GetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
{
if (!lastSeen.TryGetValue(uid, out var tick))
{
tick = GameTick.First;
}
return tick;
}
private static GameTick UpdateLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick newTick)
{
if (!lastSeen.TryGetValue(uid, out var oldTick))
{
oldTick = GameTick.First;
}
lastSeen[uid] = newTick;
return oldTick;
}
private static IEnumerable<EntityUid> GetLastSeenAfter(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
{
foreach (var (uid, tick) in lastSeen)
{
if (tick > fromTick)
{
yield return uid;
}
}
}
private IEnumerable<EntityUid> GetLastSeenOn(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
{
foreach (var (uid, tick) in lastSeen)
{
if (tick == fromTick)
{
yield return uid;
}
}
}
private static void SetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick tick)
{
lastSeen[uid] = tick;
}
private static void ClearLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
{
lastSeen.Remove(uid);
}
public void DropPlayerState(IPlayerSession player)
{
lock (_playerLastSeen)
{
_playerLastSeen.Remove(player);
}
}
private void IncludeRelatives(IEnumerable<IEntity> children, HashSet<IEntity> set)
{
foreach (var child in children)
{
var ent = child!;
while (ent != null && !ent.Deleted)
{
if (set.Add(ent))
{
AddContainedRecursive(ent, set);
ent = ent.Transform.Parent?.Owner!;
}
else
{
// Already processed this entity once.
break;
}
}
}
}
private static void AddContainedRecursive(IEntity ent, HashSet<IEntity> set)
{
if (!ent.TryGetComponent(out ContainerManagerComponent? contMgr))
{
return;
}
foreach (var container in contMgr.GetAllContainers())
{
// Manual for loop to cut out allocations.
// ReSharper disable once ForCanBeConvertedToForeach
for (var i = 0; i < container.ContainedEntities.Count; i++)
{
var contEnt = container.ContainedEntities[i];
set.Add(contEnt);
AddContainedRecursive(contEnt, set);
}
}
}
private class PlayerSeenEntityStatesResources
{
public readonly HashSet<EntityUid> IncludedEnts = new();
public readonly List<EntityState> EntityStates = new();
public readonly HashSet<EntityUid> NeededEnts = new();
public readonly HashSet<IEntity> Relatives = new();
}
private readonly ThreadLocal<PlayerSeenEntityStatesResources> _playerSeenEntityStatesResources
= new(() => new PlayerSeenEntityStatesResources());
/// <inheritdoc />
public List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range)
public override void TickUpdate(float frameTime, Histogram? histogram)
{
var playerEnt = player.AttachedEntity;
if (playerEnt == null)
{
// super-observer?
return GetEntityStates(fromTick, player);
}
base.TickUpdate(frameTime, histogram);
var playerUid = playerEnt.Uid;
var transform = playerEnt.Transform;
var position = transform.WorldPosition;
var mapId = transform.MapID;
var viewbox = new Box2(position, position).Enlarged(MaxUpdateRange);
var seenMovers = GetSeenMovers(player);
var lSeen = GetLastSeen(player);
var pseStateRes = _playerSeenEntityStatesResources.Value!;
var checkedEnts = pseStateRes.IncludedEnts;
var entityStates = pseStateRes.EntityStates;
var neededEnts = pseStateRes.NeededEnts;
var relatives = pseStateRes.Relatives;
checkedEnts.Clear();
entityStates.Clear();
neededEnts.Clear();
relatives.Clear();
foreach (var uid in seenMovers.ToList())
{
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
{
seenMovers.Remove(uid);
continue;
}
if (entity.TryGetComponent(out IPhysBody? body))
{
if (body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
{
if (AnyParentInSet(uid, seenMovers))
{
// parent is moving
continue;
}
if (MathF.Abs(body.AngularVelocity) > 0)
{
if (entity.TryGetComponent(out TransformComponent? txf) && txf.ChildCount > 0)
{
// has children spinning
continue;
}
}
seenMovers.Remove(uid);
}
}
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (checkedEnts.Add(uid))
{
entityStates.Add(state);
// mover did not change
if (state.ComponentStates != null)
{
// mover can be seen
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// mover changed and can't be seen
var idx = Array.FindIndex(state.ComponentStates,
x => x is TransformComponentState);
if (idx != -1)
{
// mover changed positional data and can't be seen
var oldState =
(TransformComponentState) state.ComponentStates[idx];
var newState = new TransformComponentState(Vector2NaN,
oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
checkedEnts.Add(uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// either no parent attached or parent already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
neededEnts.Add(needed);
}
}
}
}
}
else
{
// mover already added?
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// mover can't be seen
var oldState =
(TransformComponentState) entity.Transform.GetComponentState(player);
entityStates.Add(new EntityState(uid,
new ComponentChanged[]
{
new(false, NetIDs.TRANSFORM, "Transform")
},
new ComponentState[]
{
new TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation)
}));
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
checkedEnts.Add(uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// either no parent attached or parent already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
neededEnts.Add(needed);
}
}
}
}
var currentTick = CurrentTick;
// scan pvs box and include children and parents recursively
IncludeRelatives(GetEntitiesInRange(mapId, position, range, true), relatives);
if (player.AttachedEntity is null || !player.AttachedEntity.TryGetComponent<EyeComponent>(out var eyeComp))
eyeComp = null;
// Exclude any entities that are currently invisible to the player.
ExcludeInvisible(relatives, (int)(eyeComp?.VisibilityMask ?? 0));
// Always send updates for all grid and map entities.
// If we don't, the client-side game state manager WILL blow up.
// TODO: Make map manager netcode aware of PVS to avoid the need for this workaround.
IncludeMapCriticalEntities(relatives);
foreach (var entity in relatives)
{
DebugTools.Assert(entity.Initialized && !entity.Deleted);
var lastChange = entity.LastModifiedTick;
var uid = entity.Uid;
var lastSeen = UpdateLastSeenTick(lSeen, uid, currentTick);
DebugTools.Assert(lastSeen != currentTick);
/*
if (uid != playerUid && entity.Prototype == playerEnt.Prototype && lastSeen < fromTick)
{
Logger.DebugS("pvs", $"Player {playerUid} is seeing player {uid}.");
}
*/
if (checkedEnts.Contains(uid))
{
// already have it
continue;
}
if (lastChange <= lastSeen)
{
// hasn't changed since last seen
continue;
}
// should this be lastSeen or fromTick?
var entityState = GetEntityState(ComponentManager, uid, lastSeen, player);
checkedEnts.Add(uid);
if (entityState.ComponentStates == null)
{
// no changes
continue;
}
entityStates.Add(entityState);
if (uid == playerUid)
{
continue;
}
if (!entity.TryGetComponent(out IPhysBody? body))
{
// can't be a mover w/o physics
continue;
}
if (!body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
{
// has motion
seenMovers.Add(uid);
}
else
{
// not moving
seenMovers.Remove(uid);
}
}
var priorTick = new GameTick(fromTick.Value - 1);
foreach (var uid in GetLastSeenOn(lSeen, priorTick))
{
if (checkedEnts.Contains(uid))
{
continue;
}
if (uid == playerUid)
{
continue;
}
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
{
// TODO: remove from states list being sent?
continue;
}
if (viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// can be seen
continue;
}
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (state.ComponentStates == null)
{
// nothing changed
continue;
}
checkedEnts.Add(uid);
entityStates.Add(state);
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
var idx = Array.FindIndex(state.ComponentStates, x => x is TransformComponentState);
if (idx == -1)
{
// no transform changes
continue;
}
var oldState = (TransformComponentState) state.ComponentStates[idx];
var newState =
new TransformComponentState(Vector2NaN, oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// don't need to include parent or already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.First)
{
neededEnts.Add(needed);
}
}
do
{
var moreNeededEnts = new HashSet<EntityUid>();
foreach (var uid in moreNeededEnts)
{
if (checkedEnts.Contains(uid))
{
continue;
}
var entity = GetEntity(uid);
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (state.ComponentStates == null || viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// no states or should already be seen
continue;
}
checkedEnts.Add(uid);
entityStates.Add(state);
var idx = Array.FindIndex(state.ComponentStates,
x => x is TransformComponentState);
if (idx == -1)
{
// no transform state
continue;
}
var oldState = (TransformComponentState) state.ComponentStates[idx];
var newState =
new TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// done here
continue;
}
// check if further needed
if (!checkedEnts.Contains(uid) && GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
moreNeededEnts.Add(needed);
}
}
neededEnts = moreNeededEnts;
} while (neededEnts.Count > 0);
// help the client out
entityStates.Sort((a, b) => a.Uid.CompareTo(b.Uid));
#if DEBUG_NULL_ENTITY_STATES
foreach ( var state in entityStates ) {
if (state.ComponentStates == null)
{
throw new NotImplementedException("Shouldn't send null states.");
}
}
#endif
// no point sending an empty collection
return entityStates.Count == 0 ? default : entityStates;
EntitiesCount.Set(AllEntities.Count);
}
public override void DeleteEntity(IEntity e)
{
base.DeleteEntity(e);
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(e));
_deletionHistory.Add((CurrentTick, e.Uid));
}
public List<EntityUid>? GetDeletedEntities(GameTick fromTick)
{
var list = new List<EntityUid>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick >= fromTick)
{
list.Add(id);
}
}
// no point sending an empty collection
return list.Count == 0 ? default : list;
}
public void CullDeletionHistory(GameTick toTick)
{
_deletionHistory.RemoveAll(hist => hist.tick <= toTick);
}
public override bool UpdateEntityTree(IEntity entity, Box2? worldAABB = null)
{
var currentTick = CurrentTick;
var updated = base.UpdateEntityTree(entity, worldAABB);
if (entity.Deleted
|| !entity.Initialized
|| !Entities.ContainsKey(entity.Uid))
{
return updated;
}
DebugTools.Assert(entity.Transform.Initialized);
// note: updated can be false even if something moved a bit
worldAABB ??= GetWorldAabbFromEntity(entity);
foreach (var (player, lastSeen) in _playerLastSeen)
{
var playerEnt = player.AttachedEntity;
if (playerEnt == null)
{
// player has no entity, gaf?
continue;
}
var playerUid = playerEnt.Uid;
var entityUid = entity.Uid;
if (entityUid == playerUid)
{
continue;
}
if (!lastSeen.TryGetValue(playerUid, out var playerTick))
{
// player can't "see" itself, gaf?
continue;
}
var playerPos = playerEnt.Transform.WorldPosition;
var viewbox = new Box2(playerPos, playerPos).Enlarged(MaxUpdateRange);
if (!lastSeen.TryGetValue(entityUid, out var tick))
{
// never saw it other than first tick or was cleared
if (!AnyParentMoving(player, entityUid))
{
continue;
}
}
if (tick >= currentTick)
{
// currently seeing it
continue;
}
// saw it previously
// player can't see it now
if (!viewbox.Intersects(worldAABB.Value))
{
var addToMovers = false;
if (entity.Transform.LastModifiedTick >= currentTick)
{
addToMovers = true;
}
else if (entity.TryGetComponent(out IPhysBody? physics)
&& physics.LastModifiedTick >= currentTick)
{
addToMovers = true;
}
if (addToMovers)
{
AddToSeenMovers(player, entityUid);
}
}
}
return updated;
}
private bool AnyParentMoving(IPlayerSession player, EntityUid entityUid)
{
var seenMovers = GetSeenMoversUnlocked(player);
if (seenMovers == null)
{
return false;
}
return AnyParentInSet(entityUid, seenMovers);
}
private bool AnyParentInSet(EntityUid entityUid, SortedSet<EntityUid> set)
{
for (;;)
{
if (!TryGetEntity(entityUid, out var ent))
{
return false;
}
var txf = ent.Transform;
entityUid = txf.ParentUid;
if (entityUid == EntityUid.Invalid)
{
return false;
}
if (set.Contains(entityUid))
{
return true;
}
}
}
#endregion IEntityManager Members
IEntity IServerEntityManagerInternal.AllocEntity(string? prototypeName, EntityUid? uid)
{
return AllocEntity(prototypeName, uid);
}
protected override EntityUid GenerateEntityUid()
{
return new(_nextServerEntityUid++);
}
void IServerEntityManagerInternal.FinishEntityLoad(IEntity entity, IEntityLoadContext? context)
{
LoadEntity((Entity) entity, context);
@@ -866,95 +113,33 @@ namespace Robust.Server.GameObjects
}
/// <inheritdoc />
public override void Startup()
protected override EntityUid GenerateEntityUid()
{
base.Startup();
EntitySystemManager.Initialize();
Started = true;
return new(_nextServerEntityUid++);
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="compMan">ComponentManager that contains the components for the entity.</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="player">The player to generate this state for.</param>
/// <returns>New entity State for the given entity.</returns>
private static EntityState GetEntityState(IComponentManager compMan, EntityUid entityUid, GameTick fromTick, IPlayerSession player)
private Entity CreateEntityServer(string? prototypeName)
{
var compStates = new List<ComponentState>();
var changed = new List<ComponentChanged>();
var entity = CreateEntity(prototypeName);
foreach (var comp in compMan.GetNetComponents(entityUid))
if (prototypeName != null)
{
DebugTools.Assert(comp.Initialized);
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
// 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 / ComponentChanged would send.
// As such, we can avoid sending this data in this case since the client "already has it".
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
compStates.Add(comp.GetComponentState(player));
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
}
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
if (prototype.Components.ContainsKey(component.Name)) ((Component) component).ClearTicks();
}
}
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
}
private void IncludeMapCriticalEntities(HashSet<IEntity> set)
{
foreach (var mapId in _mapManager.GetAllMapIds())
{
if (_mapManager.HasMapEntity(mapId))
{
set.Add(_mapManager.GetMapEntity(mapId));
}
}
foreach (var grid in _mapManager.GetAllGrids())
{
if (grid.GridEntityId != EntityUid.Invalid)
{
set.Add(GetEntity(grid.GridEntityId));
}
}
}
private void ExcludeInvisible(HashSet<IEntity> set, int visibilityMask)
{
set.RemoveWhere(e =>
{
if (!e.TryGetComponent(out VisibilityComponent? visibility))
{
return false;
}
return (visibilityMask & visibility.Layer) == 0;
});
}
public override void TickUpdate(float frameTime, Histogram? histogram)
{
base.TickUpdate(frameTime, histogram);
EntitiesCount.Set(AllEntities.Count);
return entity;
}
}
}

View File

@@ -0,0 +1,324 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
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 class EntityViewCulling
{
private const int ViewSetCapacity = 128; // starting number of entities that are in view
private const int PlayerSetSize = 64; // Starting number of players
private const int MaxVisPoolSize = 1024; // Maximum number of pooled objects
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
private readonly IServerEntityManager _entMan;
private readonly IComponentManager _compMan;
private readonly IMapManager _mapManager;
private readonly Dictionary<ICommonSession, HashSet<EntityUid>> _playerVisibleSets = new(PlayerSetSize);
private readonly ConcurrentDictionary<ICommonSession, GameTick> _playerLastFullMap = new();
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
private readonly ObjectPool<HashSet<EntityUid>> _visSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _viewerEntsPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
/// <summary>
/// Is view culling enabled, or will we send the whole map?
/// </summary>
public bool CullingEnabled { get; set; }
/// <summary>
/// Size of the side of the view bounds square.
/// </summary>
public float ViewSize { get; set; }
public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager)
{
_entMan = entMan;
_compMan = entMan.ComponentManager;
_mapManager = mapManager;
_compMan = _entMan.ComponentManager;
}
// 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)
{
_deletionHistory.RemoveAll(hist => hist.tick < oldestAck);
}
private List<EntityUid> GetDeletedEntities(GameTick fromTick)
{
var list = new List<EntityUid>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick >= fromTick) list.Add(id);
}
return list;
}
// Not thread safe
public void AddPlayer(ICommonSession session)
{
_playerVisibleSets.Add(session, new HashSet<EntityUid>(ViewSetCapacity));
}
// Not thread safe
public void RemovePlayer(ICommonSession session)
{
_playerVisibleSets.Remove(session);
_playerLastFullMap.Remove(session, out _);
}
// thread safe
public bool IsPointVisible(ICommonSession session, in MapCoordinates position)
{
var viewables = GetSessionViewers(session);
bool CheckInView(MapCoordinates mapCoordinates, HashSet<EntityUid> 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;
}
private HashSet<EntityUid> GetSessionViewers(ICommonSession session)
{
var viewers = _viewerEntsPool.Get();
if (session.Status != SessionStatus.InGame || session.AttachedEntityUid is null)
return viewers;
var query = _compMan.EntityQuery<BasicActorComponent>();
foreach (var actorComp in query)
{
if (actorComp.playerSession == session)
viewers.Add(actorComp.Owner.Uid);
}
return viewers;
}
// thread safe
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(ICommonSession session, GameTick fromTick, GameTick toTick)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
//TODO: Stop sending all entities to every player first tick
List<EntityUid>? deletions;
if (!CullingEnabled || fromTick == GameTick.Zero)
{
var allStates = ServerGameStateManager.GetAllEntityStates(_entMan, session, fromTick);
deletions = GetDeletedEntities(fromTick);
_playerLastFullMap.AddOrUpdate(session, toTick, (_, _) => toTick);
return (allStates, deletions);
}
var lastMapUpdate = _playerLastFullMap.GetValueOrDefault(session);
var currentSet = CalcCurrentViewSet(session);
// If they don't have a usable eye, nothing to send, and map remove will deal with ent removal
if (currentSet is null)
return (null, null);
deletions = GetDeletedEntities(fromTick);
// pretty big allocations :(
List<EntityState> entityStates = new(currentSet.Count);
var previousSet = _playerVisibleSets[session];
// complement set
foreach (var entityUid in previousSet)
{
if (!currentSet.Contains(entityUid) && !deletions.Contains(entityUid))
{
if(_compMan.HasComponent<SnapGridComponent>(entityUid))
continue;
// PVS leave message
//TODO: Remove NaN as the signal to leave PVS
var xform = _compMan.GetComponent<ITransformComponent>(entityUid);
var oldState = (TransformComponent.TransformComponentState)xform.GetComponentState(session);
entityStates.Add(new EntityState(entityUid,
new ComponentChanged[]
{
new(false, NetIDs.TRANSFORM, "Transform")
},
new ComponentState[]
{
new TransformComponent.TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation)
}));
}
}
foreach (var entityUid in currentSet)
{
if (previousSet.Contains(entityUid))
{
//Still Visible
// only send new changes
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, fromTick);
if (!newState.Empty)
entityStates.Add(newState);
}
else
{
// PVS enter message
// skip sending anchored entities (walls)
if (_compMan.HasComponent<SnapGridComponent>(entityUid) && _entMan.GetEntity(entityUid).LastModifiedTick <= lastMapUpdate)
continue;
// don't assume the client knows anything about us
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, GameTick.Zero);
entityStates.Add(newState);
}
}
// swap out vis sets
_playerVisibleSets[session] = currentSet;
previousSet.Clear();
_visSetPool.Return(previousSet);
// no point sending an empty collection
deletions = deletions?.Count == 0 ? default : deletions;
return (entityStates, deletions);
}
private HashSet<EntityUid>? CalcCurrentViewSet(ICommonSession session)
{
if (!CullingEnabled)
return null;
// if you don't have an attached entity, you don't see the world.
if (session.AttachedEntityUid is null)
return null;
var visibleEnts = _visSetPool.Get();
var viewers = GetSessionViewers(session);
foreach (var eyeEuid in viewers)
{
var (viewBox, mapId) = CalcViewBounds(in eyeEuid);
uint visMask = 0;
if (_compMan.TryGetComponent<EyeComponent>(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
//Always include the map entity
visibleEnts.Add(_mapManager.GetMapEntityId(mapId));
//Always include viewable ent itself
visibleEnts.Add(eyeEuid);
// grid entity should be added through this
// assume there are no deleted ents in here, cull them first in ent/comp manager
_entMan.FastEntitiesIntersecting(in mapId, ref viewBox, entity => RecursiveAdd((TransformComponent)entity.Transform, visibleEnts, visMask));
}
viewers.Clear();
_viewerEntsPool.Return(viewers);
return visibleEnts;
}
// Read Safe
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool RecursiveAdd(TransformComponent xform, HashSet<EntityUid> visSet, uint visMask)
{
var xformUid = xform.Owner.Uid;
// we are done, this ent has already been checked and is visible
if (visSet.Contains(xformUid))
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 (_compMan.TryGetComponent<VisibilityComponent>(xformUid, out var visComp))
{
if ((visMask & visComp.Layer) == 0)
return false;
}
var xformParentUid = xform.ParentUid;
// this is the world entity, it is always visible
if (!xformParentUid.IsValid())
{
visSet.Add(xformUid);
return true;
}
// parent is already in the set
if (visSet.Contains(xformParentUid))
{
visSet.Add(xformUid);
return true;
}
// parent was not added, so we are not either
var xformParent = _compMan.GetComponent<TransformComponent>(xformParentUid);
if (!RecursiveAdd(xformParent, visSet, visMask))
return false;
// add us
visSet.Add(xformUid);
return true;
}
// Read Safe
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid)
{
var xform = _compMan.GetComponent<ITransformComponent>(euid);
var view = Box2.UnitCentered.Scale(ViewSize).Translated(xform.WorldPosition);
var map = xform.MapID;
return (view, map);
}
}
}

View File

@@ -1,18 +1,21 @@
namespace Robust.Server.GameStates
{
/// <summary>
/// Engine service that provides creating and dispatching of game states.
/// Engine service that provides creating and dispatching of game states.
/// </summary>
public interface IServerGameStateManager
{
/// <summary>
/// One time initialization of the service.
/// One time initialization of the service.
/// </summary>
void Initialize();
/// <summary>
/// Create and dispatch game states to all connected sessions.
/// Create and dispatch game states to all connected sessions.
/// </summary>
void SendGameStateUpdate();
bool PvsEnabled { get; }
float PvsRange { get; }
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
@@ -9,22 +10,24 @@ using Robust.Shared.Enums;
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;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates
{
/// <inheritdoc />
public class ServerGameStateManager : IServerGameStateManager
/// <inheritdoc cref="IServerGameStateManager"/>
public class ServerGameStateManager : IServerGameStateManager, IPostInjectInit
{
// Mapping of net UID of clients -> last known acked state.
private readonly Dictionary<long, GameTick> _ackedStates = new();
private GameTick _lastOldestAck = GameTick.Zero;
private EntityViewCulling _entityView = null!;
[Dependency] private readonly IServerEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IServerNetManager _networkManager = default!;
@@ -35,6 +38,12 @@ namespace Robust.Server.GameStates
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public bool PvsEnabled => _configurationManager.GetCVar(CVars.NetPVS);
public float PvsRange => _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
public void PostInject()
{
_entityView = new EntityViewCulling(_entityManager, _mapManager);
}
/// <inheritdoc />
public void Initialize()
@@ -44,6 +53,27 @@ namespace Robust.Server.GameStates
_networkManager.Connected += HandleClientConnected;
_networkManager.Disconnect += HandleClientDisconnect;
_playerManager.PlayerStatusChanged += HandlePlayerStatusChanged;
_entityManager.EntityDeleted += HandleEntityDeleted;
}
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);
}
}
private void HandleClientConnected(object? sender, NetChannelArgs e)
@@ -57,13 +87,6 @@ namespace Robust.Server.GameStates
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
{
_ackedStates.Remove(e.Channel.ConnectionId);
if (!_playerManager.TryGetSessionByChannel(e.Channel, out var session))
{
return;
}
_entityManager.DropPlayerState(session);
}
private void HandleStateAck(MsgStateAck msg)
@@ -98,12 +121,13 @@ namespace Robust.Server.GameStates
{
DebugTools.Assert(_networkManager.IsServer);
_entityManager.Update();
_entityView.ViewSize = PvsRange * 2;
_entityView.CullingEnabled = PvsEnabled;
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
_entityManager.CullDeletionHistory(GameTick.MaxValue);
_entityView.CullDeletionHistory(GameTick.MaxValue);
_mapManager.CullDeletionHistory(GameTick.MaxValue);
return;
}
@@ -112,16 +136,14 @@ namespace Robust.Server.GameStates
var oldestAck = GameTick.MaxValue;
var oldDeps = IoCManager.Resolve<IDependencyCollection>();
var deps = new DependencyCollection();
deps.RegisterInstance<ILogManager>(new ProxyLogManager(IoCManager.Resolve<ILogManager>()));
deps.BuildGraph();
var mainThread = Thread.CurrentThread;
(MsgState, INetChannel) GenerateMail(IPlayerSession session)
{
IoCManager.InitThread(deps, true);
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(), true);
// people not in the game don't get states
if (session.Status != SessionStatus.InGame)
{
return default;
@@ -134,11 +156,8 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var entStates = lastAck == GameTick.Zero || !PvsEnabled
? _entityManager.GetEntityStates(lastAck, session)
: _entityManager.UpdatePlayerSeenEntityStates(lastAck, session, _entityManager.MaxUpdateRange);
var (entStates, deletions) = _entityView.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
var playerStates = _playerManager.GetPlayerStates(lastAck);
var deletions = _entityManager.GetDeletedEntities(lastAck);
var mapData = _mapManager.GetStateData(lastAck);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
@@ -167,15 +186,9 @@ namespace Robust.Server.GameStates
}
var mailBag = _playerManager.GetAllPlayers()
.AsParallel().Select(GenerateMail).ToList();
// TODO: oh god oh fuck kill it with fire.
// PLINQ *seems* to be scheduling to the main thread partially (I guess that makes sense?)
// Which causes that IoC "hack" up there to override IoC in the main thread, nuking the game.
// At least, that's our running theory. I reproduced it once locally and can't reproduce it again.
// Throwing shit at the wall to hope it fixes it.
IoCManager.InitThread(oldDeps, true);
.AsParallel()
.Where(s=>s.Status == SessionStatus.InGame).Select(GenerateMail);
foreach (var (msg, chan) in mailBag)
{
// see session.Status != SessionStatus.InGame above
@@ -187,9 +200,78 @@ namespace Robust.Server.GameStates
if (oldestAck > _lastOldestAck)
{
_lastOldestAck = oldestAck;
_entityManager.CullDeletionHistory(oldestAck);
_entityView.CullDeletionHistory(oldestAck);
_mapManager.CullDeletionHistory(oldestAck);
}
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="compMan">ComponentManager that contains the components for the entity.</param>
/// <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>
/// <returns>New entity State for the given entity.</returns>
internal static EntityState GetEntityState(IComponentManager compMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
{
var compStates = new List<ComponentState>();
var changed = new List<ComponentChanged>();
foreach (var comp in compMan.GetNetComponents(entityUid))
{
DebugTools.Assert(comp.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 / ComponentChanged would send.
// As such, we can avoid sending this data in this case since the client "already has it".
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
compStates.Add(comp.GetComponentState(player));
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
}
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
}
}
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
internal static List<EntityState>? GetAllEntityStates(IEntityManager entityMan, ICommonSession player, GameTick fromTick)
{
var stateEntities = new List<EntityState>();
foreach (var entity in entityMan.GetEntities())
{
if (entity.Deleted)
{
continue;
}
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick <= fromTick)
continue;
stateEntities.Add(GetEntityState(entityMan.ComponentManager, player, entity.Uid, fromTick));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
}
}
}

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.4" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="Serilog.Sinks.Loki" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="5.0.0" />

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -14,18 +15,20 @@ using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
public delegate void EntityQueryCallback(IEntity entity);
/// <inheritdoc />
public abstract class EntityManager : IEntityManager
{
#region Dependencies
[Dependency] private readonly IEntityNetworkManager EntityNetworkManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] private readonly IComponentFactory ComponentFactory = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[IoC.Dependency] private readonly IEntityNetworkManager EntityNetworkManager = default!;
[IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[IoC.Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[IoC.Dependency] private readonly IComponentFactory ComponentFactory = default!;
[IoC.Dependency] private readonly IComponentManager _componentManager = default!;
[IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
[IoC.Dependency] private readonly IMapManager _mapManager = default!;
#endregion Dependencies
@@ -277,6 +280,7 @@ namespace Robust.Shared.GameObjects
entity.LifeStage = EntityLifeStage.Deleted;
EntityDeleted?.Invoke(this, entity.Uid);
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(entity));
}
public void DeleteEntity(EntityUid uid)
@@ -456,6 +460,9 @@ namespace Robust.Shared.GameObjects
}
}
/// <summary>
/// Factory for generating a new EntityUid for an entity currently being created.
/// </summary>
protected abstract EntityUid GenerateEntityUid();
#region Spatial Queries
@@ -477,6 +484,17 @@ namespace Robust.Shared.GameObjects
return found;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback)
{
if (mapId == MapId.Nullspace)
return;
_entityTreesPerMap[mapId]._b2Tree
.FastQuery(ref position, (ref IEntity data) => callback(data));
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position, bool approximate = false)
{
@@ -717,18 +735,8 @@ namespace Robust.Shared.GameObjects
}
#endregion
public virtual void Update()
{
}
}
/// <summary>
/// The children of this entity are about to be deleted.
/// </summary>
public class EntityTerminatingEvent : EntityEventArgs { }
public enum EntityMessageType : byte
{
Error = 0,

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Serialization;
using Robust.Shared.Serialization;
using System;
namespace Robust.Shared.GameObjects
@@ -10,6 +10,8 @@ namespace Robust.Shared.GameObjects
public ComponentChanged[]? ComponentChanges { get; }
public ComponentState[]? ComponentStates { get; }
public bool Empty => ComponentChanges is null && ComponentStates is null;
public EntityState(EntityUid uid, ComponentChanged[]? changedComponents, ComponentState[]? componentStates)
{
Uid = uid;

View File

@@ -1,6 +1,4 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
namespace Robust.Shared.GameObjects
{
public sealed class EntityDeletedMessage : EntityEventArgs
{

View File

@@ -0,0 +1,7 @@
namespace Robust.Shared.GameObjects
{
/// <summary>
/// The children of this entity are about to be deleted.
/// </summary>
public class EntityTerminatingEvent : EntityEventArgs { }
}

View File

@@ -126,6 +126,8 @@ namespace Robust.Shared.GameObjects
/// <param name="box"></param>
/// <param name="approximate">If true, will not recalculate precise entity AABBs, resulting in a perf increase. </param>
bool AnyEntitiesIntersecting(MapId mapId, Box2 box, bool approximate = false);
void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback);
/// <summary>
/// Gets entities with a bounding box that intersects this box
@@ -216,8 +218,5 @@ namespace Robust.Shared.GameObjects
bool RemoveFromEntityTree(IEntity entity, MapId mapId);
#endregion
void Update();
}
}

View File

@@ -1,10 +1,10 @@
using System;
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.Map
{
[Serializable, NetSerializable]
public struct MapId : IEquatable<MapId>
public readonly struct MapId : IEquatable<MapId>
{
public static readonly MapId Nullspace = new(0);

View File

@@ -915,6 +915,41 @@ namespace Robust.Shared.Physics
}
}
public delegate void FastQueryCallback(ref T userData);
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void FastQuery(ref Box2 aabb, FastQueryCallback callback)
{
var stack = new GrowableStack<Proxy>(stackalloc Proxy[256]);
stack.Push(_root);
ref var baseRef = ref _nodes[0];
while (stack.GetCount() != 0)
{
var nodeId = stack.Pop();
if (nodeId == Proxy.Free)
{
continue;
}
// Skip bounds check with Unsafe.Add().
ref var node = ref Unsafe.Add(ref baseRef, nodeId);
ref var nodeAabb = ref node.Aabb;
if (nodeAabb.Intersects(aabb))
{
if (node.IsLeaf)
{
callback(ref node.UserData);
}
else
{
stack.Push(node.Child1);
stack.Push(node.Child2);
}
}
}
}
private static readonly RayQueryCallback<RayQueryCallback> EasyRayQueryCallback =
(ref RayQueryCallback callback, Proxy proxy, in Vector2 hitPos, float distance) => callback(proxy, hitPos, distance);

View File

@@ -1,4 +1,4 @@
/*
/*
* Initially based on Box2D by Erin Catto, license follows;
*
* Copyright (c) 2009 Erin Catto http://www.box2d.org
@@ -72,7 +72,7 @@ namespace Robust.Shared.Physics
// avoids "Collection was modified; enumeration operation may not execute."
private Dictionary<T, Proxy> _nodeLookup;
private readonly B2DynamicTree<T> _b2Tree;
public readonly B2DynamicTree<T> _b2Tree;
public DynamicTree(ExtractAabbDelegate extractAabbFunc, IEqualityComparer<T>? comparer = null, float aabbExtendSize = 1f / 32, int capacity = 256, Func<int, int>? growthFunc = null)
{

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Server.GameObjects;
@@ -8,7 +8,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Timing;
using MapGrid = Robust.Shared.Map.MapGrid;
namespace Robust.UnitTesting.Shared.Map
@@ -159,7 +158,6 @@ namespace Robust.UnitTesting.Shared.Map
private static IMapGridInternal MapGridFactory(GridId id)
{
var entMan = (ServerEntityManager)IoCManager.Resolve<IEntityManager>();
entMan.CullDeletionHistory(GameTick.MaxValue);
var mapId = new MapId(5);
var mapMan = IoCManager.Resolve<IMapManager>();