Generalize component trees (#3598)

This commit is contained in:
Leon Friedrich
2022-12-27 15:33:46 +13:00
committed by GitHub
parent ace8500240
commit 054a908efd
35 changed files with 1123 additions and 1196 deletions

View File

@@ -35,7 +35,11 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* ClientOccluderComponent has been removed & OccluderComponent component functions have been moved to the occluder system.
* The OccluderDirectionsEvent namespace and properties have changed.
* The rendering and occluder trees have been refactored to use generic render tree systems.
* Several pointlight and occluder component properties now need to be set via system methods.
### New features

View File

@@ -0,0 +1,12 @@
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Robust.Client.ComponentTrees;
[RegisterComponent]
public sealed class LightTreeComponent: Component, IComponentTreeComponent<PointLightComponent>
{
public DynamicTree<ComponentTreeEntry<PointLightComponent>> Tree { get; set; } = default!;
}

View File

@@ -0,0 +1,38 @@
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.Client.ComponentTrees;
public sealed class LightTreeSystem : ComponentTreeSystem<LightTreeComponent, PointLightComponent>
{
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
protected override bool DoTickUpdate => false;
protected override bool Recursive => true;
protected override int InitialCapacity => 128;
protected override Box2 ExtractAabb(in ComponentTreeEntry<PointLightComponent> entry, Vector2 pos, Angle rot)
{
// Really we should be rotating the light offset by the relative rotation. But I assume the light offset will
// always be relatively small, so fuck it, this is probably faster than having to compute the angle every time.
var radius = entry.Component.Radius + entry.Component.Offset.Length;
return new Box2(pos - radius, pos + radius);
}
protected override Box2 ExtractAabb(in ComponentTreeEntry<PointLightComponent> entry)
{
if (entry.Component.TreeUid == null)
return default;
var pos = XformSystem.GetRelativePosition(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
return ExtractAabb(in entry, pos, default);
}
#endregion
}

View File

@@ -0,0 +1,12 @@
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Robust.Client.ComponentTrees;
[RegisterComponent]
public sealed class SpriteTreeComponent: Component, IComponentTreeComponent<SpriteComponent>
{
public DynamicTree<ComponentTreeEntry<SpriteComponent>> Tree { get; set; } = default!;
}

View File

@@ -0,0 +1,40 @@
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.Client.ComponentTrees;
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
}
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
=> QueueTreeUpdate(uid, component, args.Xform);
// TODO remove this when finally ECSing sprite components
[ByRefEvent]
internal readonly struct QueueSpriteTreeUpdateEvent
{
public readonly TransformComponent Xform;
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
{
Xform = xform;
}
}
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
protected override bool DoTickUpdate => false;
protected override bool Recursive => true;
protected override int InitialCapacity => 1024;
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
#endregion
}

View File

@@ -1,3 +1,4 @@
using Robust.Client.ComponentTrees;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -22,8 +23,6 @@ namespace Robust.Client.GameObjects
RegisterClass<ContainerManagerComponent>();
RegisterClass<InputComponent>();
RegisterClass<SpriteComponent>();
RegisterClass<ClientOccluderComponent>();
RegisterClass<OccluderTreeComponent>();
RegisterClass<EyeComponent>();
RegisterClass<AnimationPlayerComponent>();
RegisterClass<TimerComponent>();

View File

@@ -1,149 +0,0 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(OccluderComponent))]
public sealed class ClientOccluderComponent : OccluderComponent
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private (EntityUid, Vector2i) _lastPosition;
[ViewVariables] internal OccluderDir Occluding { get; private set; }
[ViewVariables] internal uint UpdateGeneration { get; set; }
public override bool Enabled
{
get => base.Enabled;
set
{
base.Enabled = value;
SendDirty();
}
}
protected override void Startup()
{
base.Startup();
if (_entityManager.GetComponent<TransformComponent>(Owner).Anchored)
{
AnchorStateChanged();
}
}
public void AnchorStateChanged()
{
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
SendDirty(xform);
if(!xform.Anchored)
return;
var gridId = xform.GridUid ?? throw new InvalidOperationException("Anchored without a grid");
var grid = _mapManager.GetGrid(gridId);
_lastPosition = (gridId, grid.TileIndicesFor(xform.Coordinates));
}
protected override void Shutdown()
{
base.Shutdown();
SendDirty();
}
private void SendDirty(TransformComponent? xform = null)
{
xform ??= _entityManager.GetComponent<TransformComponent>(Owner);
if (xform.Anchored)
{
_entityManager.EventBus.RaiseEvent(EventSource.Local,
new OccluderDirtyEvent(Owner, _lastPosition));
}
}
internal void Update()
{
Occluding = OccluderDir.None;
if (Deleted)
return;
// Content may want to override the default behavior for occlusion.
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
var ev = new OccluderDirectionsEvent
{
Component = xform,
};
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev, true);
if (ev.Handled)
{
Occluding = ev.Directions;
return;
}
if (!xform.Anchored)
return;
var grid = _mapManager.GetGrid(xform.GridUid ?? throw new InvalidOperationException("Anchored without a grid"));
var position = xform.Coordinates;
void CheckDir(Direction dir, OccluderDir oclDir)
{
foreach (var neighbor in grid.GetInDir(position, dir))
{
if (_entityManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
{
Occluding |= oclDir;
break;
}
}
}
var angle = xform.LocalRotation;
var dirRolling = angle.GetCardinalDir();
// dirRolling starts at effective south
CheckDir(dirRolling, OccluderDir.South);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.West);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.North);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.East);
}
}
[Flags]
public enum OccluderDir : byte
{
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
/// <summary>
/// Raised by occluders when trying to get occlusion directions.
/// </summary>
[ByRefEvent]
public struct OccluderDirectionsEvent
{
public bool Handled = false;
public OccluderDir Directions = OccluderDir.None;
public TransformComponent Component = default!;
public OccluderDirectionsEvent() {}
}
}

View File

@@ -1,9 +1,10 @@
using Robust.Client.Graphics;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -11,11 +12,14 @@ namespace Robust.Client.GameObjects
{
[RegisterComponent]
[ComponentReference(typeof(SharedPointLightComponent))]
public sealed class PointLightComponent : SharedPointLightComponent
public sealed class PointLightComponent : SharedPointLightComponent, IComponentTreeEntry<PointLightComponent>
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public EntityUid? TreeUid { get; set; }
internal bool TreeUpdateQueued { get; set; }
public DynamicTree<ComponentTreeEntry<PointLightComponent>>? Tree { get; set; }
public bool AddToTree => Enabled && !ContainerOccluded;
public bool TreeUpdateQueued { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
@@ -25,33 +29,8 @@ namespace Robust.Client.GameObjects
set => base.Color = value;
}
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public override bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
base.Enabled = value;
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
}
}
[ViewVariables(VVAccess.ReadWrite)]
public bool ContainerOccluded
{
get => _containerOccluded;
set
{
if (_containerOccluded == value) return;
_containerOccluded = value;
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
}
}
private bool _containerOccluded;
[Access(typeof(PointLightSystem))]
public bool ContainerOccluded;
/// <summary>
/// Determines if the light mask should automatically rotate with the entity. (like a flashlight)
@@ -96,55 +75,11 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)]
public Texture? Mask { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool VisibleNested
{
get => _visibleNested;
set => _visibleNested = value;
}
[DataField("nestedvisible")]
private bool _visibleNested = true;
[DataField("autoRot")]
private bool _maskAutoRotate;
private Angle _rotation;
[DataField("mask")]
internal string? _maskPath;
/// <summary>
/// Radius, in meters.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public override float Radius
{
get => _radius;
set
{
if (MathHelper.CloseToPercent(value, _radius)) return;
base.Radius = value;
_entityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
}
}
[ViewVariables]
internal RenderingTreeComponent? RenderTree { get; set; }
}
public sealed class PointLightRadiusChangedEvent : EntityEventArgs
{
public PointLightComponent PointLightComponent { get; }
public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent)
{
PointLightComponent = pointLightComponent;
}
}
public sealed class PointLightUpdateEvent : EntityEventArgs
{
}
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
@@ -22,10 +21,9 @@ namespace Robust.Client.GameObjects
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly RenderingTreeSystem _renderingTree = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -41,7 +39,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(_renderingTree, _eye, _entityManager);
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -60,14 +58,12 @@ namespace Robust.Client.GameObjects
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly IEyeManager _eyeManager;
private readonly IEntityManager _entityManager;
private RenderingTreeSystem _renderTree;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(RenderingTreeSystem renderTree, IEyeManager eyeManager, IEntityManager entityManager)
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
{
_renderTree = renderTree;
_eyeManager = eyeManager;
_entityManager = entityManager;
}
@@ -77,23 +73,18 @@ namespace Robust.Client.GameObjects
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var comp in _renderTree.GetRenderTrees(currentMap, viewport))
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
{
var localAABB = _entityManager.GetComponent<TransformComponent>(comp.Owner).InvWorldMatrix.TransformBox(viewport);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
foreach (var (sprite, xform) in comp.SpriteTree.QueryAabb(localAABB))
{
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
}
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
}
}
}

