Remove RobustTree & PVSCollection (#4759)

* Cut down RobustTree

* Better LoD

* Add PvsPriority flag

* Undo cvar name change

* reorganize grafana metrics

* Fix tests

* Fix replays

* Don't try process empty chunks

* Fix move benchmark

* Fix benchmark

* Remove obsolete audio methods

* Moar benchmarks

* Rename EntityData

* A

* B
This commit is contained in:
Leon Friedrich
2023-12-27 17:10:13 -05:00
committed by GitHub
parent ced6d5a8b0
commit eba58cb893
64 changed files with 2373 additions and 2621 deletions

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Robust.Server.Containers;
using Robust.Server.GameStates;
@@ -11,7 +12,9 @@ using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.UnitTesting;
namespace Robust.Benchmarks.Transform;
@@ -19,114 +22,176 @@ namespace Robust.Benchmarks.Transform;
/// This benchmark tests various transform/move related functions with an entity that has many children.
/// </summary>
[Virtual, MemoryDiagnoser]
public class RecursiveMoveBenchmark
public class RecursiveMoveBenchmark : RobustIntegrationTest
{
private ISimulation _simulation = default!;
private IEntityManager _entMan = default!;
private SharedTransformSystem _transform = default!;
private ContainerSystem _container = default!;
private PvsSystem _pvs = default!;
private EntityCoordinates _mapCoords;
private EntityCoordinates _gridCoords;
private EntityCoordinates _gridCoords2;
private EntityUid _ent;
private EntityUid _child;
private TransformComponent _childXform = default!;
private EntityQuery<TransformComponent> _query;
private ICommonSession[] _players = default!;
private PvsSession _session = default!;
[GlobalSetup]
public void GlobalSetup()
{
_simulation = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
ProgramShared.PathOffset = "../../../../";
var server = StartServer();
var client = StartClient();
if (!_simulation.Resolve<IConfigurationManager>().GetCVar(CVars.NetPVS))
throw new InvalidOperationException("PVS must be enabled");
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();
var mapMan = server.ResolveDependency<IMapManager>();
_entMan = server.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
_entMan = _simulation.Resolve<IEntityManager>();
_transform = _entMan.System<SharedTransformSystem>();
_container = _entMan.System<ContainerSystem>();
_pvs = _entMan.System<PvsSystem>();
_query = _entMan.GetEntityQuery<TransformComponent>();
// Create map & grid
var mapMan = _simulation.Resolve<IMapManager>();
var mapSys = _entMan.System<SharedMapSystem>();
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_mapCoords = new EntityCoordinates(map, 100, 100);
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
_ent = _entMan.Spawn();
var netMan = client.ResolveDependency<IClientNetManager>();
client.SetConnectTarget(server);
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
// Quick check that SetCoordinates actually changes the parent as expected
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
_transform.SetCoordinates(_ent, _gridCoords);
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
throw new Exception("Grid traversal error.");
_transform.SetCoordinates(_ent, _mapCoords);
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
throw new Exception("Grid traversal error.");
// Add 5 direct children in slots to represent clothing.
for (var i = 0; i < 5; i++)
for (int i = 0; i < 10; i++)
{
var id = $"inventory{i}";
_container.EnsureContainer<ContainerSlot>(_ent, id);
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
throw new Exception($"Failed to setup entity");
server.WaitRunTicks(1).Wait();
client.WaitRunTicks(1).Wait();
}
// body parts
_container.EnsureContainer<Container>(_ent, "body");
for (var i = 0; i < 5; i++)
// Ensure client & server ticks are synced.
// Client runs 1 tick ahead
{
// Simple organ
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
throw new Exception($"Failed to setup entity");
var sTick = (int)server.Timing.CurTick.Value;
var cTick = (int)client.Timing.CurTick.Value;
var delta = cTick - sTick;
// body part that has another body part / limb
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
throw new Exception($"Failed to setup entity");
if (delta > 1)
server.WaitRunTicks(delta - 1).Wait();
else if (delta < 1)
client.WaitRunTicks(1 - delta).Wait();
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
throw new Exception($"Failed to setup entity");
sTick = (int)server.Timing.CurTick.Value;
cTick = (int)client.Timing.CurTick.Value;
delta = cTick - sTick;
if (delta != 1)
throw new Exception("Failed setup");
}
// Backpack
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
throw new Exception($"Failed to setup entity");
// Misc backpack contents.
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
for (var i = 0; i < 10; i++)
// Set up map and spawn player
server.WaitPost(() =>
{
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_gridCoords2 = new EntityCoordinates(grid, .5f, .6f);
_mapCoords = new EntityCoordinates(map, 100, 100);
var playerUid = _entMan.SpawnEntity(null, _mapCoords);
// Attach player.
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, playerUid);
sPlayerMan.JoinGame(session);
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
_ent = _entMan.Spawn();
// Quick check that SetCoordinates actually changes the parent as expected
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
_transform.SetCoordinates(_ent, _gridCoords);
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
throw new Exception("Grid traversal error.");
_transform.SetCoordinates(_ent, _mapCoords);
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
throw new Exception("Grid traversal error.");
// Add 5 direct children in slots to represent clothing.
for (var i = 0; i < 5; i++)
{
var id = $"inventory{i}";
_container.EnsureContainer<ContainerSlot>(_ent, id);
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
throw new Exception($"Failed to setup entity");
}
// body parts
_container.EnsureContainer<Container>(_ent, "body");
for (var i = 0; i < 5; i++)
{
// Simple organ
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
throw new Exception($"Failed to setup entity");
// body part that has another body part / limb
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
throw new Exception($"Failed to setup entity");
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
throw new Exception($"Failed to setup entity");
}
// Backpack
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
throw new Exception($"Failed to setup entity");
// Misc backpack contents.
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
throw new Exception($"Failed to setup entity");
}
// Emergency box inside of the backpack
var box = backpackStorage.ContainedEntities.First();
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
throw new Exception($"Failed to setup entity");
}
// Deepest child.
_child = boxContainer.ContainedEntities.First();
_childXform = _query.GetComponent(_child);
_players = new[] {session};
_session = _pvs.PlayerData[session];
}).Wait();
for (int i = 0; i < 10; i++)
{
server.WaitRunTicks(1).Wait();
client.WaitRunTicks(1).Wait();
}
// Emergency box inside of the backpack
var box = backpackStorage.ContainedEntities.First();
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
throw new Exception($"Failed to setup entity");
}
PvsTick();
PvsTick();
}
// Deepest child.
_child = boxContainer.ContainedEntities.First();
_childXform = _query.GetComponent(_child);
_pvs.ProcessCollections();
private void PvsTick()
{
_session.ClearState();
_pvs.GetVisibleChunks(_players, null);
_pvs.ProcessVisibleChunksSequential();
}
/// <summary>
@@ -140,6 +205,13 @@ public class RecursiveMoveBenchmark
_transform.SetCoordinates(_ent, _mapCoords);
}
[Benchmark]
public void MoveEntityASmidge()
{
_transform.SetCoordinates(_ent, _gridCoords);
_transform.SetCoordinates(_ent, _gridCoords2);
}
/// <summary>
/// Like <see cref="MoveEntity"/>, but also processes queued PVS chunk updates.
/// </summary>
@@ -147,9 +219,18 @@ public class RecursiveMoveBenchmark
public void MoveAndUpdateChunks()
{
_transform.SetCoordinates(_ent, _gridCoords);
_pvs.ProcessCollections();
PvsTick();
_transform.SetCoordinates(_ent, _mapCoords);
_pvs.ProcessCollections();
PvsTick();
}
[Benchmark]
public void MoveASmidgeAndUpdateChunk()
{
_transform.SetCoordinates(_ent, _gridCoords);
PvsTick();
_transform.SetCoordinates(_ent, _gridCoords2);
PvsTick();
}
[Benchmark]

View File

@@ -119,25 +119,17 @@ namespace Robust.Client.GameObjects
base.Dirty(ent, meta);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
{
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
return base.ToPrettyString(uid);
}
public override void RaisePredictiveEvent<T>(T msg)
{
var localPlayer = _playerManager.LocalPlayer;
DebugTools.AssertNotNull(localPlayer);
var session = _playerManager.LocalSession;
DebugTools.AssertNotNull(session);
var sequence = _stateMan.SystemMessageDispatched(msg);
EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
DebugTools.Assert(!_stateMan.IsPredictionEnabled || _gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel != ClientRunLevel.Connected);
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);
var eventArgs = new EntitySessionEventArgs(session!);
EventBus.RaiseEvent(EventSource.Local, msg);
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
}

View File

@@ -1,14 +1,11 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Enumerators;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using static Robust.Shared.GameObjects.OccluderComponent;
namespace Robust.Client.GameObjects;
@@ -34,13 +31,12 @@ internal sealed class ClientOccluderSystem : OccluderSystem
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(OnShutdown);
}
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
{
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
return;
comp.Enabled = enabled;
Dirty(uid, comp);
base.SetEnabled(uid, enabled, comp, meta);
var xform = Transform(uid);
QueueTreeUpdate(uid, comp, xform);

View File

@@ -68,6 +68,10 @@ namespace Robust.Client.GameObjects
return RemCompDeferred<PointLightComponent>(uid);
}
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
{
}
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
{
SetMask(component.MaskPath, component);
@@ -95,28 +99,23 @@ namespace Robust.Client.GameObjects
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || enabled == comp.Enabled || comp is not PointLightComponent clientComp)
return;
comp.Enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(uid, comp);
base.SetEnabled(uid, enabled, comp, meta);
if (!comp.ContainerOccluded)
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius) ||
comp is not PointLightComponent clientComp)
return;
comp.Radius = radius;
Dirty(uid, comp);
base.SetRadius(uid, radius, comp, meta);
if (clientComp.TreeUid != null)
_lightTree.QueueTreeUpdate(uid, clientComp);
}

View File

@@ -43,7 +43,7 @@ public sealed class ClientDirtySystem : EntitySystem
return;
// Client-side entity deletion is not supported and will cause errors.
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity)}. Trace: {Environment.StackTrace}");
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity.Owner, ev.Entity.Comp)}. Trace: {Environment.StackTrace}");
}
private void OnCompRemoved(RemovedComponentEventArgs args)

View File

@@ -66,14 +66,14 @@ public sealed partial class AudioSystem
public override (EntityUid Entity, AudioAuxiliaryComponent Component) CreateAuxiliary()
{
var (ent, comp) = base.CreateAuxiliary();
_pvs.AddGlobalOverride(GetNetEntity(ent));
_pvs.AddGlobalOverride(ent);
return (ent, comp);
}
public override (EntityUid Entity, AudioEffectComponent Component) CreateEffect()
{
var (ent, comp) = base.CreateEffect();
_pvs.AddGlobalOverride(GetNetEntity(ent));
_pvs.AddGlobalOverride(ent);
return (ent, comp);
}
}

View File

@@ -49,8 +49,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (count == 0)
return;
var nent = GetNetEntity(uid);
_pvs.AddSessionOverrides(nent, filter);
_pvs.AddSessionOverrides(uid, filter);
var ents = new HashSet<EntityUid>(count);

View File

@@ -71,9 +71,9 @@ namespace Robust.Server.GameObjects
return _playerInputs[session];
}
public uint GetLastInputCommand(ICommonSession session)
public uint GetLastInputCommand(ICommonSession? session)
{
return _lastProcessedInputCmd[session];
return session == null ? default : _lastProcessedInputCmd[session];
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)

View File

@@ -1,15 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
namespace Robust.Server.GameObjects;
public sealed class PointLightSystem : SharedPointLightSystem
{
[Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
SubscribeLocalEvent<PointLightComponent, ComponentStartup>(OnLightStartup);
}
private void OnLightStartup(EntityUid uid, PointLightComponent component, ComponentStartup args)
{
UpdatePriority(uid, component, MetaData(uid));
}
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
{
var isHighPriority = comp.Enabled && comp.CastShadows && (comp.Radius > 7);
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, isHighPriority);
}
private void OnLightGetState(EntityUid uid, PointLightComponent component, ref ComponentGetState args)

View File

@@ -1,9 +1,32 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Server.GameObjects;
[UsedImplicitly]
public sealed class ServerOccluderSystem : OccluderSystem
{
[Dependency] private readonly MetaDataSystem _metadata = default!;
protected override void OnCompStartup(EntityUid uid, OccluderComponent component, ComponentStartup args)
{
base.OnCompStartup(uid, component, args);
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, component.Enabled);
}
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
{
if (!Resolve(uid, ref comp, false))
return;
if (enabled == comp.Enabled)
return;
if (!Resolve(uid, ref meta))
return;
base.SetEnabled(uid, enabled, comp, meta);
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, enabled);
}
}

View File

@@ -12,14 +12,14 @@ namespace Robust.Server.GameObjects
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
private EntityQuery<VisibilityComponent> _visiblityQuery;
private EntityQuery<VisibilityComponent> _visibilityQuery;
public override void Initialize()
{
base.Initialize();
_xformQuery = GetEntityQuery<TransformComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_visiblityQuery = GetEntityQuery<VisibilityComponent>();
_visibilityQuery = GetEntityQuery<VisibilityComponent>();
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
EntityManager.EntityInitialized += OnEntityInit;
@@ -41,35 +41,57 @@ namespace Robust.Server.GameObjects
public void AddLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
if ((layer & component.Layer) == layer)
AddLayer((uid, component), layer, refresh);
}
public void AddLayer(Entity<VisibilityComponent?> ent, int layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
if ((layer & ent.Comp.Layer) == layer)
return;
component.Layer |= layer;
ent.Comp.Layer |= layer;
if (refresh)
RefreshVisibility(uid, visibilityComponent: component);
RefreshVisibility(ent);
}
public void RemoveLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
if ((layer & component.Layer) != layer)
RemoveLayer((uid, component), layer, refresh);
}
public void RemoveLayer(Entity<VisibilityComponent?> ent, int layer, bool refresh = true)
{
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
component.Layer &= ~layer;
if ((layer & ent.Comp.Layer) != layer)
return;
ent.Comp.Layer &= ~layer;
if (refresh)
RefreshVisibility(uid, visibilityComponent: component);
RefreshVisibility(ent);
}
public void SetLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
if (component.Layer == layer)
SetLayer((uid, component), layer, refresh);
}
public void SetLayer(Entity<VisibilityComponent?> ent, int layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
if (ent.Comp.Layer == layer)
return;
component.Layer = layer;
ent.Comp.Layer = layer;
if (refresh)
RefreshVisibility(uid, visibilityComponent: component);
RefreshVisibility(ent);
}
private void OnParentChange(ref EntParentChangedMessage ev)
@@ -86,14 +108,19 @@ namespace Robust.Server.GameObjects
VisibilityComponent? visibilityComponent = null,
MetaDataComponent? meta = null)
{
if (!_metaQuery.Resolve(uid, ref meta, false))
RefreshVisibility((uid, visibilityComponent, meta));
}
public void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
{
if (!_metaQuery.Resolve(ent, ref ent.Comp2, false))
return;
// Iterate up through parents and calculate the cumulative visibility mask.
var mask = GetParentVisibilityMask(uid, visibilityComponent);
var mask = GetParentVisibilityMask(ent);
// Iterate down through children and propagate mask changes.
RecursivelyApplyVisibility(uid, mask, meta);
RecursivelyApplyVisibility(ent.Owner, mask, ent.Comp2);
}
private void RecursivelyApplyVisibility(EntityUid uid, int mask, MetaDataComponent meta)
@@ -103,7 +130,6 @@ namespace Robust.Server.GameObjects
var xform = _xformQuery.GetComponent(uid);
meta.VisibilityMask = mask;
_pvs.MarkDirty(uid, xform);
foreach (var child in xform._children)
{
@@ -112,21 +138,21 @@ namespace Robust.Server.GameObjects
var childMask = mask;
if (_visiblityQuery.TryGetComponent(child, out var childVis))
if (_visibilityQuery.TryGetComponent(child, out var childVis))
childMask |= childVis.Layer;
RecursivelyApplyVisibility(child, childMask, childMeta);
}
}
private int GetParentVisibilityMask(EntityUid uid, VisibilityComponent? visibilityComponent = null)
private int GetParentVisibilityMask(Entity<VisibilityComponent?> ent)
{
int visMask = 1; // apparently some content expects everything to have the first bit/flag set to true.
if (_visiblityQuery.Resolve(uid, ref visibilityComponent, false))
visMask |= visibilityComponent.Layer;
if (_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
visMask |= ent.Comp.Layer;
// Include parent vis masks
if (_xformQuery.TryGetComponent(uid, out var xform) && xform.ParentUid.IsValid())
if (_xformQuery.TryGetComponent(ent.Owner, out var xform) && xform.ParentUid.IsValid())
visMask |= GetParentVisibilityMask(xform.ParentUid);
return visMask;

View File

@@ -116,12 +116,6 @@ namespace Robust.Server.GameObjects
}
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata = null)
{
_actorQuery.TryGetComponent(uid, out ActorComponent? actor);
return base.ToPrettyString(uid) with { Session = actor?.PlayerSession };
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;
@@ -162,9 +156,9 @@ namespace Robust.Server.GameObjects
EntitiesCount.Set(Entities.Count);
}
public uint GetLastMessageSequence(ICommonSession session)
public uint GetLastMessageSequence(ICommonSession? session)
{
return _lastProcessedSequencesCmd[session];
return session == null ? default : _lastProcessedSequencesCmd[session];
}
/// <inheritdoc />

View File

@@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
/// <summary>
/// Class for storing session specific PVS data.
/// </summary>
internal sealed class SessionPvsData(ICommonSession session)
{
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, List<EntityData>> SentEntities = new(PvsSystem.DirtyBufferSize);
public readonly Dictionary<NetEntity, EntityData> EntityData = new();
/// <summary>
/// <see cref="SentEntities"/> overflow in case a player's last ack is more than
/// <see cref="PvsSystem.DirtyBufferSize"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, List<EntityData> SentEnts)? Overflow;
/// <summary>
/// If true, the client has explicitly requested a full state. Unlike the first state, we will send them all data,
/// not just data that cannot be implicitly inferred from entity prototypes.
/// </summary>
public bool RequestedFull = false;
public GameTick LastReceivedAck;
public readonly ICommonSession Session = session;
}
/// <summary>
/// Class for storing session-specific information about when an entity was last sent to a player.
/// </summary>
internal sealed class EntityData(Entity<MetaDataComponent> entity) : IEquatable<EntityData>
{
public readonly Entity<MetaDataComponent> Entity = entity;
public readonly NetEntity NetEntity = entity.Comp.NetEntity;
/// <summary>
/// Tick at which this entity was last sent to a player.
/// </summary>
public GameTick LastSent;
/// <summary>
/// Tick at which an entity last left a player's PVS view.
/// </summary>
public GameTick LastLeftView;
/// <summary>
/// Stores the last tick at which a given entity was acked by a player. Used to avoid re-sending the whole entity
/// state when an item re-enters PVS. This is only the same as the player's last acked tick if the entity was
/// present in that state.
/// </summary>
public GameTick EntityLastAcked;
/// <summary>
/// Entity visibility state when it was last sent to this player.
/// </summary>
public PvsEntityVisibility Visibility;
public bool Equals(EntityData? other)
{
#if DEBUG
// Each this class should be unique for each entity-session combination, and should never be getting compared
// across sessions.
if (Entity.Owner == other?.Entity.Owner)
DebugTools.Assert(ReferenceEquals(this, other));
#endif
return Entity.Owner == other?.Entity.Owner;
}
public override int GetHashCode()
{
return Entity.Owner.GetHashCode();
}
public override string ToString()
{
var rep = new EntityStringRepresentation(Entity);
return $"PVS Entity: {rep} - {LastSent}/{LastLeftView}/{EntityLastAcked} - {Visibility}";
}
}

