using System;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
[Flags]
public enum LookupFlags : byte
{
None = 0,
///
/// Should we use the approximately intersecting entities or check tighter bounds.
///
Approximate = 1 << 0,
///
/// Also return entities from an anchoring query.
///
Anchored = 1 << 1,
///
/// Include entities that are currently in containers.
///
Contained = 1 << 2,
// IncludeGrids = 1 << 2,
// IncludePhysics (whenever it gets split off)
// Include maps
}
public sealed partial class EntityLookupSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
///
/// Returns all non-grid entities. Consider using your own flags if you wish for a faster query.
///
public const LookupFlags DefaultFlags = LookupFlags.Contained | LookupFlags.Anchored;
private const int GrowthRate = 256;
private const float PointEnlargeRange = .00001f / 2;
///
/// Like RenderTree we need to enlarge our lookup range for EntityLookupComponent as an entity is only ever on
/// 1 EntityLookupComponent at a time (hence it may overlap without another lookup).
///
private float _lookupEnlargementRange;
public override void Initialize()
{
base.Initialize();
var configManager = IoCManager.Resolve();
configManager.OnValueChanged(CVars.LookupEnlargementRange, value => _lookupEnlargementRange = value, true);
SubscribeLocalEvent(OnMove);
SubscribeLocalEvent(OnRotate);
SubscribeLocalEvent(OnParentChange);
SubscribeLocalEvent(OnAnchored);
SubscribeLocalEvent(OnContainerInsert);
SubscribeLocalEvent(OnContainerRemove);
SubscribeLocalEvent(OnLookupAdd);
SubscribeLocalEvent(OnLookupShutdown);
SubscribeLocalEvent(OnGridInit);
SubscribeLocalEvent(OnTerminate);
EntityManager.EntityInitialized += OnEntityInit;
SubscribeLocalEvent(OnMapCreated);
}
public override void Shutdown()
{
base.Shutdown();
EntityManager.EntityInitialized -= OnEntityInit;
}
///
/// Updates the entity's AABB. Uses
///
[UsedImplicitly]
public void UpdateBounds(EntityUid uid, TransformComponent? xform = null, MetaDataComponent? meta = null)
{
if (_container.IsEntityInContainer(uid, meta))
return;
var xformQuery = GetEntityQuery();
if (!xformQuery.Resolve(uid, ref xform) || xform.Anchored)
return;
var lookup = GetLookup(uid, xform, xformQuery);
if (lookup == null) return;
var coordinates = _transform.GetMoverCoordinates(xform.Coordinates, xformQuery);
var lookupRotation = _transform.GetWorldRotation(lookup.Owner, xformQuery);
// If we're contained then LocalRotation should be 0 anyway.
var aabb = GetAABB(xform.Owner, coordinates.Position, _transform.GetWorldRotation(xform, xformQuery) - lookupRotation, xform, xformQuery);
// TODO: Only container children need updating so could manually do this slightly better.
AddToEntityTree(lookup, xform, aabb, xformQuery, lookupRotation);
}
private void OnAnchored(ref AnchorStateChangedEvent args)
{
// This event needs to be handled immediately as anchoring is handled immediately
// and any callers may potentially get duplicate entities that just changed state.
if (args.Anchored)
{
RemoveFromEntityTree(args.Entity);
}
else if (EntityManager.TryGetComponent(args.Entity, out MetaDataComponent? meta) && meta.EntityLifeStage < EntityLifeStage.Terminating)
{
var xformQuery = GetEntityQuery();
var xform = xformQuery.GetComponent(args.Entity);
var lookup = GetLookup(args.Entity, xform, xformQuery);
if (lookup == null)
throw new InvalidOperationException();
var coordinates = _transform.GetMoverCoordinates(xform.Coordinates, xformQuery);
var lookupRotation = _transform.GetWorldRotation(lookup.Owner, xformQuery);
DebugTools.Assert(coordinates.EntityId == lookup.Owner);
// If we're contained then LocalRotation should be 0 anyway.
var aabb = GetAABB(args.Entity, coordinates.Position, _transform.GetWorldRotation(xform, xformQuery) - lookupRotation, xform, xformQuery);
AddToEntityTree(lookup, xform, aabb, xformQuery, lookupRotation);
}
// else -> the entity is terminating. We can ignore this un-anchor event, as this entity will be removed by the tree via OnEntityDeleted.
}
#region DynamicTree
private void OnLookupShutdown(EntityUid uid, EntityLookupComponent component, ComponentShutdown args)
{
component.Tree.Clear();
}
private void OnGridInit(GridInitializeEvent ev)
{
EntityManager.EnsureComponent(ev.EntityUid);
}
private void OnLookupAdd(EntityUid uid, EntityLookupComponent component, ComponentAdd args)
{
int capacity;
if (EntityManager.TryGetComponent(uid, out TransformComponent? xform))
{
capacity = (int) Math.Min(256, Math.Ceiling(xform.ChildCount / (float) GrowthRate) * GrowthRate);
}
else
{
capacity = 256;
}
component.Tree = new DynamicTree(
GetTreeAABB,
capacity: capacity,
growthFunc: x => x == GrowthRate ? GrowthRate * 8 : x * 2
);
}
private void OnMapCreated(MapChangedEvent eventArgs)
{
if(eventArgs.Destroyed)
return;
if (eventArgs.Map == MapId.Nullspace) return;
EntityManager.EnsureComponent(_mapManager.GetMapEntityId(eventArgs.Map));
}
private Box2 GetTreeAABB(in EntityUid entity)
{
// TODO: Should feed in AABB to lookup so it's not enlarged unnecessarily
var aabb = GetWorldAABB(entity);
var xformQuery = GetEntityQuery();
var tree = GetLookup(entity, xformQuery);
if (tree == null)
return aabb;
return xformQuery.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(aabb);
}
#endregion
#region Entity events
private void OnTerminate(ref EntityTerminatingEvent args)
{
RemoveFromEntityTree(args.Owner, false);
}
private void OnEntityInit(object? sender, EntityUid uid)
{
if (_container.IsEntityInContainer(uid)) return;
var xformQuery = GetEntityQuery();
if (!xformQuery.TryGetComponent(uid, out var xform) ||
xform.Anchored) return;
if (_mapManager.IsMap(uid) ||
_mapManager.IsGrid(uid)) return;
var lookup = GetLookup(uid, xform, xformQuery);
// If nullspace or the likes.
if (lookup == null) return;
var coordinates = _transform.GetMoverCoordinates(xform.Coordinates, xformQuery);
DebugTools.Assert(coordinates.EntityId == lookup.Owner);
var lookupRotation = _transform.GetWorldRotation(lookup.Owner, xformQuery);
// If we're contained then LocalRotation should be 0 anyway.
var aabb = GetAABB(uid, coordinates.Position, _transform.GetWorldRotation(xform, xformQuery) - lookupRotation, xform, xformQuery);
// Any child entities should be handled by their own OnEntityInit
AddToEntityTree(lookup, xform, aabb, xformQuery, lookupRotation, false);
}
private void OnMove(ref MoveEvent args)
{
UpdatePosition(args.Sender, args.Component);
}
private void OnRotate(ref RotateEvent args)
{
UpdatePosition(args.Sender, args.Component);
}
private void UpdatePosition(EntityUid uid, TransformComponent xform)
{
// Even if the entity is contained it may have children that aren't so we still need to update.
if (!CanMoveUpdate(uid)) return;
var xformQuery = GetEntityQuery();
var lookup = GetLookup(uid, xform, xformQuery);
if (lookup == null) return;
var coordinates = _transform.GetMoverCoordinates(xform.Coordinates, xformQuery);
var lookupRotation = _transform.GetWorldRotation(lookup.Owner, xformQuery);
var aabb = GetAABB(uid, coordinates.Position, _transform.GetWorldRotation(xform) - lookupRotation, xform, xformQuery);
AddToEntityTree(lookup, xform, aabb, xformQuery, lookupRotation);
}
private bool CanMoveUpdate(EntityUid uid)
{
return !_mapManager.IsMap(uid) &&
!_mapManager.IsGrid(uid) &&
!_container.IsEntityInContainer(uid);
}
private void OnParentChange(ref EntParentChangedMessage args)
{
var meta = MetaData(args.Entity);
// Parent change gets raised after container insert so we'll just drop it and let OnContainerInsert handle.
if (meta.EntityLifeStage < EntityLifeStage.Initialized ||
_container.IsEntityInContainer(args.Entity, meta) ||
_mapManager.IsGrid(args.Entity) ||
_mapManager.IsMap(args.Entity)) return;
var xformQuery = GetEntityQuery();
var xform = xformQuery.GetComponent(args.Entity);
EntityLookupComponent? oldLookup = null;
if (args.OldParent != null)
{
oldLookup = GetLookup(args.OldParent.Value, xformQuery);
}
var newLookup = GetLookup(args.Entity, xform, xformQuery);
// If parent is the same then no need to do anything as position should stay the same.
if (oldLookup == newLookup) return;
RemoveFromEntityTree(oldLookup, xform, xformQuery);
if (newLookup != null)
AddToEntityTree(newLookup, xform, xformQuery, _transform.GetWorldRotation(newLookup.Owner, xformQuery));
}
private void OnContainerRemove(EntRemovedFromContainerMessage ev)
{
// This gets handled before parent change so that should just early out from lookups matching.
var xformQuery = GetEntityQuery();
var xform = xformQuery.GetComponent(ev.Entity);
var lookup = GetLookup(ev.Entity, xform, xformQuery);
if (lookup == null) return;
AddToEntityTree(lookup, xform, xformQuery, _transform.GetWorldRotation(lookup.Owner, xformQuery));
}
private void OnContainerInsert(EntInsertedIntoContainerMessage ev)
{
var xformQuery = GetEntityQuery();
var xform = xformQuery.GetComponent(ev.Entity);
var lookup = GetLookup(ev.Entity, xform, xformQuery);
RemoveFromEntityTree(lookup, xform, xformQuery);
}
private void AddToEntityTree(
EntityLookupComponent lookup,
TransformComponent xform,
EntityQuery xformQuery,
Angle lookupRotation,
bool recursive = true)
{
var coordinates = _transform.GetMoverCoordinates(xform.Coordinates, xformQuery);
// If we're contained then LocalRotation should be 0 anyway.
var aabb = GetAABB(xform.Owner, coordinates.Position, _transform.GetWorldRotation(xform, xformQuery) - lookupRotation, xform, xformQuery);
AddToEntityTree(lookup, xform, aabb, xformQuery, lookupRotation, recursive);
}
private void AddToEntityTree(
EntityLookupComponent? lookup,
TransformComponent xform,
Box2 aabb,
EntityQuery xformQuery,
Angle lookupRotation,
bool recursive = true)
{
// If entity is in nullspace then no point keeping track of data structure.
if (lookup == null) return;
if (!xform.Anchored)
lookup.Tree.AddOrUpdate(xform.Owner, aabb);
var childEnumerator = xform.ChildEnumerator;
if (xform.ChildCount == 0 || !recursive) return;
// If they're in a container then don't add to entitylookup due to the additional cost.
// It's cheaper to just query these components at runtime given PVS no longer uses EntityLookupSystem.
if (EntityManager.TryGetComponent(xform.Owner, out var conManager))
{
while (childEnumerator.MoveNext(out var child))
{
if (conManager.ContainsEntity(child.Value)) continue;
var childXform = xformQuery.GetComponent(child.Value);
var coordinates = _transform.GetMoverCoordinates(childXform.Coordinates, xformQuery);
// TODO: If we have 0 position and not contained can optimise these further, but future problem.
var childAABB = GetAABBNoContainer(child.Value, coordinates.Position, childXform.WorldRotation - lookupRotation);
AddToEntityTree(lookup, childXform, childAABB, xformQuery, lookupRotation);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
{
var childXform = xformQuery.GetComponent(child.Value);
var coordinates = _transform.GetMoverCoordinates(childXform.Coordinates, xformQuery);
// TODO: If we have 0 position and not contained can optimise these further, but future problem.
var childAABB = GetAABBNoContainer(child.Value, coordinates.Position, childXform.WorldRotation - lookupRotation);
AddToEntityTree(lookup, childXform, childAABB, xformQuery, lookupRotation);
}
}
}
private void RemoveFromEntityTree(EntityUid uid, bool recursive = true)
{
var xformQuery = GetEntityQuery();
var xform = xformQuery.GetComponent(uid);
var lookup = GetLookup(uid, xform, xformQuery);
RemoveFromEntityTree(lookup, xform, xformQuery, recursive);
}
///
/// Recursively iterates through this entity's children and removes them from the entitylookupcomponent.
///
private void RemoveFromEntityTree(EntityLookupComponent? lookup, TransformComponent xform, EntityQuery xformQuery, bool recursive = true)
{
// TODO: Move this out of the loop
if (lookup == null) return;
lookup.Tree.Remove(xform.Owner);
if (!recursive) return;
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
RemoveFromEntityTree(lookup, xformQuery.GetComponent(child.Value), xformQuery);
}
}
#endregion
private EntityLookupComponent? GetLookup(EntityUid entity, EntityQuery xformQuery)
{
var xform = xformQuery.GetComponent(entity);
return GetLookup(entity, xform, xformQuery);
}
private EntityLookupComponent? GetLookup(EntityUid uid, TransformComponent xform, EntityQuery xformQuery)
{
if (xform.MapID == MapId.Nullspace)
return null;
var parent = xform.ParentUid;
var lookupQuery = GetEntityQuery();
// If we're querying a map / grid just return it directly.
if (lookupQuery.TryGetComponent(uid, out var lookup))
{
return lookup;
}
while (parent.IsValid())
{
if (lookupQuery.TryGetComponent(parent, out var comp)) return comp;
parent = xformQuery.GetComponent(parent).ParentUid;
}
return null;
}
#region Bounds
///
/// Get the AABB of an entity with the supplied position and angle. Tries to consider if the entity is in a container.
///
internal Box2 GetAABB(EntityUid uid, Vector2 position, Angle angle, TransformComponent xform, EntityQuery xformQuery)
{
// If we're in a container then we just use the container's bounds.
if (_container.TryGetOuterContainer(uid, xform, out var container, xformQuery))
{
return GetAABBNoContainer(container.Owner, position, angle);
}
return GetAABBNoContainer(uid, position, angle);
}
///
/// Get the AABB of an entity with the supplied position and angle without considering containers.
///
private Box2 GetAABBNoContainer(EntityUid uid, Vector2 position, Angle angle)
{
// DebugTools.Assert(!_container.IsEntityInContainer(uid, xform));
Box2 localAABB;
var transform = new Transform(position, angle);
if (TryComp(uid, out var worldLookup))
{
localAABB = worldLookup.GetAABB(transform);
}
else
{
localAABB = new Box2Rotated(new Box2(transform.Position, transform.Position), transform.Quaternion2D.Angle, transform.Position).CalcBoundingBox();
}
return localAABB;
}
public Box2 GetWorldAABB(EntityUid uid, TransformComponent? xform = null)
{
var xformQuery = GetEntityQuery();
xform ??= xformQuery.GetComponent(uid);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
return GetAABB(uid, worldPos, worldRot, xform, xformQuery);
}
#endregion
}
}