View File

@@ -9,11 +9,13 @@ using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
@@ -21,7 +23,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using TerraFX.Interop.Windows;
using static Robust.Client.ComponentTrees.SpriteTreeSystem;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using RSIDirection = Robust.Client.Graphics.RSI.State.Direction;
@@ -30,7 +32,7 @@ namespace Robust.Client.GameObjects
[ComponentReference(typeof(SharedSpriteComponent))]
[ComponentReference(typeof(ISpriteComponent))]
public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent,
IComponentDebug, ISerializationHooks
IComponentDebug, ISerializationHooks, IComponentTreeEntry<SpriteComponent>
{
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
@@ -149,7 +151,13 @@ namespace Robust.Client.GameObjects
}
[ViewVariables]
internal RenderingTreeComponent? RenderTree { get; set; } = null;
public DynamicTree<ComponentTreeEntry<SpriteComponent>>? Tree { get; set; }
public EntityUid? TreeUid { get; set; }
public bool AddToTree => Visible && !ContainerOccluded && Layers.Count > 0;
public bool TreeUpdateQueued { get; set; }
[DataField("layerDatums")]
private List<PrototypeLayerData> LayerDatums
@@ -253,10 +261,7 @@ namespace Robust.Client.GameObjects
public Box2 Bounds => _bounds;
[ViewVariables(VVAccess.ReadWrite)]
public bool TreeUpdateQueued { get; set; }
[ViewVariables(VVAccess.ReadWrite)] private bool _inertUpdateQueued;
[ViewVariables(VVAccess.ReadWrite)] internal bool _inertUpdateQueued;
/// <summary>
/// Shader instance to use when drawing the final sprite to the world.
@@ -1395,22 +1400,22 @@ namespace Robust.Client.GameObjects
private void QueueUpdateRenderTree()
{
if (TreeUpdateQueued || Owner == default || entities?.EventBus == null)
if (TreeUpdateQueued || entities?.EventBus == null)
return;
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
TreeUpdateQueued = true;
entities.EventBus.RaiseLocalEvent(Owner, new UpdateSpriteTreeEvent());
var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent<TransformComponent>(Owner));
entities.EventBus.RaiseComponentEvent(this, ref ev);
}
private void QueueUpdateIsInert()
{
if (_inertUpdateQueued || Owner == default || entities?.EventBus == null)
if (_inertUpdateQueued || entities?.EventBus == null)
return;
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
_inertUpdateQueued = true;
entities.EventBus?.RaiseEvent(EventSource.Local, new SpriteUpdateInertEvent {Sprite = this});
var ev = new SpriteUpdateInertEvent();
entities.EventBus.RaiseComponentEvent(this, ref ev);
}
internal void DoUpdateIsInert()
@@ -2214,14 +2219,9 @@ namespace Robust.Client.GameObjects
}
}
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
internal sealed class UpdateSpriteTreeEvent : EntityEventArgs
{
}
[ByRefEvent]
internal struct SpriteUpdateInertEvent
{
public SpriteComponent Sprite;
}
}

View File

@@ -1,12 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Robust.Client.GameObjects
{
[RegisterComponent]
public sealed class RenderingTreeComponent : Component
{
internal DynamicTree<ComponentTreeEntry<SpriteComponent>> SpriteTree { get; set; } = default!;
internal DynamicTree<ComponentTreeEntry<PointLightComponent>> LightTree { get; set; } = default!;
}
}

View File

@@ -1,136 +1,255 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Physics;
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
namespace Robust.Client.GameObjects;
// NOTE: this class handles both snap grid updates of occluders, as well as occluder tree updates (via its parent).
// This seems like it's doing somewhat double work because it already has an update queue for occluders but...
// See the thing is the snap grid stuff was coded earlier
// and technically it only cares about changes in the entity's SNAP GRID position.
// Whereas the tree stuff is precise.
// Also I just realized this and I cba to refactor this again.
[UsedImplicitly]
internal sealed class ClientOccluderSystem : OccluderSystem
{
// NOTE: this class handles both snap grid updates of occluders, as well as occluder tree updates (via its parent).
// This seems like it's doing somewhat double work because it already has an update queue for occluders but...
// See the thing is the snap grid stuff was coded earlier
// and technically it only cares about changes in the entity's SNAP GRID position.
// Whereas the tree stuff is precise.
// Also I just realized this and I cba to refactor this again.
[UsedImplicitly]
internal sealed class ClientOccluderSystem : OccluderSystem
private readonly HashSet<EntityUid> _dirtyEntities = new();
/// <inheritdoc />
public override void Initialize()
{
[Dependency] private readonly IMapManager _mapManager = default!;
base.Initialize();
private readonly Queue<EntityUid> _dirtyEntities = new();
SubscribeLocalEvent<OccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<OccluderComponent, ReAnchorEvent>(OnReAnchor);
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(OnShutdown);
}
private uint _updateGeneration;
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
{
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
return;
/// <inheritdoc />
public override void Initialize()
comp.Enabled = enabled;
Dirty(comp);
var xform = Transform(uid);
QueueTreeUpdate(uid, comp, xform);
QueueOccludedDirectionUpdate(uid, comp, xform);
}
private void OnShutdown(EntityUid uid, OccluderComponent comp, ComponentShutdown args)
{
if (!Terminating(uid))
QueueOccludedDirectionUpdate(uid, comp);
}
protected override void OnCompStartup(EntityUid uid, OccluderComponent comp, ComponentStartup args)
{
base.OnCompStartup(uid, comp, args);
AnchorStateChanged(uid, comp, Transform(uid));
}
public void AnchorStateChanged(EntityUid uid, OccluderComponent comp, TransformComponent xform)
{
QueueOccludedDirectionUpdate(uid, comp, xform);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (_dirtyEntities.Count == 0)
return;
var query = GetEntityQuery<OccluderComponent>();
var xforms = GetEntityQuery<TransformComponent>();
var grids = GetEntityQuery<MapGridComponent>();
try
{
base.Initialize();
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<OccluderDirtyEvent>(OnOccluderDirty);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (_dirtyEntities.Count == 0)
foreach (var entity in _dirtyEntities)
{
if (query.TryGetComponent(entity, out var occluder))
UpdateOccluder(entity, occluder, query, xforms, grids);
}
}
finally
{
_dirtyEntities.Clear();
}
}
private void OnAnchorChanged(EntityUid uid, OccluderComponent comp, ref AnchorStateChangedEvent args)
{
AnchorStateChanged(uid, comp, args.Transform);
}
private void OnReAnchor(EntityUid uid, OccluderComponent comp, ref ReAnchorEvent args)
{
AnchorStateChanged(uid, comp, args.Xform);
}
private void QueueOccludedDirectionUpdate(EntityUid sender, OccluderComponent occluder, TransformComponent? xform = null)
{
if (!Resolve(sender, ref xform))
return;
occluder.Occluding = OccluderDir.None;
var query = GetEntityQuery<OccluderComponent>();
Vector2i pos;
EntityUid gridId;
MapGridComponent? grid;
if (occluder.Enabled && xform.Anchored && TryComp(xform.GridUid, out grid))
{
pos = grid.TileIndicesFor(xform.Coordinates);
_dirtyEntities.Add(sender);
}
else if (occluder.LastPosition != null)
{
(gridId, pos) = occluder.LastPosition.Value;
occluder.LastPosition = null;
if (!TryComp(gridId, out grid))
return;
}
_updateGeneration += 1;
while (_dirtyEntities.TryDequeue(out var entity))
{
if (EntityManager.EntityExists(entity)
&& EntityManager.TryGetComponent(entity, out ClientOccluderComponent? occluder)
&& occluder.UpdateGeneration != _updateGeneration)
{
occluder.Update();
occluder.UpdateGeneration = _updateGeneration;
}
}
}
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
else
{
component.AnchorStateChanged();
return;
}
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), query);
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), query);
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), query);
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), query);
}
private void DirtyNeighbours(AnchoredEntitiesEnumerator enumerator, EntityQuery<OccluderComponent> occluderQuery)
{
while (enumerator.MoveNext(out var entity))
{
component.AnchorStateChanged();
}
private void OnOccluderDirty(OccluderDirtyEvent ev)
{
var sender = ev.Sender;
MapGridComponent? grid;
var occluderQuery = GetEntityQuery<ClientOccluderComponent>();
if (EntityManager.EntityExists(sender) &&
occluderQuery.HasComponent(sender))
if (occluderQuery.TryGetComponent(entity.Value, out var occluder))
{
var xform = EntityManager.GetComponent<TransformComponent>(sender);
if (!_mapManager.TryGetGrid(xform.GridUid, out grid))
return;
var coords = xform.Coordinates;
var localGrid = grid.TileIndicesFor(coords);
_dirtyEntities.Enqueue(sender);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, 1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, -1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(1, 0)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(-1, 0)), occluderQuery);
}
// Entity is no longer valid, update around the last position it was at.
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out grid))
{
var pos = ev.LastPosition.Value.pos;
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), occluderQuery);
_dirtyEntities.Add(entity.Value);
occluder.Occluding = OccluderDir.None;
}
}
}
private void AddValidEntities(AnchoredEntitiesEnumerator enumerator, EntityQuery<ClientOccluderComponent> occluderQuery)
private void UpdateOccluder(EntityUid uid,
OccluderComponent occluder,
EntityQuery<OccluderComponent> occluders,
EntityQuery<TransformComponent> xforms,
EntityQuery<MapGridComponent> grids)
{
// Content may want to override the default behavior for occlusion.
// Apparently OD needs this?
{
while (enumerator.MoveNext(out var entity))
{
if (!occluderQuery.HasComponent(entity.Value)) continue;
var ev = new OccluderDirectionsEvent(uid, occluder);
RaiseLocalEvent(uid, ref ev, true);
_dirtyEntities.Enqueue(entity.Value);
}
if (ev.Handled)
return;
}
if (!occluder.Enabled)
{
DebugTools.Assert(occluder.Occluding == OccluderDir.None);
DebugTools.Assert(occluder.LastPosition == null);
return;
}
var xform = xforms.GetComponent(uid);
if (!xform.Anchored || !grids.TryGetComponent(xform.GridUid, out var grid))
{
DebugTools.Assert(occluder.Occluding == OccluderDir.None);
DebugTools.Assert(occluder.LastPosition == null);
return;
}
var tile = grid.TileIndicesFor(xform.Coordinates);
DebugTools.Assert(occluder.LastPosition == null
|| occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
occluder.LastPosition = (xform.GridUid.Value, tile);
// dir starts at the relative effective south direction;
var dir = xform.LocalRotation.GetCardinalDir();
CheckDir(dir, OccluderDir.South, tile, occluder, grid, occluders, xforms);
dir = dir.GetClockwise90Degrees();
CheckDir(dir, OccluderDir.West, tile, occluder, grid, occluders, xforms);
dir = dir.GetClockwise90Degrees();
CheckDir(dir, OccluderDir.North, tile, occluder, grid, occluders, xforms);
dir = dir.GetClockwise90Degrees();
CheckDir(dir, OccluderDir.East, tile, occluder, grid, occluders, xforms);
}
private void CheckDir(
Direction dir,
OccluderDir occDir,
Vector2i tile,
OccluderComponent occluder,
MapGridComponent grid,
EntityQuery<OccluderComponent> query,
EntityQuery<TransformComponent> xforms)
{
if ((occluder.Occluding & occDir) != 0)
return;
foreach (var neighbor in grid.GetAnchoredEntities(tile.Offset(dir)))
{
if (!query.TryGetComponent(neighbor, out var otherOccluder) || !otherOccluder.Enabled)
continue;
occluder.Occluding |= occDir;
// while we are here, also set the occluder flag for the other entity;
var otherXform = xforms.GetComponent(neighbor);
DebugTools.Assert(otherXform.Anchored);
var rot = -otherXform.LocalRotation;
var otherOcDir = FromDirection(rot.RotateDir(dir.GetOpposite()));
otherOccluder.Occluding |= otherOcDir;
}
}
public static OccluderDir FromDirection(Direction dir)
{
return dir switch
{
Direction.South => OccluderDir.South,
Direction.North => OccluderDir.North,
Direction.East => OccluderDir.East,
Direction.West => OccluderDir.West,
_ => throw new ArgumentException($"Invalid dir: {dir}.")
};
}
/// <summary>
/// Event raised by a <see cref="ClientOccluderComponent"/> when it needs to be recalculated.
/// Raised by occluders when trying to get occlusion directions.
/// </summary>
internal sealed class OccluderDirtyEvent : EntityEventArgs
[ByRefEvent]
public struct OccluderDirectionsEvent
{
public OccluderDirtyEvent(EntityUid sender, (EntityUid grid, Vector2i pos)? lastPosition)
{
LastPosition = lastPosition;
Sender = sender;
}
public bool Handled = false;
public readonly EntityUid Sender = default!;
public readonly OccluderComponent Occluder = default!;
public (EntityUid grid, Vector2i pos)? LastPosition { get; }
public EntityUid Sender { get; }
public OccluderDirectionsEvent(EntityUid sender, OccluderComponent occluder)
{
Sender = sender;
Occluder = occluder;
}
}
}

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
[Dependency] private readonly PointLightSystem _lightSys = default!;
private readonly HashSet<EntityUid> _updateQueue = new();
@@ -289,9 +290,7 @@ namespace Robust.Client.GameObjects
}
if (pointQuery.TryGetComponent(entity, out var light))
{
light.ContainerOccluded = lightOccluded;
}
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
var childEnumerator = xform.ChildEnumerator;

