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; using System.Diagnostics.CodeAnalysis; using Robust.Shared.Collections; using System.Numerics; using Robust.Shared.Map.Components; using Robust.Shared.Utility; namespace Robust.Shared.ComponentTrees; /// /// Keeps track of s for various rendering-related components. /// [UsedImplicitly] public abstract partial class ComponentTreeSystem : EntitySystem where TTreeComp : Component, IComponentTreeComponent, new() where TComp : Component, IComponentTreeEntry { [Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!; [Dependency] protected SharedTransformSystem XformSystem = default!; [Dependency] private SharedMapSystem _mapSystem = default!; private readonly Queue> _updateQueue = new(); protected EntityQuery Query; /// /// Whether this lookup tree should even be enabled. /// /// /// This can be used to disable some trees if they are not required, which helps improve performance a bit. /// protected virtual bool Enabled => true; private bool _initialized; /// /// 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(); if (!Enabled) return; _initialized = true; UpdatesOutsidePrediction = DoTickUpdate; UpdatesAfter.Add(typeof(SharedTransformSystem)); UpdatesAfter.Add(typeof(SharedPhysicsSystem)); SubscribeLocalEvent(MapManagerOnMapCreated); SubscribeLocalEvent(MapManagerOnGridCreated); // Yipeee point light if (!typeof(TComp).IsAbstract) { SubscribeLocalEvent(OnCompStartup); SubscribeLocalEvent(OnCompRemoved); Query = GetEntityQuery(); } if (Recursive) { _recursiveMoveSys.OnTreeRecursiveMove += HandleRecursiveMove; _recursiveMoveSys.AddSubscription(); } else { // TODO EXCEPTION TOLERANCE // Ensure lookup trees update before content code handles move events. SubscribeLocalEvent(HandleMove); } SubscribeLocalEvent(OnTerminating); SubscribeLocalEvent(OnTreeAdd); SubscribeLocalEvent(OnTreeRemove); } public override void Shutdown() { if (!_initialized) return; _initialized = false; if (Recursive) _recursiveMoveSys.OnTreeRecursiveMove -= HandleRecursiveMove; } private bool CheckEnabled() { if (_initialized) return true; Log.Error($"Attempted to use disabled lookup tree"); return false; } #region Queue Update private void HandleRecursiveMove(EntityUid uid, TransformComponent xform) { if (Query.TryGetComponent(uid, out var component)) QueueTreeUpdate(uid, component, xform); } private void HandleMove(EntityUid uid, TComp component, ref MoveEvent args) { QueueTreeUpdate(uid, component, args.Component); OnComponentMove(uid, component, ref args); } protected virtual void OnComponentMove(EntityUid uid, TComp component, ref MoveEvent args) { } public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null) { if (!_initialized) return; if (component.TreeUpdateQueued || !Resolve(uid, ref xform)) return; component.TreeUpdateQueued = true; _updateQueue.Enqueue((component, xform)); } public void QueueTreeUpdate(Entity entity, TransformComponent? xform = null) { QueueTreeUpdate(entity.Owner, entity.Comp, 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) { foreach (var entry in component.Tree) { entry.Component.TreeUid = null; entry.Component.Tree = null; } component.Tree.Clear(); } protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args) { // IIRC, this is to prevent a tree-update spam as each of the entity's children get detached to nullspace. RemComp(uid, component); } private void MapManagerOnMapCreated(MapCreatedEvent e) { EnsureComp(e.Uid); } private void MapManagerOnGridCreated(GridInitializeEvent ev) { EnsureComp(ev.EntityUid); } #endregion #region Update Trees public override void Update(float frameTime) { if (DoTickUpdate && _initialized) UpdateTreePositions(); } public override void FrameUpdate(float frameTime) { if (DoFrameUpdate && _initialized) UpdateTreePositions(); } /// /// Processes any pending position updates. Note that this should generally always get run before directly /// querying a tree. /// public void UpdateTreePositions() { try { if (!CheckEnabled()) return; if (_updateQueue.Count == 0) return; var trees = GetEntityQuery(); while (_updateQueue.TryDequeue(out var entry)) { var (comp, xform) = entry; // Was this entity queued multiple times? DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?"); comp.TreeUpdateQueued = false; if (!comp.Running) 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); 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); newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot)); } } finally { _updateQueue.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); return ExtractAabb(in entry, pos, rot); } protected abstract Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot); #endregion #region Queries public ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds) => GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox()); public ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2 worldAABB) => GetIntersectingTreesInternal(mapId, worldAABB); internal ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTreesInternal(MapId mapId, Box2 worldAABB) { if (!CheckEnabled()) return default; // Anything that queries these trees should only do so if there are no queued updates, otherwise it can lead to // errors. Currently, there is no easy way to enforce this, but this should work as long as nothing queries the // trees directly: UpdateTreePositions(); var trees = new ValueList<(EntityUid Uid, TTreeComp Comp)>(); if (mapId == MapId.Nullspace) return trees; // TODO LOOKUPS pass in entity query, not entity manager. var state = (EntityManager, trees); _mapSystem.FindGridsIntersecting(mapId, worldAABB, ref state, (EntityUid uid, MapGridComponent grid, ref (EntityManager EntityManager, ValueList<(EntityUid, TTreeComp)> trees) tuple) => { if (tuple.EntityManager.TryGetComponent(uid, out var treeComp)) { tuple.trees.Add((uid, treeComp)); } return true; }, includeMap: false); if (_mapSystem.TryGetMap(mapId, out var mapUid) && TryComp(mapUid, out TTreeComp? mapTreeComp) && mapTreeComp.Tree.Count != 0) // TODO LOOKUPS why does space have an occluder tree? { state.trees.Add((mapUid.Value, mapTreeComp)); } return state.trees; } #region HashSet public HashSet> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true) => QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx); public void QueryAabb(HashSet> results, MapId mapId, Box2 worldBounds, bool approx = true) => QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx); public HashSet> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true) { var state = new HashSet>(); QueryAabb(state, mapId, worldBounds, approx); return state; } [Obsolete("Use Entity variant")] internal void QueryAabb( HashSet> results, MapId mapId, Box2Rotated worldBounds, bool approx = true) { if (!CheckEnabled()) return; foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds); treeComp.Tree.QueryAabb(ref results, static (ref HashSet> state, in ComponentTreeEntry value) => { state.Add(value); return true; }, bounds, approx); } } public void QueryAabb( HashSet> results, MapId mapId, Box2Rotated worldBounds, bool approx = true) { if (!CheckEnabled()) return; foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds); treeComp.Tree.QueryAabb(ref results, static (ref HashSet> state, in ComponentTreeEntry value) => { state.Add(value); return true; }, bounds, approx); } } #endregion #region List public void QueryAabb(List> results, MapId mapId, Box2 worldBounds, bool approx = true) => QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx); public void QueryAabb( List> results, MapId mapId, Box2Rotated worldBounds, bool approx = true) { if (!CheckEnabled()) return; foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds); treeComp.Tree.QueryAabb(ref results, static (ref List> state, in ComponentTreeEntry value) => { state.Add(value); return true; }, bounds, approx); } } #endregion 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) { if (!CheckEnabled()) return; foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds); treeComp.Tree.QueryAabb(ref state, callback, bounds, approx); } } #endregion #region Rays [Obsolete("use IntersectRay")] public List IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength, TState state, Func predicate, bool returnOnFirstHit = true) { var list = new List(); if (!returnOnFirstHit) { IntersectRay(list, mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s)); return list; } var result = IntersectRay(mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s)); if (result != null) list.Add(result.Value); return list; } /// /// Perform a ray intersection and return on the first hit. /// public RayCastResults? IntersectRay(MapId mapId, in Ray ray, float length) { var state = new QueryState(length); IntersectRayInternal(mapId, in ray, length, ref state, QueryCallback); return state.Result; } /// /// Perform a ray intersection and populate a provided list of results. /// public void IntersectRay(List results, MapId mapId, in Ray ray, float maxLength) { results.Clear(); var state = new QueryState(maxLength, results); IntersectRayInternal(mapId, in ray, maxLength, ref state, QueryCallback); } /// /// Perform a ray intersection with a predicate and return on the first hit. /// public RayCastResults? IntersectRay( MapId mapId, in Ray ray, float length, TState predicateState, Func, TState, bool> ignore) { var state = new QueryState(new(length), predicateState, ignore); IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback); return state.Inner.Result; } /// /// Perform a ray intersection with a predicate and populate a provided list of results. /// public void IntersectRay( List results, MapId mapId, in Ray ray, float length, TState predicateState, Func, TState, bool> ignore) { var state = new QueryState(new(length, results), predicateState, ignore); IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback); } private void IntersectRayInternal( MapId mapId, in Ray ray, float maxLength, ref TState state, DynamicTree>.RayQueryCallbackDelegate callback) where TState : IDone { if (mapId == MapId.Nullspace) return; if (!CheckEnabled()) return; var endPoint = ray.Position + ray.Direction * maxLength; var worldBox = new Box2(Vector2.Min(ray.Position, endPoint), Vector2.Max(ray.Position, endPoint)); foreach (var (treeUid, comp) in GetIntersectingTrees(mapId, worldBox)) { var (_, treeRot, matrix) = XformSystem.GetWorldPositionRotationInvMatrix(treeUid); var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction); var treeRay = new Ray(Vector2.Transform(ray.Position, matrix), relativeAngle); comp.Tree.QueryRay(ref state, callback, treeRay); if (state.Done) return; } } static bool QueryCallback( ref QueryState state, in ComponentTreeEntry value, in Vector2 point, float dist) { if (dist > state.MaxLength) return true; if (state.ReturnOnFirstHit) { state.Result = new RayCastResults(dist, point, value.Uid); return false; } state.List.Add(new RayCastResults(dist, point, value.Uid)); return true; } private static bool PredicateQueryCallback( ref QueryState state, in ComponentTreeEntry value, in Vector2 point, float dist) { if (dist > state.Inner.MaxLength) return true; if (state.Ignore.Invoke(value, state.PredicateState)) return true; if (state.Inner.ReturnOnFirstHit) { state.Inner.Result = new RayCastResults(dist, point, value.Uid); return false; } state.Inner.List.Add(new RayCastResults(dist, point, value.Uid)); return true; } private struct QueryState( QueryState inner, TPredicateState predicateState, Func, TPredicateState, bool> ignore) : IDone { public readonly TPredicateState PredicateState = predicateState; public readonly Func, TPredicateState, bool> Ignore = ignore; public QueryState Inner = inner; public bool Done => Inner.Done; } private struct QueryState(float maxLength, List? list = null) : IDone { public readonly float MaxLength = maxLength; [MemberNotNullWhen(false, nameof(List))] public readonly bool ReturnOnFirstHit => List == null; public readonly List? List = list; public RayCastResults? Result; public bool Done => Result != null; } private interface IDone { bool Done { get; } } #endregion }