using System; using System.Collections.Generic; using JetBrains.Annotations; 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; 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!; 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.GridEntityId; 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.GridEntityId; 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. SubscribeLocalEvent(AnythingMoved); SubscribeLocalEvent(SpriteParentChanged); SubscribeLocalEvent(RemoveSprite); SubscribeLocalEvent(HandleSpriteUpdate); SubscribeLocalEvent(LightParentChanged); 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, SpriteUpdateEvent args) { if (component.TreeUpdateQueued) return; QueueSpriteUpdate(component); } private void AnythingMoved(ref MoveEvent args) { var pointQuery = EntityManager.GetEntityQuery(); var spriteQuery = EntityManager.GetEntityQuery(); var xformQuery = EntityManager.GetEntityQuery(); AnythingMovedSubHandler(args.Sender, xformQuery, pointQuery, spriteQuery); } private void AnythingMovedSubHandler( EntityUid uid, EntityQuery xformQuery, EntityQuery pointQuery, EntityQuery spriteQuery) { // To avoid doing redundant updates (and we don't need to update a grid's children ever) if (!_checkedChildren.Add(uid) || EntityManager.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); if (!xformQuery.TryGetComponent(uid, out var xform)) return; var childEnumerator = xform.ChildEnumerator; while (childEnumerator.MoveNext(out var child)) { AnythingMovedSubHandler(child.Value, xformQuery, pointQuery, spriteQuery); } } // 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 SpriteParentChanged(EntityUid uid, SpriteComponent component, ref EntParentChangedMessage args) { QueueSpriteUpdate(component); } 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 LightParentChanged(EntityUid uid, PointLightComponent component, ref EntParentChangedMessage args) { QueueLightUpdate(component); } 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(_mapManager.GetMapEntityId(e.Map)); } private void MapManagerOnGridCreated(GridInitializeEvent ev) { EntityManager.EnsureComponent(_mapManager.GetGrid(ev.GridId).GridEntityId); } 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); 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)); } } }