View File

@@ -1,4 +1,5 @@
#if DEBUG
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
@@ -28,7 +29,7 @@ namespace Robust.Client.GameObjects
EntitySystem.Get<EntityLookupSystem>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>(),
Get<RenderingTreeSystem>());
Get<LightTreeSystem>());
overlayManager.AddOverlay(_lightOverlay);
}
@@ -48,16 +49,16 @@ namespace Robust.Client.GameObjects
private IEyeManager _eyeManager;
private IMapManager _mapManager;
private RenderingTreeSystem _tree;
private LightTreeSystem _trees;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, RenderingTreeSystem tree)
public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, LightTreeSystem trees)
{
_lookup = lookup;
_eyeManager = eyeManager;
_mapManager = mapManager;
_tree = tree;
_trees = trees;
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -65,9 +66,9 @@ namespace Robust.Client.GameObjects
var map = _eyeManager.CurrentMap;
if (map == MapId.Nullspace) return;
foreach (var tree in _tree.GetRenderTrees(map, args.WorldBounds))
foreach (var treeComp in _trees.GetIntersectingTrees(map, args.WorldBounds))
{
foreach (var (light, xform) in tree.LightTree)
foreach (var (light, xform) in treeComp.Tree)
{
var aabb = _lookup.GetWorldAABB(light.Owner, xform);
if (!aabb.Intersects(args.WorldAABB)) continue;

View File

@@ -1,34 +1,25 @@
using Robust.Client.ComponentTrees;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
public sealed class PointLightSystem : SharedPointLightSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly RenderingTreeSystem _renderingTreeSystem = default!;
[Dependency] private readonly LightTreeSystem _lightTree = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<PointLightComponent, ComponentRemove>(HandleRemove);
}
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
{
UpdateMask(component);
RaiseLocalEvent(uid, new PointLightUpdateEvent(), true);
}
private void HandleRemove(EntityUid uid, PointLightComponent component, ComponentRemove args)
{
if (Transform(uid).MapID != MapId.Nullspace)
{
_renderingTreeSystem.ClearLight(component);
}
}
internal void UpdateMask(PointLightComponent component)
@@ -38,5 +29,46 @@ namespace Robust.Client.GameObjects
else
component.Mask = null;
}
#region Setters
public void SetContainerOccluded(EntityUid uid, bool occluded, PointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || occluded == comp.ContainerOccluded)
return;
comp.ContainerOccluded = occluded;
Dirty(comp);
if (comp.Enabled)
_lightTree.QueueTreeUpdate(uid, comp);
}
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || enabled == comp.Enabled)
return;
comp._enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(comp);
var cast = (PointLightComponent)comp;
if (!cast.ContainerOccluded)
_lightTree.QueueTreeUpdate(uid, cast);
}
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius))
return;
comp._radius = radius;
Dirty(comp);
var cast = (PointLightComponent)comp;
if (cast.TreeUid != null)
_lightTree.QueueTreeUpdate(uid, cast);
}
#endregion
}
}

View File