View File

@@ -0,0 +1,277 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed class PvsChunk
{
/// <summary>
/// The root of this chunk. This should either be a map or a grid.
/// </summary>
public Entity<MetaDataComponent> Root;
/// <summary>
/// The map that this grid is on. Tis might be the same entity as <see cref="Root"/>.
/// </summary>
public Entity<MetaDataComponent> Map;
/// <summary>
/// If true, then some entity was added or removed from this chunks and has to be reconstructed
/// </summary>
public bool Dirty { get; private set; } = true;
public bool UpdateQueued = false;
/// <summary>
/// Set of entities that are directly parented to this grid.
/// </summary>
public HashSet<EntityUid> Children = new();
/// <summary>
/// Sorted list of all entities on this chunk. The list is sorted based on their "proximity" to the root entity in
/// the transform hierarchy. I.e., it will list all entities that are directly parented to the grid before listing
/// any entities that are parented to those entities and so on.
/// </summary>
/// <remarks>
/// This already includes <see cref="Map"/>, <see cref="Root"/>, and <see cref="Children"/>
/// </remarks>
public readonly List<Entity<MetaDataComponent>> Contents = new();
/// <summary>
/// The unique location identifier for this chunk.
/// </summary>
public PvsChunkLocation Location;
/// <summary>
/// The location of the centre of this chunk, relative to the <see cref="Root"/>
/// </summary>
public Vector2 Centre;
/// <summary>
/// The map position of the chunk's centre during the last PVS update.
/// </summary>
public MapCoordinates Position;
/// <summary>
/// The <see cref="Root"/>'s inverse world matrix.
/// </summary>
public Matrix3 InvWorldMatrix { get; set; }
// These are only used while populating the chunk. They aren't local variables because the chunks are pooled, so
// the same chunk can be repopulated more than once.
private List<HashSet<EntityUid>> _childSets = new();
private List<HashSet<EntityUid>> _nextChildSets = new();
private List<Entity<MetaDataComponent>> _lowPriorityChildren = new();
private List<Entity<MetaDataComponent>> _anchoredChildren = new();
/// <summary>
/// Effective "counts" of <see cref="Contents"/> that should be used to limit the number of entities in a chunk that
/// get sent to players. This can be used to add a crude "level of detail" for distant chunks.
/// The counts correspond to entities that are:
/// <list type="bullet">
/// <item>Directly attached to the <see cref="Root"/> and have the <see cref="MetaDataFlags.PvsPriority"/> flag.</item>
/// <item>Directly attached to the <see cref="Root"/> and are anchored (or high priority).</item>
/// <item>Directly attached to the <see cref="Root"/>.</item>
/// <item>Directly attached to the <see cref="Root"/> and their direct children.</item>
/// <item>All entities.</item>
/// </list>
/// <remarks>
/// Note that the chunk will not be re-populated if an entity gets (un)anchored, or if their metadata flags changes.
/// So if somebody anchors an occluder and it starts occluding, it won't become a high priority entity untill
/// the chunk gets dirtied & rebuilt.
/// </remarks>
/// </summary>
public readonly int[] LodCounts = new int[5];
public void Initialize(PvsChunkLocation location,
EntityQuery<MetaDataComponent> meta,
EntityQuery<TransformComponent> xform)
{
DebugTools.Assert(Dirty);
if (Root != default)
throw new InvalidOperationException($"Chunk has not been cleared.");
Location = location;
var root = Location.Uid;
if (!meta.TryGetComponent(root, out var rootMeta)
|| !xform.TryGetComponent(root, out var rootXform))
{
Wipe();
throw new InvalidOperationException($"Root {root} does not exist");
}
Root = (root, rootMeta);
if (!meta.TryGetComponent(rootXform.MapUid, out var mapMeta))
{
var rep = new EntityStringRepresentation(Root);
Wipe();
throw new InvalidOperationException($"Root {rep} does not exist on nay map!");
}
Map = new(rootXform.MapUid.Value, mapMeta);
Centre = (Location.Indices + new Vector2(0.5f)) * PvsSystem.ChunkSize;
}
/// <summary>
/// Populates the contents of this chunk. Returns false if some error occurs (e.g., contains deleted entities).
/// </summary>
public bool PopulateContents(EntityQuery<MetaDataComponent> meta, EntityQuery<TransformComponent> xform)
{
DebugTools.AssertEqual(Contents.Count, 0);
DebugTools.AssertEqual(_childSets.Count, 0);
DebugTools.AssertEqual(_nextChildSets.Count, 0);
DebugTools.AssertEqual(_anchoredChildren.Count, 0);
DebugTools.AssertEqual(_lowPriorityChildren.Count, 0);
Contents.EnsureCapacity(Children.Count);
_lowPriorityChildren.EnsureCapacity(Children.Count);
var nextSetTotal = 0;
// First, we add all high-priority children.
foreach (var child in Children)
{
// TODO ARCH multi-component queries
if (!meta.TryGetComponent(child, out var childMeta)
|| !xform.TryGetComponent(child, out var childXform))
{
DebugTools.Assert($"PVS chunk contains a deleted entity: {child}");
MarkDirty();
return false;
}
childMeta.LastPvsLocation = Location;
if ((childMeta.Flags & MetaDataFlags.PvsPriority) == MetaDataFlags.PvsPriority)
Contents.Add((child, childMeta));
else if (childXform.Anchored)
_anchoredChildren.Add((child, childMeta));
else
_lowPriorityChildren.Add((child, childMeta));
var subCount = childXform._children.Count;
if (subCount == 0)
continue;
nextSetTotal += subCount;
_childSets.Add(childXform._children);
}
// Populate LoD counts
LodCounts[0] = Contents.Count;
LodCounts[1] = LodCounts[0] + _anchoredChildren.Count;
LodCounts[2] = LodCounts[1] + _lowPriorityChildren.Count;
LodCounts[3] = LodCounts[2] + nextSetTotal;
// Next, add the lower priority children.
Contents.AddRange(_anchoredChildren);
Contents.AddRange(_lowPriorityChildren);
_lowPriorityChildren.Clear();
_anchoredChildren.Clear();
// Next, we recursively add all grand-children
while (nextSetTotal > 0)
{
Contents.EnsureCapacity(Contents.Count + nextSetTotal);
nextSetTotal = 0;
foreach (var childSet in _childSets)
{
foreach (var child in childSet)
{
// TODO ARCH multi-component queries
if (!meta.TryGetComponent(child, out var childMeta)
|| !xform.TryGetComponent(child, out var childXform))
{
DebugTools.Assert($"PVS chunk contains a deleted entity: {child}");
MarkDirty();
return false;
}
childMeta.LastPvsLocation = Location;
Contents.Add((child, childMeta));
var subCount = childXform._children.Count;
if (subCount == 0)
continue;
nextSetTotal += subCount;
_nextChildSets.Add(childXform._children);
}
}
_childSets.Clear();
(_childSets, _nextChildSets) = (_nextChildSets, _childSets);
}
LodCounts[4] = Contents.Count;
Dirty = false;
ValidateChunk(xform);
return true;
}
[Conditional("DEBUG")]
private void ValidateChunk(EntityQuery<TransformComponent> query)
{
DebugTools.Assert(LodCounts[0] <= LodCounts[1]);
DebugTools.Assert(LodCounts[1] <= LodCounts[2]);
DebugTools.Assert(LodCounts[2] <= LodCounts[3]);
DebugTools.Assert(LodCounts[3] <= LodCounts[4]);
DebugTools.AssertEqual(LodCounts[4], Contents.Count());
DebugTools.AssertEqual(_childSets.Count, 0);
DebugTools.AssertEqual(_nextChildSets.Count, 0);
DebugTools.AssertEqual(_anchoredChildren.Count, 0);
DebugTools.AssertEqual(_lowPriorityChildren.Count, 0);
foreach (var c in Children)
{
DebugTools.AssertEqual(query.GetComponent(c).ParentUid, Root.Owner,
"Direct child is not actually directly attached to the root.");
}
var set = new HashSet<EntityUid>(Contents.Count);
set.Add(Root.Owner);
set.Add(Map.Owner);
foreach (var child in Contents)
{
var parent = query.GetComponent(child).ParentUid;
DebugTools.Assert(set.Contains(parent),
"A child's parent is not in the chunk, or is not listed first.");
DebugTools.Assert(set.Add(child), "Child appears more than once in the chunk.");
}
}
public void MarkDirty()
{
if (Dirty)
return;
Dirty = true;
Contents.Clear();
_nextChildSets.Clear();
_childSets.Clear();
}
public void Wipe()
{
Root = default;
Map = default;
Location = default;
Children.Clear();
MarkDirty();
}
public override string ToString()
{
return Map.Owner == Root.Owner
? $"map-{Root.Owner}-{Location.Indices}"
: $"grid-{Root.Owner}-{Location.Indices}";
}
}

View File

