mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
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:
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
277
Robust.Server/GameStates/PvsChunk.cs
Normal file
277
Robust.Server/GameStates/PvsChunk.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
192
Robust.Server/GameStates/PvsData.cs
Normal file
192
Robust.Server/GameStates/PvsData.cs
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
297
Robust.Server/GameStates/PvsSystem.Chunks.cs
Normal file
297
Robust.Server/GameStates/PvsSystem.Chunks.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
Robust.Server/GameStates/PvsSystem.Entity.cs
Normal file
114
Robust.Server/GameStates/PvsSystem.Entity.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
190
Robust.Server/GameStates/PvsSystem.Overrides.cs
Normal file
190
Robust.Server/GameStates/PvsSystem.Overrides.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Robust.Server/GameStates/PvsSystem.Pooling.cs
Normal file
39
Robust.Server/GameStates/PvsSystem.Pooling.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Robust.Server/GameStates/PvsSystem.Session.cs
Normal file
125
Robust.Server/GameStates/PvsSystem.Session.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user