using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Shared.Containers; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; 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, /// /// Should we query dynamic physics bodies. /// Dynamic = 1 << 1, /// /// Should we query static physics bodies. /// Static = 1 << 2, /// /// Should we query non-collidable physics bodies. /// Sundries = 1 << 3, /// /// Include entities that are currently in containers. /// Contained = 1 << 5, /// /// Do we include non-hard fixtures. /// Sensors = 1 << 6, Uncontained = Dynamic | Static | Sundries | Sensors, StaticSundries = Static | Sundries, All = Contained | Dynamic | Static | Sundries | Sensors } /// /// Raised on entities to try to get its WorldAABB. /// [ByRefEvent] public record struct WorldAABBEvent { public Box2 AABB; } public sealed partial class EntityLookupSystem : EntitySystem { [Dependency] private IManifoldManager _manifoldManager = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netMan = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private FixtureSystem _fixtures = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private SharedTransformSystem _transform = default!; private EntityQuery _broadQuery; private EntityQuery _containerQuery; private EntityQuery _fixturesQuery; private EntityQuery _mapQuery; private EntityQuery _gridQuery; private EntityQuery _metaQuery; private EntityQuery _physicsQuery; private EntityQuery _xformQuery; /// /// 1 x 1 polygons can overlap neighboring tiles (even without considering the polygon skin around them. /// When querying for specific tile fixtures we shrink the bounds by this amount to avoid this overlap. /// public const float TileEnlargementRadius = -PhysicsConstants.PolygonRadius * 4f; /// /// The minimum size an entity is assumed to be for point purposes. /// public const float LookupEpsilon = float.Epsilon * 10f; /// /// Returns all non-grid entities. Consider using your own flags if you wish for a faster query. /// public const LookupFlags DefaultFlags = LookupFlags.All; public override void Initialize() { base.Initialize(); _broadQuery = GetEntityQuery(); _containerQuery = GetEntityQuery(); _fixturesQuery = GetEntityQuery(); _mapQuery = GetEntityQuery(); _gridQuery = GetEntityQuery(); _metaQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); SubscribeLocalEvent(OnBroadphaseTerminating); SubscribeLocalEvent(OnBroadphaseShutdown); SubscribeLocalEvent(OnBroadphaseAdd); SubscribeLocalEvent(OnBroadphaseInit); SubscribeLocalEvent(OnGridAdd); SubscribeLocalEvent(OnMapChange); _transform.OnBeforeMoveEvent += OnMove; EntityManager.EntityInitialized += OnEntityInit; SubscribeLocalEvent(OnBodyTypeChange); SubscribeLocalEvent(OnBodyStartup); SubscribeLocalEvent(OnPhysicsUpdate); } private void OnBodyStartup(EntityUid uid, PhysicsComponent component, ComponentStartup args) { UpdatePhysicsBroadphase(uid, Transform(uid), component); } public override void Shutdown() { base.Shutdown(); EntityManager.EntityInitialized -= OnEntityInit; _transform.OnBeforeMoveEvent -= OnMove; } #region DynamicTree private void OnBroadphaseTerminating(EntityUid uid, BroadphaseComponent component, ref EntityTerminatingEvent args) { var xform = _xformQuery.GetComponent(uid); RemoveChildrenFromTerminatingBroadphase(xform, component); RemComp(uid, component); } private void OnBroadphaseShutdown(EntityUid uid, BroadphaseComponent component, ComponentShutdown args) { var xform = _xformQuery.GetComponent(uid); RemoveChildrenFromTerminatingBroadphase(xform, component); } private void RemoveChildrenFromTerminatingBroadphase(TransformComponent xform, BroadphaseComponent component) { foreach (var child in xform._children) { if (!_xformQuery.TryGetComponent(child, out var childXform)) continue; if (childXform.GridUid == child) continue; if (childXform.Broadphase == null) continue; DebugTools.Assert(childXform.Broadphase.Value.Uid == component.Owner); DebugTools.Assert(!_gridQuery.HasComp(child)); if (childXform.Broadphase.Value.CanCollide && _fixturesQuery.TryGetComponent(child, out var fixtures)) { var tree = childXform.Broadphase.Value.Static ? component.StaticTree : component.DynamicTree; foreach (var fixture in fixtures.Fixtures.Values) { DestroyProxies(fixture, tree); } } childXform.Broadphase = null; RemoveChildrenFromTerminatingBroadphase(childXform, component); } } private void OnMapChange(MapCreatedEvent ev) { if (ev.MapId != MapId.Nullspace) { EnsureComp(ev.Uid); } } private void OnGridAdd(GridAddEvent ev) { // Must be done before initialization as that's when broadphase data starts getting set. EnsureComp(ev.EntityUid); } private void OnBroadphaseAdd(Entity broadphase, ref ComponentAdd args) { broadphase.Comp.StaticSundriesTree = new DynamicTree( (in EntityUid value) => GetTreeAABB(value, broadphase.Owner)); broadphase.Comp.SundriesTree = new DynamicTree( (in EntityUid value) => GetTreeAABB(value, broadphase.Owner)); } private void OnBroadphaseInit(Entity broadphase, ref ComponentInit args) { var xform = Transform(broadphase.Owner); _transform.InitializeMapUid(broadphase.Owner, xform); // If in broadphase then skip this for now because no physicsmap to init physics entities properly // This mainly happens in replays or otherwise spawning grids in nullspace. PhysicsMap is getting dumped in box2c anyway if (xform.MapUid == null) return; var ent = new Entity(broadphase, xform, broadphase); var enumerator = xform.ChildEnumerator; while (enumerator.MoveNext(out var child)) { if (!_broadQuery.HasComp(child)) InitializeChild(child, ent); } } private void InitializeChild( EntityUid child, Entity broadphase) { if (LifeStage(child) <= EntityLifeStage.PreInit) return; var xform = Transform(child); if (xform.Broadphase != null) { if (!xform.Broadphase.Value.IsValid()) return; // Entity is intentionally not on a broadphase (deferred updating?). if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out var oldBroadphase)) { AssertMissingBroadphaseExpected(xform.Broadphase.Value.Uid); ClearFixtureProxiesAfterBroadphaseDeleted(child); xform.Broadphase = null; } else if (oldBroadphase != broadphase.Comp2) { RemoveFromEntityTree(xform.Broadphase.Value.Uid, oldBroadphase,child, xform); } } DebugTools.Assert(xform.Broadphase is not {} x || x.Uid == broadphase.Owner && !x.CanCollide); AddOrUpdateEntityTree( broadphase.Owner, broadphase.Comp2, broadphase.Comp1, child, xform); } private Box2 GetTreeAABB(EntityUid entity, EntityUid tree) { if (!_xformQuery.TryGetComponent(entity, out var xform)) { Log.Error($"Entity tree contains a deleted entity? Tree: {ToPrettyString(tree)}, entity: {entity}"); return default; } if (xform.ParentUid == tree) return GetAABBNoContainer(entity, xform.LocalPosition, xform.LocalRotation); if (!_xformQuery.TryGetComponent(tree, out var treeXform)) { Log.Error($"Entity tree has no transform? Tree Uid: {tree}"); return default; } return _transform.GetInvWorldMatrix(treeXform).TransformBox(GetWorldAABB(entity, xform)); } internal void CreateProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, PhysicsComponent body) { if (!TryGetCurrentBroadphase(xform, out var broadphase)) return; var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); var mapTransform = new Transform(worldPos, worldRot); var (_, broadWorldRot, _, broadInvMatrix) = _transform.GetWorldPositionRotationMatrixWithInv(broadphase.Owner); var broadphaseTransform = new Transform(Vector2.Transform(mapTransform.Position, broadInvMatrix), mapTransform.Quaternion2D.Angle - broadWorldRot); var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree; DebugTools.Assert(fixture.ProxyCount == 0); AddOrMoveProxies((uid, body, xform), fixtureId, fixture, tree, broadphaseTransform); } internal void DestroyProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, BroadphaseComponent broadphase) { DebugTools.AssertNotNull(xform.Broadphase); DebugTools.Assert(xform.Broadphase!.Value.Uid == broadphase.Owner); if (!xform.Broadphase.Value.CanCollide || xform.GridUid == uid) return; if (fixture.ProxyCount == 0) { Log.Warning($"Tried to destroy fixture {fixtureId} on {ToPrettyString(uid)} that already has no proxies?"); return; } var tree = xform.Broadphase.Value.Static ? broadphase.StaticTree : broadphase.DynamicTree; DestroyProxies(fixture, tree); } internal IBroadPhase? GetProxyBroadphaseTree(EntityUid uid, TransformComponent? xform = null) { if (!_xformQuery.Resolve(uid, ref xform, false)) return null; if (xform.Broadphase is not { Valid: true } old) return null; if (!old.CanCollide || xform.GridUid == uid) return null; if (!_broadQuery.TryGetComponent(old.Uid, out var broadphase)) { return null; } return old.Static ? broadphase.StaticTree : broadphase.DynamicTree; } internal void ReleaseProxies(EntityUid uid, Fixture fixture, TransformComponent? xform = null) { ReleaseProxies(fixture, GetProxyBroadphaseTree(uid, xform)); } #endregion #region Entity events private void OnPhysicsUpdate(ref CollisionChangeEvent ev) { var xform = Transform(ev.BodyUid); UpdatePhysicsBroadphase(ev.BodyUid, xform, ev.Body); // ensure that the cached broadphase is correct. DebugTools.Assert(_timing.ApplyingState || xform.Broadphase == null || ev.Body.LifeStage <= ComponentLifeStage.Initializing || !xform.Broadphase.Value.IsValid() || ((xform.Broadphase.Value.CanCollide == ev.Body.CanCollide) && (xform.Broadphase.Value.Static == (ev.Body.BodyType == BodyType.Static)))); } private void OnBodyTypeChange(EntityUid uid, TransformComponent xform, ref PhysicsBodyTypeChangedEvent args) { // only matters if we swapped from static to non-static or vice versa. if (args.Old != BodyType.Static && args.New != BodyType.Static) return; UpdatePhysicsBroadphase(uid, xform, args.Component); } private void UpdatePhysicsBroadphase(EntityUid uid, TransformComponent xform, PhysicsComponent body) { if (body.LifeStage <= ComponentLifeStage.Initializing) return; if (xform.GridUid == uid) return; DebugTools.Assert(!HasComp(uid)); if (xform.Broadphase is not { Valid: true } old) return; // entity is not on any broadphase if (!_broadQuery.TryGetComponent(old.Uid, out var broadphase)) { ClearFixtureProxiesAfterBroadphaseDeleted(uid); xform.Broadphase = null; return; // broadphase probably got deleted. } xform.Broadphase = null; // remove from the old broadphase if (old.CanCollide) { if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) RemoveBroadTree(broadphase, fixtures, old.Static); } else (old.Static ? broadphase.StaticSundriesTree : broadphase.SundriesTree).Remove(uid); // Add to new broadphase if (body.CanCollide) { if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) AddPhysicsTree(uid, old.Uid, broadphase, xform, body, fixtures); } else AddOrUpdateSundriesTree(old.Uid, broadphase, uid, xform, body.BodyType == BodyType.Static); } private void RemoveBroadTree(BroadphaseComponent lookup, FixturesComponent manager, bool staticBody) { var tree = staticBody ? lookup.StaticTree : lookup.DynamicTree; foreach (var fixture in manager.Fixtures.Values) { DestroyProxies(fixture, tree); } } internal void DestroyProxies(Fixture fixture, IBroadPhase tree) { ReleaseProxies(fixture, tree); } internal void ReleaseProxies(Fixture fixture, IBroadPhase? tree) { var buffer = _physics.MoveBuffer; for (var i = 0; i < fixture.ProxyCount; i++) { var proxy = fixture.Proxies[i]; tree?.RemoveProxy(proxy.ProxyId); buffer.Remove(proxy); } fixture.ProxyCount = 0; fixture.Proxies = Array.Empty(); } private void AddPhysicsTree(EntityUid uid, EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent xform, PhysicsComponent body, FixturesComponent fixtures) { var broadphaseXform = _xformQuery.GetComponent(broadUid); if (broadphaseXform.MapID == MapId.Nullspace) return; AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, xform, body, fixtures); } private void AddOrUpdatePhysicsTree( EntityUid uid, EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent broadphaseXform, TransformComponent xform, PhysicsComponent body, FixturesComponent manager) { DebugTools.Assert(!_container.IsEntityOrParentInContainer(body.Owner, null, xform)); DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, body.CanCollide, body.BodyType == BodyType.Static)); DebugTools.Assert(broadphase.Owner == broadUid); xform.Broadphase ??= new(broadUid, body.CanCollide, body.BodyType == BodyType.Static); var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree; var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); var mapTransform = new Transform(worldPos, worldRot); // TODO BROADPHASE PARENTING this just assumes local = world var broadphaseTransform = new Transform(Vector2.Transform(mapTransform.Position, broadphaseXform.InvLocalMatrix), mapTransform.Quaternion2D.Angle - broadphaseXform.LocalRotation); AddOrUpdatePhysicsTree(uid, broadUid, xform, body, manager, tree, broadphaseTransform); } private void AddOrUpdatePhysicsTree( EntityUid uid, EntityUid broadUid, TransformComponent xform, PhysicsComponent body, FixturesComponent manager, IBroadPhase tree, Transform broadphaseTransform) { DebugTools.Assert(body.Owner == uid); xform.Broadphase ??= new(broadUid, body.CanCollide, body.BodyType == BodyType.Static); foreach (var (id, fixture) in manager.Fixtures) { AddOrMoveProxies((uid, body, xform), id, fixture, tree, broadphaseTransform); } } private void AddOrMoveProxies( Entity ent, string fixtureId, Fixture fixture, IBroadPhase tree, Transform broadphaseTransform) { var moveBuffer = _physics.MoveBuffer; // Moving if (fixture.ProxyCount > 0) { for (var i = 0; i < fixture.ProxyCount; i++) { var bounds = fixture.Shape.ComputeAABB(broadphaseTransform, i); var proxy = fixture.Proxies[i]; tree.MoveProxy(proxy.ProxyId, bounds); proxy.AABB = bounds; moveBuffer.Add(proxy); } return; } var count = fixture.Shape.ChildCount; var proxies = new FixtureProxy[count]; for (var i = 0; i < count; i++) { var bounds = fixture.Shape.ComputeAABB(broadphaseTransform, i); var proxy = new FixtureProxy(ent.Owner, ent.Comp1, ent.Comp2, bounds, fixtureId, fixture, i); proxy.ProxyId = tree.AddProxy(ref proxy); proxy.AABB = bounds; proxies[i] = proxy; moveBuffer.Add(proxy); } fixture.Proxies = proxies; fixture.ProxyCount = count; } private void AddOrUpdateSundriesTree(EntityUid broadUid, BroadphaseComponent broadphase, EntityUid uid, TransformComponent xform, bool staticBody, Box2? aabb = null) { DebugTools.Assert(!_container.IsEntityOrParentInContainer(uid)); DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadUid, false, staticBody)); xform.Broadphase ??= new(broadUid, false, staticBody); (staticBody ? broadphase.StaticSundriesTree : broadphase.SundriesTree).AddOrUpdate(uid, aabb); } private void OnEntityInit(Entity uid) { if (_container.IsEntityOrParentInContainer(uid, uid) || _mapQuery.HasComp(uid) || _gridQuery.HasComp(uid)) return; // TODO can this just be done implicitly via transform startup? // or do things need to be in trees for other component startup logic? FindAndAddToEntityTree(uid, false); } private void OnMove(ref MoveEvent args) { if (args.Component.GridUid == args.Sender) { // If grid changes map MoveBuffer will have incorrect worldpositions for all children. if (args.ParentChanged) { OnGridChangedMap(args); } return; } DebugTools.Assert(!_gridQuery.HasComp(args.Sender)); if (args.Component.MapUid == args.Sender) return; DebugTools.Assert(!_mapQuery.HasComp(args.Sender)); if (args.ParentChanged) UpdateParent(args.Sender, args.Component); else UpdateEntityTree(args.Sender, args.Component); } private void OnGridChangedMap(MoveEvent args) { var grid = args.Sender; var xform = args.Component; var newMap = xform.MapUid; var oldMap = args.OldPosition.EntityId; if (Terminating(oldMap) || Terminating(grid)) { CleanupGridMapTransitionRecursive(grid, xform); _physics.MovedGrids.Remove(grid); return; } // We need to recursively update the cached data and remove children from the move buffer DebugTools.Assert(HasComp(grid)); DebugTools.Assert(newMap == null || HasComp(newMap)); DebugTools.Assert(!oldMap.IsValid() || HasComp(oldMap)); // Grid-local fixture proxies are stored in the grid broadphase and remain valid across map // transitions, but queued global move-buffer entries depend on a valid map context. Clear those before // invalidating contacts or cached lookup data so contact generation never uses a cooked map state. if (newMap == null) { CleanupGridMapTransitionRecursive(grid, xform, invalidateLookup: true); _physics.MovedGrids.Remove(grid); return; } if (!_broadQuery.TryGetComponent(grid, out var gridBroadphase)) { CleanupGridMapTransitionRecursive(grid, xform); return; } // Rebuild cached lookup state against the grid broadphase and touch preserved proxies after the grid has a // valid destination map. This updates any local moves that happened while the grid was outside a map without // forcing proxy recreation. var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); CleanupGridMapTransitionRecursive(grid, xform, grid, gridBroadphase, xform, worldPos, worldRot); } private void CleanupGridMapTransitionRecursive( EntityUid uid, TransformComponent xform, bool invalidateLookup = false) { CleanupGridMapTransition(uid, xform, invalidateLookup); foreach (var child in xform._children) { if (_xformQuery.TryGetComponent(child, out var childXform)) CleanupGridMapTransitionRecursive(child, childXform, invalidateLookup); } } private void CleanupGridMapTransitionRecursive( EntityUid uid, TransformComponent xform, EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent broadphaseXform, Vector2 worldPos, Angle worldRot, bool updateLookup = false) { CleanupGridMapTransition(uid, xform); if (updateLookup) { AddOrUpdateEntityTreeDown( broadUid, broadphase, broadphaseXform, uid, xform, worldPos, worldRot, recursive: false); } foreach (var child in xform._children) { if (_xformQuery.TryGetComponent(child, out var childXform)) { var updateChild = updateLookup || uid == broadUid; if (updateChild && _containerQuery.HasComponent(uid) && (_metaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) != 0x0) { updateChild = false; } CleanupGridMapTransitionRecursive( child, childXform, broadUid, broadphase, broadphaseXform, worldRot.RotateVec(childXform.LocalPosition) + worldPos, worldRot + childXform.LocalRotation, updateChild); } } } private void CleanupGridMapTransition(EntityUid uid, TransformComponent xform, bool invalidateLookup = false) { if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) { var buffer = _physics.MoveBuffer; foreach (var fixture in fixtures.Fixtures.Values) { for (var i = 0; i < fixture.ProxyCount; i++) { buffer.Remove(fixture.Proxies[i]); } } } if (_physicsQuery.TryGetComponent(uid, out var body)) _physics.DestroyContacts(body); if (invalidateLookup && xform.GridUid != uid) xform.Broadphase = null; } private void UpdateParent(EntityUid uid, TransformComponent xform) { BroadphaseComponent? oldBroadphase = null; if (xform.Broadphase != null) { if (!xform.Broadphase.Value.IsValid()) return; // Entity is intentionally not on a broadphase (deferred updating?). if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out oldBroadphase)) { AssertMissingBroadphaseExpected(xform.Broadphase.Value.Uid); ClearFixtureProxiesAfterBroadphaseDeleted(uid); xform.Broadphase = null; } } TryFindBroadphase(xform, out var newBroadphase); if (oldBroadphase != null && oldBroadphase != newBroadphase) { RemoveFromEntityTree(oldBroadphase.Owner, oldBroadphase, uid, xform); } if (newBroadphase == null) return; var newBroadphaseXform = _xformQuery.GetComponent(newBroadphase.Owner); AddOrUpdateEntityTree( newBroadphase.Owner, newBroadphase, newBroadphaseXform, uid, xform); } public void FindAndAddToEntityTree(EntityUid uid, bool recursive = true, TransformComponent? xform = null) { if (!_xformQuery.Resolve(uid, ref xform)) return; if (TryFindBroadphase(xform, out var broadphase)) AddOrUpdateEntityTreeDown( broadphase.Owner, broadphase, _xformQuery.GetComponent(broadphase.Owner), uid, xform, recursive); } /// /// Variant of that just re-adds the entity to the current tree (updates positions). /// public void UpdateEntityTree(EntityUid uid, TransformComponent? xform = null) { if (!_xformQuery.Resolve(uid, ref xform)) return; if (!TryGetCurrentBroadphase(xform, out var broadphase)) return; AddOrUpdateEntityTree(broadphase.Owner, broadphase, uid, xform); } private void AddOrUpdateEntityTree(EntityUid broadUid, BroadphaseComponent broadphase, EntityUid uid, TransformComponent xform, bool recursive = true) { var broadphaseXform = _xformQuery.GetComponent(broadphase.Owner); AddOrUpdateEntityTree( broadUid, broadphase, broadphaseXform, uid, xform, recursive); } private void AddOrUpdateEntityTree( EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent broadphaseXform, EntityUid uid, TransformComponent xform, bool recursive = true) { AddOrUpdateEntityTreeDown(broadUid, broadphase, broadphaseXform, uid, xform, recursive); } private void AddOrUpdateEntityTreeDown( EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent broadphaseXform, EntityUid uid, TransformComponent xform, bool recursive = true) { var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); AddOrUpdateEntityTreeDown( broadUid, broadphase, broadphaseXform, uid, xform, worldPos, worldRot, recursive); } private void AddOrUpdateEntityTreeDown( EntityUid broadUid, BroadphaseComponent broadphase, TransformComponent broadphaseXform, EntityUid uid, TransformComponent xform, Vector2 worldPos, Angle worldRot, bool recursive = true) { if (xform.Broadphase != null && !xform.Broadphase.Value.IsValid()) { // This entity was explicitly removed from lookup trees, possibly because it is in a container or has // been detached by the PVS system. Do nothing. return; } var relativePosition = Vector2.Transform(worldPos, broadphaseXform.InvLocalMatrix); var relativeRotation = worldRot - broadphaseXform.LocalRotation; if (!_physicsQuery.TryGetComponent(uid, out var body) || !body.CanCollide) { var aabb = GetAABBNoContainer(uid, relativePosition, relativeRotation); AddOrUpdateSundriesTree(broadUid, broadphase, uid, xform, body?.BodyType == BodyType.Static, aabb); } else { var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree; var broadphaseTransform = new Transform(relativePosition, relativeRotation); AddOrUpdatePhysicsTree(uid, broadUid, xform, body, _fixturesQuery.GetComponent(uid), tree, broadphaseTransform); } if (xform.ChildCount == 0 || !recursive) return; if (!_containerQuery.HasComponent(uid)) { foreach (var child in xform._children) { var childXform = _xformQuery.GetComponent(child); var childWorldPos = worldRot.RotateVec(childXform.LocalPosition) + worldPos; var childWorldRot = worldRot + childXform.LocalRotation; AddOrUpdateEntityTreeDown( broadUid, broadphase, broadphaseXform, child, childXform, childWorldPos, childWorldRot, recursive); } return; } foreach (var child in xform._children) { if ((_metaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) != 0x0) continue; var childXform = _xformQuery.GetComponent(child); var childWorldPos = worldRot.RotateVec(childXform.LocalPosition) + worldPos; var childWorldRot = worldRot + childXform.LocalRotation; AddOrUpdateEntityTreeDown( broadUid, broadphase, broadphaseXform, child, childXform, childWorldPos, childWorldRot, recursive); } } /// /// Recursively iterates through this entity's children and removes them from the BroadphaseComponent. /// public void RemoveFromEntityTree(EntityUid uid, TransformComponent xform) { if (!TryGetCurrentBroadphase(xform, out var broadphase)) return; DebugTools.Assert(!HasComp(uid)); DebugTools.Assert(!HasComp(uid)); RemoveFromEntityTree(broadphase.Owner, broadphase, uid, xform); } /// /// Recursively iterates through this entity's children and removes them from the BroadphaseComponent. /// private void RemoveFromEntityTree( EntityUid broadUid, BroadphaseComponent broadphase, EntityUid uid, TransformComponent xform, bool recursive = true) { if (xform.Broadphase is not { Valid: true } old) { // this entity was probably inside of a container during a recursive iteration. This should mean all of // its own children are also not on any broadphase. return; } if (old.Uid != broadUid) { // Because this gets called recursively, and because we cache the map & broadphase data, this may fail // when the client has deferred broadphase updates, where maybe an entity from one broadphase was // parented to one from another. DebugTools.Assert(_netMan.IsClient); broadUid = old.Uid; if (!_broadQuery.TryGetComponent(broadUid, out var currentBroadphase)) { AssertMissingBroadphaseExpected(broadUid); ClearFixtureProxiesAfterBroadphaseDeleted(uid); xform.Broadphase = null; return; } broadphase = currentBroadphase; } if (old.CanCollide) { RemoveBroadTree(broadphase, _fixturesQuery.GetComponent(uid), old.Static); } else if (old.Static) broadphase.StaticSundriesTree.Remove(uid); else broadphase.SundriesTree.Remove(uid); xform.Broadphase = null; if (!recursive) return; foreach (var child in xform._children) { RemoveFromEntityTree( broadUid, broadphase, child, _xformQuery.GetComponent(child)); } } public bool TryGetCurrentBroadphase(TransformComponent xform, [NotNullWhen(true)] out BroadphaseComponent? broadphase) { broadphase = null; if (xform.Broadphase is not { Valid: true } old) return false; if (!_broadQuery.TryGetComponent(old.Uid, out broadphase)) { // broadphase was probably deleted AssertMissingBroadphaseExpected(old.Uid); ClearFixtureProxiesAfterBroadphaseDeleted(xform.Owner); xform.Broadphase = null; return false; } return true; } private void AssertMissingBroadphaseExpected(EntityUid broadphaseUid) { if (TerminatingOrDeleted(broadphaseUid)) return; DebugTools.Assert("Encountered deleted broadphase."); } /// /// Clears fixture proxies after their broadphase tree has already been deleted. /// The tree proxy is gone, but queued global move-buffer references must still be released. /// private void ClearFixtureProxiesAfterBroadphaseDeleted(EntityUid uid) { if (!_fixturesQuery.TryGetComponent(uid, out FixturesComponent? fixtures)) return; foreach (var fixture in fixtures.Fixtures.Values) { ReleaseProxies(fixture, null); } } public BroadphaseComponent? GetCurrentBroadphase(TransformComponent xform) { TryGetCurrentBroadphase(xform, out var broadphase); return broadphase; } public BroadphaseComponent? FindBroadphase(EntityUid uid) { TryFindBroadphase(uid, out var broadphase); return broadphase; } public bool TryFindBroadphase(EntityUid uid, [NotNullWhen(true)] out BroadphaseComponent? broadphase) { return TryFindBroadphase(_xformQuery.GetComponent(uid), out broadphase); } public bool TryFindBroadphase( TransformComponent xform, [NotNullWhen(true)] out BroadphaseComponent? broadphase) { if (xform.MapID == MapId.Nullspace || _container.IsEntityOrParentInContainer(xform.Owner, null, xform)) { broadphase = null; return false; } var parent = xform.ParentUid; // TODO provide variant that also returns world rotation (and maybe position). Avoids having to iterate though parents twice. while (parent.IsValid()) { if (_broadQuery.TryGetComponent(parent, out broadphase)) return true; parent = _xformQuery.GetComponent(parent).ParentUid; } broadphase = null; return false; } #endregion #region Bounds /// /// Get the AABB of an entity with the supplied position and angle. Tries to consider if the entity is in a container. /// public 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. /// public Box2 GetAABBNoContainer(EntityUid uid, Vector2 position, Angle angle) { if (_fixturesQuery.TryGetComponent(uid, out var fixtures)) { var transform = new Transform(position, angle); var bounds = new Box2(transform.Position, transform.Position); // TODO cache this to speed up entity lookups & tree updating foreach (var fixture in fixtures.Fixtures.Values) { for (var i = 0; i < fixture.Shape.ChildCount; i++) { // TODO don't transform each fixture, just transform the final AABB var boundy = fixture.Shape.ComputeAABB(transform, i); bounds = bounds.Union(boundy); } } return bounds; } var ev = new WorldAABBEvent() { AABB = new Box2(position, position), }; RaiseLocalEvent(uid, ref ev); return ev.AABB; } public Box2 GetWorldAABB(EntityUid uid, TransformComponent? xform = null) { var xformQuery = GetEntityQuery(); xform ??= xformQuery.GetComponent(uid); var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform, xformQuery); return GetAABB(uid, worldPos, worldRot, xform, xformQuery); } #endregion }