@@ -1,673 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
public interface IPVSCollection
{
/// <summary>
/// Processes all previous additions, removals and updates of indices.
/// </summary>
public void Process();
/// <summary>
/// Adds a player session to the collection. Returns false if the player was already present.
/// </summary>
public bool AddPlayer(ICommonSession session);
public void AddGrid(EntityUid gridId);
public void AddMap(MapId mapId);
/// <summary>
/// Removes a player session from the collection. Returns false if the player was not present in the collection.
/// </summary>
public bool RemovePlayer(ICommonSession session);
public void RemoveGrid(EntityUid gridId);
public void RemoveMap(MapId mapId);
/// <summary>
/// Remove all deletions up to a <see cref="GameTick"/>.
/// </summary>
/// <param name="tick">The <see cref="GameTick"/> before which all deletions should be removed.</param>
public void CullDeletionHistoryUntil(GameTick tick);
public bool IsDirty(IChunkIndexLocation location);
public bool MarkDirty(IChunkIndexLocation location);
public void ClearDirty();
}
public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
{
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transformSystem;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2i GetChunkIndices(Vector2 coordinates)
{
return (coordinates / PvsSystem.ChunkSize).Floored();
}
/// <summary>
/// Index of which <see cref="TIndex"/> are contained in which mapchunk, indexed by <see cref="Vector2i"/>.
/// </summary>
private readonly Dictionary<MapId, Dictionary<Vector2i, HashSet<TIndex>>> _mapChunkContents = new();
/// <summary>
/// Index of which <see cref="TIndex"/> are contained in which gridchunk, indexed by <see cref="Vector2i"/>.
/// </summary>
private readonly Dictionary<EntityUid, Dictionary<Vector2i, HashSet<TIndex>>> _gridChunkContents = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent.
/// </summary>
private readonly HashSet<TIndex> _globalOverrides = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent along with all of their children.
/// </summary>
private readonly HashSet<TIndex> _globalRecursiveOverrides = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent.
/// </summary>
public HashSet<TIndex>.Enumerator GlobalOverridesEnumerator => _globalOverrides.GetEnumerator();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent along with all of their children.
/// </summary>
public HashSet<TIndex>.Enumerator GlobalRecursiveOverridesEnumerator => _globalRecursiveOverrides.GetEnumerator();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent to a certain <see cref="ICommonSession"/>.
/// </summary>
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _sessionOverrides = new();
/// <summary>
/// Which <see cref="TIndex"/> where last seen/sent to a certain <see cref="ICommonSession"/>.
/// </summary>
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _lastSeen = new();
/// <summary>
/// History of deletion-tuples, containing the <see cref="GameTick"/> of the deletion, as well as the <see cref="TIndex"/> of the object which was deleted.
/// </summary>
private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new();
/// <summary>
/// An index containing the <see cref="IIndexLocation"/>s of all <see cref="TIndex"/>.
/// </summary>
private readonly Dictionary<TIndex, IIndexLocation> _indexLocations = new();
/// <summary>
/// Buffer of all locationchanges since the last process call
/// </summary>
private readonly Dictionary<TIndex, IIndexLocation> _locationChangeBuffer = new();
/// <summary>
/// Buffer of all indexremovals since the last process call
/// </summary>
private readonly Dictionary<TIndex, GameTick> _removalBuffer = new();
/// <summary>
/// To avoid re-allocating the hashset every tick we'll just store it.
/// </summary>
private HashSet<TIndex> _changedIndices = new();
/// <summary>
/// A set of all chunks changed last tick
/// </summary>
private HashSet<IChunkIndexLocation> _dirtyChunks = new();
private ISawmill _sawmill;
public PVSCollection(ISawmill sawmill, IEntityManager entityManager, SharedTransformSystem transformSystem)
{
_sawmill = sawmill;
_entityManager = entityManager;
_transformSystem = transformSystem;
}
public void Process()
{
_changedIndices.EnsureCapacity(_locationChangeBuffer.Count);
foreach (var key in _locationChangeBuffer.Keys)
{
_changedIndices.Add(key);
}
foreach (var (index, tick) in _removalBuffer)
{
_deletionHistory.Add((tick, index));
_changedIndices.Remove(index);
var location = RemoveIndexInternal(index);
if (location == null)
continue;
if(location is GridChunkLocation or MapChunkLocation)
_dirtyChunks.Add((IChunkIndexLocation) location);
}
foreach (var index in _changedIndices)
{
var oldLoc = RemoveIndexInternal(index);
if(oldLoc is GridChunkLocation or MapChunkLocation)
_dirtyChunks.Add((IChunkIndexLocation) oldLoc);
AddIndexInternal(index, _locationChangeBuffer[index], _dirtyChunks);
}
// remove empty chunk-subsets
foreach (var chunkLocation in _dirtyChunks)
{
switch (chunkLocation)
{
case GridChunkLocation gridChunkLocation:
if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunks)) continue;
if(!gridChunks.TryGetValue(gridChunkLocation.ChunkIndices, out var chunk)) continue;
if(chunk.Count == 0)
gridChunks.Remove(gridChunkLocation.ChunkIndices);
break;
case MapChunkLocation mapChunkLocation:
if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunks)) continue;
if(!mapChunks.TryGetValue(mapChunkLocation.ChunkIndices, out chunk)) continue;
if(chunk.Count == 0)
mapChunks.Remove(mapChunkLocation.ChunkIndices);
break;
}
}
_changedIndices.Clear();
_locationChangeBuffer.Clear();
_removalBuffer.Clear();
}
public bool IsDirty(IChunkIndexLocation location) => _dirtyChunks.Contains(location);
public bool MarkDirty(IChunkIndexLocation location) => _dirtyChunks.Add(location);
public void ClearDirty() => _dirtyChunks.Clear();
public bool TryGetChunk(MapId mapId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
_mapChunkContents[mapId].TryGetValue(chunkIndices, out indices);
public bool TryGetChunk(EntityUid gridId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
_gridChunkContents[gridId].TryGetValue(chunkIndices, out indices);
public HashSet<TIndex>.Enumerator GetSessionOverrides(ICommonSession session) => _sessionOverrides[session].GetEnumerator();
private void AddIndexInternal(TIndex index, IIndexLocation location, HashSet<IChunkIndexLocation> dirtyChunks)
{
switch (location)
{
case GlobalOverride global:
if (global.Recursive)
_globalRecursiveOverrides.Add(index);
else
_globalOverrides.Add(index);
break;
case GridChunkLocation gridChunkLocation:
// might be gone due to grid-deletions
if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunk)) return;
var gridLoc = gridChunk.GetOrNew(gridChunkLocation.ChunkIndices);
gridLoc.Add(index);
dirtyChunks.Add(gridChunkLocation);
break;
case SessionsOverride sessionOverride:
foreach (var sesh in sessionOverride.Sessions)
{
if (!_sessionOverrides.TryGetValue(sesh, out var set))
continue;
set.Add(index);
}
break;
case MapChunkLocation mapChunkLocation:
// might be gone due to map-deletions
if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunk)) return;
var mapLoc = mapChunk.GetOrNew(mapChunkLocation.ChunkIndices);
mapLoc.Add(index);
dirtyChunks.Add(mapChunkLocation);
break;
}
// we want this to throw if there is already an entry because if that happens we fucked up somewhere
_indexLocations.Add(index, location);
}
private IIndexLocation? RemoveIndexInternal(TIndex index)
{
// the index might be gone due to disconnects/grid-/map-deletions
if (!_indexLocations.Remove(index, out var location))
return null;
// since we can find the index, we can assume the dicts will be there too & dont need to do any checks. gaming.
switch (location)
{
case GlobalOverride global:
var set = global.Recursive ? _globalRecursiveOverrides : _globalOverrides;
set.Remove(index);
break;
case GridChunkLocation gridChunkLocation:
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
break;
case SessionsOverride sessionOverride:
foreach (var sesh in sessionOverride.Sessions)
{
_sessionOverrides.GetValueOrDefault(sesh)?.Remove(index);
}
break;
case MapChunkLocation mapChunkLocation:
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index);
break;
}
return location;
}
#region Init Functions
/// <inheritdoc />
public bool AddPlayer(ICommonSession session)
{
return _sessionOverrides.TryAdd(session, new()) & _lastSeen.TryAdd(session, new());
}
/// <inheritdoc />
public void AddGrid(EntityUid gridId) => _gridChunkContents[gridId] = new();
/// <inheritdoc />
public void AddMap(MapId mapId) => _mapChunkContents[mapId] = new();
#endregion
#region ShutdownFunctions
/// <inheritdoc />
public bool RemovePlayer(ICommonSession session)
{
if (_sessionOverrides.Remove(session, out var indices))
{
foreach (var index in indices)
{
_indexLocations.Remove(index);
}
}
return _lastSeen.Remove(session) && indices != null;
}
/// <inheritdoc />
public void RemoveGrid(EntityUid gridId)
{
foreach (var (_, indices) in _gridChunkContents[gridId])
{
foreach (var index in indices)
{
_indexLocations.Remove(index);
}
}
_gridChunkContents.Remove(gridId);
}
/// <inheritdoc />
public void RemoveMap(MapId mapId)
{
foreach (var (_, indices) in _mapChunkContents[mapId])
{
foreach (var index in indices)
{
_indexLocations.Remove(index);
}
}
_mapChunkContents.Remove(mapId);
}
#endregion
#region DeletionHistory & RemoveIndex
/// <summary>
/// Registers a deletion of an <see cref="TIndex"/> on a <see cref="GameTick"/>. WARNING: this also clears the index out of the internal cache!!!
/// </summary>
/// <param name="tick">The <see cref="GameTick"/> at which the deletion took place.</param>
/// <param name="index">The <see cref="TIndex"/> of the removed object.</param>
public void RemoveIndex(GameTick tick, TIndex index)
{
_removalBuffer[index] = tick;
}
/// <inheritdoc />
public void CullDeletionHistoryUntil(GameTick tick)
{
if (tick == GameTick.MaxValue)
{
_deletionHistory.Clear();
return;
}
for (var i = _deletionHistory.Count - 1; i >= 0; i--)
{
var hist = _deletionHistory[i].tick;
if (hist <= tick)
{
_deletionHistory.RemoveSwap(i);
if (_largestCulled < hist)
_largestCulled = hist;
}
}
}
private GameTick _largestCulled;
public List<TIndex>? GetDeletedIndices(GameTick fromTick)
{
if (fromTick == GameTick.Zero)
return null;
// I'm 99% sure this can never happen, but it is hard to test real laggy/lossy networks with many players.
if (_largestCulled > fromTick)
{
_sawmill.Error($"Culled required deletion history! culled: {_largestCulled}. requested: > {fromTick}");
_largestCulled = GameTick.Zero;
}
var list = new List<TIndex>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick > fromTick)
list.Add(id);
}
return list.Count > 0 ? list : null;
}
#endregion
#region UpdateIndex
private bool TryGetLocation(TIndex index, out IIndexLocation? location)
{
return _locationChangeBuffer.TryGetValue(index, out location)
|| _indexLocations.TryGetValue(index, out location);
}
/// <summary>
/// Updates an <see cref="TIndex"/> to be sent to all players at all times.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddGlobalOverride(TIndex index, bool removeFromOverride, bool recursive)
{
if (!TryGetLocation(index, out var oldLocation))
{
RegisterUpdate(index, new GlobalOverride(recursive));
return;
}
if (!removeFromOverride && oldLocation is SessionsOverride)
return;
if (oldLocation is GlobalOverride global &&
(!removeFromOverride || global.Recursive == recursive))
{
return;
}
RegisterUpdate(index, new GlobalOverride(recursive));
}
/// <summary>
/// Updates an <see cref="TIndex"/> to be sent to a specific <see cref="ICommonSession"/> at all times.
/// This will always also send all children of the given entity.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="session">The <see cref="ICommonSession"/> receiving the object.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void AddSessionOverride(TIndex index, ICommonSession session, bool removeFromOverride)
{
if (!TryGetLocation(index, out var oldLocation))
{
RegisterUpdate(index, new SessionsOverride(new HashSet<ICommonSession>()
{
session
}));
return;
}
if (!removeFromOverride && oldLocation is GlobalOverride)
return;
if (oldLocation is SessionsOverride local)
{
if (!removeFromOverride || local.Sessions.Contains(session))
return;
local.Sessions.Add(session);
return;
}
RegisterUpdate(index, new SessionsOverride(new HashSet<ICommonSession>()
{
session
}));
}
/// <summary>
/// Updates an <see cref="TIndex"/> with the location based on the provided <see cref="EntityCoordinates"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="coordinates">The <see cref="EntityCoordinates"/> to use when adding the <see cref="TIndex"/> to the internal cache.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, EntityCoordinates coordinates, bool removeFromOverride = false)
{
if (!removeFromOverride
&& TryGetLocation(index, out var oldLocation)
&& oldLocation is GlobalOverride or SessionsOverride)
{
return;
}
if (!_entityManager.TryGetComponent(coordinates.EntityId, out TransformComponent? xform))
return;
if (xform.GridUid is { } gridId && gridId.IsValid())
{
var gridIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
return;
}
var worldPos = _transformSystem.GetWorldMatrix(xform).Transform(coordinates.Position);
var mapIndices = GetChunkIndices(worldPos);
UpdateIndex(index, xform.MapID, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
public IChunkIndexLocation GetChunkIndex(EntityCoordinates coordinates)
{
if (!_entityManager.TryGetComponent(coordinates.EntityId, out TransformComponent? xform))
return new MapChunkLocation(default, default);
if (xform.GridUid is { } gridId && gridId.IsValid())
{
var gridIndices = GetChunkIndices(coordinates.Position);
return new GridChunkLocation(gridId, gridIndices);
}
var worldPos = _transformSystem.GetWorldMatrix(xform).Transform(coordinates.Position);
var mapIndices = GetChunkIndices(worldPos);
return new MapChunkLocation(xform.MapID, mapIndices);
}
/// <summary>
/// Updates an <see cref="TIndex"/> using the provided <see cref="gridId"/> and <see cref="chunkIndices"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="gridId">The id of the grid.</param>
/// <param name="chunkIndices">The indices of the chunk.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
/// <param name="forceDirty">If true, this will mark the previous chunk as dirty even if the entity did not move from that chunk.</param>
public void UpdateIndex(TIndex index, EntityUid gridId, Vector2i chunkIndices, bool removeFromOverride = false, bool forceDirty = false)
{
_locationChangeBuffer.TryGetValue(index, out var bufferedLocation);
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionsOverride && !removeFromOverride)
return;
if (oldLocation is GridChunkLocation oldGrid &&
oldGrid.ChunkIndices == chunkIndices &&
oldGrid.GridId == gridId)
{
_locationChangeBuffer.Remove(index);
if (forceDirty)
_dirtyChunks.Add(oldGrid);
return;
}
RegisterUpdate(index, new GridChunkLocation(gridId, chunkIndices));
}
/// <summary>
/// Updates an <see cref="TIndex"/> using the provided <see cref="mapId"/> and <see cref="chunkIndices"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="mapId">The id of the map.</param>
/// <param name="chunkIndices">The indices of the mapchunk.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
/// <param name="forceDirty">If true, this will mark the previous chunk as dirty even if the entity did not move from that chunk.</param>
public void UpdateIndex(TIndex index, MapId mapId, Vector2i chunkIndices, bool removeFromOverride = false, bool forceDirty = false)
{
_locationChangeBuffer.TryGetValue(index, out var bufferedLocation);
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionsOverride && !removeFromOverride)
return;
// Is this entity just returning to its old location?
if (oldLocation is MapChunkLocation oldMap &&
oldMap.ChunkIndices == chunkIndices &&
oldMap.MapId == mapId)
{
if (bufferedLocation != null)
_locationChangeBuffer.Remove(index);
if (forceDirty)
_dirtyChunks.Add(oldMap);
return;
}
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
}
private void RegisterUpdate(TIndex index, IIndexLocation location)
{
_locationChangeBuffer[index] = location;
}
#endregion
}
#region IndexLocations
public interface IIndexLocation {};
public interface IChunkIndexLocation{ };
public struct MapChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<MapChunkLocation>
{
public MapChunkLocation(MapId mapId, Vector2i chunkIndices)
{
MapId = mapId;
ChunkIndices = chunkIndices;
}
public MapId MapId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(MapChunkLocation other)
{
return MapId.Equals(other.MapId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is MapChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(MapId, ChunkIndices);
}
}
public struct GridChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<GridChunkLocation>
{
public GridChunkLocation(EntityUid gridId, Vector2i chunkIndices)
{
GridId = gridId;
ChunkIndices = chunkIndices;
}
public EntityUid GridId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(GridChunkLocation other)
{
return GridId.Equals(other.GridId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is GridChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(GridId, ChunkIndices);
}
}
public struct GlobalOverride : IIndexLocation
{
/// <summary>
/// If true, this will also send all children of the override.
/// </summary>
public readonly bool Recursive;
public GlobalOverride(bool recursive)
{
Recursive = recursive;
}
}
/// <summary>
/// Adds overrides for the specified sessions for this entity.
/// </summary>
public struct SessionsOverride : IIndexLocation
{
public SessionsOverride(HashSet<ICommonSession> sessions)
{
Sessions = sessions;
}
public readonly HashSet<ICommonSession> Sessions;
}
#endregion

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
/// <summary>
/// Class for storing session specific PVS data.
/// </summary>
internal sealed class PvsSession(ICommonSession session)
{
public readonly ICommonSession Session = session;
public INetChannel Channel => Session.Channel;
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, List<PvsData>> PreviouslySent = new(PvsSystem.DirtyBufferSize);
/// <summary>
/// Dictionary containing data about all entities that this client has ever seen.
/// </summary>
public readonly Dictionary<NetEntity, PvsData> Entities = new();
/// <summary>
/// <see cref="PreviouslySent"/> overflow in case a player's last ack is more than
/// <see cref="PvsSystem.DirtyBufferSize"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, List<PvsData> SentEnts)? Overflow;
/// <summary>
/// The client's current visibility mask.
/// </summary>
public int VisMask;
/// <summary>
/// The list that is currently being prepared for sending.
/// </summary>
public List<PvsData>? ToSend;
/// <summary>
/// The <see cref="ToSend"/> list from the previous tick. Also caches the current tick that the PVS leave message
/// should belong to, in case the processing is ever run asynchronously with normal system/game ticking.
/// </summary>
public (GameTick ToTick, List<PvsData> PreviouslySent)? LastSent;
/// <summary>
/// Visible chunks, sorted by proximity to the clients's viewers;
/// </summary>
public readonly List<(PvsChunk Chunk, float ChebyshevDistance)> Chunks = new();
/// <summary>
/// Squared distance ta all of the visible chunks.
/// </summary>
public readonly List<float> ChunkDistanceSq = new();
/// <summary>
/// The client's current eyes/viewers.
/// </summary>
public Entity<TransformComponent, EyeComponent?>[] Viewers
= Array.Empty<Entity<TransformComponent, EyeComponent?>>();
/// <summary>
/// If true, the client has explicitly requested a full state. Unlike the first state, we will send them all data,
/// not just data that cannot be implicitly inferred from entity prototypes.
/// </summary>
public bool RequestedFull = false;
/// <summary>
/// List of entity states to send to the client.
/// </summary>
public readonly List<EntityState> States = new();
/// <summary>
/// Information about the current number of entities that are being sent to the player this tick. Used to enforce
/// pvs budgets.
/// </summary>
public PvsBudget Budget;
/// <summary>
/// The tick of the last acknowledged game state.
/// </summary>
public GameTick LastReceivedAck;
/// <summary>
/// Start tick for the time window of data that has to be sent to this player.
/// </summary>
public GameTick FromTick;
// TODO PVS support this properly. I.e., add a command, and remove from _seenAllEnts
public bool DisableCulling;
/// <summary>
/// List of entities that have left the player's view this tick.
/// </summary>
public readonly List<NetEntity> LeftView = new();
public readonly List<SessionState> PlayerStates = new();
public uint LastMessage;
public uint LastInput;
/// <summary>
/// The game state for this tick,
/// </summary>
public GameState? State;
/// <summary>
/// Clears all stored game state data. This should only be used after the game state has been serialized.
/// </summary>
public void ClearState()
{
PlayerStates.Clear();
Chunks.Clear();
States.Clear();
State = null;
}
}
/// <summary>
/// Class for storing session-specific information about when an entity was last sent to a player.
/// </summary>
internal sealed class PvsData(Entity<MetaDataComponent> entity) : IEquatable<PvsData>
{
public readonly Entity<MetaDataComponent> Entity = entity;
public readonly NetEntity NetEntity = entity.Comp.NetEntity;
/// <summary>
/// Tick at which this entity was last sent to a player.
/// </summary>
public GameTick LastSeen;
/// <summary>
/// Tick at which an entity last left a player's PVS view.
/// </summary>
public GameTick LastLeftView;
/// <summary>
/// Stores the last tick at which a given entity was acked by a player. Used to avoid re-sending the whole entity
/// state when an item re-enters PVS. This is only the same as the player's last acked tick if the entity was
/// present in that state.
/// </summary>
public GameTick EntityLastAcked;
/// <summary>
/// Entity visibility state when it was last sent to this player.
/// </summary>
public PvsEntityVisibility Visibility;
// this is currently no longer strictly required, but maybe in future we want to separate out the get-state code
// from the get-visible/to-send code. If we do that, we need to have this to quickly distinguish between dirty,
// entering, and unmodified entities.
public bool Equals(PvsData? other)
{
#if DEBUG
// Each this class should be unique for each entity-session combination, and should never be getting compared
// across sessions.
if (Entity.Owner == other?.Entity.Owner)
DebugTools.Assert(ReferenceEquals(this, other));
#endif
return Entity.Owner == other?.Entity.Owner;
}
public override int GetHashCode()
{
return Entity.Owner.GetHashCode();
}
public override string ToString()
{
var rep = new EntityStringRepresentation(Entity);
return $"PVS Entity: {rep} - {LastSeen}/{LastLeftView}/{EntityLastAcked} - {Visibility}";
}
}
/// <summary>
/// Struct for storing information about the current number of entities that are being sent to the player this tick.
/// Used to enforce pvs budgets.
internal struct PvsBudget
{
public int NewLimit;
public int EnterLimit;
public int DirtyCount;
public int EnterCount;
public int NewCount;
}

View File

@@ -1,69 +1,206 @@
using System;
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
/// <summary>
/// Placeholder system to expose some parts of the internal <see cref="PvsSystem"/> that allows entities to ignore
/// normal PVS rules, such that they are always sent to clients.
/// </summary>
public sealed class PvsOverrideSystem : EntitySystem
{
[Shared.IoC.Dependency] private readonly PvsSystem _pvs = default!;
[Dependency] private readonly IPlayerManager _player = default!;
/// <summary>
/// Yields the NetEntity session overrides for the specified session.
/// </summary>
/// <param name="session"></param>
public IEnumerable<NetEntity> GetSessionOverrides(ICommonSession session)
private readonly HashSet<EntityUid> _hasOverride = new();
internal HashSet<EntityUid> GlobalOverride = new();
internal HashSet<EntityUid> ForceSend = new();
internal Dictionary<ICommonSession, HashSet<EntityUid>> SessionOverrides = new();
public override void Initialize()
{
var enumerator = _pvs.EntityPVSCollection.GetSessionOverrides(session);
while (enumerator.MoveNext())
{
var current = enumerator.Current;
yield return current;
}
base.Initialize();
EntityManager.EntityDeleted += OnDeleted;
_player.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
}
/// <summary>
/// Used to ensure that an entity is always sent to every client. By default this overrides any client-specific overrides.
/// </summary>
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddGlobalOverride(NetEntity entity, bool removeExistingOverride = true, bool recursive = false)
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs ev)
{
_pvs.EntityPVSCollection.AddGlobalOverride(entity, removeExistingOverride, recursive);
}
/// <summary>
/// Used to ensure that an entity is always sent to a specific client. Overrides any global or pre-existing
/// client-specific overrides.
/// </summary>
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
public void AddSessionOverride(NetEntity entity, ICommonSession session, bool removeExistingOverride = true)
{
_pvs.EntityPVSCollection.AddSessionOverride(entity, session, removeExistingOverride);
}
// 'placeholder'
public void AddSessionOverrides(NetEntity entity, Filter filter, bool removeExistingOverride = true)
{
foreach (var player in filter.Recipients)
{
AddSessionOverride(entity, player, removeExistingOverride);
}
}
/// <summary>
/// Removes any global or client-specific overrides.
/// </summary>
public void ClearOverride(NetEntity entity, TransformComponent? xform = null)
{
if (!TryGetEntity(entity, out var uid) || !Resolve(uid.Value, ref xform))
if (ev.NewStatus != SessionStatus.Disconnected)
return;
_pvs.EntityPVSCollection.UpdateIndex(entity, xform.Coordinates, true);
SessionOverrides.Remove(ev.Session);
}
public override void Shutdown()
{
base.Shutdown();
EntityManager.EntityDeleted -= OnDeleted;
_player.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnDeleted(EntityUid uid, MetaDataComponent meta)
{
Clear(uid);
}
private void Clear(EntityUid uid)
{
if (!_hasOverride.Remove(uid))
return;
ForceSend.Remove(uid);
GlobalOverride.Remove(uid);
foreach (var (session, set) in SessionOverrides)
{
if (set.Remove(uid) && set.Count == 0)
SessionOverrides.Remove(session);
}
}
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
/// causing them to always be sent to all clients.
/// </summary>
public void AddGlobalOverride(EntityUid uid)
{
if (GlobalOverride.Add(uid))
_hasOverride.Add(uid);
}
/// <summary>
/// Removes an entity from the global overrides.
/// </summary>
public void RemoveGlobalOverride(EntityUid uid)
{
GlobalOverride.Remove(uid);
// Not bothering to clear _hasOverride, as we'd have to check all the other collections, and at that point we
// might as well just do that when the entity gets deleted anyways.
}
/// <summary>
/// This causes an entity and all of its parents to always be sent to all players.
/// </summary>
/// <remarks>
/// This differs from <see cref="AddGlobalOverride"/> as it does not send children, and will ignore a players usual
/// PVS budget. You generally shouldn't use this.
/// </remarks>
public void AddForceSend(EntityUid uid)
{
if (ForceSend.Add(uid))
_hasOverride.Add(uid);
}
public void RemoveForceSend(EntityUid uid)
{
ForceSend.Remove(uid);
// Not bothering to clear _hasOverride, as we'd have to check all the other collections, and at that point we
// might as well just do that when the entity gets deleted anyways.
}
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations for a
/// specific session.
/// </summary>
public void AddSessionOverride(EntityUid uid, ICommonSession session)
{
if (SessionOverrides.GetOrNew(session).Add(uid))
_hasOverride.Add(uid);
}
/// <summary>
/// Removes an entity from a session's overrides.
/// </summary>
public void RemoveSessionOverride(EntityUid uid, ICommonSession session)
{
if (!SessionOverrides.TryGetValue(session, out var overrides))
return;
if (overrides.Remove(uid) && overrides.Count == 0)
SessionOverrides.Remove(session);
// Not bothering to clear _hasOverride, as we'd have to check all the other collections, and at that point we
// might as well just do that when the entity gets deleted anyways.
}
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
/// causing them to always be sent to all clients.
/// </summary>
public void AddSessionOverrides(EntityUid uid, Filter filter)
{
foreach (var session in filter.Recipients)
{
AddSessionOverride(uid, session);
}
}
[Obsolete("Use variant that takes in an EntityUid")]
public void AddGlobalOverride(NetEntity entity, bool removeExistingOverride = true, bool recursive = false)
{
if (TryGetEntity(entity, out var uid))
AddGlobalOverride(uid.Value);
}
[Obsolete("Use variant that takes in an EntityUid")]
public void AddSessionOverride(NetEntity entity, ICommonSession session, bool removeExistingOverride = true)
{
if (TryGetEntity(entity, out var uid))
AddSessionOverride(uid.Value, session);
}
[Obsolete("Use variant that takes in an EntityUid")]
public void AddSessionOverrides(NetEntity entity, Filter filter, bool removeExistingOverride = true)
{
if (TryGetEntity(entity, out var uid))
AddSessionOverrides(uid.Value, filter);
}
[Obsolete("Don't use this, clear specific overrides")]
public void ClearOverride(NetEntity entity)
{
if (TryGetEntity(entity, out var uid))
Clear(uid.Value);
}
#region Map/Grid Events
private void OnMapChanged(MapChangedEvent ev)
{
if (ev.Created)
OnMapCreated(ev);
else
OnMapDestroyed(ev);
}
private void OnGridRemoved(GridRemovalEvent ev)
{
RemoveForceSend(ev.EntityUid);
}
private void OnGridCreated(GridInitializeEvent ev)
{
// TODO PVS remove this requirement.
// I think this just required refactoring client game state logic so it doesn't send grids to nullspace?
AddForceSend(ev.EntityUid);
}
private void OnMapDestroyed(MapChangedEvent ev)
{
RemoveForceSend(ev.Uid);
}
private void OnMapCreated(MapChangedEvent ev)
{
// TODO PVS remove this requirement.
// I think this just required refactoring client game state logic so it doesn't sending maps/grids to nullspace.
AddForceSend(ev.Uid);
}
#endregion
}

View File

@@ -1,8 +1,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.GameObjects;
using Prometheus;
using Robust.Shared.Player;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
@@ -33,12 +32,11 @@ internal sealed partial class PvsSystem
/// <summary>
/// Processes queued client acks in parallel
/// </summary>
internal WaitHandle ProcessQueuedAcks()
/// <param name="histogram"></param>
internal WaitHandle? ProcessQueuedAcks(Histogram? histogram)
{
if (PendingAcks.Count == 0)
{
return ParallelManager.DummyResetEvent.WaitHandle;
}
return null;
_toAck.Clear();
@@ -48,6 +46,14 @@ internal sealed partial class PvsSystem
}
PendingAcks.Clear();
if (!_async)
{
using var _= histogram?.WithLabels("Process Acks").NewTimer();
_parallelManager.ProcessNow(_ackJob, _toAck.Count);
return null;
}
return _parallelManager.Process(_ackJob, _toAck.Count);
}
@@ -64,6 +70,28 @@ internal sealed partial class PvsSystem
}
}
private record struct PvsChunkJob : IParallelRobustJob
{
public int BatchSize => 2;
public PvsSystem Pvs;
public int Count => Pvs._dirtyChunks.Count + 2;
public void Execute(int index)
{
if (index > 1)
{
Pvs.UpdateDirtyChunks(index-2);
return;
}
// 1st batch/job performs some extra processing.
if (index == 0)
Pvs.CacheGlobalOverrides();
else if (index == 1)
Pvs.UpdateCleanChunks();
}
}
/// <summary>
/// Process a given client's queued ack.
/// </summary>
@@ -73,7 +101,7 @@ internal sealed partial class PvsSystem
return;
var ackedTick = sessionData.LastReceivedAck;
List<EntityData>? ackedEnts;
List<PvsData>? ackedEnts;
if (sessionData.Overflow != null && sessionData.Overflow.Value.Tick <= ackedTick)
{
@@ -86,19 +114,19 @@ internal sealed partial class PvsSystem
if (overflowTick != ackedTick)
{
_entDataListPool.Return(overflowEnts);
DebugTools.Assert(!sessionData.SentEntities.Values.Contains(overflowEnts));
DebugTools.Assert(!sessionData.PreviouslySent.Values.Contains(overflowEnts));
return;
}
}
else if (!sessionData.SentEntities.TryGetValue(ackedTick, out ackedEnts))
else if (!sessionData.PreviouslySent.TryGetValue(ackedTick, out ackedEnts))
return;
foreach (var data in CollectionsMarshal.AsSpan(ackedEnts))
{
data.EntityLastAcked = ackedTick;
DebugTools.Assert(data.Visibility > PvsEntityVisibility.Unsent);
DebugTools.Assert(data.LastSent >= ackedTick); // LastSent may equal ackedTick if the packet was sent reliably.
DebugTools.Assert(!sessionData.EntityData.TryGetValue(data.NetEntity, out var old)
DebugTools.Assert(data.LastSeen >= ackedTick); // LastSent may equal ackedTick if the packet was sent reliably.
DebugTools.Assert(!sessionData.Entities.TryGetValue(data.NetEntity, out var old)
|| ReferenceEquals(data, old));
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Prometheus;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// Partial class for handling PVS chunks.
internal sealed partial class PvsSystem
{
public const float ChunkSize = 8;
private readonly Dictionary<PvsChunkLocation, PvsChunk> _chunks = new();
private readonly List<PvsChunk> _dirtyChunks = new(64);
private readonly List<PvsChunk> _cleanChunks = new(64);
// Store chunks grouped by the root node, for when maps/grids get deleted.
private readonly Dictionary<EntityUid, HashSet<PvsChunkLocation>> _chunkSets = new();
private List<Entity<MapGridComponent>> _grids = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2i GetChunkIndices(Vector2 coordinates) => (coordinates / ChunkSize).Floored();
/// <summary>
/// Iterate over all visible chunks and, if necessary, re-construct their list of entities.
/// </summary>
private void UpdateDirtyChunks(int index)
{
var chunk = _dirtyChunks[index];
DebugTools.Assert(chunk.Dirty);
DebugTools.Assert(chunk.UpdateQueued);
if (!chunk.PopulateContents(_metaQuery, _xformQuery))
return; // Failed to populate a dirty chunk.
UpdateChunkPosition(chunk);
}
private void UpdateCleanChunks()
{
foreach (var chunk in CollectionsMarshal.AsSpan(_cleanChunks))
{
UpdateChunkPosition(chunk);
}
}
/// <summary>
/// Update a chunk's world position. This is used to prioritize sending chunks that a closer to players.
/// </summary>
private void UpdateChunkPosition(PvsChunk chunk)
{
var xform = Transform(chunk.Root);
chunk.InvWorldMatrix = xform.InvLocalMatrix;
var worldPos = xform.LocalMatrix.Transform(chunk.Centre);
chunk.Position = new(worldPos, xform.MapID);
chunk.UpdateQueued = false;
}
/// <summary>
/// Update the list of all currently visible chunks.
/// </summary>
public void GetVisibleChunks(ICommonSession[] sessions, Histogram? histogram)
{
using var _= histogram?.WithLabels("Get Chunks").NewTimer();
// TODO PVS try parallelize
// I.e., get the per-player List<PvsChunk> in parallel.
// Then, add them together into a single (unique) List on the main thread
// However, I'm not sure if this would actually be any faster with parallel overhead.
// Per-player chunk enumeration should be reasonably fast? But its still a few component lookups and O(N^2)
// dictionary lookups per player, where N ~ pvs size.
DebugTools.Assert(!_chunks.Values.Any(x=> x.UpdateQueued));
_dirtyChunks.Clear();
_cleanChunks.Clear();
foreach (var session in sessions)
{
var data = PlayerData[session];
data.Chunks.Clear();
GetSessionViewers(data);
foreach (var eye in data.Viewers)
{
GetVisibleChunks(eye, data.Chunks);
}
// The list of visible chunks should be unique.
DebugTools.Assert(data.Chunks.Select(x => x.Chunk).ToHashSet().Count == data.Chunks.Count);
}
DebugTools.Assert(_dirtyChunks.ToHashSet().Count == _dirtyChunks.Count);
DebugTools.Assert(_cleanChunks.ToHashSet().Count == _cleanChunks.Count);
}
/// <summary>
/// Get the chunks visible to a single entity and add them to a player's set of visible chunks.
/// </summary>
private void GetVisibleChunks(Entity<TransformComponent, EyeComponent?> eye,
List<(PvsChunk Chunk, float ChebyshevDistance)> playerChunks)
{
var (viewPos, range, mapUid) = CalcViewBounds(eye);
if (mapUid is not {} map)
return;
var mapChunkEnumerator = new ChunkIndicesEnumerator(viewPos, range, ChunkSize);
while (mapChunkEnumerator.MoveNext(out var chunkIndices))
{
var loc = new PvsChunkLocation(map, chunkIndices.Value);
if (!_chunks.TryGetValue(loc, out var chunk))
continue;
playerChunks.Add((chunk, default));
if (chunk.UpdateQueued)
continue;
chunk.UpdateQueued = true;
if (chunk.Dirty)
_dirtyChunks.Add(chunk);
else
_cleanChunks.Add(chunk);
}
_grids.Clear();
var rangeVec = new Vector2(range, range);
var box = new Box2(viewPos - rangeVec, viewPos + rangeVec);
_mapManager.FindGridsIntersecting(map, box, ref _grids, approx: true, includeMap: false);
foreach (var (grid, _) in _grids)
{
var localPos = _transform.GetInvWorldMatrix(grid).Transform(viewPos);
var gridChunkEnumerator = new ChunkIndicesEnumerator(localPos, range, ChunkSize);
while (gridChunkEnumerator.MoveNext(out var gridChunkIndices))
{
var loc = new PvsChunkLocation(grid, gridChunkIndices.Value);
if (!_chunks.TryGetValue(loc, out var chunk))
continue;
playerChunks.Add((chunk, default));
if (chunk.UpdateQueued)
continue;
chunk.UpdateQueued = true;
if (chunk.Dirty)
_dirtyChunks.Add(chunk);
else
_cleanChunks.Add(chunk);
}
}
}
/// <summary>
/// Get all viewers for a given session. This is required to get a list of visible chunks.
/// </summary>
private void GetSessionViewers(PvsSession pvsSession)
{
var session = pvsSession.Session;
if (session.Status != SessionStatus.InGame)
{
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
return;
}
// Fast path
if (session.ViewSubscriptions.Count == 0)
{
if (session.AttachedEntity is not {} attached)
{
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
return;
}
Array.Resize(ref pvsSession.Viewers, 1);
pvsSession.Viewers[0] = (attached, Transform(attached), _eyeQuery.CompOrNull(attached));
return;
}
int i = 0;
if (session.AttachedEntity is { } local)
{
DebugTools.Assert(!session.ViewSubscriptions.Contains(local));
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count + 1);
pvsSession.Viewers[i++] = (local, Transform(local), _eyeQuery.CompOrNull(local));
}
else
{
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count);
}
foreach (var ent in session.ViewSubscriptions)
{
pvsSession.Viewers[i++] = (ent, Transform(ent), _eyeQuery.CompOrNull(ent));
}
}
public void ProcessVisibleChunks(Histogram? histogram)
{
using var _= histogram?.WithLabels("Update Chunks").NewTimer();
_parallelMgr.ProcessNow(_chunkJob, _chunkJob.Count);
}
/// <summary>
/// Variant of <see cref="ProcessVisibleChunks"/> that isn't multithreaded.
/// </summary>
public void ProcessVisibleChunksSequential()
{
UpdateCleanChunks();
for (var i = 0; i < _dirtyChunks.Count; i++)
{
UpdateDirtyChunks(i);
}
}
/// <summary>
/// Add an entity to the set of entities that are directly attached to a chunk and mark the chunk as dirty.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddEntityToChunk(EntityUid uid, MetaDataComponent meta, PvsChunkLocation location)
{
ref var chunk = ref CollectionsMarshal.GetValueRefOrAddDefault(_chunks, location, out var existing);
if (!existing)
{
chunk = _chunkPool.Get();
chunk.Initialize(location, _metaQuery, _xformQuery);
_chunkSets.GetOrNew(location.Uid).Add(location);
}
chunk!.MarkDirty();
chunk.Children.Add(uid);
meta.LastPvsLocation = location;
}
/// <summary>
/// Remove an entity from a chunk and mark it as dirty.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveEntityFromChunk(EntityUid uid, MetaDataComponent meta)
{
if (meta.LastPvsLocation is not {} old)
return;
meta.LastPvsLocation = null;
if (!_chunks.TryGetValue(old, out var chunk))
return;
chunk.MarkDirty();
chunk.Children.Remove(uid);
if (chunk.Children.Count > 0)
return;
_chunks.Remove(old);
_chunkPool.Return(chunk);
_chunkSets[old.Uid].Remove(old);
}
/// <summary>
/// Mark a chunk as dirty.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DirtyChunk(PvsChunkLocation location)
{
if (_chunks.TryGetValue(location, out var chunk))
chunk.MarkDirty();
}
private void OnGridRemoved(GridRemovalEvent ev)
{
RemoveRoot(ev.EntityUid);
}
private void OnMapChanged(MapChangedEvent ev)
{
if (!ev.Destroyed)
RemoveRoot(ev.Uid);
}
private void RemoveRoot(EntityUid root)
{
if (!_chunkSets.Remove(root, out var locations))
return;
foreach (var loc in locations)
{
if (_chunks.Remove(loc, out var chunk))
_chunkPool.Return(chunk);
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Prometheus;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
@@ -68,8 +69,9 @@ namespace Robust.Server.GameStates
return true;
}
public void CleanupDirty(IEnumerable<ICommonSession> sessions)
public void CleanupDirty(ICommonSession[] sessions, Histogram? histogram)
{
using var _ = histogram?.WithLabels("Clean Dirty").NewTimer();
if (!CullingEnabled)
{
_seenAllEnts.Clear();
@@ -82,20 +84,6 @@ namespace Robust.Server.GameStates
_currentIndex = ((int)_gameTiming.CurTick.Value + 1) % DirtyBufferSize;
_addEntities[_currentIndex].Clear();
_dirtyEntities[_currentIndex].Clear();
foreach (var collection in _pvsCollections)
{
collection.ClearDirty();
}
}
/// <summary>
/// Marks an entity's current chunk as dirty.
/// </summary>
internal void MarkDirty(EntityUid uid, TransformComponent xform)
{
var coordinates = _transform.GetMoverCoordinates(uid, xform);
_entityPvsCollection.MarkDirty(_entityPvsCollection.GetChunkIndex(coordinates));
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Diagnostics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// Partial class for handling entity events (move, deletion, etc)
internal sealed partial class PvsSystem
{
private void OnEntityMove(ref MoveEvent ev)
{
UpdatePosition(ev.Entity.Owner, ev.Entity.Comp1, ev.Entity.Comp2, ev.OldPosition.EntityId);
}
private void OnTransformStartup(EntityUid uid, TransformComponent component, ref TransformStartupEvent args)
{
if (component.ParentUid == EntityUid.Invalid)
return;
UpdatePosition(uid, component, MetaData(uid), EntityUid.Invalid);
}
private void OnEntityTerminating(ref EntityTerminatingEvent ev)
{
var meta = ev.Entity.Comp;
foreach (var sessionData in PlayerData.Values)
{
sessionData.Entities.Remove(meta.NetEntity);
}
_deletedEntities.Add(meta.NetEntity);
_deletedTick.Add(_gameTiming.CurTick);
RemoveEntityFromChunk(ev.Entity.Owner, meta);
}
private void UpdatePosition(EntityUid uid, TransformComponent xform, MetaDataComponent meta, EntityUid oldParent)
{
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
DebugTools.AssertNull(meta.LastPvsLocation);
return;
}
// GridUid is only set after init.
if (!xform._gridInitialized)
_transform.InitializeGridUid(uid, xform);
if (xform.GridUid == uid)
return;
DebugTools.Assert(!HasComp<MapGridComponent>(uid));
DebugTools.Assert(!HasComp<MapComponent>(uid));
if (oldParent != xform.ParentUid)
{
HandleParentChange(uid, xform, meta);
return;
}
var root = (xform.GridUid ?? xform.MapUid);
DebugTools.AssertNotNull(root);
if (xform.ParentUid != root)
return;
var location = new PvsChunkLocation(root.Value, GetChunkIndices(xform._localPosition));
if (meta.LastPvsLocation == location)
return;
RemoveEntityFromChunk(uid, meta);
AddEntityToChunk(uid, meta, location);
}
private void HandleParentChange(EntityUid uid, TransformComponent xform, MetaDataComponent meta)
{
RemoveEntityFromChunk(uid, meta);
// moving to null space?
if (xform.ParentUid == EntityUid.Invalid || xform.ParentUid == uid)
return;
var newRoot = (xform.GridUid ?? xform.MapUid);
if (newRoot == null)
{
AssertNullspace(xform.ParentUid);
return;
}
// If directly parented to the chunk, add as a direct child.
if (xform.ParentUid == newRoot)
{
var location = new PvsChunkLocation(newRoot.Value, GetChunkIndices(xform._localPosition));
AddEntityToChunk(uid, meta, location);
return;
}
// Else, mark the new parent's last chunk as dirty. Null implies it is already dirty.
if (MetaData(xform.ParentUid).LastPvsLocation is { } loc)
DirtyChunk(loc);
}
[Conditional("DEBUG")]
private void AssertNullspace(EntityUid uid)
{
if (uid == EntityUid.Invalid || !_xformQuery.TryGetComponent(uid, out var xform))
return;
DebugTools.AssertNull(xform.GridUid);
DebugTools.AssertNull(xform.MapUid);
AssertNullspace(xform.ParentUid);
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -11,58 +10,6 @@ namespace Robust.Server.GameStates;
// This partial class contains code for turning a list of visible entities into actual entity states.
internal sealed partial class PvsSystem
{
public void GetStateList(
List<EntityState> states,
List<EntityData> toSend,
SessionPvsData sessionData,
GameTick fromTick)
{
DebugTools.Assert(states.Count == 0);
var entData = sessionData.EntityData;
var session = sessionData.Session;
if (sessionData.RequestedFull)
{
foreach (var data in CollectionsMarshal.AsSpan(toSend))
{
DebugTools.AssertNotNull(data.Entity.Comp);
DebugTools.Assert(data.LastSent == _gameTiming.CurTick);
DebugTools.Assert(data.Visibility > PvsEntityVisibility.Unsent);
DebugTools.Assert(ReferenceEquals(data, entData[data.NetEntity]));
states.Add(GetFullEntityState(session, data.Entity.Owner, data.Entity.Comp));
}
return;
}
foreach (var data in CollectionsMarshal.AsSpan(toSend))
{
DebugTools.AssertNotNull(data.Entity.Comp);
DebugTools.Assert(data.LastSent == _gameTiming.CurTick);
DebugTools.Assert(data.Visibility > PvsEntityVisibility.Unsent);
DebugTools.Assert(ReferenceEquals(data, entData[data.NetEntity]));
if (data.Visibility == PvsEntityVisibility.Unchanged)
continue;
var (uid, meta) = data.Entity;
var entered = data.Visibility == PvsEntityVisibility.Entered;
var entFromTick = entered ? data.EntityLastAcked : fromTick;
// TODO PVS turn into debug assert
// This is should really be a debug assert, but I want to check for errors on live servers
// If an entity is not marked as "entering" this tick, then it HAS to have been in the last acked state
if (!entered && data.EntityLastAcked < fromTick)
{
Log.Error($"un-acked entity is not marked as entering. Entity{ToPrettyString(uid)}. FromTick: {fromTick}. CurTick: {_gameTiming.CurTick}. Data: {data}");
}
var state = GetEntityState(session, uid, entFromTick, meta);
if (entered || !state.Empty)
states.Add(state);
}
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
@@ -151,20 +98,25 @@ internal sealed partial class PvsSystem
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
public (List<EntityState>?, List<NetEntity>?, GameTick fromTick) GetAllEntityStates(ICommonSession? player, GameTick fromTick, GameTick toTick)
public void GetAllEntityStates(PvsSession pvsSession)
{
List<EntityState>? stateEntities;
var session = pvsSession.Session;
var toTick = _gameTiming.CurTick;
var fromTick = pvsSession.FromTick;
var toSend = _uidSetPool.Get();
DebugTools.Assert(toSend.Count == 0);
bool enumerateAll = false;
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
DebugTools.Assert(toTick > fromTick);
if (player == null)
// Null sessions imply this is a replay.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (session == null)
{
enumerateAll = fromTick == GameTick.Zero;
}
else if (!_seenAllEnts.Contains(player))
else if (!_seenAllEnts.Contains(session))
{
enumerateAll = true;
fromTick = GameTick.Zero;
@@ -178,7 +130,6 @@ internal sealed partial class PvsSystem
if (enumerateAll)
{
stateEntities = new List<EntityState>(EntityManager.EntityCount);
var query = EntityManager.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var md))
{
@@ -187,7 +138,7 @@ internal sealed partial class PvsSystem
if (md.EntityLastModifiedTick <= fromTick)
continue;
var state = GetEntityState(player, uid, fromTick, md);
var state = GetEntityState(session, uid, fromTick, md);
if (state.Empty)
{
@@ -199,12 +150,11 @@ Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
}
stateEntities.Add(state);
pvsSession.States.Add(state);
}
}
else
{
stateEntities = new();
for (var i = fromTick.Value + 1; i <= toTick.Value; i++)
{
if (!TryGetDirtyEntities(new GameTick(i), out var add, out var dirty))
@@ -223,7 +173,7 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(player, uid, fromTick, md);
var state = GetEntityState(session, uid, fromTick, md);
if (state.Empty)
{
@@ -236,7 +186,7 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
continue;
}
stateEntities.Add(state);
pvsSession.States.Add(state);
}
foreach (var uid in dirty)
@@ -250,19 +200,13 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(player, uid, fromTick, md);
var state = GetEntityState(session, uid, fromTick, md);
if (!state.Empty)
stateEntities.Add(state);
pvsSession.States.Add(state);
}
}
}
_uidSetPool.Return(toSend);
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
if (stateEntities.Count == 0)
stateEntities = null;
return (stateEntities, deletions, fromTick);
}
}

View File

@@ -0,0 +1,190 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates;
// This partial class contains code for handling sending PVS overrides, i.e., entities that ignore normal PVS
// range/chunk restrictions.
internal sealed partial class PvsSystem
{
private readonly List<Entity<MetaDataComponent>> _cachedForceOverride = new();
private readonly List<Entity<MetaDataComponent>> _cachedGlobalOverride = new();
private readonly HashSet<EntityUid> _forceOverrideSet = new();
private readonly HashSet<EntityUid> _globalOverrideSet = new();
private void AddAllOverrides(PvsSession session)
{
var fromTick = session.FromTick;
RaiseExpandEvent(session, fromTick);
foreach (var entity in _cachedGlobalOverride)
{
if (!AddEntity(session, entity, fromTick))
break;
}
if (!_pvsOverride.SessionOverrides.TryGetValue(session.Session, out var sessionOverrides))
return;
foreach (var uid in sessionOverrides)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
}
}
/// <summary>
/// Adds all entities that ignore normal pvs budgets.
/// </summary>
private void AddForcedEntities(PvsSession session)
{
// Ignore PVS budgets
session.Budget = new() {NewLimit = int.MaxValue, EnterLimit = int.MaxValue};
var fromTick = session.FromTick;
foreach (var entity in _cachedForceOverride)
{
AddEntity(session, entity, fromTick);
}
foreach (var uid in session.Viewers)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
}
}
private void RaiseExpandEvent(PvsSession session, GameTick fromTick)
{
var expandEvent = new ExpandPvsEvent(session.Session);
if (session.Session.AttachedEntity != null)
RaiseLocalEvent(session.Session.AttachedEntity.Value, ref expandEvent, true);
else
RaiseLocalEvent(ref expandEvent);
if (expandEvent.Entities != null)
{
foreach (var uid in expandEvent.Entities)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
}
}
if (expandEvent.RecursiveEntities == null)
return;
foreach (var uid in expandEvent.RecursiveEntities)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
}
}
/// <summary>
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
/// </summary>
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren)
{
var xform = _xformQuery.GetComponent(uid);
var parent = xform.ParentUid;
// First we process all parents. This is because while this entity may already have been added
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget.
if (parent.IsValid() && !RecursivelyAddOverride(session, parent, fromTick, false))
return false;
if (!_metaQuery.TryGetComponent(uid, out var meta))
return false;
if (!AddEntity(session, (uid, meta), fromTick))
return false;
if (addChildren)
RecursivelyAddChildren(session, xform, fromTick);
return true;
}
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddChildren(PvsSession session, TransformComponent xform, GameTick fromTick)
{
foreach (var child in xform._children)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
{
Log.Error($"Encountered deleted child {child} while recursively adding children.");
continue;
}
var metadata = _metaQuery.GetComponent(child);
if (!AddEntity(session, (child, metadata), fromTick))
return;
RecursivelyAddChildren(session, childXform, fromTick);
}
}
private void CacheGlobalOverrides()
{
_cachedForceOverride.Clear();
_forceOverrideSet.Clear();
foreach (var uid in _pvsOverride.ForceSend)
{
CacheOverrideParents(uid, _cachedForceOverride, _forceOverrideSet, out _);
}
_cachedGlobalOverride.Clear();
_globalOverrideSet.Clear();
foreach (var uid in _pvsOverride.GlobalOverride)
{
CacheOverrideParents(uid, _cachedGlobalOverride, _globalOverrideSet, out var xform);
CacheOverrideChildren(xform, _cachedGlobalOverride, _globalOverrideSet);
}
}
private bool CacheOverrideParents(
EntityUid uid,
List<Entity<MetaDataComponent>> list,
HashSet<EntityUid> set,
out TransformComponent xform)
{
xform = _xformQuery.GetComponent(uid);
if (xform.ParentUid != EntityUid.Invalid && !CacheOverrideParents(xform.ParentUid, list, set, out _))
return false;
if (!set.Add(uid))
return true;
if (!_metaQuery.TryGetComponent(uid, out var meta))
{
Log.Error($"Encountered deleted entity in global overrides: {uid}");
set.Remove(uid);
return false;
}
list.Add((uid, meta));
return true;
}
private void CacheOverrideChildren(TransformComponent xform, List<Entity<MetaDataComponent>> list, HashSet<EntityUid> set)
{
foreach (var child in xform._children)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
{
Log.Error($"Encountered deleted child {child} while recursively adding children.");
continue;
}
if (set.Add(child))
list.Add((child, _metaQuery.GetComponent(child)));
CacheOverrideChildren(childXform, list, set);
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains code for pooling objects to avoid allocations.
// This file is now blessedly small, so maybe we can just get rid of it.
internal sealed partial class PvsSystem
{
/// <summary>
/// Maximum number of pooled objects.
/// </summary>
private const int MaxVisPoolSize = 1024;
private readonly ObjectPool<List<PvsData>> _entDataListPool
= new DefaultObjectPool<List<PvsData>>(new ListPolicy<PvsData>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
private readonly ObjectPool<PvsChunk> _chunkPool =
new DefaultObjectPool<PvsChunk>(new PvsChunkPolicy(), 256);
public sealed class PvsChunkPolicy : PooledObjectPolicy<PvsChunk>
{
public override PvsChunk Create()
{
return new PvsChunk();
}
public override bool Return(PvsChunk obj)
{
obj.Wipe();
return true;
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PvsSystem
{
/// <summary>
/// If PVS disabled then we'll track if we've dumped all entities on the player.
/// This way any future ticks can be orders of magnitude faster as we only send what changes.
/// </summary>
private HashSet<ICommonSession> _seenAllEnts = new();
internal readonly Dictionary<ICommonSession, PvsSession> PlayerData = new();
internal PvsSession GetSessionData(ICommonSession session)
=> GetSessionData(PlayerData[session]);
internal PvsSession GetSessionData(PvsSession session)
{
UpdateSessionData(session);
if (CullingEnabled && !session.DisableCulling)
GetEntityStates(session);
else
GetAllEntityStates(session);
_playerManager.GetPlayerStates(session.FromTick, session.PlayerStates);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
DebugTools.Assert(session.States.Select(x=> x.NetEntity).ToHashSet().Count == session.States.Count);
DebugTools.AssertNull(session.State);
session.State = new GameState(
session.FromTick,
_gameTiming.CurTick,
Math.Max(session.LastInput, session.LastMessage),
session.States,
session.PlayerStates,
_deletedEntities);
if (_gameTiming.CurTick.Value > session.LastReceivedAck.Value + ForceAckThreshold)
session.State.ForceSendReliably = true;
return session;
}
internal void UpdateSessionData(PvsSession session)
{
DebugTools.AssertEqual(session.LeftView.Count, 0);
DebugTools.AssertEqual(session.PlayerStates.Count, 0);
DebugTools.AssertEqual(session.States.Count, 0);
DebugTools.Assert(CullingEnabled && !session.DisableCulling || session.Chunks.Count == 0);
DebugTools.AssertNull(session.ToSend);
DebugTools.AssertNull(session.State);
session.FromTick = session.RequestedFull ? GameTick.Zero : session.LastReceivedAck;
session.LastInput = _input.GetLastInputCommand(session.Session);
session.LastMessage = _netEntMan.GetLastMessageSequence(session.Session);
session.VisMask = EyeComponent.DefaultVisibilityMask;
// Update visibility masks & viewer positions
// TODO PVS do this before sending state.
// I,e, we already enumerate over all eyes when computing visible chunks.
Span<MapCoordinates> positions = stackalloc MapCoordinates[session.Viewers.Length];
int i = 0;
foreach (var viewer in session.Viewers)
{
if (viewer.Comp2 != null)
session.VisMask |= viewer.Comp2.VisibilityMask;
positions[i++] = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
}
if (!CullingEnabled || session.DisableCulling)
return;
var chunks = session.Chunks;
var distances = session.ChunkDistanceSq;
distances.Clear();
distances.EnsureCapacity(chunks.Count);
// Assemble list of chunks and their distances to the nearest eye.
foreach (ref var tuple in CollectionsMarshal.AsSpan(chunks))
{
var chunk = tuple.Chunk;
var dist = float.MaxValue;
var chebDist = float.MaxValue;
DebugTools.Assert(!chunk.UpdateQueued);
DebugTools.Assert(!chunk.Dirty);
foreach (var pos in positions)
{
if (pos.MapId != chunk.Position.MapId)
continue;
dist = Math.Min(dist, (pos.Position - chunk.Position.Position).LengthSquared());
var relative = chunk.InvWorldMatrix.Transform(pos.Position) - chunk.Centre;
relative = Vector2.Abs(relative);
chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y));
}
distances.Add(dist);
tuple.ChebyshevDistance = chebDist;
}
// Sort chunks based on distances
CollectionsMarshal.AsSpan(distances).Sort(CollectionsMarshal.AsSpan(chunks));
session.ToSend = _entDataListPool.Get();
if (session.PreviouslySent.TryGetValue(_gameTiming.CurTick - 1, out var lastSent))
session.LastSent = (_gameTiming.CurTick, lastSent);
}
}

View File

@@ -1,43 +1,141 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains contains methods for adding entities to the set of entities that are about to get sent
// to a player.
// This partial class contains functions for adding entities to a the collections of entities that are getting sent to
// a player this tick..
internal sealed partial class PvsSystem
{
/// <summary>
/// This method adds an entity to the to-send list, updates the last-sent tick, and updates the entity's visibility.
/// Iterate over chunks that are visible to a player and add entities to the game-state.
/// </summary>
private void AddToSendList(
EntityData data,
List<EntityData> list,
GameTick fromTick,
GameTick toTick,
bool entered,
ref int dirtyEntityCount)
private void AddPvsChunks(PvsSession pvsSession)
{
foreach (var (chunk, distance) in CollectionsMarshal.AsSpan(pvsSession.Chunks))
{
AddPvsChunk(chunk, distance, pvsSession);
}
}
/// <summary>
/// A chunks that is visible to a player and add entities to the game-state.
/// </summary>
private void AddPvsChunk(PvsChunk chunk, float distance, PvsSession session)
{
#if DEBUG
// Each root nodes should simply be a map or a grid entity.
DebugTools.Assert(Exists(chunk.Root), $"Root node does not exist. Node {chunk.Root.Owner}. Session: {session.Session}");
DebugTools.Assert(HasComp<MapComponent>(chunk.Root) || HasComp<MapGridComponent>(chunk.Root));
#endif
var fromTick = session.FromTick;
var mask = session.VisMask;
// Send the map.
if (!AddEntity(session, chunk.Map, fromTick))
return;
// Send the grid
if (chunk.Map.Owner != chunk.Root.Owner && !AddEntity(session, chunk.Root, fromTick))
return;
// Get the number of entities to send (i.e., basic LOD restrictions)
// We add chunk-size here so that its consistent with the normal PVS range setting.
// I.e., distance here is the Chebyshev distance to the centre of each chunk, but the normal pvs range only
// required that the chunk be touching the box, not the centre.
var count = distance < (_lowLodDistance + ChunkSize)/2
? chunk.Contents.Count
: chunk.LodCounts[0];
// Send entities on the chunk.
var span = CollectionsMarshal.AsSpan(chunk.Contents);
for (var i = 0; i < count; i++)
{
var ent = span[i];
if ((mask & ent.Comp.VisibilityMask) == ent.Comp.VisibilityMask)
AddEntity(session, ent, fromTick);
}
}
/// <summary>
/// Attempt to add an entity to the to-send lists, while respecting pvs budgets.
/// </summary>
private bool AddEntity(PvsSession session, Entity<MetaDataComponent> entity, GameTick fromTick)
{
ref var data = ref CollectionsMarshal.GetValueRefOrAddDefault(session.Entities, entity.Comp.NetEntity, out var exists);
if (!exists)
data = new(entity);
if (entity.Comp.Deleted)
{
Log.Error($"Attempted to send deleted entity: {ToPrettyString(entity, entity)}");
session.Entities.Remove(entity.Comp.NetEntity);
return false;
}
DebugTools.AssertEqual(data!.NetEntity, entity.Comp.NetEntity);
DebugTools.AssertEqual(data.LastSeen == GameTick.Zero, data.Visibility <= PvsEntityVisibility.Unsent);
DebugTools.AssertEqual(data.Entity, entity);
if (data.LastSeen == _gameTiming.CurTick)
return true;
var (entered,budgetExceeded) = IsEnteringPvsRange(data, fromTick, ref session.Budget);
if (!budgetExceeded)
{
if (!AddToSendList(session, data, fromTick, entered))
return false;
DebugTools.AssertNotEqual(data.LastSeen, GameTick.Zero);
return true;
}
// Sending this entity would go over the player's budget, so we will not add it. However, we do not
// stop iterating over this (or other chunks). This is to avoid sending bad pvs-leave messages.
// I.e., other entities may have just stayed in view, and we can send them without exceeding our
// budget. E.g., this might be the very first chunk we are iterating over, and it just so happens
// to be a chunk that just entered their PVS range.
if (data.Visibility != PvsEntityVisibility.Invalid)
return false;
// This entity was never sent to the player, and isn't being sent now.
// However, the data has already been added to the entityData dictionary.
// In order for debug asserts and other sanity checks to keep working, we mark the entity as
// explicitly unsent.
data.Visibility = PvsEntityVisibility.Unsent;
return false;
}
/// <summary>
/// This method adds an entity to the list of visible entities, updates the last-seen tick, and computes any
/// required game states.
/// </summary>
private bool AddToSendList(PvsSession session, PvsData data, GameTick fromTick, bool entered)
{
DebugTools.Assert(fromTick < _gameTiming.CurTick);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data == null)
{
Log.Error($"Encountered null EntityData.");
return;
return false;
}
var meta = data.Entity.Comp;
DebugTools.Assert(fromTick < toTick);
DebugTools.AssertNotEqual(data.LastSent, toTick);
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
DebugTools.AssertNotEqual(data.LastSeen, _gameTiming.CurTick);
DebugTools.Assert(data.EntityLastAcked <= fromTick || fromTick == GameTick.Zero);
var (uid, meta) = data.Entity;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (meta == null)
{
Log.Error($"Encountered null metadata in EntityData. Entity: {ToPrettyString(data?.Entity)}");
return;
return false;
}
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
@@ -48,54 +146,63 @@ internal sealed partial class PvsSystem
// This can happen if some entity was some removed from it's parent while that parent was being deleted.
// As a result the entity was marked for deletion but was never actually properly deleted.
EntityManager.QueueDeleteEntity(data.Entity);
return;
return false;
}
data.LastSent = toTick;
list.Add(data);
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(data);
EntityState state;
if (session.RequestedFull)
{
state = GetFullEntityState(session.Session, data.Entity.Owner, data.Entity.Comp);
session.States.Add(state);
return true;
}
if (entered)
{
data.Visibility = PvsEntityVisibility.Entered;
dirtyEntityCount++;
return;
state = GetEntityState(session.Session, uid, data.EntityLastAcked, meta);
session.States.Add(state);
return true;
}
if (meta.EntityLastModifiedTick <= fromTick)
{
//entity has been sent before and hasn't been updated since
data.Visibility = PvsEntityVisibility.Unchanged;
return;
return true;
}
//add us
data.Visibility = PvsEntityVisibility.Dirty;
dirtyEntityCount++;
state = GetEntityState(session.Session, uid, fromTick , meta);
if (!state.Empty)
session.States.Add(state);
return true;
}
/// <summary>
/// This method figures out whether a given entity is currently entering a player's PVS range.
/// This method will also check that the player's PVS entry budget is not being exceeded.
/// </summary>
private (bool Entering, bool BudgetExceeded) IsEnteringPvsRange(EntityData entity,
private (bool Entering, bool BudgetExceeded) IsEnteringPvsRange(
PvsData data,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
int newEntityBudget,
int enteredEntityBudget)
ref PvsBudget budget)
{
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
DebugTools.AssertEqual(entity.LastSent == GameTick.Zero, entity.Visibility <= PvsEntityVisibility.Unsent);
DebugTools.AssertEqual(data.LastSeen == GameTick.Zero, data.Visibility <= PvsEntityVisibility.Unsent);
var enteredSinceLastSent = fromTick == GameTick.Zero
|| entity.LastSent == GameTick.Zero
|| entity.LastSent.Value != toTick.Value - 1;
|| data.LastSeen == GameTick.Zero
|| data.LastSeen != _gameTiming.CurTick - 1;
var entering = enteredSinceLastSent
|| entity.EntityLastAcked == GameTick.Zero
|| entity.EntityLastAcked < fromTick // this entity was not in the last acked state.
|| entity.LastLeftView >= fromTick; // entity left and re-entered sometime after the last acked tick
|| data.EntityLastAcked == GameTick.Zero
|| data.EntityLastAcked < fromTick // this entity was not in the last acked state.
|| data.LastLeftView >= fromTick; // entity left and re-entered sometime after the last acked tick
// If the entity is entering, but we already sent this entering entity in the last message, we won't add it to
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
@@ -103,186 +210,15 @@ internal sealed partial class PvsSystem
// 2x or more times the normal entity creation budget.
if (enteredSinceLastSent)
{
if (newEntityCount >= newEntityBudget || enteredEntityCount >= enteredEntityBudget)
if (budget.NewCount >= budget.NewLimit || budget.EnterCount >= budget.EnterLimit)
return (entering, true);
enteredEntityCount++;
budget.EnterCount++;
if (entity.EntityLastAcked == GameTick.Zero)
newEntityCount++;
if (data.EntityLastAcked == GameTick.Zero)
budget.NewCount++;
}
return (entering, false);
}
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddTreeNode(in NetEntity nodeIndex,
RobustTree<NetEntity> tree,
List<EntityData> toSend,
Dictionary<NetEntity, EntityData> entityData,
Stack<NetEntity> stack,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget)
{
stack.Push(nodeIndex);
while (stack.TryPop(out var currentNodeIndex))
{
DebugTools.Assert(currentNodeIndex.IsValid());
// As every map is parented to uid 0 in the tree we still need to get their children, plus because we go top-down
// we may find duplicate parents with children we haven't encountered before
// on different chunks (this is especially common with direct grid children)
var data = GetOrNewEntityData(entityData, currentNodeIndex);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data == null)
continue;
if (data.LastSent != toTick)
{
var (entered, budgetExceeded) = IsEnteringPvsRange(data, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
if (budgetExceeded)
{
if (data.Visibility == PvsEntityVisibility.Invalid)
{
// This entity was never sent to the player, and isn't being sent now.
// However, the data has already been added to the entityData dictionary.
// In order for debug asserts and other sanity checks to keep working, we mark the entity as
// explicitly unsent.
data.Visibility = PvsEntityVisibility.Unsent;
}
// Sending this entity would go over the player's budget, so we will not add it. However, we do not
// stop iterating over this (or other chunks). This is to avoid sending bad pvs-leave messages.
// I.e., other entities may have just stayed in view, and we can send them without exceeding our
// budget. E.g., this might be the very first chunk we are iterating over, and it just so happens
// to be a chunk that just entered their PVS range.
continue;
}
AddToSendList(data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
DebugTools.AssertNotEqual(data.LastSent, GameTick.Zero);
}
if (!tree.TryGet(currentNodeIndex, out var node))
{
Log.Error($"tree is missing the current node! Node: {currentNodeIndex}");
continue;
}
if (node.Children == null)
continue;
foreach (var child in node.Children)
{
stack.Push(child);
}
}
}
/// <summary>
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
/// </summary>
public bool RecursivelyAddOverride(in EntityUid uid,
List<EntityData> toSend,
Dictionary<NetEntity, EntityData> entityData,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget,
bool addChildren = false)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid())
return false;
var xform = _xformQuery.GetComponent(uid);
var parent = xform.ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, toSend, entityData, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, ref dirtyEntityCount, newEntityBudget, enteredEntityBudget))
{
return false;
}
var netEntity = _metaQuery.GetComponent(uid).NetEntity;
// Note that we check this AFTER adding parents. This is because while this entity may already have been added
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget.
var data = GetOrNewEntityData(entityData, netEntity);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data == null)
return false;
if (data.LastSent != toTick)
{
var (entered, _) = IsEnteringPvsRange(data, fromTick, toTick, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
if (addChildren)
{
RecursivelyAddChildren(xform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
return true;
}
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddChildren(TransformComponent xform,
List<EntityData> toSend,
Dictionary<NetEntity, EntityData> entityData,
in GameTick fromTick,
in GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
in int newEntityBudget,
in int enteredEntityBudget)
{
foreach (var child in xform._children)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
continue;
var metadata = _metaQuery.GetComponent(child);
var netChild = metadata.NetEntity;
var data = GetOrNewEntityData(entityData, netChild);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data == null)
continue;
if (data.LastSent != toTick)
{
var (entered, _) = IsEnteringPvsRange(data, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
RecursivelyAddChildren(childXform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,197 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
public sealed class RobustTree<T> where T : notnull
{
private Dictionary<T, TreeNode> _nodeIndex = new();
private Dictionary<T, T> _parents = new();
public readonly HashSet<T> RootNodes = new();
private ObjectPool<HashSet<T>> _pool;
public RobustTree(ObjectPool<HashSet<T>>? pool = null)
{
_pool = pool ?? new DefaultObjectPool<HashSet<T>>(new SetPolicy<T>());
}
public void Clear()
{
foreach (var value in _nodeIndex.Values)
{
if(value.Children != null)
_pool.Return(value.Children);
}
_nodeIndex.Clear();
_parents.Clear();
RootNodes.Clear();
}
public TreeNode this[T index] => _nodeIndex[index];
public bool TryGet(T index, out TreeNode node) => _nodeIndex.TryGetValue(index, out node);
public void Remove(T value, bool mend = false)
{
if (!_nodeIndex.TryGetValue(value, out var node))
throw new InvalidOperationException("Node doesnt exist.");
if (RootNodes.Contains(value))
{
if (node.Children != null)
{
foreach (var child in node.Children)
{
_parents.Remove(child);
RootNodes.Add(child);
}
_pool.Return(node.Children);
}
RootNodes.Remove(value);
_nodeIndex.Remove(value);
return;
}
if (_parents.TryGetValue(value, out var parent))
{
if (node.Children != null)
{
foreach (var child in node.Children)
{
if (mend)
{
_parents[child] = parent;
var children = _nodeIndex[parent].Children;
if (children == null)
{
children = _pool.Get();
_nodeIndex[parent] = _nodeIndex[parent].WithChildren(children);
}
children.Add(child);
}
else
{
_parents.Remove(child);
RootNodes.Add(child);
}
}
_pool.Return(node.Children);
}
_parents.Remove(value);
_nodeIndex.Remove(value);
}
throw new InvalidOperationException("Node neither had a parent nor was a RootNode.");
}
public void Set(T rootNode)
{
ref var node = ref CollectionsMarshal.GetValueRefOrAddDefault(_nodeIndex, rootNode, out var exists);
if (exists)
{
if(!RootNodes.Contains(rootNode))
throw new InvalidOperationException("Node already exists as non-root node.");
return;
}
node = new TreeNode(rootNode);
if(!RootNodes.Add(rootNode))
throw new InvalidOperationException("Non-existent node was already a root node?");
}
public void Set(T child, T parent)
{
// Code block for where parentNode is a valid ref
{
ref var parentNode = ref CollectionsMarshal.GetValueRefOrAddDefault(_nodeIndex, parent, out var parentExists);
// If parent does not exist we make it a new root node.
if (!parentExists)
{
parentNode = new TreeNode(parent);
if (!RootNodes.Add(parent))
{
_nodeIndex.Remove(parent);
throw new InvalidOperationException("Non-existent node was already a root node?");
}
}
var children = parentNode.Children;
if (children == null)
{
children = _pool.Get();
parentNode = parentNode.WithChildren(children);
DebugTools.AssertNotNull(_nodeIndex[parent].Children);
}
children.Add(child);
}
// No longer safe to access parentNode ref after this.
ref var node = ref CollectionsMarshal.GetValueRefOrAddDefault(_nodeIndex, child, out var childExists);
if (!childExists)
{
// This is the path that PVS should take 99% of the time.
node = new TreeNode(child);
_parents.Add(child, parent);
return;
}
if (RootNodes.Remove(child))
{
DebugTools.Assert(!_parents.ContainsKey(child));
_parents.Add(child, parent);
return;
}
ref var parentEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(_parents, child, out var previousParentExists);
if (!previousParentExists || !_nodeIndex.TryGetValue(parentEntry!, out var previousParentNode))
{
parentEntry = parent;
throw new InvalidOperationException("Could not find old parent for non-root node.");
}
previousParentNode.Children?.Remove(child);
parentEntry = parent;
}
public readonly struct TreeNode : IEquatable<TreeNode>
{
public readonly T Value;
public readonly HashSet<T>? Children;
public TreeNode(T value, HashSet<T>? children = null)
{
Value = value;
Children = children;
}
public bool Equals(TreeNode other)
{
return Value.Equals(other.Value) && Children?.Equals(other.Children) == true;
}
public override bool Equals(object? obj)
{
return obj is TreeNode other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Value, Children);
}
public TreeNode WithChildren(HashSet<T> children)
{
return new TreeNode(Value, children);
}
}
}

View File

@@ -1,21 +1,15 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
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.Threading;
@@ -26,7 +20,6 @@ using Microsoft.Extensions.ObjectPool;
using Prometheus;
using Robust.Server.Replays;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
namespace Robust.Server.GameStates
@@ -38,19 +31,14 @@ namespace Robust.Server.GameStates
// Mapping of net UID of clients -> last known acked state.
private GameTick _lastOldestAck = GameTick.Zero;
private HashSet<int>[] _playerChunks = Array.Empty<HashSet<int>>();
private EntityUid[][] _viewerEntities = Array.Empty<EntityUid[]>();
private PvsSystem _pvs = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IServerNetManager _networkManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly INetworkedMapManager _mapManager = default!;
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IServerReplayRecordingManager _replay = default!;
[Dependency] private readonly IServerEntityNetworkManager _entityNetworkManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IParallelManager _parallelMgr = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
@@ -88,31 +76,12 @@ namespace Robust.Server.GameStates
_parallelMgr.AddAndInvokeParallelCountChanged(ResetParallelism);
_cfg.OnValueChanged(CVars.NetPVSCompressLevel, _ => ResetParallelism(), true);
// temporary command for debugging PVS bugs.
_conHost.RegisterCommand("print_pvs_ack", PrintPvsAckInfo);
}
private void PrintPvsAckInfo(IConsoleShell shell, string argstr, string[] args)
{
var ack = _pvs.PlayerData.Min(x => x.Value.LastReceivedAck);
var players = _pvs.PlayerData
.Where(x => x.Value.LastReceivedAck == ack)
.Select(x => x.Key)
.Select(x => $"{x.Name} ({_entityManager.ToPrettyString(x.AttachedEntity)})");
shell.WriteLine($@"Current tick: {_gameTiming.CurTick}
Stored oldest acked tick: {_lastOldestAck}
Deletion history size: {_pvs.EntityPVSCollection.GetDeletedIndices(GameTick.First)?.Count ?? 0}
Actual oldest: {ack}
Oldest acked clients: {string.Join(", ", players)}
");
_cfg.OnValueChanged(CVars.NetPvsCompressLevel, _ => ResetParallelism(), true);
}
private void ResetParallelism()
{
var compressLevel = _cfg.GetCVar(CVars.NetPVSCompressLevel);
var compressLevel = _cfg.GetCVar(CVars.NetPvsCompressLevel);
// The * 2 is because trusting .NET won't take more is what got this code into this mess in the first place.
_threadResourcesPool = new DefaultObjectPool<PvsThreadResources>(new PvsThreadResourcesObjectPolicy(compressLevel), _parallelMgr.ParallelProcessCount * 2);
}
@@ -141,12 +110,7 @@ Oldest acked clients: {string.Join(", ", players)}
internal sealed class PvsThreadResources
{
public ZStdCompressionContext CompressionContext;
public PvsThreadResources()
{
CompressionContext = new ZStdCompressionContext();
}
public ZStdCompressionContext CompressionContext = new();
~PvsThreadResources()
{
@@ -174,63 +138,18 @@ Oldest acked clients: {string.Join(", ", players)}
{
var players = _playerManager.Sessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
// Update client acks, which is used to figure out what data needs to be sent to clients
// This only needs SessionData which isn't touched during GetPVSData or ProcessCollections.
var ackJob = _pvs.ProcessQueuedAcks();
// Update entity positions in PVS chunks/collections
// TODO disable processing if culling is disabled? Need to check if toggling PVS breaks anything.
// TODO parallelize?
using (_usageHistogram.WithLabels("Update Collections").NewTimer())
{
_pvs.ProcessCollections();
}
// Figure out what chunks players can see and cache some chunk data.
PvsData? pvsData = null;
if (_pvs.CullingEnabled)
{
using var _ = _usageHistogram.WithLabels("Get Chunks").NewTimer();
pvsData = GetPVSData(players);
}
ackJob.WaitOne();
_pvs.BeforeSendState(players, _usageHistogram);
// Construct & send the game state to each player.
GameTick oldestAck;
using (_usageHistogram.WithLabels("Send States").NewTimer())
{
oldestAck = SendStates(players, pvsData);
}
var oldestAck = SendStates(players);
if (pvsData != null)
_pvs.ReturnToPool(pvsData.Value.PlayerChunks);
using (_usageHistogram.WithLabels("Clean Dirty").NewTimer())
{
_pvs.CleanupDirty(players);
}
if (oldestAck == GameTick.MaxValue)
{
// There were no connected players?
// In that case we just clear all deletion history.
_pvs.CullDeletionHistory(GameTick.MaxValue);
_lastOldestAck = GameTick.Zero;
return;
}
if (oldestAck == _lastOldestAck)
return;
_lastOldestAck = oldestAck;
using var __ = _usageHistogram.WithLabels("Cull History").NewTimer();
_pvs.CullDeletionHistory(oldestAck);
_pvs.AfterSendState(players, _usageHistogram, oldestAck, ref _lastOldestAck);
}
private GameTick SendStates(ICommonSession[] players, PvsData? pvsData)
private GameTick SendStates(ICommonSession[] players)
{
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
using var _ = _usageHistogram.WithLabels("Send States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
var oldestAckValue = GameTick.MaxValue.Value;
@@ -246,7 +165,7 @@ Oldest acked clients: {string.Join(", ", players)}
PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
if (i >= 0)
SendStateUpdate(i, resource, inputSystem, players[i], pvsData, ref oldestAckValue);
SendStateUpdate(resource, players[i], ref oldestAckValue);
else
_replay.Update();
@@ -263,133 +182,38 @@ Oldest acked clients: {string.Join(", ", players)}
return new GameTick(oldestAckValue);
}
private struct PvsData
{
public HashSet<int>[] PlayerChunks;
public EntityUid[][] ViewerEntities;
public RobustTree<NetEntity>?[] ChunkCache;
}
private PvsData? GetPVSData(ICommonSession[] players)
{
var chunks = _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
var chunksCount = chunks.Count;
var chunkCache = new RobustTree<NetEntity>?[chunksCount];
// Update the reused trees sequentially to avoid having to lock the dictionary per chunk.
var reuse = ArrayPool<bool>.Shared.Rent(chunksCount);
if (chunksCount > 0)
{
var chunkJob = new PvsChunkJob()
{
EntManager = _entityManager,
Pvs = _pvs,
ChunkCache = chunkCache,
Reuse = reuse,
Chunks = chunks,
};
_parallelMgr.ProcessNow(chunkJob, chunksCount);
}
_pvs.RegisterNewPreviousChunkTrees(chunks, chunkCache, reuse);
ArrayPool<bool>.Shared.Return(reuse);
return new PvsData()
{
PlayerChunks = _playerChunks,
ViewerEntities = _viewerEntities,
ChunkCache = chunkCache,
};
}
private void SendStateUpdate(int i,
private void SendStateUpdate(
PvsThreadResources resources,
InputSystem inputSystem,
ICommonSession session,
PvsData? pvsData,
ref uint oldestAckValue)
{
var channel = session.Channel;
var sessionData = _pvs.PlayerData[session];
var from = sessionData.RequestedFull ? GameTick.Zero : sessionData.LastReceivedAck;
List<NetEntity>? leftPvs = null;
List<EntityState>? entStates;
List<NetEntity>? deletions;
GameTick fromTick;
DebugTools.Assert(_pvs.CullingEnabled == (pvsData != null));
if (pvsData != null)
{
(entStates, deletions, leftPvs, fromTick) = _pvs.CalculateEntityStates(
session,
from,
_gameTiming.CurTick,
pvsData.Value.ChunkCache,
pvsData.Value.PlayerChunks[i],
pvsData.Value.ViewerEntities[i]);
}
else
{
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, from, _gameTiming.CurTick);
}
var playerStates = _playerManager.GetPlayerStates(fromTick);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(
fromTick,
_gameTiming.CurTick,
Math.Max(lastInputCommand, lastSystemMessage),
entStates,
playerStates,
deletions);
InterlockedHelper.Min(ref oldestAckValue, from.Value);
var data = _pvs.GetSessionData(session);
InterlockedHelper.Min(ref oldestAckValue, data.FromTick.Value);
// actually send the state
var stateUpdateMessage = new MsgState();
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
// If the state is too big we let Lidgren send it reliably. This is to avoid a situation where a state is so
// large that it (or part of it) consistently gets dropped. When we send reliably, we immediately update the
// ack so that the next state will not also be huge.
//
// We also do this if the client's last ack is too old. This helps prevent things like the entity deletion
// history from becoming too bloated if a bad client fails to send acks for whatever reason.
if (_gameTiming.CurTick.Value > from.Value + _pvs.ForceAckThreshold)
var stateUpdateMessage = new MsgState
{
stateUpdateMessage.ForceSendReliably = true;
#if FULL_RELEASE
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
if (sessionData.LastReceivedAck > GameTick.Zero && connectedTime > 1)
_logger.Warning($"Client {session} exceeded ack-tick threshold. Last ack: {sessionData.LastReceivedAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
#endif
}
State = data.State,
CompressionContext = resources.CompressionContext
};
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
_networkManager.ServerSendMessage(stateUpdateMessage, session.Channel);
data.ClearState();
if (stateUpdateMessage.ShouldSendReliably())
{
sessionData.LastReceivedAck = _gameTiming.CurTick;
data.LastReceivedAck = _gameTiming.CurTick;
lock (_pvs.PendingAcks)
{
_pvs.PendingAcks.Add(session);
}
}
// Send PVS detach / left-view messages separately and reliably. This is not resistant to packet loss, but
// unlike game state it doesn't really matter. This also significantly reduces the size of game state
// messages as PVS chunks get moved out of view.
if (leftPvs != null && leftPvs.Count > 0)
{
var pvsMessage = new MsgStateLeavePvs {Entities = leftPvs, Tick = _gameTiming.CurTick};
_networkManager.ServerSendMessage(pvsMessage, channel);
}
// TODO parallelize this with system processing.
// Before we do that we need to:
// - Defer player connection changes untill the start of the nxt PVS tick and this job has finished
// - Defer OnEntityDeleted in pvs system. Or refactor per-session entity data to be stored as arrays on metadaat component
_pvs.ProcessLeavePvs(data);
}
[EventSource(Name = "Robust.Pvs")]
@@ -421,46 +245,5 @@ Oldest acked clients: {string.Join(", ", players)}
}
}
}
#region Jobs
/// <summary>
/// Pre-calculates chunk indices (Robust Tree) to be re-used per-player later on.
/// </summary>
private record struct PvsChunkJob : IParallelRobustJob
{
public int BatchSize => 2;
public IEntityManager EntManager;
public PvsSystem Pvs;
public List<(int, IChunkIndexLocation)> Chunks;
public bool[] Reuse;
public RobustTree<NetEntity>?[] ChunkCache;
public void Execute(int index)
{
var (visMask, chunkIndexLocation) = Chunks[index];
Reuse[index] = Pvs.TryCalculateChunk(chunkIndexLocation, visMask, out var tree);
ChunkCache[index] = tree;
#if DEBUG
if (tree == null)
return;
// Each root nodes should simply be a map or a grid entity.
DebugTools.Assert(tree.RootNodes.Count == 1,
$"Root node count is {tree.RootNodes.Count} instead of 1.");
var nent = tree.RootNodes.FirstOrDefault();
var ent = EntManager.GetEntity(nent);
DebugTools.Assert(EntManager.EntityExists(ent), $"Root node does not exist. Node {ent}.");
DebugTools.Assert(EntManager.HasComponent<MapComponent>(ent)
|| EntManager.HasComponent<MapGridComponent>(ent));
#endif
}
}
#endregion
}
}

View File

@@ -1,9 +1,7 @@
using System;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
@@ -12,11 +10,10 @@ namespace Robust.Server.Replays;
internal sealed class ReplayRecordingManager : SharedReplayRecordingManager, IServerReplayRecordingManager
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
private GameTick _fromTick = GameTick.Zero;
private PvsSystem _pvs = default!;
private PvsSession _pvsSession = new(default!) { DisableCulling = true };
public override void Initialize()
{
@@ -47,16 +44,16 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager, ISe
return;
}
var (entStates, deletions, _) = _pvs.GetAllEntityStates(null, _fromTick, Timing.CurTick);
var playerStates = _player.GetPlayerStates(_fromTick);
var state = new GameState(_fromTick, Timing.CurTick, 0, entStates, playerStates, deletions);
_fromTick = Timing.CurTick;
Update(state);
_pvs.GetSessionData(_pvsSession);
Update(_pvsSession.State);
_pvsSession.ClearState();
_pvsSession.LastReceivedAck = Timing.CurTick;
}
protected override void Reset()
{
base.Reset();
_fromTick = GameTick.Zero;
_pvsSession.LastReceivedAck = GameTick.Zero;
_pvsSession.ClearState();
}
}

View File

@@ -183,12 +183,26 @@ namespace Robust.Shared
public static readonly CVarDef<bool> NetPVS =
CVarDef.Create("net.pvs", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// If false, this will run more parts of PVS synchronously. This will generally slow it down, can be useful
/// for collecting tick timing metrics.
/// </summary>
public static readonly CVarDef<bool> NetPvsAsync =
CVarDef.Create("net.pvs_async", true, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// View size to take for PVS calculations,
/// as the size of the sides of a square centered on the view points of clients.
/// </summary>
public static readonly CVarDef<float> NetMaxUpdateRange =
CVarDef.Create("net.maxupdaterange", 12.5f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
CVarDef.Create("net.pvs_range", 25f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Chunks whose centre is further than this distance away from a player's eye will contain fewer entities.
/// This has no effect if it is smaller than <see cref="NetMaxUpdateRange"/>
/// </summary>
public static readonly CVarDef<float> NetLowLodRange =
CVarDef.Create("net.low_lod_distance", 100f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Maximum allowed delay between the current tick and a client's last acknowledged tick before we send the
@@ -222,7 +236,7 @@ namespace Robust.Shared
/// <summary>
/// ZSTD compression level to use when compressing game states. Used by both networking and replays.
/// </summary>
public static readonly CVarDef<int> NetPVSCompressLevel =
public static readonly CVarDef<int> NetPvsCompressLevel =
CVarDef.Create("net.pvs_compress_level", 3, CVar.ARCHIVE);
/// <summary>

View File

@@ -128,7 +128,7 @@ public abstract partial class SharedContainerSystem
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
DebugTools.Assert(!transform.Anchored);
DebugTools.Assert(transform.LocalPosition == Vector2.Zero);
DebugTools.Assert(transform.LocalRotation == Angle.Zero);
DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero));
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide));
Dirty(container.Owner, container.Manager);

View File

@@ -4,9 +4,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.Containers;

View File

@@ -1,9 +1,6 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
using System;
namespace Robust.Shared.Containers;

View File

@@ -50,6 +50,11 @@ namespace Robust.Shared.GameObjects
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("visMask", customTypeSerializer:typeof(FlagSerializer<VisibilityMaskLayer>)), AutoNetworkedField]
public int VisibilityMask = DefaultVisibilityMask;
/// <summary>
/// Scales the PVS view range of this eye,
/// </summary>
[DataField] public float PvsScale = 1;
}
/// <summary>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -181,6 +182,16 @@ namespace Robust.Shared.GameObjects
public bool EntityInitializing => EntityLifeStage == EntityLifeStage.Initializing;
public bool EntityDeleted => EntityLifeStage >= EntityLifeStage.Deleted;
/// <summary>
/// The PVS chunk that this entity is currently stored on.
/// This should always be set properly if the entity is directly attached to a grid or map.
/// If it is null, it implies that either:
/// - The entity nested is somewhere in some chunk that has already been marked as dirty
/// - The entity is in nullspace
/// </summary>
[ViewVariables]
internal PvsChunkLocation? LastPvsLocation;
[Obsolete("Do not use from content")]
public override void ClearTicks()
{
@@ -211,5 +222,17 @@ namespace Robust.Shared.GameObjects
/// Used by clients to indicate that an entity has left their visible set.
/// </summary>
Detached = 1 << 2,
/// <summary>
/// If true, then this entity is considered a "high priority" entity and will be sent to players from further
/// away. Useful for things like light sources and occluders. Only works if the entity is directly parented to
/// a grid or map.
/// </summary>
PvsPriority = 1 << 3,
}
/// <summary>
/// Key struct for uniquely identifying a PVS chunk.
/// </summary>
internal readonly record struct PvsChunkLocation(EntityUid Uid, Vector2i Indices);
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -150,13 +151,14 @@ namespace Robust.Shared.GameObjects
var oldRotation = _localRotation;
_localRotation = value;
_entMan.Dirty(Owner, this);
var meta = _entMan.GetComponent<MetaDataComponent>(Owner);
_entMan.Dirty(Owner, this, meta);
MatricesDirty = true;
if (!Initialized)
return;
var moveEvent = new MoveEvent(Owner, Coordinates, Coordinates, oldRotation, _localRotation, this, _gameTiming.ApplyingState);
var moveEvent = new MoveEvent((Owner, this, meta), Coordinates, Coordinates, oldRotation, _localRotation, _gameTiming.ApplyingState);
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
}
@@ -335,13 +337,14 @@ namespace Robust.Shared.GameObjects
var oldGridPos = Coordinates;
_localPosition = value;
_entMan.Dirty(Owner, this);
var meta = _entMan.GetComponent<MetaDataComponent>(Owner);
_entMan.Dirty(Owner, this, meta);
MatricesDirty = true;
if (!Initialized)
return;
var moveEvent = new MoveEvent(Owner, oldGridPos, Coordinates, _localRotation, _localRotation, this, _gameTiming.ApplyingState);
var moveEvent = new MoveEvent((Owner, this, meta), oldGridPos, Coordinates, _localRotation, _localRotation, _gameTiming.ApplyingState);
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
}
@@ -600,33 +603,28 @@ namespace Robust.Shared.GameObjects
/// move events, subscribe to the <see cref="SharedTransformSystem.OnGlobalMoveEvent"/>.
/// </summary>
[ByRefEvent]
public readonly struct MoveEvent
public readonly struct MoveEvent(Entity<TransformComponent, MetaDataComponent> entity, EntityCoordinates oldPos,
EntityCoordinates newPos, Angle oldRotation, Angle newRotation, bool stateHandling = false)
{
public MoveEvent(EntityUid sender, EntityCoordinates oldPos, EntityCoordinates newPos, Angle oldRotation, Angle newRotation, TransformComponent component, bool stateHandling)
{
Sender = sender;
OldPosition = oldPos;
NewPosition = newPos;
OldRotation = oldRotation;
NewRotation = newRotation;
Component = component;
FromStateHandling = stateHandling;
}
public readonly Entity<TransformComponent, MetaDataComponent> Entity = entity;
public readonly EntityCoordinates OldPosition = oldPos;
public readonly EntityCoordinates NewPosition = newPos;
public readonly Angle OldRotation = oldRotation;
public readonly Angle NewRotation = newRotation;
public readonly EntityUid Sender;
public readonly EntityCoordinates OldPosition;
public readonly EntityCoordinates NewPosition;
public readonly Angle OldRotation;
public readonly Angle NewRotation;
public readonly TransformComponent Component;
public EntityUid Sender => Entity.Owner;
public TransformComponent Component => Entity.Comp1;
public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId;
/// <summary>
/// If true, this event was generated during component state handling. This means it can be ignored in some instances.
/// </summary>
[Obsolete("Check IGameTiming.ApplyingState")]
public readonly bool FromStateHandling;
public readonly bool FromStateHandling = stateHandling;
[Obsolete]
public MoveEvent(EntityUid uid, EntityCoordinates oldPos, EntityCoordinates newPos, Angle oldRot, Angle newRot, TransformComponent xform, bool state)
: this((uid, xform, default!), oldPos, newPos, oldRot, newRot)
{
}
}
public struct TransformChildrenEnumerator : IDisposable

