using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Shapes; using Robust.Shared.Physics.Systems; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public sealed partial class EntityLookupSystem { #region Private private void RecursiveAdd(EntityUid uid, ref ValueList> toAdd, EntityQuery query) where T : IComponent { var xform = _xformQuery.GetComponent(uid); foreach (var child in xform._children) { if (query.TryGetComponent(child, out var compies)) { toAdd.Add((child, compies)); } RecursiveAdd(child, ref toAdd, query); } } private void AddContained(HashSet> intersecting, LookupFlags flags, EntityQuery query) where T : IComponent { if ((flags & LookupFlags.Contained) == 0x0) return; var toAdd = new ValueList>(); foreach (var comp in intersecting) { if (!_containerQuery.TryGetComponent(comp, out var conManager)) continue; foreach (var con in _container.GetAllContainers(comp, conManager)) { foreach (var contained in con.ContainedEntities) { if (query.TryGetComponent(contained, out var compies)) { toAdd.Add((contained, compies)); } RecursiveAdd(contained, ref toAdd, query); } } } foreach (var uid in toAdd.Span) { intersecting.Add(uid); } } /// /// Common method to determine if an entity overlaps the specified shape. /// private bool IsIntersecting(MapId mapId, EntityUid uid, TransformComponent xform, TShape shape, Transform shapeTransform, Box2 worldAABB, LookupFlags flags) where TShape : IPhysShape { var (entPos, entRot) = _transform.GetWorldPositionRotation(xform); if (xform.MapID != mapId || !worldAABB.Contains(entPos) || ((flags & LookupFlags.Contained) == 0x0 && _container.IsEntityOrParentInContainer(uid, _metaQuery.GetComponent(uid), xform))) { return false; } if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) { var transform = new Transform(entPos, entRot); bool anyFixture = false; var sensors = (flags & LookupFlags.Sensors) != 0x0; foreach (var fixture in fixtures.Fixtures.Values) { if (!sensors && !fixture.Hard) continue; anyFixture = true; for (var i = 0; i < fixture.Shape.ChildCount; i++) { if (_manifoldManager.TestOverlap(shape, 0, fixture.Shape, i, shapeTransform, transform)) { return true; } } } if (anyFixture) return false; } if (!_fixtures.TestPoint(shape, shapeTransform, entPos)) return false; return true; } private void AddLocalEntitiesIntersecting( EntityUid lookupUid, HashSet> intersecting, Box2 localAABB, LookupFlags flags, EntityQuery query, BroadphaseComponent? lookup = null) where T : IComponent { if (!_broadQuery.Resolve(lookupUid, ref lookup)) return; var polygon = new SlimPolygon(localAABB); AddEntitiesIntersecting(lookupUid, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, query, lookup); } private void AddEntitiesIntersecting( EntityUid lookupUid, HashSet> intersecting, TShape shape, Box2 localAABB, Transform localTransform, LookupFlags flags, EntityQuery query, BroadphaseComponent? lookup = null) where T : IComponent where TShape : IPhysShape { if (!_broadQuery.Resolve(lookupUid, ref lookup)) return; var state = new QueryState( intersecting, shape, localTransform, _fixtures, _physics, _manifoldManager, query, _fixturesQuery, (flags & LookupFlags.Sensors) != 0, (flags & LookupFlags.Approximate) != 0x0 ); if ((flags & LookupFlags.Dynamic) != 0x0) { lookup.DynamicTree.QueryAabb(ref state, PhysicsQuery, localAABB, true); } if ((flags & LookupFlags.Static) != 0x0) { lookup.StaticTree.QueryAabb(ref state, PhysicsQuery, localAABB, true); } if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries) { lookup.StaticSundriesTree.QueryAabb(ref state, SundriesQuery, localAABB, true); } if ((flags & LookupFlags.Sundries) != 0x0) { lookup.SundriesTree.QueryAabb(ref state, SundriesQuery, localAABB, true); } return; static bool PhysicsQuery(ref QueryState state, in FixtureProxy value) { if (!state.Sensors && !value.Fixture.Hard) return true; if (!state.Query.TryGetComponent(value.Entity, out var comp)) return true; if (!state.Approximate) { var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value.Entity); if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform)) { return true; } } state.Intersecting.Add((value.Entity, comp)); return true; } static bool SundriesQuery(ref QueryState state, in EntityUid value) { if (!state.Query.TryGetComponent(value, out var comp)) return true; if (state.Approximate) { state.Intersecting.Add((value, comp)); return true; } var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value); if (state.FixturesQuery.TryGetComponent(value, out var fixtures)) { bool anyFixture = false; foreach (var fixture in fixtures.Fixtures.Values) { if (!state.Sensors && !fixture.Hard) continue; anyFixture = true; for (var i = 0; i < fixture.Shape.ChildCount; i++) { if (state.Manifolds.TestOverlap(state.Shape, 0, fixture.Shape, i, state.Transform, intersectingTransform)) { state.Intersecting.Add((value, comp)); return true; } } } if (anyFixture) return true; } if (state.Fixtures.TestPoint(state.Shape, state.Transform, intersectingTransform.Position)) state.Intersecting.Add((value, comp)); return true; } } private bool AnyLocalComponentsIntersecting( EntityUid lookupUid, Box2 localAABB, LookupFlags flags, EntityQuery query, EntityUid? ignored = null, BroadphaseComponent? lookup = null) where T : IComponent { if (!_broadQuery.Resolve(lookupUid, ref lookup)) return false; var polygon = new SlimPolygon(localAABB); var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid); var transform = new Transform(lookupPos, lookupRot); var result = AnyComponentsIntersecting(lookupUid, polygon, localAABB, transform, flags, query, ignored, lookup); return result; } private bool AnyComponentsIntersecting( EntityUid lookupUid, TShape shape, Box2 localAABB, Transform shapeTransform, LookupFlags flags, EntityQuery query, EntityUid? ignored = null, BroadphaseComponent? lookup = null) where T : IComponent where TShape : IPhysShape { /* * Unfortunately this is split from the other query as we can short-circuit here, hence the code duplication. * Alternatively you can make them both use callbacks. */ if (!_broadQuery.Resolve(lookupUid, ref lookup)) return false; var state = new AnyQueryState(false, ignored, shape, shapeTransform, _fixtures, _physics, _manifoldManager, query, _fixturesQuery, flags); if ((flags & LookupFlags.Dynamic) != 0x0) { lookup.DynamicTree.QueryAabb(ref state, PhysicsQuery, localAABB, true); if (state.Found) return true; } if ((flags & LookupFlags.Static) != 0x0) { lookup.StaticTree.QueryAabb(ref state, PhysicsQuery, localAABB, true); if (state.Found) return true; } if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries) { lookup.StaticSundriesTree.QueryAabb(ref state, SundriesQuery, localAABB, true); if (state.Found) return true; } if ((flags & LookupFlags.Sundries) != 0x0) { lookup.SundriesTree.QueryAabb(ref state, SundriesQuery, localAABB, true); } return state.Found; static bool PhysicsQuery(ref AnyQueryState state, in FixtureProxy value) { if (value.Entity == state.Ignored) return true; if (!state.Query.HasComponent(value.Entity)) return true; var approx = (state.Flags & LookupFlags.Approximate) != 0x0; if (!approx) { var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity); if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform)) { return true; } } state.Found = true; return false; } static bool SundriesQuery(ref AnyQueryState state, in EntityUid value) { if (state.Ignored == value) return true; if (!state.Query.HasComponent(value)) return true; var approx = (state.Flags & LookupFlags.Approximate) != 0x0; if (approx) { state.Found = true; return false; } var intersectingTransform = state.Physics.GetPhysicsTransform(value); if (state.FixturesQuery.TryGetComponent(value, out var fixtures)) { var sensors = (state.Flags & LookupFlags.Sensors) != 0x0; bool anyFixture = false; foreach (var fixture in fixtures.Fixtures.Values) { if (!sensors && !fixture.Hard) continue; anyFixture = true; for (var i = 0; i < fixture.Shape.ChildCount; i++) { if (state.Manifolds.TestOverlap(state.Shape, 0, fixture.Shape, i, state.Transform, intersectingTransform)) { state.Found = true; return false; } } } if (anyFixture) return true; } if (state.Fixtures.TestPoint(state.Shape, state.Transform, intersectingTransform.Position)) { state.Found = true; return false; } return true; } } /// /// Should we just iterate every component and check position or do bounds checks. /// private bool UseBoundsQuery(Type type, float area) { return Count(type) > area; } /// /// Should we just iterate every component and check position or do bounds checks. /// private bool UseBoundsQuery(float area) where T : IComponent { // If the component has a low count we'll just do an estimate if it's faster to iterate every comp directly // Might be useful to have some way to expose this to content? // For now we'll assume 1 entity per metre. return Count() > area; } #endregion // Like .Queries but works with components #region Box2 public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, EntityUid? ignored = null, LookupFlags flags = DefaultFlags) { var polygon = new SlimPolygon(worldAABB); var transform = Physics.Transform.Empty; var result = AnyComponentsIntersecting(type, mapId, polygon, transform, ignored, flags); return result; } public bool AnyComponentsIntersecting(Type type, MapId mapId, T shape, Transform shapeTransform, EntityUid? ignored = null, LookupFlags flags = DefaultFlags) where T : IPhysShape { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); if (mapId == MapId.Nullspace) return false; var worldAABB = shape.ComputeAABB(shapeTransform, 0); if (!UseBoundsQuery(type, worldAABB.Height * worldAABB.Width)) { foreach (var (uid, comp) in EntityManager.GetAllComponents(type, true)) { var xform = _xformQuery.GetComponent(uid); if (!IsIntersecting(mapId, uid, xform, shape, shapeTransform, worldAABB, flags)) continue; return true; } } else { var query = EntityManager.GetEntityQuery(type); // Get grid entities var state = (this, worldAABB, flags, query, ignored, found: false); _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref (EntityLookupSystem system, Box2 worldAABB, LookupFlags flags, EntityQuery query, EntityUid? ignored, bool found) tuple) => { if (!tuple.system.AnyLocalComponentsIntersecting(uid, tuple.worldAABB, tuple.flags, tuple.query, tuple.ignored)) return true; tuple.found = true; return false; }, approx: true, includeMap: false); if (state.found) { return true; } // Get map entities var mapUid = _map.GetMapOrInvalid(mapId); AnyLocalComponentsIntersecting(mapUid, worldAABB, flags, query, ignored); } return false; } public void GetEntitiesIntersecting(Type type, MapId mapId, Box2 worldAABB, HashSet> intersecting, LookupFlags flags = DefaultFlags) { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); if (mapId == MapId.Nullspace) return; var polygon = new SlimPolygon(worldAABB); var transform = Physics.Transform.Empty; GetEntitiesIntersecting(type, mapId, polygon, transform, intersecting, flags); } public void GetEntitiesIntersecting(MapId mapId, Box2Rotated worldBounds, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { if (mapId == MapId.Nullspace) return; var polygon = new SlimPolygon(worldBounds); var shapeTransform = Physics.Transform.Empty; GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags); } public void GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { if (mapId == MapId.Nullspace) return; var polygon = new SlimPolygon(worldAABB); var shapeTransform = Physics.Transform.Empty; GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags); } #endregion #region IPhysShape public void GetEntitiesIntersecting(Type type, MapId mapId, T shape, Transform shapeTransform, HashSet> intersecting, LookupFlags flags = DefaultFlags) where T : IPhysShape { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); if (mapId == MapId.Nullspace) return; var worldAABB = shape.ComputeAABB(shapeTransform, 0); if (!UseBoundsQuery(type, worldAABB.Height * worldAABB.Width)) { foreach (var (uid, comp) in EntityManager.GetAllComponents(type, true)) { var xform = _xformQuery.GetComponent(uid); if (!IsIntersecting(mapId, uid, xform, shape, shapeTransform, worldAABB, flags)) continue; intersecting.Add((uid, comp)); } } else { var query = EntityManager.GetEntityQuery(type); // Get grid entities var state = new GridQueryState(intersecting, shape, shapeTransform, this, _physics, flags, query); _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); var localAabb = state.Shape.ComputeAABB(localTransform, 0); state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, localAabb, localTransform, state.Flags, state.Query); return true; }, approx: true, includeMap: false); var mapUid = _map.GetMapOrInvalid(mapId); var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid); var localAabb = state.Shape.ComputeAABB(localTransform, 0); AddEntitiesIntersecting(mapUid, intersecting, shape, localAabb, localTransform, flags, query); AddContained(intersecting, flags, query); } } public void GetEntitiesIntersecting(MapId mapId, TShape shape, Transform shapeTransform, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent where TShape : IPhysShape { if (mapId == MapId.Nullspace) return; var worldAABB = shape.ComputeAABB(shapeTransform, 0); if (!UseBoundsQuery(worldAABB.Height * worldAABB.Width)) { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var xform)) { if (!IsIntersecting(mapId, uid, xform, shape, shapeTransform, worldAABB, flags)) continue; entities.Add((uid, comp)); } } else { var query = GetEntityQuery(); // Get grid entities var state = new GridQueryState(entities, shape, shapeTransform, this, _physics, flags, query); _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); var localAabb = state.Shape.ComputeAABB(localTransform, 0); state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, localAabb, localTransform, state.Flags, state.Query); return true; }, approx: true, includeMap: false); // Get map entities var mapUid = _map.GetMapOrInvalid(mapId); var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid); var localAabb = state.Shape.ComputeAABB(localTransform, 0); AddEntitiesIntersecting(mapUid, entities, shape, localAabb, localTransform, flags, query); AddContained(entities, flags, query); } } #endregion #region EntityCoordinates public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { var mapPos = _transform.ToMapCoordinates(coordinates); GetEntitiesInRange(mapPos, range, entities, flags); } public HashSet> GetEntitiesInRange(EntityCoordinates coordinates, float range, LookupFlags flags = DefaultFlags) where T : IComponent { var entities = new HashSet>(); GetEntitiesInRange(coordinates, range, entities, flags); return entities; } #endregion #region MapCoordinates public HashSet> GetEntitiesInRange(Type type, MapCoordinates coordinates, float range) { var entities = new HashSet>(); GetEntitiesInRange(type, coordinates, range, entities); return entities; } public void GetEntitiesInRange(Type type, MapCoordinates coordinates, float range, HashSet> entities, LookupFlags flags = DefaultFlags) { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); GetEntitiesInRange(type, coordinates.MapId, coordinates.Position, range, entities, flags); } public void GetEntitiesInRange(MapCoordinates coordinates, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities, flags); } public HashSet> GetEntitiesInRange(MapCoordinates coordinates, float range, LookupFlags flags = DefaultFlags) where T : IComponent { var entities = new HashSet>(); GetEntitiesInRange(coordinates.MapId, coordinates.Position, range, entities, flags); return entities; } #endregion #region MapId public void GetEntitiesInRange(Type type, MapId mapId, Vector2 worldPos, float range, HashSet> entities, LookupFlags flags = DefaultFlags) { DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type)); DebugTools.Assert(range > 0, "Range must be a positive float"); if (mapId == MapId.Nullspace) return; var circle = new PhysShapeCircle(range); var transform = new Transform(worldPos, 0f); GetEntitiesIntersecting(type, mapId, circle, transform, entities, flags); } public void GetEntitiesInRange(MapId mapId, Vector2 worldPos, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { var shape = new PhysShapeCircle(range, worldPos); var transform = Physics.Transform.Empty; GetEntitiesInRange(mapId, shape, transform, entities, flags); } public void GetEntitiesInRange(MapId mapId, TShape shape, Transform transform, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent where TShape : IPhysShape { DebugTools.Assert(shape.Radius > 0, "Range must be a positive float"); if (mapId == MapId.Nullspace) return; GetEntitiesIntersecting(mapId, shape, transform, entities, flags); } /// /// Gets entities with the specified component with the specified map. /// public void GetEntitiesOnMap(MapId mapId, HashSet> entities) where TComp1 : IComponent { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var xform)) { if (xform.MapID != mapId) continue; entities.Add((uid, comp)); } } /// /// Gets entities with the specified component with the specified parent. /// public void GetEntitiesOnMap(MapId mapId, HashSet> entities) where TComp1 : IComponent where TComp2 : IComponent { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform)) { if (xform.MapID != mapId) continue; entities.Add((uid, comp, comp2)); } } #endregion #region Local /// /// Gets the entities intersecting the specified broadphase entity using a local AABB. /// public void GetLocalEntitiesIntersecting( EntityUid gridUid, Vector2i localTile, HashSet> intersecting, float enlargement = TileEnlargementRadius, LookupFlags flags = DefaultFlags, MapGridComponent? gridComp = null) where T : IComponent { ushort tileSize = 1; if (_gridQuery.Resolve(gridUid, ref gridComp)) { tileSize = gridComp.TileSize; } var localAABB = GetLocalBounds(localTile, tileSize); localAABB = localAABB.Enlarged(enlargement); GetLocalEntitiesIntersecting(gridUid, localAABB, intersecting, flags); } /// /// Gets the entities intersecting the specified broadphase entity using a local AABB. /// public void GetLocalEntitiesIntersecting( EntityUid gridUid, Box2 localAABB, HashSet> intersecting, LookupFlags flags = DefaultFlags) where T : IComponent { var query = GetEntityQuery(); AddLocalEntitiesIntersecting(gridUid, intersecting, localAABB, flags, query); AddContained(intersecting, flags, query); } /// /// Gets the entities intersecting the specified broadphase entity using a local AABB. /// public void GetLocalEntitiesIntersecting( Entity grid, Box2 localAABB, HashSet> intersecting, EntityQuery query, LookupFlags flags = DefaultFlags) where T : IComponent { AddLocalEntitiesIntersecting(grid, intersecting, localAABB, flags, query, grid.Comp); AddContained(intersecting, flags, query); } #endregion /// /// Gets entities with the specified component with the specified grid. /// public void GetGridEntities(EntityUid gridUid, HashSet> entities) where TComp1 : IComponent { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var xform)) { if (xform.GridUid != gridUid) continue; entities.Add((uid, comp)); } } /// /// Gets entities with the specified component with the specified parent. /// public void GetChildEntities(EntityUid parentUid, HashSet> entities) where TComp1 : IComponent { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var xform)) { if (xform.ParentUid != parentUid) continue; entities.Add((uid, comp)); } } /// /// Gets entities with the specified component with the specified parent. /// public void GetChildEntities(EntityUid parentUid, HashSet> entities) where TComp1 : IComponent where TComp2 : IComponent { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform)) { if (xform.ParentUid != parentUid) continue; entities.Add((uid, comp, comp2)); } } private readonly record struct GridQueryState( HashSet> Intersecting, TShape Shape, Transform Transform, EntityLookupSystem Lookup, SharedPhysicsSystem Physics, LookupFlags Flags, EntityQuery Query ) where T : IComponent where TShape : IPhysShape; private record struct AnyQueryState( bool Found, EntityUid? Ignored, TShape Shape, Transform Transform, FixtureSystem Fixtures, SharedPhysicsSystem Physics, IManifoldManager Manifolds, EntityQuery Query, EntityQuery FixturesQuery, LookupFlags Flags ) where T : IComponent where TShape : IPhysShape; private readonly record struct QueryState( HashSet> Intersecting, TShape Shape, Transform Transform, FixtureSystem Fixtures, SharedPhysicsSystem Physics, IManifoldManager Manifolds, EntityQuery Query, EntityQuery FixturesQuery, bool Sensors, bool Approximate ) where T : IComponent where TShape : IPhysShape; }