Files
RobustToolbox/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs
T

647 lines
21 KiB
C#

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;
/// <summary>
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
/// </summary>
[UsedImplicitly]
public abstract partial class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
where TComp : Component, IComponentTreeEntry<TComp>
{
[Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!;
[Dependency] protected SharedTransformSystem XformSystem = default!;
[Dependency] private SharedMapSystem _mapSystem = default!;
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
protected EntityQuery<TComp> Query;
/// <summary>
/// Whether this lookup tree should even be enabled.
/// </summary>
/// <remarks>
/// This can be used to disable some trees if they are not required, which helps improve performance a bit.
/// </remarks>
protected virtual bool Enabled => true;
private bool _initialized;
/// <summary>
/// If true, this system will update the tree positions every frame update. See also <see cref="DoTickUpdate"/>. Some systems may need to do both.
/// </summary>
protected abstract bool DoFrameUpdate { get; }
/// <summary>
/// If true, this system will update the tree positions every tick update. See also <see cref="DoFrameUpdate"/>. Some systems may need to do both.
/// </summary>
protected abstract bool DoTickUpdate { get; }
/// <summary>
/// Initial tree capacity. Note that client-side trees will remove entities as they leave PVS range.
/// </summary>
protected virtual int InitialCapacity { get; } = 256;
/// <summary>
/// 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.
/// </summary>
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<MapCreatedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
// Yipeee point light
if (!typeof(TComp).IsAbstract)
{
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
SubscribeLocalEvent<TComp, ComponentRemove>(OnCompRemoved);
Query = GetEntityQuery<TComp>();
}
if (Recursive)
{
_recursiveMoveSys.OnTreeRecursiveMove += HandleRecursiveMove;
_recursiveMoveSys.AddSubscription();
}
else
{
// TODO EXCEPTION TOLERANCE
// Ensure lookup trees update before content code handles move events.
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
}
SubscribeLocalEvent<TTreeComp, EntityTerminatingEvent>(OnTerminating);
SubscribeLocalEvent<TTreeComp, ComponentAdd>(OnTreeAdd);
SubscribeLocalEvent<TTreeComp, ComponentRemove>(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<TComp> 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<TTreeComp>(e.Uid);
}
private void MapManagerOnGridCreated(GridInitializeEvent ev)
{
EnsureComp<TTreeComp>(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();
}
/// <summary>
/// Processes any pending position updates. Note that this should generally always get run before directly
/// querying a tree.
/// </summary>
public void UpdateTreePositions()
{
try
{
if (!CheckEnabled())
return;
if (_updateQueue.Count == 0)
return;
var trees = GetEntityQuery<TTreeComp>();
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<TComp> 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<TComp> 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<TTreeComp>(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<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
public void QueryAabb(HashSet<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true)
{
var state = new HashSet<ComponentTreeEntry<TComp>>();
QueryAabb(state, mapId, worldBounds, approx);
return state;
}
[Obsolete("Use Entity<T> variant")]
internal void QueryAabb(
HashSet<ComponentTreeEntry<TComp>> 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<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
}
public void QueryAabb(
HashSet<Entity<TComp, TransformComponent>> 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<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
}
#endregion
#region List
public void QueryAabb(List<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
public void QueryAabb(
List<Entity<TComp, TransformComponent>> 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<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
}
#endregion
public void QueryAabb<TState>(
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
MapId mapId,
Box2 worldBounds,
bool approx = true)
{
QueryAabb(ref state, callback, mapId, new Box2Rotated(worldBounds, default, default), approx);
}
public void QueryAabb<TState>(
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> 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<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
{
var list = new List<RayCastResults>();
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;
}
/// <summary>
/// Perform a ray intersection and return on the first hit.
/// </summary>
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;
}
/// <summary>
/// Perform a ray intersection and populate a provided list of results.
/// </summary>
public void IntersectRay(List<RayCastResults> results, MapId mapId, in Ray ray, float maxLength)
{
results.Clear();
var state = new QueryState(maxLength, results);
IntersectRayInternal(mapId, in ray, maxLength, ref state, QueryCallback);
}
/// <summary>
/// Perform a ray intersection with a predicate and return on the first hit.
/// </summary>
public RayCastResults? IntersectRay<TState>(
MapId mapId,
in Ray ray,
float length,
TState predicateState,
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
{
var state = new QueryState<TState>(new(length), predicateState, ignore);
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
return state.Inner.Result;
}
/// <summary>
/// Perform a ray intersection with a predicate and populate a provided list of results.
/// </summary>
public void IntersectRay<TState>(
List<RayCastResults> results,
MapId mapId,
in Ray ray,
float length,
TState predicateState,
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
{
var state = new QueryState<TState>(new(length, results), predicateState, ignore);
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
}
private void IntersectRayInternal<TState>(
MapId mapId,
in Ray ray,
float maxLength,
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.RayQueryCallbackDelegate<TState> 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<TComp> 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<TState>(
ref QueryState<TState> state,
in ComponentTreeEntry<TComp> 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<TPredicateState>(
QueryState inner,
TPredicateState predicateState,
Func<Entity<TComp, TransformComponent>, TPredicateState, bool> ignore) : IDone
{
public readonly TPredicateState PredicateState = predicateState;
public readonly Func<Entity<TComp, TransformComponent>, TPredicateState, bool> Ignore = ignore;
public QueryState Inner = inner;
public bool Done => Inner.Done;
}
private struct QueryState(float maxLength, List<RayCastResults>? list = null) : IDone
{
public readonly float MaxLength = maxLength;
[MemberNotNullWhen(false, nameof(List))]
public readonly bool ReturnOnFirstHit => List == null;
public readonly List<RayCastResults>? List = list;
public RayCastResults? Result;
public bool Done => Result != null;
}
private interface IDone
{
bool Done { get; }
}
#endregion
}