diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5535526fe..9f02cd231 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -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 diff --git a/Robust.Client/ComponentTrees/LightTreeComponent.cs b/Robust.Client/ComponentTrees/LightTreeComponent.cs new file mode 100644 index 000000000..8096acc0c --- /dev/null +++ b/Robust.Client/ComponentTrees/LightTreeComponent.cs @@ -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 +{ + public DynamicTree> Tree { get; set; } = default!; +} diff --git a/Robust.Client/ComponentTrees/LightTreeSystem.cs b/Robust.Client/ComponentTrees/LightTreeSystem.cs new file mode 100644 index 000000000..086235f95 --- /dev/null +++ b/Robust.Client/ComponentTrees/LightTreeSystem.cs @@ -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 +{ + #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 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 entry) + { + if (entry.Component.TreeUid == null) + return default; + + var pos = XformSystem.GetRelativePosition( + entry.Transform, + entry.Component.TreeUid.Value, + GetEntityQuery()); + + return ExtractAabb(in entry, pos, default); + } + #endregion +} diff --git a/Robust.Client/ComponentTrees/SpriteTreeComponent.cs b/Robust.Client/ComponentTrees/SpriteTreeComponent.cs new file mode 100644 index 000000000..d62fd3ce5 --- /dev/null +++ b/Robust.Client/ComponentTrees/SpriteTreeComponent.cs @@ -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 +{ + public DynamicTree> Tree { get; set; } = default!; +} diff --git a/Robust.Client/ComponentTrees/SpriteTreeSystem.cs b/Robust.Client/ComponentTrees/SpriteTreeSystem.cs new file mode 100644 index 000000000..8d4e1476c --- /dev/null +++ b/Robust.Client/ComponentTrees/SpriteTreeSystem.cs @@ -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 +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(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 entry, Vector2 pos, Angle rot) + => entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox(); + #endregion +} diff --git a/Robust.Client/GameObjects/ClientComponentFactory.cs b/Robust.Client/GameObjects/ClientComponentFactory.cs index 0249279fe..5fd7f6745 100644 --- a/Robust.Client/GameObjects/ClientComponentFactory.cs +++ b/Robust.Client/GameObjects/ClientComponentFactory.cs @@ -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(); RegisterClass(); RegisterClass(); - RegisterClass(); - RegisterClass(); RegisterClass(); RegisterClass(); RegisterClass(); diff --git a/Robust.Client/GameObjects/Components/Light/ClientOccluderComponent.cs b/Robust.Client/GameObjects/Components/Light/ClientOccluderComponent.cs deleted file mode 100644 index 332087fef..000000000 --- a/Robust.Client/GameObjects/Components/Light/ClientOccluderComponent.cs +++ /dev/null @@ -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(Owner).Anchored) - { - AnchorStateChanged(); - } - } - - public void AnchorStateChanged() - { - var xform = _entityManager.GetComponent(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(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(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, - } - - /// - /// Raised by occluders when trying to get occlusion directions. - /// - [ByRefEvent] - public struct OccluderDirectionsEvent - { - public bool Handled = false; - public OccluderDir Directions = OccluderDir.None; - public TransformComponent Component = default!; - - public OccluderDirectionsEvent() {} - } -} diff --git a/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs b/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs index 869552b0f..f957da70b 100644 --- a/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs +++ b/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs @@ -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 { - [Dependency] private readonly IEntityManager _entityManager = default!; + public EntityUid? TreeUid { get; set; } - internal bool TreeUpdateQueued { get; set; } + public DynamicTree>? 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; /// /// 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; - - /// - /// Radius, in meters. - /// - [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 - { - } } diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs index e408314d2..1d9c8bf61 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs @@ -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(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)); } } } diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index 165275b95..eec493eaa 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -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 { [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>? 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 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; /// /// 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(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; } } diff --git a/Robust.Client/GameObjects/Components/RenderingTreeComponent.cs b/Robust.Client/GameObjects/Components/RenderingTreeComponent.cs deleted file mode 100644 index f63b8f237..000000000 --- a/Robust.Client/GameObjects/Components/RenderingTreeComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.GameObjects; -using Robust.Shared.Physics; - -namespace Robust.Client.GameObjects -{ - [RegisterComponent] - public sealed class RenderingTreeComponent : Component - { - internal DynamicTree> SpriteTree { get; set; } = default!; - internal DynamicTree> LightTree { get; set; } = default!; - } -} diff --git a/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs b/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs index 065c7c924..425b1f330 100644 --- a/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs @@ -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 _dirtyEntities = new(); + + /// + public override void Initialize() { - [Dependency] private readonly IMapManager _mapManager = default!; + base.Initialize(); - private readonly Queue _dirtyEntities = new(); + SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(OnReAnchor); + SubscribeLocalEvent(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; - /// - 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(); + var xforms = GetEntityQuery(); + var grids = GetEntityQuery(); + + try { - base.Initialize(); - - UpdatesAfter.Add(typeof(TransformSystem)); - UpdatesAfter.Add(typeof(PhysicsSystem)); - - SubscribeLocalEvent(OnOccluderDirty); - - SubscribeLocalEvent(OnAnchorChanged); - SubscribeLocalEvent(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(); + 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 occluderQuery) + { + while (enumerator.MoveNext(out var entity)) { - component.AnchorStateChanged(); - } - - private void OnOccluderDirty(OccluderDirtyEvent ev) - { - var sender = ev.Sender; - MapGridComponent? grid; - var occluderQuery = GetEntityQuery(); - - if (EntityManager.EntityExists(sender) && - occluderQuery.HasComponent(sender)) + if (occluderQuery.TryGetComponent(entity.Value, out var occluder)) { - var xform = EntityManager.GetComponent(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 occluderQuery) + private void UpdateOccluder(EntityUid uid, + OccluderComponent occluder, + EntityQuery occluders, + EntityQuery xforms, + EntityQuery 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 query, + EntityQuery 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}.") + }; } /// - /// Event raised by a when it needs to be recalculated. + /// Raised by occluders when trying to get occlusion directions. /// - 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; + } } } diff --git a/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs b/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs index 72484167f..8fd543717 100644 --- a/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs @@ -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 _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; diff --git a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs index d23c5339e..98d79da9f 100644 --- a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs @@ -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(), IoCManager.Resolve(), IoCManager.Resolve(), - Get()); + Get()); 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; diff --git a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs index 50b5e57dd..f22bafc6f 100644 --- a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs @@ -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(HandleInit); - SubscribeLocalEvent(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 } } diff --git a/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs b/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs deleted file mode 100644 index a1feceb35..000000000 --- a/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs +++ /dev/null @@ -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 -{ - /// - /// Keeps track of s for various rendering-related components. - /// - [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 _spriteQueue = new(); - private readonly List _lightQueue = new(); - - private readonly HashSet _checkedChildren = new(); - - /// - /// - /// - public float MaxLightRadius { get; private set; } - - internal IEnumerable 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(tempQualifier); - } - - var tempQualifier1 = _mapManager.GetMapEntityId(mapId); - yield return EntityManager.GetComponent(tempQualifier1); - } - - internal IEnumerable 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(tempQualifier); - } - - var tempQualifier1 = _mapManager.GetMapEntityId(mapId); - yield return EntityManager.GetComponent(tempQualifier1); - } - - public override void Initialize() - { - base.Initialize(); - - UpdatesBefore.Add(typeof(SpriteSystem)); - UpdatesAfter.Add(typeof(TransformSystem)); - UpdatesAfter.Add(typeof(PhysicsSystem)); - - SubscribeLocalEvent(MapManagerOnMapCreated); - - SubscribeLocalEvent(MapManagerOnGridCreated); - - // Due to how recursion works, this must be done. - // Note that this also implicitly handles parent changes. - SubscribeLocalEvent(AnythingMoved); - - SubscribeLocalEvent(RemoveSprite); - SubscribeLocalEvent(HandleSpriteUpdate); - - SubscribeLocalEvent(PointLightRadiusChanged); - SubscribeLocalEvent(HandleLightUpdate); - - SubscribeLocalEvent(OnTreeInit); - SubscribeLocalEvent(OnTreeRemove); - - var configManager = IoCManager.Resolve(); - 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(); - var spriteQuery = EntityManager.GetEntityQuery(); - var xformQuery = EntityManager.GetEntityQuery(); - var renderingQuery = EntityManager.GetEntityQuery(); - - AnythingMovedSubHandler(args.Sender, args.Component, xformQuery, pointQuery, spriteQuery, renderingQuery); - } - - private void AnythingMovedSubHandler( - EntityUid uid, - TransformComponent xform, - EntityQuery xformQuery, - EntityQuery pointQuery, - EntityQuery spriteQuery, - EntityQuery 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(e.Uid); - } - - private void MapManagerOnGridCreated(GridInitializeEvent ev) - { - EntityManager.EnsureComponent(_mapManager.GetGrid(ev.EntityUid).Owner); - } - - private RenderingTreeComponent? GetRenderTree(EntityUid entity, TransformComponent xform, EntityQuery xforms) - { - var lookups = EntityManager.GetEntityQuery(); - - 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(); - - 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 entry) - { - var xforms = EntityManager.GetEntityQuery(); - - var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(entry.Transform, xforms); - - return SpriteAabbFunc(entry.Component, entry.Transform, worldPos, worldRot, xforms); - } - - private Box2 LightAabbFunc(in ComponentTreeEntry entry) - { - var xforms = EntityManager.GetEntityQuery(); - 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 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 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)); - } - } -} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index ef96e6ac7..6b56bea59 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -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 _inertUpdateQueue = new(); @@ -29,8 +30,10 @@ namespace Robust.Client.GameObjects { base.Initialize(); + UpdatesAfter.Add(typeof(SpriteTreeSystem)); + _proto.PrototypesReloaded += OnPrototypesReloaded; - SubscribeLocalEvent(QueueUpdateInert); + SubscribeLocalEvent(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); } /// @@ -76,13 +83,7 @@ namespace Robust.Client.GameObjects var xforms = EntityManager.GetEntityQuery(); 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 _manualUpdate) tuple, in ComponentTreeEntry 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(); } diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs index 9677dc59d..cf4aab7ed 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs @@ -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(); + var lightTreeSys = _entitySystemManager.GetEntitySystem(); var xformSystem = _entitySystemManager.GetEntitySystem(); - 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(); 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(); - 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; diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs index 51396da3b..f1605957c 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -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 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().GetRenderTrees(map, worldBounds)) + foreach (var comp in _entitySystemManager.GetEntitySystem().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 state, in ComponentTreeEntry 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); diff --git a/Robust.Server/GameObjects/EntitySystems/ServerOccluderSystem.cs b/Robust.Server/GameObjects/EntitySystems/ServerOccluderSystem.cs index e063109ec..497ae5188 100644 --- a/Robust.Server/GameObjects/EntitySystems/ServerOccluderSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/ServerOccluderSystem.cs @@ -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 +{ } diff --git a/Robust.Server/GameObjects/ServerComponentFactory.cs b/Robust.Server/GameObjects/ServerComponentFactory.cs index 11b9c3a3a..af597fcec 100644 --- a/Robust.Server/GameObjects/ServerComponentFactory.cs +++ b/Robust.Server/GameObjects/ServerComponentFactory.cs @@ -22,8 +22,6 @@ namespace Robust.Server.GameObjects RegisterClass(); RegisterClass(); RegisterClass(); - RegisterClass(); - RegisterClass(); RegisterClass(); RegisterClass(); RegisterClass(); diff --git a/Robust.Shared.Maths/Angle.cs b/Robust.Shared.Maths/Angle.cs index ac9f3d03e..45b0d6643 100644 --- a/Robust.Shared.Maths/Angle.cs +++ b/Robust.Shared.Maths/Angle.cs @@ -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 diff --git a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs new file mode 100644 index 000000000..bd877631c --- /dev/null +++ b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs @@ -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; + +/// +/// Keeps track of s for various rendering-related components. +/// +[UsedImplicitly] +public abstract class ComponentTreeSystem : EntitySystem + where TTreeComp : Component, IComponentTreeComponent, new() + where TComp : Component, IComponentTreeEntry, new() +{ + [Dependency] private readonly RecursiveMoveSystem _recursiveMoveSys = default!; + [Dependency] protected readonly SharedTransformSystem XformSystem = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + + private readonly Queue> _updateQueue = new(); + private readonly HashSet _updated = new(); + + /// + /// If true, this system will update the tree positions every frame update. See also . Some systems may need to do both. + /// + protected abstract bool DoFrameUpdate { get; } + + /// + /// If true, this system will update the tree positions every tick update. See also . Some systems may need to do both. + /// + protected abstract bool DoTickUpdate { get; } + + /// + /// Initial tree capacity. Note that client-side trees will remove entities as they leave PVS range. + /// + protected virtual int InitialCapacity { get; } = 256; + + /// + /// 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. + /// + protected abstract bool Recursive { get; } + + public override void Initialize() + { + base.Initialize(); + + UpdatesOutsidePrediction = DoTickUpdate; + UpdatesAfter.Add(typeof(SharedTransformSystem)); + UpdatesAfter.Add(typeof(SharedPhysicsSystem)); + + SubscribeLocalEvent(MapManagerOnMapCreated); + SubscribeLocalEvent(MapManagerOnGridCreated); + + SubscribeLocalEvent(OnCompStartup); + SubscribeLocalEvent(OnCompRemoved); + + if (Recursive) + { + SubscribeLocalEvent(HandleRecursiveMove); + _recursiveMoveSys.AddSubscription(); + } + else + { + SubscribeLocalEvent(HandleMove); + } + + SubscribeLocalEvent(OnTerminating); + SubscribeLocalEvent(OnTreeAdd); + SubscribeLocalEvent(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(e.Uid); + } + + private void MapManagerOnGridCreated(GridInitializeEvent ev) + { + EnsureComp(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(); + var trees = GetEntityQuery(); + + 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 entry) + { + if (entry.Component.TreeUid == null) + return default; + + var (pos, rot) = XformSystem.GetRelativePositionRotation( + entry.Transform, + entry.Component.TreeUid.Value, + GetEntityQuery()); + + return ExtractAabb(in entry, pos, rot); + } + + protected abstract Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot); + #endregion + + #region Queries + public IEnumerable GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds) + => GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox()); + + public IEnumerable 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> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true) + => QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx); + + public HashSet> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true) + { + var state = new HashSet>(); + foreach (var treeComp in GetIntersectingTrees(mapId, worldBounds)) + { + var bounds = Transform(treeComp.Owner).InvWorldMatrix.TransformBox(worldBounds); + + treeComp.Tree.QueryAabb(ref state, static (ref HashSet> state, in ComponentTreeEntry value) => + { + state.Add(value); + return true; + }, + bounds, approx); + } + return state; + } + + public void QueryAabb( + ref TState state, + DynamicTree>.QueryCallbackDelegate callback, + MapId mapId, + Box2 worldBounds, + bool approx = true) + { + QueryAabb(ref state, callback, mapId, new Box2Rotated(worldBounds, default, default), approx); + } + + public void QueryAabb( + ref TState state, + DynamicTree>.QueryCallbackDelegate 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 IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength, + TState state, Func predicate, bool returnOnFirstHit = true) + { + if (mapId == MapId.Nullspace) + return new (); + + var queryState = new QueryState(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(); + + 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 state, + in ComponentTreeEntry 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 + { + public readonly float MaxLength; + public readonly bool ReturnOnFirstHit; + public readonly List List = new(); + public readonly TState State; + public readonly Func Predicate; + + public QueryState(float maxLength, bool returnOnFirstHit, TState state, Func predictate) + { + MaxLength = maxLength; + ReturnOnFirstHit = returnOnFirstHit; + State = state; + Predicate = predictate; + } + } + #endregion +} + diff --git a/Robust.Shared/ComponentTrees/IComponentTreeComponent.cs b/Robust.Shared/ComponentTrees/IComponentTreeComponent.cs new file mode 100644 index 000000000..47510253b --- /dev/null +++ b/Robust.Shared/ComponentTrees/IComponentTreeComponent.cs @@ -0,0 +1,32 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Physics; + +namespace Robust.Shared.ComponentTrees; + +public interface IComponentTreeComponent where TComp : Component, IComponentTreeEntry +{ + public DynamicTree> Tree { get; set; } +} + +/// +/// Interface that must be implemented by components that can be stored on component trees. +/// +public interface IComponentTreeEntry where TComp : Component +{ + /// + /// The tree that the component is currently stored on. + /// + public EntityUid? TreeUid { get; set; } + + /// + /// The tree that the component is currently stored on. + /// + public DynamicTree>? Tree { get; set; } + + /// + /// Whether or not the component should currently be added to a tree. + /// + public bool AddToTree { get; } + + public bool TreeUpdateQueued { get; set; } +} diff --git a/Robust.Shared/ComponentTrees/RecursiveMoveSystem.cs b/Robust.Shared/ComponentTrees/RecursiveMoveSystem.cs new file mode 100644 index 000000000..723507a7f --- /dev/null +++ b/Robust.Shared/ComponentTrees/RecursiveMoveSystem.cs @@ -0,0 +1,60 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Robust.Shared.ComponentTrees; + +/// +/// This system will recursively raise events to update component tree positions any time any entity moves. +/// +/// +/// 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. +/// +internal sealed class RecursiveMoveSystem : EntitySystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + + bool Subscribed = false; + + internal void AddSubscription() + { + if (Subscribed) + return; + + Subscribed = true; + SubscribeLocalEvent(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(); + AnythingMovedSubHandler(args.Sender, args.Component, xformQuery); + } + + private void AnythingMovedSubHandler( + EntityUid uid, + TransformComponent xform, + EntityQuery 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); + } + } +} diff --git a/Robust.Shared/ComponentTrees/TreeRecursiveMoveEvent.cs b/Robust.Shared/ComponentTrees/TreeRecursiveMoveEvent.cs new file mode 100644 index 000000000..5a64c70d9 --- /dev/null +++ b/Robust.Shared/ComponentTrees/TreeRecursiveMoveEvent.cs @@ -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; + } +} diff --git a/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs b/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs index e6cb8411b..033c1fe5e 100644 --- a/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs +++ b/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs @@ -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 { - [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>? 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; } } } diff --git a/Robust.Shared/GameObjects/Components/Light/OccluderTreeComponent.cs b/Robust.Shared/GameObjects/Components/Light/OccluderTreeComponent.cs index 1f3cde416..c68723c7f 100644 --- a/Robust.Shared/GameObjects/Components/Light/OccluderTreeComponent.cs +++ b/Robust.Shared/GameObjects/Components/Light/OccluderTreeComponent.cs @@ -1,12 +1,13 @@ +using Robust.Shared.ComponentTrees; using Robust.Shared.Physics; -namespace Robust.Shared.GameObjects +namespace Robust.Shared.GameObjects; + +/// +/// Stores the relevant occluder children of this entity. +/// +[RegisterComponent] +public sealed class OccluderTreeComponent : Component, IComponentTreeComponent { - /// - /// Stores the relevant occluder children of this entity. - /// - public sealed class OccluderTreeComponent : Component - { - internal DynamicTree> Tree { get; set; } = default!; - } + public DynamicTree> Tree { get; set; } = default!; } diff --git a/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs b/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs index 43d14c105..687fd2315 100644 --- a/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs +++ b/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs @@ -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; - /// - /// How far the light projects. - /// - [DataField("radius")] - protected float _radius = 5f; - /// /// Offset from the center of the entity. /// @@ -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().SetEnabled(Owner, value, this); } [ViewVariables(VVAccess.ReadWrite)] @@ -68,16 +58,20 @@ namespace Robust.Shared.GameObjects } } + /// + /// How far the light projects. + /// + [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().SetRadius(Owner, value, this); } [ViewVariables(VVAccess.ReadWrite)] diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index b1d9d0317..949a685e9 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -749,18 +749,20 @@ namespace Robust.Shared.GameObjects public readonly EntityUid Entity; public readonly EntityUid OldGrid; public readonly EntityUid Grid; + public readonly TransformComponent Xform; /// /// Tile on both the old and new grid being re-anchored. /// 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; } } diff --git a/Robust.Shared/GameObjects/Systems/OccluderSystem.cs b/Robust.Shared/GameObjects/Systems/OccluderSystem.cs index 49b1e7d4d..c8bf996ad 100644 --- a/Robust.Shared/GameObjects/Systems/OccluderSystem.cs +++ b/Robust.Shared/GameObjects/Systems/OccluderSystem.cs @@ -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 { - public abstract class OccluderSystem : EntitySystem + public override void Initialize() { - [Dependency] private readonly IMapManagerInternal _mapManager = default!; - - private const float TreeGrowthRate = 256; - - private Queue _updates = new(64); - - public override void Initialize() - { - base.Initialize(); - - UpdatesOutsidePrediction = true; - - SubscribeLocalEvent(ev => - { - if (ev.Created) - OnMapCreated(ev); - }); - - SubscribeLocalEvent(HandleGridInit); - SubscribeLocalEvent(HandleOccluderTreeInit); - SubscribeLocalEvent(HandleOccluderInit); - SubscribeLocalEvent(HandleOccluderShutdown); - - SubscribeLocalEvent(EntMoved); - SubscribeLocalEvent(EntParentChanged); - SubscribeLocalEvent(ev => _updates.Enqueue(ev)); - } - - internal IEnumerable GetOccluderTrees(MapId mapId, Box2 worldAABB) - { - if (mapId == MapId.Nullspace) yield break; - - foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB)) - { - yield return EntityManager.GetComponent(grid.Owner); - } - - yield return EntityManager.GetComponent(_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(component.Owner).ChildCount / TreeGrowthRate) * TreeGrowthRate); - - component.Tree = new(ExtractAabbFunc, capacity: capacity); - } - - private void HandleGridInit(GridInitializeEvent ev) - { - EntityManager.EnsureComponent(ev.EntityUid); - } - - private OccluderTreeComponent? GetOccluderTree(OccluderComponent component) - { - var entity = component.Owner; - var xformQuery = GetEntityQuery(); - - if (!xformQuery.TryGetComponent(entity, out var xform) || xform.MapID == MapId.Nullspace) - return null; - - var query = GetEntityQuery(); - 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(); - 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() - { - 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(e.Uid); - } - - private Box2 ExtractAabbFunc(in ComponentTreeEntry entry) - { - return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition); - } - - public IEnumerable IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength, - Func? predicate = null, bool returnOnFirstHit = true) - { - // ReSharper disable once ConvertToLocalFunction - var wrapper = (EntityUid uid, Func? wrapped) - => wrapped != null && wrapped(uid); - - return IntersectRayWithPredicate(mapId, in ray, maxLength, predicate, wrapper, returnOnFirstHit); - } - - public IEnumerable IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength, - TState state, Func predicate, bool returnOnFirstHit = true) - { - if (mapId == MapId.Nullspace) return Enumerable.Empty(); - var list = new List(); - - 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(); - - 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 listState, in ComponentTreeEntry 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(OnGetState); + SubscribeLocalEvent(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 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 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 } diff --git a/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs b/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs index b212e8f59..fe86d7b26 100644 --- a/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs @@ -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); + } } } diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index f87208074..09935b9ad 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -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 /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal (Vector2 Position, Angle Rotation) GetParentRelativePositionRotation( + public (Vector2 Position, Angle Rotation) GetRelativePositionRotation( TransformComponent component, EntityUid relative, EntityQuery 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); } + /// + /// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2 GetRelativePosition( + TransformComponent component, + EntityUid relative, + EntityQuery 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) { diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index 5ff43d128..fea870a1f 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -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(); // 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(); if (!compFactory.AllRegisteredTypes.Contains(typeof(MapComponent))) @@ -157,6 +159,26 @@ namespace Robust.UnitTesting compFactory.RegisterClass(); } + if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderComponent))) + { + compFactory.RegisterClass(); + } + + if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderTreeComponent))) + { + compFactory.RegisterClass(); + } + + if (!compFactory.AllRegisteredTypes.Contains(typeof(SpriteTreeComponent))) + { + compFactory.RegisterClass(); + } + + if (!compFactory.AllRegisteredTypes.Contains(typeof(LightTreeComponent))) + { + compFactory.RegisterClass(); + } + // 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(); diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 8f5e0b8e2..6cac556df 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -257,6 +257,7 @@ namespace Robust.UnitTesting.Server var compFactory = container.Resolve(); + // if only we had some sort of attribute for autmatically registering components. compFactory.RegisterClass(); compFactory.RegisterClass(); compFactory.RegisterClass(); @@ -269,6 +270,8 @@ namespace Robust.UnitTesting.Server compFactory.RegisterClass(); compFactory.RegisterClass(); compFactory.RegisterClass(); + compFactory.RegisterClass(); + compFactory.RegisterClass(); _regDelegate?.Invoke(compFactory);