View File

@@ -41,6 +41,8 @@ public record struct Entity<T>
owner = Owner;
comp = Comp;
}
public override int GetHashCode() => Owner.GetHashCode();
}
public record struct Entity<T1, T2>

View File

@@ -9,6 +9,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
@@ -44,6 +45,7 @@ namespace Robust.Shared.GameObjects
public EntityQuery<MetaDataComponent> MetaQuery;
public EntityQuery<TransformComponent> TransformQuery;
public EntityQuery<ActorComponent> _actorQuery;
#endregion Dependencies
@@ -204,6 +206,7 @@ namespace Robust.Shared.GameObjects
_containers = System<SharedContainerSystem>();
MetaQuery = GetEntityQuery<MetaDataComponent>();
TransformQuery = GetEntityQuery<TransformComponent>();
_actorQuery = GetEntityQuery<ActorComponent>();
}
public virtual void Shutdown()
@@ -494,7 +497,7 @@ namespace Robust.Shared.GameObjects
try
{
var ev = new EntityTerminatingEvent(uid);
var ev = new EntityTerminatingEvent((uid, metadata));
EventBus.RaiseLocalEvent(uid, ref ev, true);
}
catch (Exception e)
@@ -804,12 +807,16 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public virtual EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata = null)
{
if (!MetaQuery.Resolve(uid, ref metadata, false))
return new EntityStringRepresentation(uid, default, true);
public EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata)
=> ToPrettyString((uid, metadata));
return new EntityStringRepresentation(uid, metadata);
/// <inheritdoc />
public EntityStringRepresentation ToPrettyString(Entity<MetaDataComponent?> entity)
{
if (entity.Comp == null && !MetaQuery.Resolve(entity.Owner, ref entity.Comp, false))
return new EntityStringRepresentation(entity.Owner, default, true);
return new EntityStringRepresentation(entity.Owner, entity.Comp, _actorQuery.CompOrNull(entity));
}
/// <inheritdoc />

