diff --git a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs index 9f04c1f2d..f4747191d 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Numerics; using NUnit.Framework; @@ -73,7 +74,7 @@ public void OnAnchored_WorldPosition_TileCenter() // Act sim.System().ResetCounters(); - xformSys.AnchorEntity(ent1); + xformSys.AnchorEntity((ent1, sim.Transform(ent1)), grid, mapSys.TileIndicesFor(grid, coordinates)); Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(7.5f, 7.5f))); // centered on tile sim.System().AssertMoved(false); } @@ -83,12 +84,14 @@ public void OnAnchored_WorldPosition_TileCenter() private sealed partial class AnchorOnInitComponent : Component; [Reflect(false)] - private sealed class AnchorOnInitTestSystem : EntitySystem + private sealed partial class AnchorOnInitTestSystem : EntitySystem { + [Dependency] private SharedTransformSystem _transform = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((e, _, _) => Transform(e).Anchored = true); + SubscribeLocalEvent((e, _, _) => _transform.AnchorEntity((e, Transform(e)))); } } @@ -234,7 +237,7 @@ public void OnAnchored_NonEmptyTile_Anchors() mapSys.SetTile(grid, tileIndices, new Tile(1)); // Act - sim.Transform(ent1).Anchored = true; + xformSys.AnchorEntity(ent1); Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).First(), Is.EqualTo(ent1)); Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.Not.EqualTo(Tile.Empty)); @@ -243,6 +246,338 @@ public void OnAnchored_NonEmptyTile_Anchors() Assert.That(sim.HasComp(tempQualifier), Is.True); } + // Acruid-style names + + /* + * Assert same-spot anchoring works. + */ + + [Test] + public void AnchorEntity_CurrentMapPosition_AnchorsToGridUnderEntity() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var mapUid = mapSys.GetMap(coords.MapId); + var tileIndices = mapSys.TileIndicesFor(grid, coords); + mapSys.SetTile(grid, tileIndices, new Tile(1)); + + var ent1 = sim.SpawnEntity(null, new EntityCoordinates(mapUid, coords.Position)); + + Assert.That(xformSys.AnchorEntity(ent1), Is.True); + Assert.That(sim.Transform(ent1).Anchored, Is.True); + Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner)); + Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices), Does.Contain(ent1)); + Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(7f, 7f))); + } + + [Test] + public void AnchorEntity_GridTile_ReanchorsAndCleansOldTile() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var oldTile = mapSys.TileIndicesFor(grid, coords); + var newTile = oldTile + new Vector2i(1, 0); + mapSys.SetTile(grid, oldTile, new Tile(1)); + mapSys.SetTile(grid, newTile, new Tile(1)); + + var ent1 = sim.SpawnEntity(null, coords); + Assert.That(xformSys.AnchorEntity(ent1), Is.True); + + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), grid, newTile), Is.True); + + Assert.That(mapSys.GetAnchoredEntities(grid, oldTile), Does.Not.Contain(ent1)); + Assert.That(mapSys.GetAnchoredEntities(grid, newTile), Does.Contain(ent1)); + Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(8.5f, 7.5f))); + } + + [Test] + public void AnchorEntity_GridTile_SameTile_Noops() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var tile = mapSys.TileIndicesFor(grid, coords); + mapSys.SetTile(grid, tile, new Tile(1)); + + var ent1 = sim.SpawnEntity(null, coords); + Assert.That(xformSys.AnchorEntity(ent1), Is.True); + + // I LOVE THIS SYSTEM + sim.System().ResetCounters(); + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), grid, tile), Is.True); + + Assert.That(sim.Transform(ent1).Anchored, Is.True); + Assert.That(mapSys.GetAnchoredEntities(grid, tile), Does.Contain(ent1)); + Assert.That(sim.System().MoveCounter, Is.EqualTo(0)); + Assert.That(sim.System().ParentCounter, Is.EqualTo(0)); + } + + [Test] + public void AnchorEntity_GridTile_InvalidReanchor_MovesAndUnanchors() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var oldTile = mapSys.TileIndicesFor(grid, coords); + var emptyTile = oldTile + new Vector2i(1, 0); + mapSys.SetTile(grid, oldTile, new Tile(1)); + mapSys.SetTile(grid, emptyTile, Tile.Empty); + + var ent1 = sim.SpawnEntity(null, coords); + Assert.That(xformSys.AnchorEntity(ent1), Is.True); + + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), grid, emptyTile), Is.False); + + Assert.That(sim.Transform(ent1).Anchored, Is.False); + Assert.That(mapSys.GetAnchoredEntities(grid, oldTile), Does.Not.Contain(ent1)); + Assert.That(mapSys.GetAnchoredEntities(grid, emptyTile), Does.Not.Contain(ent1)); + Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(8.5f, 7.5f))); + } + + [Test] + public void AnchorEntity_GridTile_StaleSameTileAnchor_RepairsLookup() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var tile = mapSys.TileIndicesFor(grid, coords); + mapSys.SetTile(grid, tile, new Tile(1)); + + var ent1 = sim.SpawnEntity(null, coords); + Assert.That(xformSys.AnchorEntity(ent1), Is.True); + mapSys.RemoveFromSnapGridCell(grid, grid.Comp, tile, ent1); + + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), grid, tile), Is.True); + + Assert.That(sim.Transform(ent1).Anchored, Is.True); + Assert.That(mapSys.GetAnchoredEntities(grid, tile), Does.Contain(ent1)); + } + + [Test] + public void AnchorEntity_TileRef_MovesAndAnchors() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var targetTile = mapSys.TileIndicesFor(grid, coords) + new Vector2i(1, 0); + mapSys.SetTile(grid, targetTile, new Tile(1)); + var tileRef = mapSys.GetTileRef(grid, targetTile); + + var ent1 = sim.SpawnEntity(null, coords); + + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), tileRef), Is.True); + + Assert.That(sim.Transform(ent1).Anchored, Is.True); + Assert.That(mapSys.GetAnchoredEntities(grid, targetTile), Does.Contain(ent1)); + Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(8.5f, 7.5f))); + } + + [Test] + public void AnchorEntity_MapCoordinates_MovesEvenWhenAnchorFails() + { + var (sim, grid, coords, xformSys, mapSys) = SimulationFactory(); + var targetCoords = coords.Offset(new Vector2(1, 0)); + var targetTile = mapSys.TileIndicesFor(grid, targetCoords); + mapSys.SetTile(grid, targetTile, Tile.Empty); + + var ent1 = sim.SpawnEntity(null, coords); + + Assert.That(xformSys.AnchorEntity((ent1, sim.Transform(ent1)), targetCoords), Is.False); + + Assert.That(sim.Transform(ent1).Anchored, Is.False); + Assert.That(mapSys.GetAnchoredEntities(grid, targetTile), Does.Not.Contain(ent1)); + Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(targetCoords.Position)); + } + + /* + * Down here we test the targeted variants and not same-spot anchoring. + */ + + internal enum AnchorTargetVariant : byte + { + GridTile, + TileRef, + EntityCoordinates, + MapCoordinates, + } + + internal enum AnchorTargetScenario : byte + { + SameSpot, + DifferentGrid, + NoGrid, + } + + private static IEnumerable AnchorTargetCases() + { + foreach (var variant in new[] + { + AnchorTargetVariant.GridTile, + AnchorTargetVariant.TileRef, + AnchorTargetVariant.EntityCoordinates, + AnchorTargetVariant.MapCoordinates, + }) + { + yield return new TestCaseData(variant, AnchorTargetScenario.SameSpot) + .SetName($"AnchorEntity_TargetedVariants_SameSpot_{variant}"); + yield return new TestCaseData(variant, AnchorTargetScenario.DifferentGrid) + .SetName($"AnchorEntity_TargetedVariants_DifferentGrid_{variant}"); + } + + yield return new TestCaseData(AnchorTargetVariant.EntityCoordinates, AnchorTargetScenario.NoGrid) + .SetName("AnchorEntity_TargetedVariants_NoGrid_EntityCoordinates"); + yield return new TestCaseData(AnchorTargetVariant.MapCoordinates, AnchorTargetScenario.NoGrid) + .SetName("AnchorEntity_TargetedVariants_NoGrid_MapCoordinates"); + } + + [TestCaseSource(nameof(AnchorTargetCases))] + public void AnchorEntity_TargetedVariants_Scenarios(AnchorTargetVariant variant, AnchorTargetScenario scenario) + { + var (sim, gridA, coords, xformSys, mapSys) = SimulationFactory(); + var entMan = sim.Resolve(); + var mapMan = sim.Resolve(); + var mapUid = mapSys.GetMap(coords.MapId); + var gridB = mapMan.CreateGridEntity(coords.MapId); + + var tileA = mapSys.TileIndicesFor(gridA, coords); + var tileB = new Vector2i(10, 0); + mapSys.SetTile(gridA, tileA, new Tile(1)); + mapSys.SetTile(gridB, tileB, new Tile(1)); + + var ent = entMan.SpawnEntity(null, coords); + var xform = sim.Transform(ent); + Assert.That(xformSys.AnchorEntity(ent), Is.True); + + sim.System().ResetCounters(); + + var result = variant switch + { + AnchorTargetVariant.GridTile => xformSys.AnchorEntity((ent, xform), TargetGrid(), TargetTile()), + AnchorTargetVariant.TileRef => xformSys.AnchorEntity((ent, xform), mapSys.GetTileRef(TargetGrid(), TargetTile())), + AnchorTargetVariant.EntityCoordinates => xformSys.AnchorEntity((ent, xform), TargetCoordinates()), + AnchorTargetVariant.MapCoordinates => xformSys.AnchorEntity((ent, xform), TargetMapCoordinates()), + _ => throw new System.ArgumentOutOfRangeException(nameof(variant), variant, null), + }; + + switch (scenario) + { + case AnchorTargetScenario.SameSpot: + Assert.That(result, Is.True); + Assert.That(xform.Anchored, Is.True); + Assert.That(mapSys.GetAnchoredEntities(gridA, tileA), Does.Contain(ent)); + Assert.That(sim.System().MoveCounter, Is.EqualTo(0)); + Assert.That(sim.System().ParentCounter, Is.EqualTo(0)); + break; + case AnchorTargetScenario.DifferentGrid: + Assert.That(result, Is.True); + Assert.That(xform.Anchored, Is.True); + Assert.That(mapSys.GetAnchoredEntities(gridA, tileA), Does.Not.Contain(ent)); + Assert.That(mapSys.GetAnchoredEntities(gridB, tileB), Does.Contain(ent)); + Assert.That(xformSys.GetWorldPosition(ent), Is.EqualTo(new Vector2(10.5f, 0.5f))); + break; + case AnchorTargetScenario.NoGrid: + Assert.That(result, Is.False); + Assert.That(xform.Anchored, Is.False); + Assert.That(mapSys.GetAnchoredEntities(gridA, tileA), Does.Not.Contain(ent)); + Assert.That(xformSys.GetWorldPosition(ent), Is.EqualTo(new Vector2(1000, 1000))); + break; + default: + throw new System.ArgumentOutOfRangeException(nameof(scenario), scenario, null); + } + + Entity TargetGrid() + { + return scenario == AnchorTargetScenario.DifferentGrid ? gridB : gridA; + } + + Vector2i TargetTile() + { + return scenario == AnchorTargetScenario.DifferentGrid ? tileB : tileA; + } + + EntityCoordinates TargetCoordinates() + { + return scenario switch + { + AnchorTargetScenario.DifferentGrid => new EntityCoordinates(gridB, 10.5f, 0.5f), + AnchorTargetScenario.NoGrid => new EntityCoordinates(mapUid, 1000, 1000), + _ => new EntityCoordinates(gridA, 7.5f, 7.5f), + }; + } + + MapCoordinates TargetMapCoordinates() + { + return scenario switch + { + AnchorTargetScenario.DifferentGrid => new MapCoordinates(new Vector2(10.5f, 0.5f), coords.MapId), + AnchorTargetScenario.NoGrid => new MapCoordinates(new Vector2(1000, 1000), coords.MapId), + _ => new MapCoordinates(new Vector2(7.5f, 7.5f), coords.MapId), + }; + } + } + + internal enum UnanchorVariant : byte + { + Uid, + TryAnchor, + } + + internal enum UnanchorScenario : byte + { + SameSpot, + DifferentGrid, + NoGrid, + } + + private static IEnumerable UnanchorCases() + { + foreach (var variant in new[] {UnanchorVariant.Uid, UnanchorVariant.TryAnchor}) + { + yield return new TestCaseData(variant, UnanchorScenario.SameSpot) + .SetName($"UnanchorEntity_Variants_SameSpot_{variant}"); + yield return new TestCaseData(variant, UnanchorScenario.DifferentGrid) + .SetName($"UnanchorEntity_Variants_DifferentGrid_{variant}"); + yield return new TestCaseData(variant, UnanchorScenario.NoGrid) + .SetName($"UnanchorEntity_Variants_NoGrid_{variant}"); + } + } + + [TestCaseSource(nameof(UnanchorCases))] + public void UnanchorEntity_Variants_Scenarios(UnanchorVariant variant, UnanchorScenario scenario) + { + var (sim, gridA, coords, xformSys, mapSys) = SimulationFactory(); + var entMan = sim.Resolve(); + var mapMan = sim.Resolve(); + var mapUid = mapSys.GetMap(coords.MapId); + var gridB = mapMan.CreateGridEntity(coords.MapId); + + var tileA = mapSys.TileIndicesFor(gridA, coords); + var tileB = tileA + new Vector2i(10, 0); + mapSys.SetTile(gridA, tileA, new Tile(1)); + mapSys.SetTile(gridB, tileB, new Tile(1)); + + var ent = entMan.SpawnEntity(null, coords); + var xform = sim.Transform(ent); + var meta = entMan.GetComponent(ent); + + switch (scenario) + { + case UnanchorScenario.SameSpot: + Assert.That(xformSys.AnchorEntity(ent), Is.True); + break; + case UnanchorScenario.DifferentGrid: + Assert.That(xformSys.AnchorEntity((ent, xform), gridB, tileB), Is.True); + break; + case UnanchorScenario.NoGrid: + xformSys.SetCoordinates((ent, xform, meta), new EntityCoordinates(mapUid, 1000, 1000)); + break; + default: + throw new System.ArgumentOutOfRangeException(nameof(scenario), scenario, null); + } + + var result = variant switch + { + UnanchorVariant.Uid => xformSys.Unanchor(ent), + UnanchorVariant.TryAnchor => xformSys.TryAnchor((ent, xform, meta), false), + _ => throw new System.ArgumentOutOfRangeException(nameof(variant), variant, null), + }; + + Assert.That(result, Is.False); + Assert.That(xform.Anchored, Is.False); + Assert.That(mapSys.GetAnchoredEntities(gridA, tileA), Does.Not.Contain(ent)); + Assert.That(mapSys.GetAnchoredEntities(gridB, tileB), Does.Not.Contain(ent)); + } + /// /// Local position of an anchored entity cannot be changed (can still change world position via parent). /// Writing to the property is a no-op and is silently ignored. @@ -260,7 +595,7 @@ public void Anchored_SetPosition_Nop() mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1)); var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after - sim.Transform(ent1).Anchored = true; + xformSys.AnchorEntity(ent1); sim.System().FailOnMove = true; // Act @@ -306,7 +641,7 @@ public void Anchored_SetParentSame_Nop() var ent1 = entMan.SpawnEntity(null, coords); var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates); mapSys.SetTile(grid, tileIndices, new Tile(1)); - sim.Transform(ent1).Anchored = true; + xformSys.AnchorEntity(ent1); // Act xformSys.SetParent(ent1, grid.Owner); @@ -425,7 +760,7 @@ public void OnUnanchored_HasPhysicsComp_IsDynamicBody() var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates); mapSys.SetTile(grid, tileIndices, new Tile(1)); var physComp = entMan.AddComponent(ent1); - sim.Transform(ent1).Anchored = true; + xformSys.AnchorEntity(ent1); // Act xformSys.Unanchor(ent1); diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index f2e132d06..bbd4ef583 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -363,30 +363,10 @@ public Vector2 LocalPosition /// /// Is this transform anchored to a grid tile? /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables] public bool Anchored { get => _anchored; - [Obsolete("Use the SharedTransformSystem.AnchorEntity/Unanchor methods instead.")] - set - { - // This will be set again when the transform initializes, actually anchoring it. - if (!Initialized) - { - _anchored = value; - } - else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out _, out var grid)) - { - _anchored = _entMan.EntitySysManager.GetEntitySystem().AnchorEntity(Owner, this, grid); - } - else if (!value && _anchored) - { - // An anchored entity is always parented to the grid. - // If Transform.Anchored is true in the prototype but the entity was not spawned with a grid as the parent, - // then this will be false. - _entMan.EntitySysManager.GetEntitySystem().Unanchor(Owner, this); - } - } } public TransformChildrenEnumerator ChildEnumerator => new(_children.GetEnumerator()); @@ -602,32 +582,6 @@ public readonly struct AnchorStateChangedEvent( public readonly bool Detaching = detaching; } - /// - /// Raised when an entity is re-anchored to another grid. - /// - [ByRefEvent] - public readonly struct ReAnchorEvent - { - public readonly EntityUid Entity; - public readonly EntityUid OldGrid; - public readonly EntityUid Grid; - public readonly TransformComponent Xform; - - /// - /// Tile on both the old and new grid being re-anchored. - /// - public readonly Vector2i TilePos; - - public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos, TransformComponent xform) - { - Entity = uid; - OldGrid = oldGrid; - Grid = grid; - TilePos = tilePos; - Xform = xform; - } - } - /// /// Data used to store information about the broad-phase that any given entity is currently on. /// diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 2df50e9db..9b9ecdc05 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -351,7 +351,7 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo if (coordinates.MapId == MapId.Nullspace) { transform._parent = EntityUid.Invalid; - transform.Anchored = false; + _xforms.SetAnchored((newEntity, transform), false); return newEntity; } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 5e4709ffc..da40f7d01 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -1287,7 +1287,16 @@ private Vector2i SnapGridLocalCellFor(EntityUid uid, MapGridComponent grid, Vect public bool IsAnchored(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid) { var tilePos = TileIndicesFor(uid, grid, coords); + return IsAnchored(uid, grid, tilePos, euid); + } + public bool IsAnchored(Entity grid, Vector2i tilePos, EntityUid euid) + { + return IsAnchored(grid.Owner, grid.Comp, tilePos, euid); + } + + public bool IsAnchored(EntityUid uid, MapGridComponent grid, Vector2i tilePos, EntityUid euid) + { if (!TryChunkAndOffsetForTile(uid, grid, tilePos, out var chunk, out var chunkTile)) return false; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 2fa10c984..508a46881 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -31,32 +31,17 @@ internal void ReAnchor( TransformComponent newGridXform, Angle rotation) { - // Bypass some of the expensive stuff in unanchoring / anchoring. - _map.RemoveFromSnapGridCell(oldGridUid, oldGrid, oldTilePos, uid); - _map.AddToSnapGridCell(newGridUid, newGrid, tilePos, uid); - // TODO: Could do this re-parent way better. - // Unfortunately we don't want any anchoring events to go out hence... this. - xform._anchored = false; - oldGridXform._children.Remove(uid); - newGridXform._children.Add(uid); - xform._parent = newGridUid; - xform._anchored = true; - var oldPos = xform._localPosition; - var oldRot = xform._localRotation; - var oldMap = xform.MapUid; - xform._localPosition = tilePos + newGrid.TileSizeHalfVector; - xform._localRotation += rotation; - var meta = MetaData(uid); - SetGridId((uid, xform, meta), newGridUid); - RaiseMoveEvent((uid, xform, meta), oldGridUid, oldPos, oldRot, oldMap); + var localRotation = xform.LocalRotation + rotation; + var coordinates = new EntityCoordinates(newGridUid, tilePos + newGrid.TileSizeHalfVector); + + Unanchor(uid, xform); + SetCoordinates((uid, xform, meta), coordinates, localRotation); + AnchorEntityResolved((uid, xform, meta), (newGridUid, newGrid), tilePos); DebugTools.Assert(XformQuery.GetComponent(oldGridUid).MapID == XformQuery.GetComponent(newGridUid).MapID); DebugTools.Assert(xform._anchored); - Dirty(uid, xform, meta); - var ev = new ReAnchorEvent(uid, oldGridUid, newGridUid, tilePos, xform); - RaiseLocalEvent(uid, ref ev); } [Obsolete("Use Entity variant")] @@ -67,43 +52,209 @@ public bool AnchorEntity( MapGridComponent grid, Vector2i tileIndices) { - return AnchorEntity((uid, xform), (gridUid, grid), tileIndices); + return AnchorEntity((uid, xform, MetaData(uid)), (gridUid, grid), tileIndices); } + /// + /// Moves an entity to the specified grid tile and attempts to anchor it there. + /// + /// Whether the entity is anchored after this call. public bool AnchorEntity( + Entity entity, + Entity grid, + Vector2i tileIndices) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return AnchorEntityResolved((uid, xform, meta), grid, tileIndices); + } + + /// + /// Moves an entity to the specified tile and attempts to anchor it there. + /// + /// Whether the entity is anchored after this call. + public bool AnchorEntity(Entity entity, TileRef tile) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return AnchorEntityResolved((uid, xform, meta), tile); + } + + /// + /// Moves an entity to the specified coordinates and attempts to anchor it there. + /// + /// Whether the entity is anchored after this call. + public bool AnchorEntity(Entity entity, EntityCoordinates coordinates) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return AnchorEntityResolved((uid, xform, meta), coordinates); + } + + /// + /// Moves an entity to the specified map coordinates and attempts to anchor it there. + /// + /// Whether the entity is anchored after this call. + public bool AnchorEntity(Entity entity, MapCoordinates coordinates) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return AnchorEntityResolved((uid, xform, meta), coordinates); + } + + private bool AnchorEntityResolved(Entity entity, TileRef tile) + { + if (!_gridQuery.TryGetComponent(tile.GridUid, out var grid)) + return entity.Comp1.Anchored; + + return AnchorEntityResolved(entity, (tile.GridUid, grid), tile.GridIndices); + } + + private bool AnchorEntityResolved(Entity entity, EntityCoordinates coordinates) + { + var (uid, xform, _) = entity; + var wasAnchored = xform._anchored; + + if (TryGetTargetAnchor(coordinates, out var grid, out var tile)) + return AnchorEntityResolved(entity, grid, tile); + + if (wasAnchored) + Unanchor(uid, xform, setPhysics: false); + + SetCoordinates(entity, coordinates); + if (wasAnchored) + _physics.TrySetBodyType(uid, BodyType.Dynamic, xform: xform); + + return xform.Anchored; + } + + private bool AnchorEntityResolved(Entity entity, MapCoordinates coordinates) + { + var (uid, xform, _) = entity; + var wasAnchored = xform._anchored; + + if (TryGetTargetAnchor(coordinates, out var grid, out var tile)) + return AnchorEntityResolved(entity, grid, tile); + + if (wasAnchored) + Unanchor(uid, xform, setPhysics: false); + + SetMapCoordinates((entity.Owner, entity.Comp1), coordinates); + if (wasAnchored) + _physics.TrySetBodyType(uid, BodyType.Dynamic, xform: xform); + + return xform.Anchored; + } + + /// + /// Checks whether the entity is already in the target anchort spot. + /// + private bool IsSameValidAnchor( Entity entity, Entity grid, Vector2i tileIndices) { - var (uid, xform) = entity; - if (!_map.AddToSnapGridCell(grid, grid, tileIndices, uid)) + if (!entity.Comp._anchored) + return false; + + if (!TryGetCurrentAnchor(entity, out var oldGridUid, out _, out var oldTile)) return false; - var wasAnchored = entity.Comp._anchored; + return oldGridUid == grid.Owner + && oldTile == tileIndices + && _map.IsAnchored(grid, tileIndices, entity.Owner); + } + + /// + /// The actual authoritative anchoring method. + /// + private bool AnchorEntityResolved( + Entity entity, + Entity grid, + Vector2i tileIndices, + bool snapToTileCenter = true) + { + var (uid, xform, meta) = entity; + var wasAnchored = xform._anchored; + if (IsSameValidAnchor((uid, xform), grid, tileIndices)) + return true; + + if (wasAnchored) + Unanchor(uid, xform, setPhysics: false); + + if (snapToTileCenter) + { + var pos = new EntityCoordinates(grid, _map.GridTileToLocal(grid, grid, tileIndices).Position); + SetCoordinates((uid, xform, meta), pos); + } + else if (xform.ParentUid != grid.Owner) + { + var localPos = Vector2.Transform(GetMapCoordinates(xform).Position, GetInvWorldMatrix(grid.Owner)); + SetCoordinates((uid, xform, meta), new EntityCoordinates(grid, localPos), unanchor: false); + } + + if (!_map.AddToSnapGridCell(grid, grid, tileIndices, uid)) + { + DebugTools.Assert(!xform.Anchored); + if (wasAnchored) + _physics.TrySetBodyType(uid, BodyType.Dynamic, xform: xform); + + return xform.Anchored; + } + xform._anchored = true; - var meta = MetaData(uid); - Dirty(entity, meta); + Dirty(uid, xform, meta); - // Mark as static before doing position changes, to avoid the velocity change on parent change. _physics.TrySetBodyType(uid, BodyType.Static, xform: xform); - if (!wasAnchored && xform.Running) + if (xform.Running) { var ev = new AnchorStateChangedEvent(uid, xform); RaiseLocalEvent(uid, ref ev, true); } - // Anchor snapping. If there is a coordinate change, it will dirty the component for us. - var pos = new EntityCoordinates(grid, _map.GridTileToLocal(grid, grid, tileIndices).Position); - SetCoordinates((uid, xform, meta), pos, unanchor: false); return true; } + private bool TryGetTargetAnchor(EntityCoordinates coordinates, out Entity grid, out Vector2i tile) + { + if (_gridQuery.TryGetComponent(coordinates.EntityId, out var gridComp)) + { + grid = (coordinates.EntityId, gridComp); + tile = _map.TileIndicesFor(grid, coordinates); + return true; + } + + var worldPos = Vector2.Transform(coordinates.Position, GetWorldMatrix(coordinates.EntityId)); + return TryGetTargetAnchor(new MapCoordinates(worldPos, GetMapId(coordinates)), out grid, out tile); + } + + private bool TryGetTargetAnchor(MapCoordinates coordinates, out Entity grid, out Vector2i tile) + { + if (_mapManager.TryFindGridAt(coordinates, out var gridUid, out var gridComp)) + { + grid = (gridUid, gridComp); + tile = _map.TileIndicesFor(grid, coordinates); + return true; + } + + grid = default; + tile = default; + return false; + } + [Obsolete("Use Entity variants")] public bool AnchorEntity(EntityUid uid, TransformComponent xform, MapGridComponent grid) { - var tileIndices = _map.TileIndicesFor(grid.Owner, grid, xform.Coordinates); - return AnchorEntity(uid, xform, grid.Owner, grid, tileIndices); + return AnchorEntity((uid, xform, MetaData(uid)), (grid.Owner, grid)); } public bool AnchorEntity(EntityUid uid) @@ -113,37 +264,154 @@ public bool AnchorEntity(EntityUid uid) public bool AnchorEntity(EntityUid uid, TransformComponent xform) { - return AnchorEntity((uid, xform)); + return AnchorEntity((uid, xform, MetaData(uid))); } - public bool AnchorEntity(Entity entity, Entity? grid = null) + /// + /// Attempts to anchor or unanchor an entity at its current position. + /// + /// Whether the entity is anchored after this call. + [Obsolete("Use AnchorEntity")] + public bool TryAnchor(EntityUid uid, bool value, TransformComponent? xform = null) { - if (grid != null && grid.Value.Owner != entity.Comp.GridUid) - { - Log.Error($"Tried to anchor entity {Name(entity)} to a grid ({grid.Value.Owner}) different from its GridUid ({entity.Comp.GridUid})"); + MetaDataComponent? meta = null; + if (!Resolve(uid, ref xform, ref meta)) return false; + + return TryAnchorResolved((uid, xform, meta), value); + } + + /// + /// Attempts to anchor or unanchor an entity at its current position. + /// + /// Whether the entity is anchored after this call. + public bool TryAnchor(Entity entity, bool value) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return TryAnchorResolved((uid, xform, meta), value); + } + + private bool TryAnchorResolved(Entity entity, bool value) + { + if (!entity.Comp1.Initialized) + { + entity.Comp1._anchored = value; + return entity.Comp1.Anchored; } + return value + ? AnchorEntityResolved(entity) + : Unanchor(entity.Owner, entity.Comp1); + } + + /// + /// Attempts to anchor an entity at its current map position without snapping it to the tile center. + /// + /// Whether the entity is anchored after this call. + public bool AnchorEntity(Entity entity, Entity? grid = null) + { + var (uid, xform, meta) = entity; + if (!Resolve(uid, ref xform, ref meta)) + return false; + + return AnchorEntityResolved((uid, xform, meta), grid); + } + + private bool AnchorEntityResolved(Entity entity, Entity? grid = null) + { + var xform = entity.Comp1; if (grid == null) { - if (!TryComp(entity.Comp.GridUid, out MapGridComponent? gridComp)) - return false; - grid = (entity.Comp.GridUid.Value, gridComp); + if (xform.GridUid is { } gridUid && _gridQuery.TryGetComponent(gridUid, out var gridComp)) + { + grid = (gridUid, gridComp); + } + else + { + var coords = GetMapCoordinates(xform); + if (!_mapManager.TryFindGridAt(coords, out gridUid, out gridComp)) + return xform.Anchored; + + grid = (gridUid, gridComp); + } + } + else if (grid.Value.Owner != xform.GridUid && grid.Value.Owner != xform.ParentUid) + { + var coords = GetMapCoordinates(xform); + if (!_mapManager.TryFindGridAt(coords, out var currentGridUid, out _) || currentGridUid != grid.Value.Owner) + { + Log.Error($"Tried to anchor entity {Name(entity)} to a grid ({grid.Value.Owner}) that it is not currently on"); + return xform.Anchored; + } } - var tileIndices = _map.TileIndicesFor(grid.Value, grid.Value, entity.Comp.Coordinates); - return AnchorEntity(entity, grid.Value, tileIndices); + var tileIndices = grid.Value.Owner == xform.ParentUid + ? _map.TileIndicesFor(grid.Value, xform.Coordinates) + : _map.TileIndicesFor(grid.Value, GetMapCoordinates(xform)); + + return AnchorEntityResolved(entity, grid.Value, tileIndices, snapToTileCenter: false); } - public void Unanchor(EntityUid uid) + internal void SetAnchored(Entity entity, bool value) { - Unanchor(uid, XformQuery.GetComponent(uid)); + // Internal just because of this shenanigan. + if (!entity.Comp.Initialized) + { + entity.Comp._anchored = value; + return; + } + + if (value) + { + TryAnchorResolved((entity.Owner, entity.Comp, MetaData(entity.Owner)), true); + } + else + { + TryAnchorResolved((entity.Owner, entity.Comp, MetaData(entity.Owner)), false); + } } - public void Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = true) + private bool TryGetCurrentAnchor( + Entity entity, + out EntityUid? gridUid, + [NotNullWhen(true)] out MapGridComponent? grid, + out Vector2i? tile) + { + gridUid = null; + grid = null; + tile = null; + + if (!entity.Comp._anchored) + return false; + + if (!entity.Comp.ParentUid.IsValid() || !_gridQuery.TryGetComponent(entity.Comp.ParentUid, out grid)) + return false; + + gridUid = entity.Comp.ParentUid; + tile = _map.TileIndicesFor(entity.Comp.ParentUid, grid, entity.Comp.Coordinates); + return true; + } + + /// + /// Unanchors an entity. + /// + /// Whether the entity is anchored after this call. + public bool Unanchor(EntityUid uid) + { + return Unanchor(uid, XformQuery.GetComponent(uid)); + } + + /// + /// Unanchors an entity. + /// + /// Whether the entity is anchored after this call. + public bool Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = true) { if (!xform._anchored) - return; + return false; Dirty(uid, xform); xform._anchored = false; @@ -152,7 +420,7 @@ public void Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = _physics.TrySetBodyType(uid, BodyType.Dynamic, xform: xform); if (xform.LifeStage < ComponentLifeStage.Initialized) - return; + return xform.Anchored; if (_gridQuery.TryGetComponent(xform.GridUid, out var grid)) { @@ -161,10 +429,11 @@ public void Unanchor(EntityUid uid, TransformComponent xform, bool setPhysics = } if (!xform.Running) - return; + return xform.Anchored; var ev = new AnchorStateChangedEvent(uid, xform); RaiseLocalEvent(uid, ref ev, true); + return xform.Anchored; } #endregion @@ -861,7 +1130,7 @@ internal void OnHandleState(EntityUid uid, TransformComponent xform, ref Compone } else { - xform.Anchored = newState.Anchored; + SetAnchored((uid, xform), newState.Anchored); } if (oldAnchored != newState.Anchored && xform.Initialized)