@@ -1,396 +0,0 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
/// <summary>
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
/// </summary>
[UsedImplicitly]
public sealed class RenderingTreeSystem : EntitySystem
{
[Dependency] private readonly TransformSystem _xformSystem = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
internal const string LoggerSawmill = "rendertree";
// Nullspace is not indexed. Keep that in mind.
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly List<SpriteComponent> _spriteQueue = new();
private readonly List<PointLightComponent> _lightQueue = new();
private readonly HashSet<EntityUid> _checkedChildren = new();
/// <summary>
/// <see cref="CVars.MaxLightRadius"/>
/// </summary>
public float MaxLightRadius { get; private set; }
internal IEnumerable<RenderingTreeComponent> GetRenderTrees(MapId mapId, Box2Rotated worldBounds)
{
if (mapId == MapId.Nullspace) yield break;
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
var tempQualifier = grid.Owner;
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier);
}
var tempQualifier1 = _mapManager.GetMapEntityId(mapId);
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier1);
}
internal IEnumerable<RenderingTreeComponent> GetRenderTrees(MapId mapId, Box2 worldAABB)
{
if (mapId == MapId.Nullspace) yield break;
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
{
var tempQualifier = grid.Owner;
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier);
}
var tempQualifier1 = _mapManager.GetMapEntityId(mapId);
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier1);
}
public override void Initialize()
{
base.Initialize();
UpdatesBefore.Add(typeof(SpriteSystem));
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
// Due to how recursion works, this must be done.
// Note that this also implicitly handles parent changes.
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
SubscribeLocalEvent<SpriteComponent, ComponentRemove>(RemoveSprite);
SubscribeLocalEvent<SpriteComponent, UpdateSpriteTreeEvent>(HandleSpriteUpdate);
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
SubscribeLocalEvent<PointLightComponent, PointLightUpdateEvent>(HandleLightUpdate);
SubscribeLocalEvent<RenderingTreeComponent, ComponentInit>(OnTreeInit);
SubscribeLocalEvent<RenderingTreeComponent, ComponentRemove>(OnTreeRemove);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.MaxLightRadius, value => MaxLightRadius = value, true);
}
private void OnTreeInit(EntityUid uid, RenderingTreeComponent component, ComponentInit args)
{
component.LightTree = new(LightAabbFunc);
component.SpriteTree = new(SpriteAabbFunc);
}
private void HandleLightUpdate(EntityUid uid, PointLightComponent component, PointLightUpdateEvent args)
{
if (component.TreeUpdateQueued) return;
QueueLightUpdate(component);
}
private void HandleSpriteUpdate(EntityUid uid, SpriteComponent component, UpdateSpriteTreeEvent args)
{
_spriteQueue.Add(component);
}
private void AnythingMoved(ref MoveEvent args)
{
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var renderingQuery = EntityManager.GetEntityQuery<RenderingTreeComponent>();
AnythingMovedSubHandler(args.Sender, args.Component, xformQuery, pointQuery, spriteQuery, renderingQuery);
}
private void AnythingMovedSubHandler(
EntityUid uid,
TransformComponent xform,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery,
EntityQuery<RenderingTreeComponent> renderingQuery)
{
DebugTools.Assert(xform.Owner == uid);
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
if (!_checkedChildren.Add(uid) || renderingQuery.HasComponent(uid)) return;
// This recursive search is needed, as MoveEvent is defined to not care about indirect events like children.
// WHATEVER YOU DO, DON'T REPLACE THIS WITH SPAMMING EVENTS UNLESS YOU HAVE A GUARANTEE IT WON'T LAG THE GC.
// (Struct-based events ok though)
// Ironically this was lagging the GC lolz
if (spriteQuery.TryGetComponent(uid, out var sprite))
QueueSpriteUpdate(sprite);
if (pointQuery.TryGetComponent(uid, out var light))
QueueLightUpdate(light);
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
if (xformQuery.TryGetComponent(child.Value, out var childXform))
AnythingMovedSubHandler(child.Value, childXform, xformQuery, pointQuery, spriteQuery, renderingQuery);
}
}
// For the RemoveX methods
// If the Transform is removed BEFORE the Sprite/Light,
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
// Otherwise these will still have their past MapId and that's all we need..
#region SpriteHandlers
private void RemoveSprite(EntityUid uid, SpriteComponent component, ComponentRemove args)
{
ClearSprite(component);
}
private void ClearSprite(SpriteComponent component)
{
if (component.RenderTree == null) return;
component.RenderTree.SpriteTree.Remove(new() { Component = component });
component.RenderTree = null;
}
private void QueueSpriteUpdate(SpriteComponent component)
{
if (component.TreeUpdateQueued) return;
component.TreeUpdateQueued = true;
_spriteQueue.Add(component);
}
#endregion
#region LightHandlers
private void PointLightRadiusChanged(EntityUid uid, PointLightComponent component, PointLightRadiusChangedEvent args)
{
QueueLightUpdate(component);
}
public void ClearLight(PointLightComponent component)
{
if (component.RenderTree == null) return;
component.RenderTree.LightTree.Remove(new() { Component = component });
component.RenderTree = null;
}
private void QueueLightUpdate(PointLightComponent component)
{
if (component.TreeUpdateQueued) return;
component.TreeUpdateQueued = true;
_lightQueue.Add(component);
}
#endregion
private void OnTreeRemove(EntityUid uid, RenderingTreeComponent component, ComponentRemove args)
{
foreach (var sprite in component.SpriteTree)
{
sprite.Component.RenderTree = null;
}
foreach (var light in component.LightTree)
{
light.Component.RenderTree = null;
}
component.SpriteTree.Clear();
component.LightTree.Clear();
}
private void MapManagerOnMapCreated(MapChangedEvent e)
{
if (e.Destroyed || e.Map == MapId.Nullspace)
{
return;
}
EntityManager.EnsureComponent<RenderingTreeComponent>(e.Uid);
}
private void MapManagerOnGridCreated(GridInitializeEvent ev)
{
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetGrid(ev.EntityUid).Owner);
}
private RenderingTreeComponent? GetRenderTree(EntityUid entity, TransformComponent xform, EntityQuery<TransformComponent> xforms)
{
var lookups = EntityManager.GetEntityQuery<RenderingTreeComponent>();
if (!EntityManager.EntityExists(entity) ||
xform.MapID == MapId.Nullspace ||
lookups.HasComponent(entity)) return null;
var parent = xform.ParentUid;
while (parent.IsValid())
{
if (lookups.TryGetComponent(parent, out var comp)) return comp;
parent = xforms.GetComponent(parent).ParentUid;
}
return null;
}
private bool IsVisible(SpriteComponent component)
{
return component.Visible && !component.ContainerOccluded && !component.Deleted;
}
public override void FrameUpdate(float frameTime)
{
_checkedChildren.Clear();
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
foreach (var sprite in _spriteQueue)
{
sprite.TreeUpdateQueued = false;
if (!IsVisible(sprite))
{
ClearSprite(sprite);
continue;
}
var xform = xforms.GetComponent(sprite.Owner);
var oldMapTree = sprite.RenderTree;
var newMapTree = GetRenderTree(sprite.Owner, xform, xforms);
// TODO: Temp PVS guard
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform, xforms);
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
ClearSprite(sprite);
continue;
}
var aabb = SpriteAabbFunc(sprite, xform, worldPos, worldRot, xforms);
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
ClearSprite(sprite);
newMapTree?.SpriteTree.Add((sprite,xform) , aabb);
}
else
{
newMapTree?.SpriteTree.Update((sprite, xform), aabb);
}
sprite.RenderTree = newMapTree;
}
foreach (var light in _lightQueue)
{
light.TreeUpdateQueued = false;
if (light.Deleted || !light.Enabled || light.ContainerOccluded)
{
ClearLight(light);
continue;
}
var xform = xforms.GetComponent(light.Owner);
var oldMapTree = light.RenderTree;
var newMapTree = GetRenderTree(light.Owner, xform, xforms);
// TODO: Temp PVS guard
var worldPos = _xformSystem.GetWorldPosition(xform, xforms);
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
ClearLight(light);
continue;
}
// TODO: Events need a bit of cleanup so we only validate this on initialize and radius changed events
// this is fine for now IMO as it's 1 float check for every light that moves
if (light.Radius > MaxLightRadius)
{
Logger.WarningS(LoggerSawmill, $"Light radius for {light.Owner} set above max radius of {MaxLightRadius}. This may lead to pop-in.");
}
var aabb = LightAabbFunc(light, xform, worldPos, xforms);
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
ClearLight(light);
newMapTree?.LightTree.Add((light, xform), aabb);
}
else
{
newMapTree?.LightTree.Update((light, xform), aabb);
}
light.RenderTree = newMapTree;
}
_spriteQueue.Clear();
_lightQueue.Clear();
}
private Box2 SpriteAabbFunc(in ComponentTreeEntry<SpriteComponent> entry)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(entry.Transform, xforms);
return SpriteAabbFunc(entry.Component, entry.Transform, worldPos, worldRot, xforms);
}
private Box2 LightAabbFunc(in ComponentTreeEntry<PointLightComponent> entry)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var worldPos = _xformSystem.GetWorldPosition(entry.Transform, xforms);
var tree = GetRenderTree(entry.Uid, entry.Transform, xforms);
var boxSize = entry.Component.Radius * 2;
var localPos = tree == null ? worldPos : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).Transform(worldPos);
return Box2.CenteredAround(localPos, (boxSize, boxSize));
}
private Box2 SpriteAabbFunc(SpriteComponent value, TransformComponent xform, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
{
var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation);
var tree = GetRenderTree(value.Owner, xform, xforms);
return tree == null ? bounds.CalcBoundingBox() : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).TransformBox(bounds);
}
private Box2 LightAabbFunc(PointLightComponent value, TransformComponent xform, Vector2 worldPos, EntityQuery<TransformComponent> xforms)
{
// Lights are circles so don't need entity's rotation
var tree = GetRenderTree(value.Owner, xform, xforms);
var boxSize = value.Radius * 2;
var localPos = tree == null ? worldPos : xforms.GetComponent(tree.Owner).InvWorldMatrix.Transform(worldPos);
return Box2.CenteredAround(localPos, (boxSize, boxSize));
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -19,7 +20,7 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;
[Dependency] private readonly SpriteTreeSystem _treeSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
@@ -29,8 +30,10 @@ namespace Robust.Client.GameObjects
{
base.Initialize();
UpdatesAfter.Add(typeof(SpriteTreeSystem));
_proto.PrototypesReloaded += OnPrototypesReloaded;
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
}
@@ -46,9 +49,13 @@ namespace Robust.Client.GameObjects
SpriteComponent.DirectionBias = value;
}
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev)
{
_inertUpdateQueue.Enqueue(ev.Sprite);
if (sprite._inertUpdateQueued)
return;
sprite._inertUpdateQueued = true;
_inertUpdateQueue.Enqueue(sprite);
}
/// <inheritdoc />
@@ -76,13 +83,7 @@ namespace Robust.Client.GameObjects
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var spriteState = (frameTime, _manualUpdate);
foreach (var comp in _treeSystem.GetRenderTrees(currentMap, pvsBounds))
{
var invMatrix = _transform.GetInvWorldMatrix(comp.Owner, xforms);
var bounds = invMatrix.TransformBox(pvsBounds);
comp.SpriteTree.QueryAabb(ref spriteState, static (ref (
float frameTime,
_treeSystem.QueryAabb( ref spriteState, static (ref (float frameTime,
HashSet<ISpriteComponent> _manualUpdate) tuple, in ComponentTreeEntry<SpriteComponent> value) =>
{
if (value.Component.IsInert)
@@ -92,8 +93,7 @@ namespace Robust.Client.GameObjects
value.Component.FrameUpdate(tuple.frameTime);
return true;
}, bounds, true);
}
}, currentMap, pvsBounds, true);
_manualUpdate.Clear();
}