View File

@@ -23,8 +23,8 @@ public readonly record struct EntityStringRepresentation
{
}
public EntityStringRepresentation(EntityUid uid, MetaDataComponent meta)
: this(uid, meta.NetEntity, meta.EntityDeleted, meta.EntityName, meta.EntityPrototype?.ID)
public EntityStringRepresentation(EntityUid uid, MetaDataComponent meta, ActorComponent? actor = null)
: this(uid, meta.NetEntity, meta.EntityDeleted, meta.EntityName, meta.EntityPrototype?.ID, actor?.PlayerSession)
{
}

View File

@@ -386,12 +386,12 @@ public partial class EntitySystem
/// <inheritdoc cref="IEntityManager.ToPrettyString(EntityUid, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata)
=> EntityManager.ToPrettyString(uid, metadata);
=> EntityManager.ToPrettyString((uid, metadata));
/// <inheritdoc cref="IEntityManager.ToPrettyString(EntityUid, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityStringRepresentation ToPrettyString(EntityUid uid)
=> EntityManager.ToPrettyString(uid);
protected EntityStringRepresentation ToPrettyString(Entity<MetaDataComponent?> entity)
=> EntityManager.ToPrettyString(entity);
/// <inheritdoc cref="IEntityManager.ToPrettyString(EntityUid, MetaDataComponent?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -4,13 +4,8 @@ namespace Robust.Shared.GameObjects
/// The children of this entity are about to be deleted.
/// </summary>
[ByRefEvent]
public readonly struct EntityTerminatingEvent
public readonly struct EntityTerminatingEvent(Entity<MetaDataComponent> entity)
{
public readonly EntityUid Entity;
public EntityTerminatingEvent(EntityUid entity)
{
Entity = entity;
}
public readonly Entity<MetaDataComponent> Entity = entity;
}
}

