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
}