View File

@@ -11,10 +11,11 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using static Robust.Client.GameObjects.ClientOccluderComponent;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Robust.Shared.Physics;
using Robust.Client.ComponentTrees;
using static Robust.Shared.GameObjects.OccluderComponent;
namespace Robust.Client.Graphics.Clyde
{
@@ -538,19 +539,18 @@ namespace Robust.Client.Graphics.Clyde
expandedBounds)
GetLightsToRender(MapId map, in Box2Rotated worldBounds, in Box2 worldAABB)
{
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var lightTreeSys = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
var xformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
var enlargedBounds = worldAABB.Enlarged(renderingTreeSystem.MaxLightRadius);
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
var state = (this, count: 0, shadowCastingCount: 0, xformSystem, xforms, worldAABB);
foreach (var comp in renderingTreeSystem.GetRenderTrees(map, enlargedBounds))
foreach (var comp in lightTreeSys.GetIntersectingTrees(map, worldAABB))
{
var bounds = xformSystem.GetInvWorldMatrix(comp.Owner, xforms).TransformBox(worldBounds);
comp.LightTree.QueryAabb(ref state, static (ref (
comp.Tree.QueryAabb(ref state, static (ref (
Clyde clyde,
int count,
int shadowCastingCount,
@@ -933,7 +933,7 @@ namespace Robust.Client.Graphics.Clyde
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var comp in occluderSystem.GetOccluderTrees(map, expandedBounds))
foreach (var comp in occluderSystem.GetIntersectingTrees(map, expandedBounds))
{
var treeBounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(expandedBounds);
@@ -945,7 +945,7 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
var occluder = (ClientOccluderComponent)sOccluder;
var occluder = (OccluderComponent)sOccluder;
var worldTransform = xformSystem.GetWorldMatrix(transform, xforms);
var box = sOccluder.BoundingBox;

View File

@@ -1,3 +1,4 @@
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -38,7 +39,6 @@ internal partial class Clyde
Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList));
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds, RefList<SpriteData> list)
{
@@ -59,7 +59,7 @@ internal partial class Clyde
var index = 0;
var added = 0;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
foreach (var comp in _entitySystemManager.GetEntitySystem<SpriteTreeSystem>().GetIntersectingTrees(map, worldBounds))
{
var treeOwner = comp.Owner;
var treeXform = query.GetComponent(comp.Owner);
@@ -75,7 +75,7 @@ internal partial class Clyde
Cos = MathF.Cos((float)treeXform.LocalRotation),
};
comp.SpriteTree.QueryAabb(ref list,
comp.Tree.QueryAabb(ref list,
static (ref RefList<SpriteData> state, in ComponentTreeEntry<SpriteComponent> value) =>
{
ref var entry = ref state.AllocAdd();
@@ -125,7 +125,7 @@ internal partial class Clyde
// var spriteWorldBB = data.Sprite.CalculateRotatedBoundingBox(data.WorldPos, data.WorldRot, batch.ViewRotation);
// data.SpriteScreenBB = Viewport.GetWorldToLocalMatrix().TransformBox(spriteWorldBB);
var (pos, rot) = batch.Sys.GetParentRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query);
var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query);
pos = new Vector2(
batch.TreePos.X + batch.Cos * pos.X - batch.Sin * pos.Y,
batch.TreePos.Y + batch.Sin * pos.X + batch.Cos * pos.Y);

View File

@@ -1,16 +1,9 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
[UsedImplicitly]
public sealed class ServerOccluderSystem : OccluderSystem
{
public override void Initialize()
{
base.Initialize();
namespace Robust.Server.GameObjects;
UpdatesAfter.Add(typeof(PhysicsSystem));
}
}
[UsedImplicitly]
public sealed class ServerOccluderSystem : OccluderSystem
{
}

View File

@@ -22,8 +22,6 @@ namespace Robust.Server.GameObjects
RegisterClass<PhysicsComponent>();
RegisterClass<CollisionWakeComponent>();
RegisterClass<ContainerManagerComponent>();
RegisterClass<OccluderComponent>();
RegisterClass<OccluderTreeComponent>();
RegisterClass<SpriteComponent>();
RegisterClass<ServerUserInterfaceComponent>();
RegisterClass<TimerComponent>();

View File

@@ -74,12 +74,21 @@ namespace Robust.Shared.Maths
{
var ang = Theta % (2 * Math.PI);
if (ang < 0.0f) // convert -PI > PI to 0 > 2PI
ang += 2 * (float) Math.PI;
if (ang < 0) // convert -PI > PI to 0 > 2PI
ang += 2 * Math.PI;
return (Direction) (Math.Floor((ang + Offset) / Segment) % 8);
}
public Direction RotateDir(Direction dir)
{
var ang = (Theta + Segment * (int)dir) % (2 * Math.PI);
if (ang < 0)
ang += 2 * Math.PI;
return (Direction)(Math.Floor((ang + Offset) / Segment) % 8);
}
private const double CardinalSegment = 2 * Math.PI / 4.0; // Cut the circle into 4 pieces
private const double CardinalOffset = CardinalSegment / 2.0; // offset the pieces by 1/2 their size

View File

@@ -0,0 +1,351 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using System;
using System.Collections.Generic;
namespace Robust.Shared.ComponentTrees;
/// <summary>
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
/// </summary>
[UsedImplicitly]
public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
where TComp : Component, IComponentTreeEntry<TComp>, new()
{
[Dependency] private readonly RecursiveMoveSystem _recursiveMoveSys = default!;
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
private readonly HashSet<EntityUid> _updated = new();
/// <summary>
/// If true, this system will update the tree positions every frame update. See also <see cref="DoTickUpdate"/>. Some systems may need to do both.
/// </summary>
protected abstract bool DoFrameUpdate { get; }
/// <summary>
/// If true, this system will update the tree positions every tick update. See also <see cref="DoFrameUpdate"/>. Some systems may need to do both.
/// </summary>
protected abstract bool DoTickUpdate { get; }
/// <summary>
/// Initial tree capacity. Note that client-side trees will remove entities as they leave PVS range.
/// </summary>
protected virtual int InitialCapacity { get; } = 256;
/// <summary>
/// If true, this tree requires all children to be recursively updated whenever ANY entity moves. If false, this
/// will only update when an entity with the given component moves.
/// </summary>
protected abstract bool Recursive { get; }
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = DoTickUpdate;
UpdatesAfter.Add(typeof(SharedTransformSystem));
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
SubscribeLocalEvent<TComp, ComponentRemove>(OnCompRemoved);
if (Recursive)
{
SubscribeLocalEvent<TComp, TreeRecursiveMoveEvent>(HandleRecursiveMove);
_recursiveMoveSys.AddSubscription();
}
else
{
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
}
SubscribeLocalEvent<TTreeComp, EntityTerminatingEvent>(OnTerminating);
SubscribeLocalEvent<TTreeComp, ComponentAdd>(OnTreeAdd);
SubscribeLocalEvent<TTreeComp, ComponentRemove>(OnTreeRemove);
}
#region Queue Update
private void HandleRecursiveMove(EntityUid uid, TComp component, ref TreeRecursiveMoveEvent args)
=> QueueTreeUpdate(uid, component, args.Xform);
private void HandleMove(EntityUid uid, TComp component, ref MoveEvent args)
=> QueueTreeUpdate(uid, component, args.Component);
public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null)
{
if (component.TreeUpdateQueued || !Resolve(uid, ref xform))
return;
component.TreeUpdateQueued = true;
_updateQueue.Enqueue((component, xform));
}
#endregion
#region Component Management
protected virtual void OnCompStartup(EntityUid uid, TComp component, ComponentStartup args)
=> QueueTreeUpdate(uid, component);
protected virtual void OnCompRemoved(EntityUid uid, TComp component, ComponentRemove args)
=> RemoveFromTree(component);
protected virtual void OnTreeAdd(EntityUid uid, TTreeComp component, ComponentAdd args)
{
component.Tree = new(ExtractAabb, capacity: InitialCapacity);
}
protected virtual void OnTreeRemove(EntityUid uid, TTreeComp component, ComponentRemove args)
{
if (Terminating(uid))
return;
foreach (var entry in component.Tree)
{
entry.Component.TreeUid = null;
}
component.Tree.Clear();
}
protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args)
{
RemComp(uid, component);
}
private void MapManagerOnMapCreated(MapChangedEvent e)
{
if (e.Destroyed || e.Map == MapId.Nullspace)
return;
EnsureComp<TTreeComp>(e.Uid);
}
private void MapManagerOnGridCreated(GridInitializeEvent ev)
{
EnsureComp<TTreeComp>(ev.EntityUid);
}
#endregion
#region Update Trees
public override void Update(float frameTime)
{
if (DoTickUpdate)
UpdateTreePositions();
}
public override void FrameUpdate(float frameTime)
{
if (DoFrameUpdate)
UpdateTreePositions();
}
private void UpdateTreePositions()
{
var xforms = GetEntityQuery<TransformComponent>();
var trees = GetEntityQuery<TTreeComp>();
while (_updateQueue.TryDequeue(out var entry))
{
var (comp, xform) = entry;
comp.TreeUpdateQueued = false;
if (!_updated.Add(comp.Owner))
continue;
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
{
RemoveFromTree(comp);
continue;
}
var newTree = xform.GridUid ?? xform.MapUid;
if (!trees.TryGetComponent(newTree, out var newTreeComp) && comp.TreeUid == null)
continue;
Vector2 pos;
Angle rot;
if (comp.TreeUid == newTree)
{
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value,
xforms);
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
continue;
}
RemoveFromTree(comp);
if (newTreeComp == null)
return;
comp.TreeUid = newTree;
comp.Tree = newTreeComp.Tree;
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree.Value,
xforms);
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
}
_updated.Clear();
}
private void RemoveFromTree(TComp component)
{
component.Tree?.Remove(new() { Component = component });
component.Tree = null;
component.TreeUid = null;
}
#endregion
#region AABBs
protected virtual Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry)
{
if (entry.Component.TreeUid == null)
return default;
var (pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
return ExtractAabb(in entry, pos, rot);
}
protected abstract Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry, Vector2 pos, Angle rot);
#endregion
#region Queries
public IEnumerable<TTreeComp> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds)
=> GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox());
public IEnumerable<TTreeComp> GetIntersectingTrees(MapId mapId, Box2 worldAABB)
{
if (mapId == MapId.Nullspace) yield break;
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
{
if (TryComp(grid.Owner, out TTreeComp? treeComp))
yield return treeComp;
}
if (TryComp(_mapManager.GetMapEntityId(mapId), out TTreeComp? mapTreeComp))
yield return mapTreeComp;
}
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true)
{
var state = new HashSet<ComponentTreeEntry<TComp>>();
foreach (var treeComp in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = Transform(treeComp.Owner).InvWorldMatrix.TransformBox(worldBounds);
treeComp.Tree.QueryAabb(ref state, static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds, approx);
}
return state;
}
public void QueryAabb<TState>(
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
MapId mapId,
Box2 worldBounds,
bool approx = true)
{
QueryAabb(ref state, callback, mapId, new Box2Rotated(worldBounds, default, default), approx);
}
public void QueryAabb<TState>(
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
MapId mapId,
Box2Rotated worldBounds,
bool approx = true)
{
foreach (var treeComp in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = Transform(treeComp.Owner).InvWorldMatrix.TransformBox(worldBounds);
treeComp.Tree.QueryAabb(ref state, callback, bounds, approx);
}
}
public List<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
{
if (mapId == MapId.Nullspace)
return new ();
var queryState = new QueryState<TState>(maxLength, returnOnFirstHit, state, predicate);
var endPoint = ray.Position + ray.Direction * maxLength;
var worldBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint), Vector2.ComponentMax(ray.Position, endPoint));
var xforms = GetEntityQuery<TransformComponent>();
foreach (var comp in GetIntersectingTrees(mapId, worldBox))
{
var transform = xforms.GetComponent(comp.Owner);
var (_, treeRot, matrix) = transform.GetWorldPositionRotationInvMatrix(xforms);
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
var treeRay = new Ray(matrix.Transform(ray.Position), relativeAngle);
comp.Tree.QueryRay(ref queryState, QueryCallback, treeRay);
if (returnOnFirstHit && queryState.List.Count > 0)
break;
}
return queryState.List;
static bool QueryCallback(
ref QueryState<TState> state,
in ComponentTreeEntry<TComp> value,
in Vector2 point,
float distFromOrigin)
{
if (distFromOrigin > state.MaxLength || state.Predicate.Invoke(value.Uid, state.State))
return true;
state.List.Add(new RayCastResults(distFromOrigin, point, value.Uid));
return !state.ReturnOnFirstHit;
}
}
private readonly struct QueryState<TState>
{
public readonly float MaxLength;
public readonly bool ReturnOnFirstHit;
public readonly List<RayCastResults> List = new();
public readonly TState State;
public readonly Func<EntityUid, TState, bool> Predicate;
public QueryState(float maxLength, bool returnOnFirstHit, TState state, Func<EntityUid, TState, bool> predictate)
{
MaxLength = maxLength;
ReturnOnFirstHit = returnOnFirstHit;
State = state;
Predicate = predictate;
}
}
#endregion
}