View File

@@ -145,7 +145,12 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Returns a string representation of an entity with various information regarding it.
/// </summary>
EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata = null);
EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata);
/// <summary>
/// Returns a string representation of an entity with various information regarding it.
/// </summary>
EntityStringRepresentation ToPrettyString(Entity<MetaDataComponent?> uid);
/// <summary>
/// Returns a string representation of an entity with various information regarding it.

View File

@@ -48,6 +48,9 @@ public readonly struct NetEntity : IEquatable<NetEntity>, IComparable<NetEntity>
/// </summary>
public static NetEntity Parse(ReadOnlySpan<char> uid)
{
if (uid.Length == 0)
return default;
if (uid[0] != 'c')
return new NetEntity(int.Parse(uid));

View File

@@ -18,6 +18,7 @@ using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TerraFX.Interop.Windows;
namespace Robust.Shared.GameObjects;
@@ -669,7 +670,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!_physicsQuery.TryGetComponent(uid, out var body) || !body.CanCollide)
{
// TOOD optimize this. This function iterates UP through parents, while we are currently iterating down.
// TODO optimize this. This function iterates UP through parents, while we are currently iterating down.
var (coordinates, rotation) = _transform.GetMoverCoordinateRotation(uid, xform);
// TODO BROADPHASE PARENTING this just assumes local = world
@@ -716,6 +717,8 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!TryGetCurrentBroadphase(xform, out var broadphase))
return;
DebugTools.Assert(!HasComp<MapGridComponent>(uid));
DebugTools.Assert(!HasComp<MapComponent>(uid));
PhysicsMapComponent? physMap = null;
if (xform.Broadphase!.Value.PhysicsMap is { Valid: true } map && !_mapQuery.TryGetComponent(map, out physMap))
{

View File

@@ -123,13 +123,20 @@ public abstract class MetaDataSystem : EntitySystem
time += paused;
}
public void AddFlag(EntityUid uid, MetaDataFlags flags, MetaDataComponent? component = null)
public void SetFlag(Entity<MetaDataComponent?> entity, MetaDataFlags flags, bool enabled)
{
if (!_metaQuery.Resolve(uid, ref component)) return;
if (!_metaQuery.Resolve(entity, ref entity.Comp))
return;
component.Flags |= flags;
if (enabled)
entity.Comp.Flags |= flags;
else
RemoveFlag(entity, flags, entity.Comp);
}
public void AddFlag(EntityUid uid, MetaDataFlags flags, MetaDataComponent? comp = null)
=> SetFlag((uid, comp), flags, true);
/// <summary>
/// Attempts to remove the specific flag from metadata.
/// Other systems can choose not to allow the removal if it's still relevant.

