From d72185933a135a8fec4914f87d73fae219609bb3 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 23 Aug 2021 16:00:07 +1000 Subject: [PATCH] Add support for grid chunk removals (#1941) * Add support for grid chunk removals Also allows grids to be removed when they have no more chunks remaining. * No more crashing pog * Slightly better * Minor optimisations and fix bounds * Avoid creating new chunks for anchoring * chucky * comment * Tests * Remove some logs * Remove another log * Review --- .../GameObjects/EntitySystems/MapSystem.cs | 9 +++ .../Graphics/Clyde/Clyde.GridRendering.cs | 4 + Robust.Client/Map/ClientMapManager.cs | 16 +++- Robust.Client/Placement/Modes/AlignTileAny.cs | 3 +- .../GameObjects/EntitySystems/MapSystem.cs | 67 ++++++++++++++++ Robust.Server/Map/ServerMapManager.cs | 59 ++++++++++++-- Robust.Server/Physics/GridFixtureSystem.cs | 15 +++- Robust.Server/Placement/PlacementManager.cs | 2 +- Robust.Shared/CVars.cs | 6 ++ .../{MapSystem.cs => SharedMapSystem.cs} | 6 +- Robust.Shared/GameStates/GameStateMapData.cs | 15 +++- Robust.Shared/Map/CoordinatesExtensions.cs | 2 + Robust.Shared/Map/IMapGridInternal.cs | 5 ++ Robust.Shared/Map/IMapManagerInternal.cs | 3 + Robust.Shared/Map/MapChunk.cs | 41 +++++++++- Robust.Shared/Map/MapGrid.cs | 40 ++++++++-- Robust.Shared/Map/MapManager.cs | 24 ++++-- .../Physics/SharedBroadphaseSystem.cs | 1 - ...CollisionTest.cs => GridCollision_Test.cs} | 2 +- .../Shared/Map/GridContraction_Test.cs | 78 +++++++++++++++++++ 20 files changed, 361 insertions(+), 37 deletions(-) create mode 100644 Robust.Client/GameObjects/EntitySystems/MapSystem.cs create mode 100644 Robust.Server/GameObjects/EntitySystems/MapSystem.cs rename Robust.Shared/GameObjects/Systems/{MapSystem.cs => SharedMapSystem.cs} (90%) rename Robust.UnitTesting/Shared/Map/{GridCollisionTest.cs => GridCollision_Test.cs} (97%) create mode 100644 Robust.UnitTesting/Shared/Map/GridContraction_Test.cs diff --git a/Robust.Client/GameObjects/EntitySystems/MapSystem.cs b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs new file mode 100644 index 0000000000..7ea32e0020 --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Client.GameObjects +{ + internal sealed class MapSystem : SharedMapSystem + { + + } +} diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 5311b01502..2a7b20edfb 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using OpenToolkit.Graphics.OpenGL4; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -203,11 +204,14 @@ namespace Robust.Client.Graphics.Clyde private void _updateOnGridCreated(MapId mapId, GridId gridId) { + Logger.DebugS("grid", $"Adding {gridId} to grid renderer"); _mapChunkData.Add(gridId, new Dictionary()); } private void _updateOnGridRemoved(MapId mapId, GridId gridId) { + Logger.DebugS("grid", $"Removing {gridId} from grid renderer"); + var data = _mapChunkData[gridId]; foreach (var chunkDatum in data.Values) { diff --git a/Robust.Client/Map/ClientMapManager.cs b/Robust.Client/Map/ClientMapManager.cs index bd92662004..4be20e4c9c 100644 --- a/Robust.Client/Map/ClientMapManager.cs +++ b/Robust.Client/Map/ClientMapManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -134,15 +135,24 @@ namespace Robust.Client.Map } } } - - chunk.SuppressCollisionRegeneration = false; - chunk.RegenerateCollision(); } if (modified.Count != 0) { InvokeGridChanged(this, new GridChangedEventArgs(grid, modified)); } + + foreach (var chunkData in gridDatum.ChunkData) + { + var chunk = grid.GetChunk(chunkData.Index); + chunk.SuppressCollisionRegeneration = false; + chunk.RegenerateCollision(); + } + + foreach (var chunkData in gridDatum.DeletedChunkData) + { + grid.RemoveChunk(chunkData.Index); + } } SuppressOnTileChanged = false; diff --git a/Robust.Client/Placement/Modes/AlignTileAny.cs b/Robust.Client/Placement/Modes/AlignTileAny.cs index b93eccdb32..3e78b76d5d 100644 --- a/Robust.Client/Placement/Modes/AlignTileAny.cs +++ b/Robust.Client/Placement/Modes/AlignTileAny.cs @@ -11,7 +11,8 @@ namespace Robust.Client.Placement.Modes public override void AlignPlacementMode(ScreenCoordinates mouseScreen) { - const float SearchBoxSize = 1.5f; // size of search box in meters + // Go over diagonal size so when placing in a line it doesn't stop snapping. + const float SearchBoxSize = 2f; // size of search box in meters MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(SearchBoxSize, pManager.EntityManager, pManager.MapManager); diff --git a/Robust.Server/GameObjects/EntitySystems/MapSystem.cs b/Robust.Server/GameObjects/EntitySystems/MapSystem.cs new file mode 100644 index 0000000000..863d2eaac7 --- /dev/null +++ b/Robust.Server/GameObjects/EntitySystems/MapSystem.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Robust.Server.GameObjects +{ + internal sealed class MapSystem : SharedMapSystem + { + private bool _deleteEmptyGrids; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(HandleGridEmpty); + + var configManager = IoCManager.Resolve(); + configManager.OnValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion, true); + } + + private void SetGridDeletion(bool value) + { + _deleteEmptyGrids = value; + + // If we have any existing empty ones then cull them on setting the cvar + if (_deleteEmptyGrids) + { + var toDelete = new List(); + + foreach (var grid in MapManager.GetAllGrids()) + { + if (!GridEmpty(grid)) continue; + toDelete.Add(grid); + } + + foreach (var grid in toDelete) + { + MapManager.DeleteGrid(grid.Index); + } + } + } + + private bool GridEmpty(IMapGrid grid) + { + return !(grid.GetAllTiles().Any()); + } + + public override void Shutdown() + { + base.Shutdown(); + var configManager = IoCManager.Resolve(); + configManager.UnsubValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion); + } + + private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args) + { + if (!_deleteEmptyGrids || + !EntityManager.TryGetEntity(uid, out var gridEnt) || + gridEnt.LifeStage >= EntityLifeStage.Terminating) return; + + MapManager.DeleteGrid(args.GridId); + } + } +} diff --git a/Robust.Server/Map/ServerMapManager.cs b/Robust.Server/Map/ServerMapManager.cs index 6f494d512f..5c594a870d 100644 --- a/Robust.Server/Map/ServerMapManager.cs +++ b/Robust.Server/Map/ServerMapManager.cs @@ -1,18 +1,21 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using Robust.Server.Physics; +using Robust.Shared.GameObjects; using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Network; +using Robust.Shared.Maths; +using Robust.Shared.Physics; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Robust.Server.Map { [UsedImplicitly] internal sealed class ServerMapManager : MapManager, IServerMapManager { + private readonly Dictionary> _chunkDeletionHistory = new(); private readonly List<(GameTick tick, GridId gridId)> _gridDeletionHistory = new(); private readonly List<(GameTick tick, MapId mapId)> _mapDeletionHistory = new(); @@ -26,6 +29,29 @@ namespace Robust.Server.Map { base.DeleteGrid(gridID); _gridDeletionHistory.Add((GameTiming.CurTick, gridID)); + // No point syncing chunk removals anymore! + _chunkDeletionHistory.Remove(gridID); + } + + public override void ChunkRemoved(MapChunk chunk) + { + base.ChunkRemoved(chunk); + if (!_chunkDeletionHistory.TryGetValue(chunk.GridId, out var chunks)) + { + chunks = new List<(GameTick tick, Vector2i indices)>(); + _chunkDeletionHistory[chunk.GridId] = chunks; + } + + chunks.Add((GameTiming.CurTick, chunk.Indices)); + + // Seemed easier than having this method on GridFixtureSystem + if (!TryGetGrid(chunk.GridId, out var grid) || + !ComponentManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) || + chunk.Fixture == null) return; + + // TODO: Like MapManager injecting this is a PITA so need to work out an easy way to do it. + // Maybe just add like a PostInject method that gets called way later? + EntitySystem.Get().DestroyFixture(body, chunk.Fixture); } public GameStateMapData? GetStateData(GameTick fromTick) @@ -38,7 +64,20 @@ namespace Robust.Server.Map continue; } + var deletedChunkData = new List(); + + if (_chunkDeletionHistory.TryGetValue(grid.Index, out var chunks)) + { + foreach (var (tick, indices) in chunks) + { + if (tick < fromTick) continue; + + deletedChunkData.Add(new GameStateMapData.DeletedChunkDatum(indices)); + } + } + var chunkData = new List(); + foreach (var (index, chunk) in grid.GetMapChunks()) { if (chunk.LastModifiedTick < fromTick) @@ -62,8 +101,10 @@ namespace Robust.Server.Map chunkData.Add(new GameStateMapData.ChunkDatum(index, tileBuffer)); } - var gridDatum = - new GameStateMapData.GridDatum(chunkData.ToArray(), new MapCoordinates(grid.WorldPosition, grid.ParentMapId)); + var gridDatum = new GameStateMapData.GridDatum( + chunkData.ToArray(), + deletedChunkData.ToArray(), + new MapCoordinates(grid.WorldPosition, grid.ParentMapId)); gridDatums.Add(grid.Index, gridDatum); } @@ -86,11 +127,17 @@ namespace Robust.Server.Map if (gridDatums == null && gridDeletionsData == null && mapDeletionsData == null && mapCreations == null && gridCreations == null) return default; - return new GameStateMapData(gridDatums?.ToArray(), gridDeletionsData?.ToArray(), mapDeletionsData?.ToArray(), mapCreations?.ToArray(), gridCreations?.ToArray()); + return new GameStateMapData(gridDatums?.ToArray>(), gridDeletionsData?.ToArray(), mapDeletionsData?.ToArray(), mapCreations?.ToArray(), gridCreations?.ToArray>()); } public void CullDeletionHistory(GameTick uptoTick) { + foreach (var (gridId, chunks) in _chunkDeletionHistory.ToArray()) + { + chunks.RemoveAll(t => t.tick < uptoTick); + if (chunks.Count == 0) _chunkDeletionHistory.Remove(gridId); + } + _mapDeletionHistory.RemoveAll(t => t.tick < uptoTick); _gridDeletionHistory.RemoveAll(t => t.tick < uptoTick); } diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs index 796addf47f..a30f0871c1 100644 --- a/Robust.Server/Physics/GridFixtureSystem.cs +++ b/Robust.Server/Physics/GridFixtureSystem.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; 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.Physics.Collision.Shapes; using Robust.Shared.Physics.Dynamics; +using Robust.Shared.Utility; namespace Robust.Server.Physics { @@ -73,10 +75,17 @@ namespace Robust.Server.Physics private void RegenerateCollision(MapChunk chunk) { - // Currently this is gonna be hella simple. if (!_mapManager.TryGetGrid(chunk.GridId, out var grid) || - !EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt) || - !gridEnt.TryGetComponent(out PhysicsComponent? physicsComponent)) return; + !EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) return; + + DebugTools.Assert(chunk.ValidTiles > 0); + + // Currently this is gonna be hella simple. + if (!gridEnt.TryGetComponent(out PhysicsComponent? physicsComponent)) + { + Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEnt} that doesn't have {nameof(physicsComponent)}"); + return; + } // TODO: Lots of stuff here etc etc, make changes to mapgridchunk. var bounds = chunk.CalcLocalBounds(); diff --git a/Robust.Server/Placement/PlacementManager.cs b/Robust.Server/Placement/PlacementManager.cs index 6d55e30334..e29bde3183 100644 --- a/Robust.Server/Placement/PlacementManager.cs +++ b/Robust.Server/Placement/PlacementManager.cs @@ -189,7 +189,7 @@ namespace Robust.Server.Placement var pos = closest.WorldToTile(position); closest.SetTile(pos, new Tile(tileType)); } - else // create a new grid + else if (tileType != 0) // create a new grid { var newGrid = _mapManager.CreateGrid(mapId); newGrid.WorldPosition = position + (newGrid.TileSize / 2f); // assume bottom left tile origin diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 372dfd8e46..3c88cdbfcb 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -192,6 +192,12 @@ namespace Robust.Shared public static readonly CVarDef GameHostName = CVarDef.Create("game.hostname", "MyServer", CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + /// + /// If a grid is shrunk to include no more tiles should it be deleted. + /// + public static readonly CVarDef GameDeleteEmptyGrids = + CVarDef.Create("game.delete_empty_grids", true, CVar.ARCHIVE | CVar.SERVER); + /* * LOG */ diff --git a/Robust.Shared/GameObjects/Systems/MapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs similarity index 90% rename from Robust.Shared/GameObjects/Systems/MapSystem.cs rename to Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index de4023e759..20a0eee706 100644 --- a/Robust.Shared/GameObjects/Systems/MapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -5,9 +5,9 @@ using Robust.Shared.Map; namespace Robust.Shared.GameObjects { [UsedImplicitly] - internal sealed class MapSystem : EntitySystem + internal abstract class SharedMapSystem : EntitySystem { - [Dependency] private readonly IMapManagerInternal _mapManager = default!; + [Dependency] protected readonly IMapManagerInternal MapManager = default!; public override void Initialize() { @@ -26,7 +26,7 @@ namespace Robust.Shared.GameObjects private void RemoveHandler(EntityUid uid, MapGridComponent component, ComponentRemove args) { - _mapManager.OnComponentRemoved(component); + MapManager.OnComponentRemoved(component); } private void HandleGridInitialize(EntityUid uid, MapGridComponent component, ComponentInit args) diff --git a/Robust.Shared/GameStates/GameStateMapData.cs b/Robust.Shared/GameStates/GameStateMapData.cs index 1841ca2ce2..fb12f18140 100644 --- a/Robust.Shared/GameStates/GameStateMapData.cs +++ b/Robust.Shared/GameStates/GameStateMapData.cs @@ -41,10 +41,12 @@ namespace Robust.Shared.GameStates { public readonly MapCoordinates Coordinates; public readonly ChunkDatum[] ChunkData; + public readonly DeletedChunkDatum[] DeletedChunkData; - public GridDatum(ChunkDatum[] chunkData, MapCoordinates coordinates) + public GridDatum(ChunkDatum[] chunkData, DeletedChunkDatum[] deletedChunkData, MapCoordinates coordinates) { ChunkData = chunkData; + DeletedChunkData = deletedChunkData; Coordinates = coordinates; } } @@ -65,5 +67,16 @@ namespace Robust.Shared.GameStates TileData = tileData; } } + + [Serializable, NetSerializable] + public struct DeletedChunkDatum + { + public readonly Vector2i Index; + + public DeletedChunkDatum(Vector2i index) + { + Index = index; + } + } } } diff --git a/Robust.Shared/Map/CoordinatesExtensions.cs b/Robust.Shared/Map/CoordinatesExtensions.cs index 5c7dff81e2..8567cfa766 100644 --- a/Robust.Shared/Map/CoordinatesExtensions.cs +++ b/Robust.Shared/Map/CoordinatesExtensions.cs @@ -40,6 +40,8 @@ namespace Robust.Shared.Map var intersect = new Box2(); foreach (var grid in gridsInArea) { + // TODO: Use CollisionManager to get nearest edge. + // figure out closest intersect var gridIntersect = gridSearchBox.Intersect(grid.WorldBounds); var gridDist = (gridIntersect.Center - mapCoords.Position).LengthSquared; diff --git a/Robust.Shared/Map/IMapGridInternal.cs b/Robust.Shared/Map/IMapGridInternal.cs index 5be8ca7d83..6075d3386c 100644 --- a/Robust.Shared/Map/IMapGridInternal.cs +++ b/Robust.Shared/Map/IMapGridInternal.cs @@ -35,6 +35,11 @@ namespace Robust.Shared.Map /// The existing or new chunk. IMapChunkInternal GetChunk(int xIndex, int yIndex); + /// + /// Removes the chunk with the specified origin. + /// + void RemoveChunk(Vector2i origin); + /// /// Returns the chunk at the given indices. If the chunk does not exist, /// then a new one is generated that is filled with empty space. diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs index eff9c26b67..61cd4aee0b 100644 --- a/Robust.Shared/Map/IMapManagerInternal.cs +++ b/Robust.Shared/Map/IMapManagerInternal.cs @@ -1,4 +1,5 @@ using Robust.Shared.GameObjects; +using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Robust.Shared.Map @@ -11,6 +12,8 @@ namespace Robust.Shared.Map void OnComponentRemoved(MapGridComponent comp); + void ChunkRemoved(MapChunk chunk); + /// /// Raises the OnTileChanged event. /// diff --git a/Robust.Shared/Map/MapChunk.cs b/Robust.Shared/Map/MapChunk.cs index 79b4e9415b..5500a26c92 100644 --- a/Robust.Shared/Map/MapChunk.cs +++ b/Robust.Shared/Map/MapChunk.cs @@ -26,6 +26,11 @@ namespace Robust.Shared.Map private readonly Tile[,] _tiles; private readonly SnapGridCell[,] _snapGrid; + // We'll keep a running count of how many tiles are non-empty. + // If this ever hits 0 then we know the chunk can be deleted. + // The alternative is that every time we SetTile we iterate every tile in the chunk. + internal int ValidTiles { get; private set; } + private Box2i _cachedBounds; public Fixture? Fixture { get; set; } @@ -127,6 +132,22 @@ namespace Robust.Shared.Map if (_tiles[xIndex, yIndex].TypeId == tile.TypeId) return; + var oldIsEmpty = _tiles[xIndex, yIndex].IsEmpty; + var oldValidTiles = ValidTiles; + + if (oldIsEmpty != tile.IsEmpty) + { + if (oldIsEmpty) + { + ValidTiles += 1; + } + else + { + ValidTiles -= 1; + } + } + + DebugTools.Assert(ValidTiles >= 0); var gridTile = ChunkTileToGridTile(new Vector2i(xIndex, yIndex)); var newTileRef = new TileRef(_grid.ParentMapId, _grid.Index, gridTile, tile); var oldTile = _tiles[xIndex, yIndex]; @@ -134,12 +155,13 @@ namespace Robust.Shared.Map _tiles[xIndex, yIndex] = tile; - if (!SuppressCollisionRegeneration) + // As the collision regeneration can potentially delete the chunk we'll notify of the tile changed first. + _grid.NotifyTileChanged(newTileRef, oldTile); + + if (!SuppressCollisionRegeneration && oldValidTiles != ValidTiles) { RegenerateCollision(); } - - _grid.NotifyTileChanged(newTileRef, oldTile); } /// @@ -234,6 +256,14 @@ namespace Robust.Shared.Map public void RegenerateCollision() { + // Even if the chunk is still removed still need to make sure bounds are updated (for now...) + if (ValidTiles == 0) + { + var grid = (IMapGridInternal) IoCManager.Resolve().GetGrid(GridId); + + grid.RemoveChunk(_gridIndices); + } + // generate collision rects GridChunkPartition.PartitionChunk(this, out _cachedBounds); _grid.NotifyChunkCollisionRegenerated(this); @@ -296,4 +326,9 @@ namespace Robust.Shared.Map Chunk = chunk; } } + + internal sealed class ChunkRemovedEvent : EntityEventArgs + { + public MapChunk Chunk = default!; + } } diff --git a/Robust.Shared/Map/MapGrid.cs b/Robust.Shared/Map/MapGrid.cs index 7c3b0c87db..8527b8c6ba 100644 --- a/Robust.Shared/Map/MapGrid.cs +++ b/Robust.Shared/Map/MapGrid.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -201,7 +202,8 @@ namespace Robust.Shared.Map // Not raising directed because the grid's EntityUid isn't set yet. // Don't call GridFixtureSystem directly because it's server-only. - IoCManager + if (chunk.ValidTiles > 0) + IoCManager .Resolve() .EventBus .RaiseEvent(EventSource.Local, new RegenerateChunkCollisionEvent(chunk)); @@ -347,12 +349,6 @@ namespace Robust.Shared.Map #endregion TileAccess #region ChunkAccess - - public void RegenerateCollision() - { - throw new NotImplementedException(); - } - /// /// The total number of allocated chunks in the grid. /// @@ -364,6 +360,20 @@ namespace Robust.Shared.Map return GetChunk(new Vector2i(xIndex, yIndex)); } + public void RemoveChunk(Vector2i origin) + { + if (!_chunks.TryGetValue(origin, out var chunk)) return; + + _chunks.Remove(origin); + + _mapManager.ChunkRemoved((MapChunk) chunk); + + if (_chunks.Count == 0) + { + _entityManager.EventBus.RaiseLocalEvent(GridEntityId, new EmptyGridEvent {GridId = Index}); + } + } + /// public IMapChunkInternal GetChunk(Vector2i chunkIndices) { @@ -419,7 +429,13 @@ namespace Robust.Shared.Map /// public IEnumerable GetAnchoredEntities(Vector2i pos) { - var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); + // Because some content stuff checks neighboring tiles (which may not actually exist) we won't just + // create an entire chunk for it. + var gridChunkPos = GridTileToChunkIndices(pos); + + if (!_chunks.TryGetValue(gridChunkPos, out var chunk)) return Enumerable.Empty(); + + var chunkTile = chunk.GridTileToChunkTile(pos); return chunk.GetSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y); } @@ -697,4 +713,12 @@ namespace Robust.Shared.Map #endregion Transforms } + + /// + /// Raised whenever a grid becomes empty due to no more tiles with data. + /// + public sealed class EmptyGridEvent : EntityEventArgs + { + public GridId GridId { get; init; } + } } diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs index 82f8d80e1a..05c2985abd 100644 --- a/Robust.Shared/Map/MapManager.cs +++ b/Robust.Shared/Map/MapManager.cs @@ -16,6 +16,7 @@ namespace Robust.Shared.Map internal class MapManager : IMapManagerInternal { [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] protected readonly IComponentManager ComponentManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; private SharedGridFixtureSystem _gridFixtures = default!; @@ -118,6 +119,11 @@ namespace Robust.Shared.Map } } + public virtual void ChunkRemoved(MapChunk chunk) + { + return; + } + /// public void Shutdown() { @@ -645,20 +651,26 @@ namespace Robust.Shared.Map #if DEBUG DebugTools.Assert(_dbgGuardRunning); #endif - - if (gridID == GridId.Invalid) + // Possible the grid was already deleted / is invalid + if (!_grids.TryGetValue(gridID, out var grid)) return; - var grid = _grids[gridID]; var mapId = grid.ParentMapId; - if (_entityManager.TryGetEntity(grid.GridEntityId, out var gridEnt) && - gridEnt.LifeStage <= EntityLifeStage.Initialized) - gridEnt.Delete(); + if (_entityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) + { + // Because deleting a grid also removes its MapGridComponent which also deletes its grid again we'll check for that here. + if (gridEnt.LifeStage >= EntityLifeStage.Terminating) + return; + + if (gridEnt.LifeStage <= EntityLifeStage.Initialized) + gridEnt.Delete(); + } grid.Dispose(); _grids.Remove(grid.Index); + Logger.DebugS("map", $"Deleted grid {gridID}"); OnGridRemoved?.Invoke(mapId, gridID); } diff --git a/Robust.Shared/Physics/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/SharedBroadphaseSystem.cs index 7f9fd60b31..62f1b4d95d 100644 --- a/Robust.Shared/Physics/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/SharedBroadphaseSystem.cs @@ -506,7 +506,6 @@ namespace Robust.Shared.Physics if (!body._fixtures.Remove(fixture)) { - DebugTools.Assert(false); Logger.ErrorS("physics", $"Tried to remove fixture from {body.Owner} that was already removed."); return; } diff --git a/Robust.UnitTesting/Shared/Map/GridCollisionTest.cs b/Robust.UnitTesting/Shared/Map/GridCollision_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Map/GridCollisionTest.cs rename to Robust.UnitTesting/Shared/Map/GridCollision_Test.cs index 7330556e43..ebf3453133 100644 --- a/Robust.UnitTesting/Shared/Map/GridCollisionTest.cs +++ b/Robust.UnitTesting/Shared/Map/GridCollision_Test.cs @@ -7,7 +7,7 @@ using Robust.Shared.Physics; namespace Robust.UnitTesting.Shared.Map { - public class GridCollisionTest : RobustIntegrationTest + public class GridCollision_Test : RobustIntegrationTest { [Test] public async Task TestGridsCollide() diff --git a/Robust.UnitTesting/Shared/Map/GridContraction_Test.cs b/Robust.UnitTesting/Shared/Map/GridContraction_Test.cs new file mode 100644 index 0000000000..e8b0da4528 --- /dev/null +++ b/Robust.UnitTesting/Shared/Map/GridContraction_Test.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using Robust.Shared; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Robust.UnitTesting.Shared.Map +{ + [TestFixture] + public class GridContraction_Test : RobustIntegrationTest + { + [Test] + public async Task TestGridDeletes() + { + var server = StartServer(); + await server.WaitIdleAsync(); + + var entManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mapId = mapManager.CreateMap(); + var grid = mapManager.CreateGrid(mapId); + + for (var i = 0; i < 10; i++) + { + grid.SetTile(new Vector2i(i, 0), new Tile(1)); + } + + for (var i = 10; i >= 0; i--) + { + grid.SetTile(new Vector2i(i, 0), Tile.Empty); + } + + Assert.That(entManager.GetEntity(grid.GridEntityId).Deleted); + }); + } + + [Test] + public async Task TestGridNoDeletes() + { + var options = new ServerIntegrationOptions() + { + CVarOverrides = + { + { + CVars.GameDeleteEmptyGrids.Name, "false" + } + } + }; + var server = StartServer(options); + await server.WaitIdleAsync(); + + var entManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mapId = mapManager.CreateMap(); + var grid = mapManager.CreateGrid(mapId); + + for (var i = 0; i < 10; i++) + { + grid.SetTile(new Vector2i(i, 0), new Tile(1)); + } + + for (var i = 10; i >= 0; i--) + { + grid.SetTile(new Vector2i(i, 0), Tile.Empty); + } + + Assert.That(!entManager.GetEntity(grid.GridEntityId).Deleted); + }); + } + } +}