View File

@@ -0,0 +1,32 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Robust.Shared.ComponentTrees;
public interface IComponentTreeComponent<TComp> where TComp : Component, IComponentTreeEntry<TComp>
{
public DynamicTree<ComponentTreeEntry<TComp>> Tree { get; set; }
}
/// <summary>
/// Interface that must be implemented by components that can be stored on component trees.
/// </summary>
public interface IComponentTreeEntry<TComp> where TComp : Component
{
/// <summary>
/// The tree that the component is currently stored on.
/// </summary>
public EntityUid? TreeUid { get; set; }
/// <summary>
/// The tree that the component is currently stored on.
/// </summary>
public DynamicTree<ComponentTreeEntry<TComp>>? Tree { get; set; }
/// <summary>
/// Whether or not the component should currently be added to a tree.
/// </summary>
public bool AddToTree { get; }
public bool TreeUpdateQueued { get; set; }
}

View File

@@ -0,0 +1,60 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Shared.ComponentTrees;
/// <summary>
/// This system will recursively raise events to update component tree positions any time any entity moves.
/// </summary>
/// <remarks>
/// This is used by some client-side systems (e.g., sprites, lights, etc). However this can be quite expensive and if possible should not be used by the server.
/// </remarks>
internal sealed class RecursiveMoveSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
bool Subscribed = false;
internal void AddSubscription()
{
if (Subscribed)
return;
Subscribed = true;
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
}
private void AnythingMoved(ref MoveEvent args)
{
if (args.Component.MapUid == args.Sender || args.Component.GridUid == args.Sender)
return;
DebugTools.Assert(!_mapManager.IsMap(args.Sender));
DebugTools.Assert(!_mapManager.IsGrid(args.Sender));
var xformQuery = GetEntityQuery<TransformComponent>();
AnythingMovedSubHandler(args.Sender, args.Component, xformQuery);
}
private void AnythingMovedSubHandler(
EntityUid uid,
TransformComponent xform,
EntityQuery<TransformComponent> xformQuery)
{
var ev = new TreeRecursiveMoveEvent(xform);
RaiseLocalEvent(uid, ref ev);
// TODO only enumerate over entities in containers if necessary?
// annoyingly, containers aren't guaranteed to occlude sprites & lights
// but AFAIK thats currently unused???
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
if (xformQuery.TryGetComponent(child.Value, out var childXform))
AnythingMovedSubHandler(child.Value, childXform, xformQuery);
}
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameObjects;
namespace Robust.Shared.ComponentTrees;
[ByRefEvent]
internal readonly struct TreeRecursiveMoveEvent
{
public readonly TransformComponent Xform;
public TreeRecursiveMoveEvent(TransformComponent xform)
{
Xform = xform;
}
}

View File