View File

@@ -59,13 +59,13 @@ public abstract class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent
QueueTreeUpdate(uid, comp);
}
public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
{
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
return;
comp.Enabled = enabled;
Dirty(uid, comp);
Dirty(uid, comp, meta);
QueueTreeUpdate(uid, comp);
}
#endregion

View File

@@ -184,7 +184,7 @@ public abstract partial class SharedMapSystem
return;
// yipeee grids are being spontaneously moved to nullspace.
Log.Info($"Grid {ToPrettyString(uid, meta)} changed parent. Old parent: {ToPrettyString(args.OldPosition.EntityId)}. New parent: {xform.ParentUid}");
Log.Info($"Grid {ToPrettyString(uid, meta)} changed parent. Old parent: {ToPrettyString(args.OldPosition.EntityId)}. New parent: {ToPrettyString(xform.ParentUid)}");
if (xform.MapUid == null && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
Log.Error($"Grid {ToPrettyString(uid, meta)} was moved to nullspace! AAAAAAAAAAAAAAAAAAAAAAAAA! {Environment.StackTrace}");

View File

@@ -13,13 +13,19 @@ public abstract class SharedPointLightSystem : EntitySystem
public abstract bool RemoveLightDeferred(EntityUid uid);
public void SetCastShadows(EntityUid uid, bool value, SharedPointLightComponent? comp = null)
protected abstract void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta);
public void SetCastShadows(EntityUid uid, bool value, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || value == comp.CastShadows)
return;
comp.CastShadows = value;
Dirty(uid, comp);
if (!Resolve(uid, ref meta))
return;
Dirty(uid, comp, meta);
UpdatePriority(uid, comp, meta);
}
public void SetColor(EntityUid uid, Color value, SharedPointLightComponent? comp = null)
@@ -31,14 +37,18 @@ public abstract class SharedPointLightSystem : EntitySystem
Dirty(uid, comp);
}
public virtual void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
public virtual void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || enabled == comp.Enabled)
return;
comp.Enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(uid, comp);
if (!Resolve(uid, ref meta))
return;
Dirty(uid, comp, meta);
UpdatePriority(uid, comp, meta);
}
public void SetEnergy(EntityUid uid, float value, SharedPointLightComponent? comp = null)
@@ -50,13 +60,17 @@ public abstract class SharedPointLightSystem : EntitySystem
Dirty(uid, comp);
}
public virtual void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
public virtual void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(comp.Radius, radius))
return;
comp.Radius = radius;
Dirty(uid, comp);
if (!Resolve(uid, ref meta))
return;
Dirty(uid, comp, meta);
UpdatePriority(uid, comp, meta);
}
public void SetSoftness(EntityUid uid, float value, SharedPointLightComponent? comp = null)

