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 Robust.Shared.Collections; using System.Numerics; using Robust.Shared.Map.Components; 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!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; private readonly Queue> _updateQueue = new(); private readonly HashSet _updated = new(); protected EntityQuery Query; /// /// 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) { _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); Query = GetEntityQuery(); } public override void Shutdown() { if (Recursive) { _recursiveMoveSys.OnTreeRecursiveMove -= HandleRecursiveMove; } } #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); 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)); } 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) { 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(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) UpdateTreePositions(); } public override void FrameUpdate(float frameTime) { if (DoFrameUpdate) UpdateTreePositions(); } /// /// Processes any pending position updates. Note that this should generally always get run before directly /// querying a tree. /// public void UpdateTreePositions() { if (_updateQueue.Count == 0) return; var xforms = GetEntityQuery(); var trees = GetEntityQuery(); while (_updateQueue.TryDequeue(out var entry)) { var (comp, xform) = entry; comp.TreeUpdateQueued = false; if (!comp.Running) continue; if (!_updated.Add(entry.Uid)) 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<(EntityUid, TTreeComp)> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds) => GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox()); public IEnumerable<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2 worldAABB) { // 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; var state = (EntityManager, trees); _mapManager.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)) { state.trees.Add((mapUid.Value, mapTreeComp)); } return state.trees; } 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 (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).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 (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds)) { var bounds = XformSystem.GetInvWorldMatrix(tree).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.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 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 }