@@ -1,91 +1,54 @@
using System;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using System;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
[RegisterComponent]
[NetworkedComponent()]
[Access(typeof(OccluderSystem))]
public sealed class OccluderComponent : Component, IComponentTreeEntry<OccluderComponent>
{
[NetworkedComponent()]
[Virtual]
public class OccluderComponent : Component
[DataField("enabled")]
public bool Enabled = true;
[DataField("boundingBox")]
public Box2 BoundingBox = new(-0.5f, -0.5f, 0.5f, 0.5f);
public EntityUid? TreeUid { get; set; }
public DynamicTree<ComponentTreeEntry<OccluderComponent>>? Tree { get; set; }
public bool AddToTree => Enabled;
public bool TreeUpdateQueued { get; set; } = false;
[ViewVariables] public (EntityUid Grid, Vector2i Tile)? LastPosition;
[ViewVariables] public OccluderDir Occluding;
[Flags]
public enum OccluderDir : byte
{
[Dependency] private readonly IEntityManager _entMan = default!;
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
[DataField("enabled")]
private bool _enabled = true;
[DataField("boundingBox")]
private Box2 _boundingBox = new(-0.5f, -0.5f, 0.5f, 0.5f);
[NetSerializable, Serializable]
public sealed class OccluderComponentState : ComponentState
{
public bool Enabled { get; }
public Box2 BoundingBox { get; }
internal OccluderTreeComponent? Tree = null;
[ViewVariables(VVAccess.ReadWrite)]
public Box2 BoundingBox
public OccluderComponentState(bool enabled, Box2 boundingBox)
{
get => _boundingBox;
set
{
_boundingBox = value;
Dirty();
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderUpdateEvent(this), true);
}
}
[ViewVariables(VVAccess.ReadWrite)]
public virtual bool Enabled
{
get => _enabled;
set
{
if (_enabled == value)
return;
_enabled = value;
if (_enabled)
{
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderAddEvent(this), true);
}
else
{
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderRemoveEvent(this), true);
}
Dirty();
}
}
public override ComponentState GetComponentState()
{
return new OccluderComponentState(Enabled, BoundingBox);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState == null)
{
return;
}
var cast = (OccluderComponentState) curState;
Enabled = cast.Enabled;
BoundingBox = cast.BoundingBox;
}
[NetSerializable, Serializable]
private sealed class OccluderComponentState : ComponentState
{
public bool Enabled { get; }
public Box2 BoundingBox { get; }
public OccluderComponentState(bool enabled, Box2 boundingBox)
{
Enabled = enabled;
BoundingBox = boundingBox;
}
Enabled = enabled;
BoundingBox = boundingBox;
}
}
}

View File

@@ -1,12 +1,13 @@
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
/// <summary>
/// Stores the relevant occluder children of this entity.
/// </summary>
[RegisterComponent]
public sealed class OccluderTreeComponent : Component, IComponentTreeComponent<OccluderComponent>
{
/// <summary>
/// Stores the relevant occluder children of this entity.
/// </summary>
public sealed class OccluderTreeComponent : Component
{
internal DynamicTree<ComponentTreeEntry<OccluderComponent>> Tree { get; set; } = default!;
}
public DynamicTree<ComponentTreeEntry<OccluderComponent>> Tree { get; set; } = default!;
}

View File

@@ -1,31 +1,21 @@
using System;
using Robust.Shared.Animations;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using System;
namespace Robust.Shared.GameObjects
{
[NetworkedComponent]
public abstract class SharedPointLightComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
[DataField("enabled")]
protected bool _enabled = true;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[DataField("color")]
protected Color _color = Color.White;
/// <summary>
/// How far the light projects.
/// </summary>
[DataField("radius")]
protected float _radius = 5f;
/// <summary>
/// Offset from the center of the entity.
/// </summary>
@@ -43,17 +33,17 @@ namespace Robust.Shared.GameObjects
[DataField("castShadows")]
public bool CastShadows = true;
[Access(typeof(SharedPointLightSystem))]
[DataField("enabled")]
public bool _enabled = true;
[ViewVariables(VVAccess.ReadWrite)]
public virtual bool Enabled
[Animatable] // please somebody ECS animations
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
_entMan.EventBus.RaiseLocalEvent(Owner, new PointLightToggleEvent(_enabled), true);
Dirty();
}
[Obsolete("Use the system's setter")]
set => _sysMan.GetEntitySystem<SharedPointLightSystem>().SetEnabled(Owner, value, this);
}
[ViewVariables(VVAccess.ReadWrite)]
@@ -68,16 +58,20 @@ namespace Robust.Shared.GameObjects
}
}
/// <summary>
/// How far the light projects.
/// </summary>
[DataField("radius")]
[Access(typeof(SharedPointLightSystem))]
public float _radius = 5f;
[ViewVariables(VVAccess.ReadWrite)]
public virtual float Radius
[Animatable] // please somebody ECS animations
public float Radius
{
get => _radius;
set
{
if (MathHelper.CloseToPercent(_radius, value)) return;
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
Dirty();
}
[Obsolete("Use the system's setter")]
set => _sysMan.GetEntitySystem<SharedPointLightSystem>().SetRadius(Owner, value, this);
}
[ViewVariables(VVAccess.ReadWrite)]

View File

@@ -749,18 +749,20 @@ namespace Robust.Shared.GameObjects
public readonly EntityUid Entity;
public readonly EntityUid OldGrid;
public readonly EntityUid Grid;
public readonly TransformComponent Xform;
/// <summary>
/// Tile on both the old and new grid being re-anchored.
/// </summary>
public readonly Vector2i TilePos;
public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos)
public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos, TransformComponent xform)
{
Entity = uid;
OldGrid = oldGrid;
Grid = grid;
TilePos = tilePos;
Xform = xform;
}
}

View File

