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
}