mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
324
Robust.Server/GameStates/EntityViewCulling.cs
Normal file
324
Robust.Server/GameStates/EntityViewCulling.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
public sealed class EntityDeletedMessage : EntityEventArgs
|
||||
{
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// The children of this entity are about to be deleted.
|
||||
/// </summary>
|
||||
public class EntityTerminatingEvent : EntityEventArgs { }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user