@@ -1,261 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
public abstract class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent, OccluderComponent>
{
public abstract class OccluderSystem : EntitySystem
public override void Initialize()
{
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
private const float TreeGrowthRate = 256;
private Queue<OccluderEvent> _updates = new(64);
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<MapChangedEvent>(ev =>
{
if (ev.Created)
OnMapCreated(ev);
});
SubscribeLocalEvent<GridInitializeEvent>(HandleGridInit);
SubscribeLocalEvent<OccluderTreeComponent, ComponentInit>(HandleOccluderTreeInit);
SubscribeLocalEvent<OccluderComponent, ComponentInit>(HandleOccluderInit);
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(HandleOccluderShutdown);
SubscribeLocalEvent<OccluderComponent, MoveEvent>(EntMoved);
SubscribeLocalEvent<OccluderComponent, EntParentChangedMessage>(EntParentChanged);
SubscribeLocalEvent<OccluderEvent>(ev => _updates.Enqueue(ev));
}
internal IEnumerable<OccluderTreeComponent> GetOccluderTrees(MapId mapId, Box2 worldAABB)
{
if (mapId == MapId.Nullspace) yield break;
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
{
yield return EntityManager.GetComponent<OccluderTreeComponent>(grid.Owner);
}
yield return EntityManager.GetComponent<OccluderTreeComponent>(_mapManager.GetMapEntityId(mapId));
}
private void HandleOccluderInit(EntityUid uid, OccluderComponent component, ComponentInit args)
{
if (!component.Enabled) return;
_updates.Enqueue(new OccluderAddEvent(component));
}
private void HandleOccluderShutdown(EntityUid uid, OccluderComponent component, ComponentShutdown args)
{
if (!component.Enabled) return;
_updates.Enqueue(new OccluderRemoveEvent(component));
}
private void HandleOccluderTreeInit(EntityUid uid, OccluderTreeComponent component, ComponentInit args)
{
var capacity = (int) Math.Min(256, Math.Ceiling(EntityManager.GetComponent<TransformComponent>(component.Owner).ChildCount / TreeGrowthRate) * TreeGrowthRate);
component.Tree = new(ExtractAabbFunc, capacity: capacity);
}
private void HandleGridInit(GridInitializeEvent ev)
{
EntityManager.EnsureComponent<OccluderTreeComponent>(ev.EntityUid);
}
private OccluderTreeComponent? GetOccluderTree(OccluderComponent component)
{
var entity = component.Owner;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(entity, out var xform) || xform.MapID == MapId.Nullspace)
return null;
var query = GetEntityQuery<OccluderTreeComponent>();
while (xform.ParentUid.IsValid())
{
if (query.TryGetComponent(xform.ParentUid, out var comp))
return comp;
xform = xformQuery.GetComponent(xform.ParentUid);
}
return null;
}
public override void Shutdown()
{
base.Shutdown();
_updates.Clear();
}
public override void FrameUpdate(float frameTime)
{
UpdateTrees();
}
public override void Update(float frameTime)
{
UpdateTrees();
}
private void UpdateTrees()
{
var query = GetEntityQuery<TransformComponent>();
while (_updates.TryDequeue(out var occluderUpdate))
{
OccluderTreeComponent? tree;
var component = occluderUpdate.Component;
switch (occluderUpdate)
{
case OccluderAddEvent:
if (component.Tree != null || component.Deleted) break;
tree = GetOccluderTree(component);
if (tree == null) break;
component.Tree = tree;
tree.Tree.Add(new()
{
Component = component,
Transform = query.GetComponent(component.Owner)
});
break;
case OccluderUpdateEvent:
if (component.Deleted) break;
var oldTree = component.Tree;
tree = GetOccluderTree(component);
var entry = new ComponentTreeEntry<OccluderComponent>()
{
Component = component,
Transform = query.GetComponent(component.Owner)
};
if (oldTree != tree)
{
oldTree?.Tree.Remove(entry);
tree?.Tree.Add(entry);
component.Tree = tree;
break;
}
tree?.Tree.Update(entry);
break;
case OccluderRemoveEvent:
tree = component.Tree;
tree?.Tree.Remove(new() { Component = component });
break;
default:
throw new ArgumentOutOfRangeException($"No implemented occluder update for {occluderUpdate.GetType()}");
}
}
}
private void EntMoved(EntityUid uid, OccluderComponent component, ref MoveEvent args)
{
_updates.Enqueue(new OccluderUpdateEvent(component));
}
private void EntParentChanged(EntityUid uid, OccluderComponent component, ref EntParentChangedMessage args)
{
_updates.Enqueue(new OccluderUpdateEvent(component));
}
private void OnMapCreated(MapChangedEvent e)
{
if (e.Map == MapId.Nullspace) return;
EnsureComp<OccluderTreeComponent>(e.Uid);
}
private Box2 ExtractAabbFunc(in ComponentTreeEntry<OccluderComponent> entry)
{
return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition);
}
public IEnumerable<RayCastResults> IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength,
Func<EntityUid, bool>? predicate = null, bool returnOnFirstHit = true)
{
// ReSharper disable once ConvertToLocalFunction
var wrapper = (EntityUid uid, Func<EntityUid, bool>? wrapped)
=> wrapped != null && wrapped(uid);
return IntersectRayWithPredicate(mapId, in ray, maxLength, predicate, wrapper, returnOnFirstHit);
}
public IEnumerable<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
{
if (mapId == MapId.Nullspace) return Enumerable.Empty<RayCastResults>();
var list = new List<RayCastResults>();
var endPoint = ray.Position + ray.Direction * maxLength;
var worldBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint), Vector2.ComponentMax(ray.Position, endPoint));
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
foreach (var comp in GetOccluderTrees(mapId, worldBox))
{
var transform = xforms.GetComponent(comp.Owner);
var (_, treeRot, matrix) = transform.GetWorldPositionRotationInvMatrix(xforms);
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
var treeRay = new Ray(matrix.Transform(ray.Position), relativeAngle);
comp.Tree.QueryRay(ref list,
(ref List<RayCastResults> listState, in ComponentTreeEntry<OccluderComponent> value, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength)
return true;
if (!value.Component.Enabled)
return true;
if (predicate.Invoke(value.Uid, state))
return true;
var result = new RayCastResults(distFromOrigin, point, value.Uid);
listState.Add(result);
return !returnOnFirstHit;
}, treeRay);
}
return list;
}
base.Initialize();
SubscribeLocalEvent<OccluderComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<OccluderComponent, ComponentHandleState>(OnHandleState);
}
internal sealed class OccluderAddEvent : OccluderEvent
private void OnGetState(EntityUid uid, OccluderComponent comp, ref ComponentGetState args)
{
public OccluderAddEvent(OccluderComponent component) : base(component) {}
args.State = new OccluderComponent.OccluderComponentState(comp.Enabled, comp.BoundingBox);
}
private void OnHandleState(EntityUid uid, OccluderComponent comp, ref ComponentHandleState args)
{
if (args.Current is not OccluderComponent.OccluderComponentState state)
return;
SetEnabled(uid, state.Enabled, comp);
SetBoundingBox(uid, state.BoundingBox, comp);
}
internal sealed class OccluderUpdateEvent : OccluderEvent
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
protected override bool DoTickUpdate => true;
// this system relies on the assumption that all occluders are parented directly to a grid or map.
// if this ever changes, this will make server move events very expensive.
protected override bool Recursive => false;
protected override Box2 ExtractAabb(in ComponentTreeEntry<OccluderComponent> entry)
{
public OccluderUpdateEvent(OccluderComponent component) : base(component) {}
DebugTools.Assert(entry.Transform.ParentUid == entry.Component.TreeUid);
return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition);
}
internal sealed class OccluderRemoveEvent : OccluderEvent
protected override Box2 ExtractAabb(in ComponentTreeEntry<OccluderComponent> entry, Vector2 pos, Angle rot)
=> ExtractAabb(in entry);
#endregion
#region Setters
public void SetBoundingBox(EntityUid uid, Box2 box, OccluderComponent? comp = null)
{
public OccluderRemoveEvent(OccluderComponent component) : base(component) {}
if (!Resolve(uid, ref comp))
return;
comp.BoundingBox = box;
Dirty(comp);
if (comp.TreeUid != null)
QueueTreeUpdate(uid, comp);
}
internal abstract class OccluderEvent : EntityEventArgs
public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
{
public OccluderComponent Component { get; }
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
return;
public OccluderEvent(OccluderComponent component)
{
Component = component;
}
comp.Enabled = enabled;
Dirty(comp);
QueueTreeUpdate(uid, comp);
}
#endregion
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects
{
@@ -19,13 +20,33 @@ namespace Robust.Shared.GameObjects
private void HandleCompState(EntityUid uid, SharedPointLightComponent component, ref ComponentHandleState args)
{
if (args.Current is not PointLightComponentState newState) return;
component.Enabled = newState.Enabled;
component.Radius = newState.Radius;
SetEnabled(uid, newState.Enabled, component);
SetRadius(uid, newState.Radius, component);
component.Offset = newState.Offset;
component.Color = newState.Color;
component.Energy = newState.Energy;
component.Softness = newState.Softness;
component.CastShadows = newState.CastShadows;
}
public virtual void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || enabled == comp.Enabled)
return;
comp._enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(comp);
}
public virtual void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || MathHelper.CloseToPercent(comp.Radius, radius))
return;
comp._radius = radius;
Dirty(comp);
}
}
}

View File

@@ -58,7 +58,7 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert(xform._anchored);
Dirty(xform);
var ev = new ReAnchorEvent(xform.Owner, ((Component) oldGrid).Owner, ((Component) newGrid).Owner, tilePos);
var ev = new ReAnchorEvent(xform.Owner, ((Component) oldGrid).Owner, ((Component) newGrid).Owner, tilePos, xform);
RaiseLocalEvent(xform.Owner, ref ev);
}
@@ -731,7 +731,7 @@ public abstract partial class SharedTransformSystem
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal (Vector2 Position, Angle Rotation) GetParentRelativePositionRotation(
public (Vector2 Position, Angle Rotation) GetRelativePositionRotation(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
@@ -749,7 +749,7 @@ public abstract partial class SharedTransformSystem
}
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetParentRelativePositionRotation)}.");
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
var relXform = query.GetComponent(relative);
pos = relXform.InvWorldMatrix.Transform(pos);
rot = rot - relXform.WorldRotation;
@@ -759,6 +759,36 @@ public abstract partial class SharedTransformSystem
return (pos, rot);
}
/// <summary>
/// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy.
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 GetRelativePosition(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
{
var pos = component._localPosition;
var xform = component;
while (xform.ParentUid != relative)
{
if (xform.ParentUid.IsValid() && query.TryGetComponent(xform.ParentUid, out xform))
{
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
continue;
}
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
var relXform = query.GetComponent(relative);
pos = relXform.InvWorldMatrix.Transform(pos);
break;
}
return pos;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(EntityUid uid, Vector2 worldPos)
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Server.Containers;
using Robust.Server.Debugging;
@@ -120,6 +121,7 @@ namespace Robust.UnitTesting
var mapMan = deps.Resolve<IMapManager>();
// Required components for the engine to work
// Why are we still here? Just to suffer? Why can't we just use [RegisterComponent] magic?
var compFactory = deps.Resolve<IComponentFactory>();
if (!compFactory.AllRegisteredTypes.Contains(typeof(MapComponent)))
@@ -157,6 +159,26 @@ namespace Robust.UnitTesting
compFactory.RegisterClass<JointComponent>();
}
if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderComponent)))
{
compFactory.RegisterClass<OccluderComponent>();
}
if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderTreeComponent)))
{
compFactory.RegisterClass<OccluderTreeComponent>();
}
if (!compFactory.AllRegisteredTypes.Contains(typeof(SpriteTreeComponent)))
{
compFactory.RegisterClass<SpriteTreeComponent>();
}
if (!compFactory.AllRegisteredTypes.Contains(typeof(LightTreeComponent)))
{
compFactory.RegisterClass<LightTreeComponent>();
}
// So by default EntityManager does its own EntitySystemManager initialize during Startup.
// We want to bypass this and load our own systems hence we will manually initialize it here.
entMan.Initialize();

View File

@@ -257,6 +257,7 @@ namespace Robust.UnitTesting.Server
var compFactory = container.Resolve<IComponentFactory>();
// if only we had some sort of attribute for autmatically registering components.
compFactory.RegisterClass<MetaDataComponent>();
compFactory.RegisterClass<TransformComponent>();
compFactory.RegisterClass<MapGridComponent>();
@@ -269,6 +270,8 @@ namespace Robust.UnitTesting.Server
compFactory.RegisterClass<PhysicsMapComponent>();
compFactory.RegisterClass<FixturesComponent>();
compFactory.RegisterClass<CollisionWakeComponent>();
compFactory.RegisterClass<OccluderComponent>();
compFactory.RegisterClass<OccluderTreeComponent>();
_regDelegate?.Invoke(compFactory);