View File

@@ -44,13 +44,12 @@ public abstract partial class SharedTransformSystem
SetGridId(uid, xform, newGridUid, xformQuery);
var reParent = new EntParentChangedMessage(uid, oldGridUid, xform.MapID, xform);
RaiseLocalEvent(uid, ref reParent, true);
// TODO: Ideally shouldn't need to call the moveevent
var movEevee = new MoveEvent(uid,
var meta = MetaData(uid);
var movEevee = new MoveEvent((uid, xform, meta),
new EntityCoordinates(oldGridUid, xform._localPosition),
new EntityCoordinates(newGridUid, xform._localPosition),
xform.LocalRotation,
xform.LocalRotation,
xform,
_gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref movEevee);
InvokeGlobalMoveEvent(ref movEevee);
@@ -58,7 +57,7 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert(xformQuery.GetComponent(oldGridUid).MapID == xformQuery.GetComponent(newGridUid).MapID);
DebugTools.Assert(xform._anchored);
Dirty(uid, xform);
Dirty(uid, xform, meta);
var ev = new ReAnchorEvent(uid, oldGridUid, newGridUid, tilePos, xform);
RaiseLocalEvent(uid, ref ev);
}
@@ -598,7 +597,7 @@ public abstract partial class SharedTransformSystem
if (xform.ParentUid == xform.MapUid)
DebugTools.Assert(xform.GridUid == null || xform.GridUid == uid || xform.GridUid == xform.MapUid);
#endif
var moveEvent = new MoveEvent(uid, oldPosition, newPosition, oldRotation, xform._localRotation, xform, _gameTiming.ApplyingState);
var moveEvent = new MoveEvent((uid, xform, meta), oldPosition, newPosition, oldRotation, xform._localRotation, _gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref moveEvent);
InvokeGlobalMoveEvent(ref moveEvent);
}
@@ -1142,13 +1141,14 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0);
Dirty(uid, xform);
var meta = MetaData(uid);
Dirty(uid, xform, meta);
xform.MatricesDirty = true;
if (!xform.Initialized)
return;
var moveEvent = new MoveEvent(uid, oldPosition, xform.Coordinates, oldRotation, rot, xform, _gameTiming.ApplyingState);
var moveEvent = new MoveEvent((uid, xform, meta), oldPosition, xform.Coordinates, oldRotation, rot, _gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref moveEvent);
InvokeGlobalMoveEvent(ref moveEvent);
}

View File

@@ -2,6 +2,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using System;
using System.Diagnostics;
using System.Linq;
using NetSerializer;
using Robust.Shared.Timing;
@@ -17,6 +18,8 @@ namespace Robust.Shared.GameStates
[field:NonSerialized]
public int PayloadSize { get; set; }
public bool ForceSendReliably;
/// <summary>
/// Constructor!
/// </summary>
@@ -44,5 +47,22 @@ namespace Robust.Shared.GameStates
public readonly NetListAsArray<EntityState> EntityStates;
public readonly NetListAsArray<SessionState> PlayerStates;
public readonly NetListAsArray<NetEntity> EntityDeletions;
/// <summary>
/// Clone the game state's collections. Required for integration tests, to avoid the server/client referencing
/// the same objects
/// </summary>
public GameState Clone()
{
// TODO integration test serialization.
return new(
FromSequence,
ToSequence,
LastProcessedInput,
EntityStates.Value.ToArray(),
PlayerStates.Value.Select(x=> x.Clone()).ToArray(),
EntityDeletions.Value.ToArray());
}
}
}

View File

@@ -142,6 +142,7 @@ namespace Robust.Shared.Map
void FindGridsIntersecting<TState>(MapId mapId, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = false, bool includeMap = true);
void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List<Entity<MapGridComponent>> state, bool approx = false, bool includeMap = true);
void FindGridsIntersecting(EntityUid map, Box2 worldAABB, ref List<Entity<MapGridComponent>> state, bool approx = false, bool includeMap = true);
void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = false, bool includeMap = true);

View File

@@ -21,11 +21,14 @@ internal partial class MapManager
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = false, bool includeMap = true)
{
if (!_mapEntities.TryGetValue(mapId, out var mapEnt) ||
!EntityManager.TryGetComponent<GridTreeComponent>(mapEnt, out var gridTree))
{
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldAABB, callback, approx, includeMap);
}
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = false, bool includeMap = true)
{
if (!EntityManager.TryGetComponent<GridTreeComponent>(mapEnt, out var gridTree))
return;
}
var state = (worldAABB, gridTree.Tree, callback, approx, this, _transformSystem);
@@ -46,25 +49,24 @@ internal partial class MapManager
}
return tuple.callback(data.Uid, data.Grid);
}, worldAABB);
}, worldAABB); ;
var mapUid = GetMapEntityId(mapId);
if (includeMap && EntityManager.TryGetComponent<MapGridComponent>(mapUid, out var grid))
if (includeMap && EntityManager.TryGetComponent<MapGridComponent>(mapEnt, out var grid))
{
callback(mapUid, grid);
callback(mapEnt, grid);
}
}
public void FindGridsIntersecting<TState>(MapId mapId, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = false, bool includeMap = true)
{
if (!_mapEntities.TryGetValue(mapId, out var mapEnt) ||
!EntityManager.TryGetComponent<GridTreeComponent>(mapEnt, out var gridTree))
{
return;
}
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldAABB, ref state, callback, approx, includeMap);
}
var mapUid = GetMapEntityId(mapId);
public void FindGridsIntersecting<TState>(EntityUid mapUid, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = false, bool includeMap = true)
{
if (!EntityManager.TryGetComponent<GridTreeComponent>(mapUid, out var gridTree))
return;
if (includeMap && EntityManager.TryGetComponent<MapGridComponent>(mapUid, out var grid))
{
@@ -99,7 +101,14 @@ internal partial class MapManager
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List<Entity<MapGridComponent>> state,
bool approx = false, bool includeMap = true)
{
FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid,
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldAABB, ref state, approx, includeMap);
}
public void FindGridsIntersecting(EntityUid map, Box2 worldAABB, ref List<Entity<MapGridComponent>> state,
bool approx = false, bool includeMap = true)
{
FindGridsIntersecting(map, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid,
ref List<Entity<MapGridComponent>> list) =>
{
list.Add((uid, grid));
@@ -107,12 +116,24 @@ internal partial class MapManager
}, approx, includeMap);
}
public void FindGridsIntersecting(EntityUid mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = false,
bool includeMap = true)
{
FindGridsIntersecting(mapId, worldBounds.CalcBoundingBox(), callback, approx, includeMap);
}
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = false,
bool includeMap = true)
{
FindGridsIntersecting(mapId, worldBounds.CalcBoundingBox(), callback, approx, includeMap);
}
public void FindGridsIntersecting<TState>(EntityUid mapId, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
bool approx = false, bool includeMap = true)
{
FindGridsIntersecting(mapId, worldBounds.CalcBoundingBox(), ref state, callback, approx, includeMap);
}
public void FindGridsIntersecting<TState>(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
bool approx = false, bool includeMap = true)
{
@@ -121,6 +142,13 @@ internal partial class MapManager
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> state,
bool approx = false, bool includeMap = true)
{
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldBounds, ref state, approx, includeMap);
}
public void FindGridsIntersecting(EntityUid mapId, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> state,
bool approx = false, bool includeMap = true)
{
FindGridsIntersecting(mapId, worldBounds, ref state, static (EntityUid uid, MapGridComponent grid,
ref List<Entity<MapGridComponent>> list) =>
@@ -185,6 +213,21 @@ internal partial class MapManager
EntityQuery<TransformComponent> xformQuery,
out EntityUid uid,
[NotNullWhen(true)] out MapGridComponent? grid)
{
if (_mapEntities.TryGetValue(mapId, out var map))
return TryFindGridAt(map, worldPos, xformQuery, out uid, out grid);
uid = default;
grid = null;
return false;
}
public bool TryFindGridAt(
EntityUid mapId,
Vector2 worldPos,
EntityQuery<TransformComponent> xformQuery,
out EntityUid uid,
[NotNullWhen(true)] out MapGridComponent? grid)
{
var rangeVec = new Vector2(0.2f, 0.2f);
@@ -229,11 +272,9 @@ internal partial class MapManager
return false;
}, approx: true);
var mapUid = GetMapEntityId(mapId);
if (state.grid == null && EntityManager.TryGetComponent<MapGridComponent>(mapUid, out var mapGrid))
if (state.grid == null && EntityManager.TryGetComponent<MapGridComponent>(mapId, out var mapGrid))
{
uid = mapUid;
uid = mapId;
grid = mapGrid;
return true;
}
@@ -246,11 +287,24 @@ internal partial class MapManager
/// <summary>
/// Attempts to find the map grid under the map location.
/// </summary>
public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid)
public bool TryFindGridAt(EntityUid mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid)
{
return TryFindGridAt(mapId, worldPos, _xformQuery, out uid, out grid);
}
/// <summary>
/// Attempts to find the map grid under the map location.
/// </summary>
public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid)
{
if (_mapEntities.TryGetValue(mapId, out var map))
return TryFindGridAt(map, worldPos, _xformQuery, out uid, out grid);
uid = default;
grid = null;
return false;
}
/// <summary>
/// Attempts to find the map grid under the map location.
/// </summary>

View File

@@ -94,8 +94,6 @@ namespace Robust.Shared.Network.Messages
MsgSize = buffer.LengthBytes;
}
public bool ForceSendReliably;
/// <summary>
/// Whether this state message is large enough to warrant being sent reliably.
/// This is only valid after
@@ -104,7 +102,7 @@ namespace Robust.Shared.Network.Messages
public bool ShouldSendReliably()
{
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
return ForceSendReliably || MsgSize > ReliableThreshold;
return State.ForceSendReliably || MsgSize > ReliableThreshold;
}
public override NetDeliveryMethod DeliveryMethod

View File

@@ -117,7 +117,7 @@ namespace Robust.Shared.Physics.Systems
{
moveBuffer[proxy] = worldAABB;
// If something is in our AABB then try grid traversal for it
_traversal.CheckTraverse(proxy.Entity, _xformQuery.GetComponent(proxy.Entity));
_traversal.CheckTraverse(proxy.Entity, Transform(proxy.Entity));
}
}

View File

@@ -114,8 +114,7 @@ public interface ISharedPlayerManager
bool HasPlayerData(NetUserId userId);
IEnumerable<SessionData> GetAllPlayerData();
List<SessionState>? GetPlayerStates(GameTick fromTick);
void GetPlayerStates(GameTick fromTick, List<SessionState> states);
void UpdateState(ICommonSession commonSession);
void RemoveSession(ICommonSession session, bool removeData = false);

View File

@@ -13,31 +13,16 @@ internal abstract partial class SharedPlayerManager
LastStateUpdate = Timing.CurTick;
}
public List<SessionState>? GetPlayerStates(GameTick fromTick)
public void GetPlayerStates(GameTick fromTick, List<SessionState> states)
{
states.Clear();
if (LastStateUpdate < fromTick)
{
return null;
}
return;
Lock.EnterReadLock();
try
states.EnsureCapacity(InternalSessions.Count);
foreach (var player in InternalSessions.Values)
{
#if FULL_RELEASE
return InternalSessions.Values
.Select(s => s.State)
.ToList();
#else
// Integration tests need to clone data before "sending" it to the client. Otherwise they reference the
// same object.
return InternalSessions.Values
.Select(s => s.State.Clone())
.ToList();
#endif
}
finally
{
Lock.ExitReadLock();
states.Add(player.State);
}
}

View File

@@ -66,7 +66,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
NetConf.OnValueChanged(CVars.ReplayMaxCompressedSize, (v) => _maxCompressedSize = v * 1024, true);
NetConf.OnValueChanged(CVars.ReplayMaxUncompressedSize, (v) => _maxUncompressedSize = v * 1024, true);
NetConf.OnValueChanged(CVars.ReplayTickBatchSize, (v) => _tickBatchSize = v * 1024, true);
NetConf.OnValueChanged(CVars.NetPVSCompressLevel, OnCompressionChanged);
NetConf.OnValueChanged(CVars.NetPvsCompressLevel, OnCompressionChanged);
}
public void Shutdown()
@@ -191,7 +191,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
var zip = new ZipArchive(file, ZipArchiveMode.Create);
var context = new ZStdCompressionContext();
context.SetParameter(ZSTD_cParameter.ZSTD_c_compressionLevel, NetConf.GetCVar(CVars.NetPVSCompressLevel));
context.SetParameter(ZSTD_cParameter.ZSTD_c_compressionLevel, NetConf.GetCVar(CVars.NetPvsCompressLevel));
var buffer = new MemoryStream(_tickBatchSize * 2);
TimeSpan? recordingEnd = null;

View File

@@ -249,10 +249,19 @@ namespace Robust.UnitTesting
{
DebugTools.Assert(IsServer);
// MsgState sending method depends on the size of the possible compressed buffer. But tests bypass buffer read/write.
if (message is MsgState stateMsg)
{
// MsgState sending method depends on the size of the possible compressed buffer. But tests bypass buffer read/write.
stateMsg._hasWritten = true;
// Need to duplicate the state to avoid the server & client storing references to the same collections.
stateMsg.State = stateMsg.State.Clone();
}
else if (message is MsgStateLeavePvs leaveMsg)
{
leaveMsg.Entities = leaveMsg.Entities.ShallowClone();
}
var channel = (IntegrationNetChannel) recipient;
channel.OtherChannel.TryWrite(new DataMessage(message, channel.RemoteUid));
}

View File

@@ -24,6 +24,7 @@ using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using InputSystem = Robust.Server.GameObjects.InputSystem;
using MapSystem = Robust.Server.GameObjects.MapSystem;
namespace Robust.UnitTesting
@@ -120,6 +121,8 @@ namespace Robust.UnitTesting
systems.LoadExtraSystemType<DebugRayDrawingSystem>();
systems.LoadExtraSystemType<PrototypeReloadSystem>();
systems.LoadExtraSystemType<DebugPhysicsSystem>();
systems.LoadExtraSystemType<InputSystem>();
systems.LoadExtraSystemType<PvsOverrideSystem>();
}
var entMan = deps.Resolve<IEntityManager>();

View File

@@ -221,8 +221,9 @@ namespace Robust.UnitTesting.Server
//Tier 2: Simulation
container.RegisterInstance<IConsoleHost>(new Mock<IConsoleHost>().Object); //Console is technically a frontend, we want to run headless
container.Register<IEntityManager, EntityManager>();
container.Register<EntityManager, EntityManager>();
container.Register<IEntityManager, ServerEntityManager>();
container.Register<IServerEntityNetworkManager, ServerEntityManager>();
container.Register<EntityManager, ServerEntityManager>();
container.Register<IMapManager, NetworkedMapManager>();
container.Register<INetworkedMapManager, NetworkedMapManager>();
container.Register<IMapManagerInternal, NetworkedMapManager>();
@@ -235,6 +236,7 @@ namespace Robust.UnitTesting.Server
container.Register<IAuthManager, AuthManager>();
container.Register<ITileDefinitionManager, TileDefinitionManager>();
container.Register<IParallelManager, TestingParallelManager>();
container.Register<IParallelManagerInternal, TestingParallelManager>();
// Needed for grid fixture debugging.
container.Register<IConGroupController, ConGroupController>();
@@ -285,6 +287,7 @@ namespace Robust.UnitTesting.Server
compFactory.RegisterClass<OccluderTreeComponent>();
compFactory.RegisterClass<Gravity2DComponent>();
compFactory.RegisterClass<CollideOnAnchorComponent>();
compFactory.RegisterClass<ActorComponent>();
_regDelegate?.Invoke(compFactory);
@@ -311,6 +314,8 @@ namespace Robust.UnitTesting.Server
entitySystemMan.LoadExtraSystemType<EntityLookupSystem>();
entitySystemMan.LoadExtraSystemType<ServerMetaDataSystem>();
entitySystemMan.LoadExtraSystemType<PvsSystem>();
entitySystemMan.LoadExtraSystemType<InputSystem>();
entitySystemMan.LoadExtraSystemType<PvsOverrideSystem>();
_systemDelegate?.Invoke(entitySystemMan);

View File

@@ -90,8 +90,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
var container = sContainerSys.EnsureContainer<Container>(entityUid, "dummy");
Assert.That(sContainerSys.Insert(itemUid, container));
// Move item out of PVS so that it doesn't get sent to the client
sEntManager.GetComponent<TransformComponent>(itemUid).LocalPosition = new Vector2(100000, 0);
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<VisibilitySystem>().AddLayer(itemUid, 10 );
});
// Needs minimum 4 to sync to client because buffer size is 3
@@ -118,8 +118,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
await server.WaitAssertion(() =>
{
// Move item into PVS so it gets sent to the client
sEntManager.GetComponent<TransformComponent>(itemUid).LocalPosition = new Vector2(0, 0);
// Modify visibility layer so it now gets sent to the client
sEntManager.System<VisibilitySystem>().RemoveLayer(itemUid, 10 );
});
await server.WaitRunTicks(1);
@@ -218,8 +218,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
var container = sContainerSys.GetContainer(sEntityUid, "dummy");
sContainerSys.Insert(sItemUid, container);
// Move item out of PVS so that it doesn't get sent to the client
sEntManager.GetComponent<TransformComponent>(sItemUid).LocalPosition = new Vector2(100000, 0);
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<VisibilitySystem>().AddLayer(sItemUid, 10 );
});
await server.WaitRunTicks(1);

View File

@@ -105,13 +105,13 @@ namespace Robust.UnitTesting.Shared.Map
public void NoParent_OffsetZero()
{
var mapManager = IoCManager.Resolve<IMapManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var uid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var xform = entMan.GetComponent<TransformComponent>(uid);
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
var mapId = mapManager.CreateMap();
var mapEntity = mapManager.CreateNewMapEntity(mapId);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEntity).Coordinates.Position, Is.EqualTo(Vector2.Zero));
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEntity).LocalPosition = Vector2.One;
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEntity).Coordinates.Position, Is.EqualTo(Vector2.Zero));
xform.LocalPosition = Vector2.One;
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
}
[Test]

View File

@@ -7,7 +7,7 @@ namespace Robust.UnitTesting;
/// <summary>
/// Only allows 1 parallel process for testing purposes.
/// </summary>j
public sealed class TestingParallelManager : IParallelManager
public sealed class TestingParallelManager : IParallelManagerInternal
{
public event Action? ParallelCountChanged;
public int ParallelProcessCount => 1;
@@ -52,4 +52,8 @@ public sealed class TestingParallelManager : IParallelManager
ev.Set();
return ev.WaitHandle;
}
public void Initialize()
{
}
}