From 3aa04a3c863225fda75fdcd499933a78ec034bad Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 28 Oct 2023 13:57:54 +1100 Subject: [PATCH] Fix grid chunk bugs (#4525) * Fix grid rendering * Use TileChangedEvent * Other empty chunk fixes * Remove assert Good ol integration tests at it again, adding invalid components --- .../Graphics/Clyde/Clyde.GridRendering.cs | 76 +++--- Robust.Client/Graphics/Clyde/Clyde.cs | 2 - .../Systems/SharedMapSystem.Grid.cs | 249 ++++++++++++------ .../Systems/SharedMapSystem.GridChunk.cs | 5 +- .../GameObjects/Systems/SharedMapSystem.cs | 33 +-- Robust.Shared/GameStates/GameStateMapData.cs | 8 +- Robust.Shared/Map/IMapManagerInternal.cs | 3 +- Robust.Shared/Map/MapChunk.cs | 45 ++-- .../Map/MapManager.GridCollection.cs | 5 +- Robust.Shared/Map/MapManager.Queries.cs | 6 +- 10 files changed, 249 insertions(+), 183 deletions(-) diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 8c87f3f3a5..93c65cfb48 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -50,14 +50,18 @@ namespace Robust.Client.Graphics.Clyde var transform = _entityManager.GetComponent(mapGrid); gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix); var enumerator = mapGrid.Comp.GetMapChunks(worldBounds); + var data = _mapChunkData[mapGrid]; while (enumerator.MoveNext(out var chunk)) { - if (_isChunkDirty(mapGrid, chunk)) - _updateChunkMesh(mapGrid, chunk); + DebugTools.Assert(chunk.FilledTiles > 0); + if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum)) + data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk); - var datum = _mapChunkData[mapGrid][chunk.Indices]; + if (datum.Dirty) + _updateChunkMesh(mapGrid, chunk, datum); + DebugTools.Assert(datum.TileCount > 0); if (datum.TileCount == 0) continue; @@ -69,17 +73,31 @@ namespace Robust.Client.Graphics.Clyde CheckGlError(); } } + + CullEmptyChunks(); } - private void _updateChunkMesh(Entity grid, MapChunk chunk) + private void CullEmptyChunks() { - var data = _mapChunkData[grid]; - - if (!data.TryGetValue(chunk.Indices, out var datum)) + foreach (var (grid, chunks) in _mapChunkData) { - datum = _initChunkBuffers(grid, chunk); - } + var gridComp = _mapManager.GetGridComp(grid); + foreach (var (index, chunk) in chunks) + { + if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index)) + { + DebugTools.Assert(gridComp.Chunks[index].FilledTiles > 0); + continue; + } + DeleteChunk(chunk); + chunks.Remove(index); + } + } + } + + private void _updateChunkMesh(Entity grid, MapChunk chunk, MapChunkData datum) + { Span indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)]; Span vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)]; @@ -159,41 +177,22 @@ namespace Robust.Client.Graphics.Clyde Dirty = true }; - _mapChunkData[grid].Add(chunk.Indices, datum); return datum; } - private bool _isChunkDirty(Entity grid, MapChunk chunk) + private void DeleteChunk(MapChunkData data) { - var data = _mapChunkData[grid]; - return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty; - } - - public void _setChunkDirty(Entity grid, Vector2i chunk) - { - var data = _mapChunkData.GetOrNew(grid); - if (data.TryGetValue(chunk, out var datum)) - { - datum.Dirty = true; - } - // Don't need to set it if we don't have an entry since lack of an entry is treated as dirty. - } - - private void _updateOnGridModified(GridModifiedEvent args) - { - foreach (var (pos, _) in args.Modified) - { - var grid = args.Grid; - var chunk = grid.GridTileToChunkIndices(pos); - _setChunkDirty((args.GridEnt, grid), chunk); - } + DeleteVertexArray(data.VAO); + CheckGlError(); + data.VBO.Delete(); + data.EBO.Delete(); } private void _updateTileMapOnUpdate(ref TileChangedEvent args) { - var grid = _mapManager.GetGrid(args.NewTile.GridUid); - var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y)); - _setChunkDirty((args.NewTile.GridUid, grid), chunk); + var gridData = _mapChunkData.GetOrNew(args.Entity); + if (gridData.TryGetValue(args.ChunkIndex, out var data)) + data.Dirty = true; } private void _updateOnGridCreated(GridStartupEvent ev) @@ -209,10 +208,7 @@ namespace Robust.Client.Graphics.Clyde var data = _mapChunkData[gridId]; foreach (var chunkDatum in data.Values) { - DeleteVertexArray(chunkDatum.VAO); - CheckGlError(); - chunkDatum.VBO.Delete(); - chunkDatum.EBO.Delete(); + DeleteChunk(chunkDatum); } _mapChunkData.Remove(gridId); diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index 81852f34cd..b85f33b3c5 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -173,7 +173,6 @@ namespace Robust.Client.Graphics.Clyde _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _updateTileMapOnUpdate); _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _updateOnGridCreated); _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _updateOnGridRemoved); - _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _updateOnGridModified); } public void ShutdownGridEcsEvents() @@ -181,7 +180,6 @@ namespace Robust.Client.Graphics.Clyde _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); - _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); } private void GLInitBindings(bool gles) diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 4a258c4d30..03089623c9 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -211,52 +211,23 @@ public abstract partial class SharedMapSystem if (args.Current is not MapGridComponentState state) return; + DebugTools.Assert(component.ChunkSize == state.ChunkSize || component.Chunks.Count == 0, + "Can't modify chunk size of an existing grid."); + component.ChunkSize = state.ChunkSize; + DebugTools.Assert(state.ChunkData != null || state.FullGridData != null); if (state.ChunkData == null && state.FullGridData == null) return; - var modified = new List<(Vector2i position, Tile tile)>(); - MapManager.SuppressOnTileChanged = true; + var modifiedChunks = new HashSet(); // delta state if (state.ChunkData != null) { foreach (var chunkData in state.ChunkData) { - if (chunkData.IsDeleted()) - continue; - - var chunk = GetOrAddChunk(uid, component, chunkData.Index); - chunk.SuppressCollisionRegeneration = true; - DebugTools.Assert(chunkData.TileData.Length == component.ChunkSize * component.ChunkSize); - - var counter = 0; - for (ushort x = 0; x < component.ChunkSize; x++) - { - for (ushort y = 0; y < component.ChunkSize; y++) - { - var tile = chunkData.TileData[counter++]; - if (chunk.GetTile(x, y) == tile) - continue; - - SetChunkTile(uid, component, chunk, x, y, tile); - modified.Add((new Vector2i(chunk.X * component.ChunkSize + x, chunk.Y * component.ChunkSize + y), tile)); - } - } - } - - foreach (var chunkData in state.ChunkData) - { - if (chunkData.IsDeleted()) - { - RemoveChunk(uid, component, chunkData.Index); - continue; - } - - var chunk = GetOrAddChunk(uid, component, chunkData.Index); - chunk.SuppressCollisionRegeneration = false; - RegenerateCollision(uid, component, chunk); + ApplyChunkData(uid, component, chunkData, modifiedChunks); } } @@ -266,37 +237,86 @@ public abstract partial class SharedMapSystem foreach (var index in component.Chunks.Keys) { if (!state.FullGridData.ContainsKey(index)) - RemoveChunk(uid, component, index); + ApplyChunkData(uid, component, ChunkDatum.CreateDeleted(index), modifiedChunks); } foreach (var (index, tiles) in state.FullGridData) { - var chunk = GetOrAddChunk(uid, component, index); - chunk.SuppressCollisionRegeneration = true; - DebugTools.Assert(tiles.Length == component.ChunkSize * component.ChunkSize); - - var counter = 0; - for (ushort x = 0; x < component.ChunkSize; x++) - { - for (ushort y = 0; y < component.ChunkSize; y++) - { - var tile = tiles[counter++]; - if (chunk.GetTile(x, y) == tile) - continue; - - SetChunkTile(uid, component, chunk, x, y, tile); - modified.Add((new Vector2i(chunk.X * component.ChunkSize + x, chunk.Y * component.ChunkSize + y), tile)); - } - } - - chunk.SuppressCollisionRegeneration = false; - RegenerateCollision(uid, component, chunk); + ApplyChunkData(uid, component, ChunkDatum.CreateModified(index, tiles), modifiedChunks); } } - MapManager.SuppressOnTileChanged = false; - if (modified.Count != 0) - RaiseLocalEvent(uid, new GridModifiedEvent(uid, component, modified), true); + var count = component.Chunks.Count; + RegenerateCollision(uid, component, modifiedChunks); + + // Regeneration can remove chunks in general, but it shouldn't do that here as the state handling + // should already have removed all the chunks. + DebugTools.AssertEqual(component.Chunks.Count, count); + +#if DEBUG + foreach (var chunk in component.Chunks.Values) + { + chunk.ValidateChunk(); + DebugTools.Assert(chunk.FilledTiles > 0); + } +#endif + } + + private void ApplyChunkData(EntityUid uid, MapGridComponent component, ChunkDatum data, + HashSet modifiedChunks) + { + bool shapeChanged = false; + var counter = 0; + + if (data.IsDeleted()) + { + if (!component.Chunks.TryGetValue(data.Index, out var deletedChunk)) + return; + + // Deleted chunks still need to raise tile-changed events. + deletedChunk.SuppressCollisionRegeneration = true; + for (ushort x = 0; x < component.ChunkSize; x++) + { + for (ushort y = 0; y < component.ChunkSize; y++) + { + if (!deletedChunk.TrySetTile(x, y, Tile.Empty, out var oldTile, out var chunkShapeChanged)) + continue; + + var gridIndices = deletedChunk.ChunkTileToGridTile((x, y)); + var newTileRef = new TileRef(uid, gridIndices, Tile.Empty); + _mapInternal.RaiseOnTileChanged(newTileRef, oldTile, data.Index); + } + } + + component.Chunks.Remove(data.Index); + + // TODO is this required? + modifiedChunks.Add(deletedChunk); + return; + } + + var chunk = GetOrAddChunk(uid, component, data.Index); + chunk.SuppressCollisionRegeneration = true; + DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty)); + DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize); + for (ushort x = 0; x < component.ChunkSize; x++) + { + for (ushort y = 0; y < component.ChunkSize; y++) + { + var tile = data.TileData[counter++]; + if (!chunk.TrySetTile(x, y, tile, out var oldTile, out var tileShapeChanged)) + continue; + + shapeChanged |= tileShapeChanged; + var gridIndices = chunk.ChunkTileToGridTile((x, y)); + var newTileRef = new TileRef(uid, gridIndices, tile); + _mapInternal.RaiseOnTileChanged(newTileRef, oldTile, data.Index); + } + } + + chunk.SuppressCollisionRegeneration = false; + if (shapeChanged) + modifiedChunks.Add(chunk); } private void OnGridGetState(EntityUid uid, MapGridComponent component, ref ComponentGetState args) @@ -317,14 +337,21 @@ public abstract partial class SharedMapSystem else { chunkData = new List(); - var chunks = component.ChunkDeletionHistory; - foreach (var (tick, indices) in chunks) + foreach (var (tick, indices) in component.ChunkDeletionHistory) { if (tick < fromTick && fromTick != GameTick.Zero) continue; - chunkData.Add(ChunkDatum.CreateDeleted(indices)); + // Chunk may have been re-added sometime after it was deleted, but before deletion history was culled. + if (!component.Chunks.TryGetValue(indices, out var chunk)) + { + chunkData.Add(ChunkDatum.CreateDeleted(indices)); + continue; + } + + if (chunk.LastTileModifiedTick < fromTick) + Log.Error($"Encountered un-deleted chunk with an old last-modified tick on grid {ToPrettyString(uid)}"); } foreach (var (index, chunk) in GetMapChunks(uid, component)) @@ -349,6 +376,21 @@ public abstract partial class SharedMapSystem } args.State = new MapGridComponentState(component.ChunkSize, chunkData); + +#if DEBUG + if (chunkData == null) + return; + + HashSet keys = new(); + foreach (var chunk in chunkData) + { + if (chunk.TileData == null) + continue; + + DebugTools.Assert(keys.Add(chunk.Index), "Duplicate chunk"); + DebugTools.Assert(chunk.TileData.Any(x => !x.IsEmpty), "Empty non-deleted chunk"); + } +#endif } private void GetFullState(EntityUid uid, MapGridComponent component, ref ComponentGetState args) @@ -370,6 +412,13 @@ public abstract partial class SharedMapSystem } args.State = new MapGridComponentState(component.ChunkSize, chunkData); + +#if DEBUG + foreach (var chunk in chunkData.Values) + { + DebugTools.Assert(chunk.Any(x => !x.IsEmpty)); + } +#endif } private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args) @@ -432,14 +481,6 @@ public abstract partial class SharedMapSystem component.MapProxy = DynamicTree.Proxy.Free; RaiseLocalEvent(uid, new GridRemovalEvent(uid), true); - - if (uid == EntityUid.Invalid) - return; - - if (!MapManager.GridExists(uid)) - return; - - MapManager.DeleteGrid(uid); } private Box2 GetWorldAABB(EntityUid uid, MapGridComponent grid, TransformComponent? xform = null) @@ -660,11 +701,20 @@ public abstract partial class SharedMapSystem public void SetTile(EntityUid uid, MapGridComponent grid, Vector2i gridIndices, Tile tile) { - var (chunk, chunkTile) = ChunkAndOffsetForTile(uid, grid, gridIndices); - SetChunkTile(uid, grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y, tile); - // Ideally we'd to this here for consistency but apparently tile modified does it or something. - // Yeah it's noodly. - // RegenerateCollision(chunk); + var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices); + if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk)) + { + if (tile.IsEmpty) + return; + + grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize) + { + LastTileModifiedTick = _timing.CurTick + }; + } + + var offset = chunk.GridTileToChunkTile(gridIndices); + SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile); } public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIndices, Tile Tile)> tiles) @@ -676,10 +726,22 @@ public abstract partial class SharedMapSystem foreach (var (gridIndices, tile) in tiles) { - var (chunk, chunkTile) = ChunkAndOffsetForTile(uid, grid, gridIndices); + var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices); + if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk)) + { + if (tile.IsEmpty) + return; + + grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize) + { + LastTileModifiedTick = _timing.CurTick + }; + } + + var offset = chunk.GridTileToChunkTile(gridIndices); chunks.Add(chunk); chunk.SuppressCollisionRegeneration = true; - SetChunkTile(uid, grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y, tile); + SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile); } foreach (var chunk in chunks) @@ -988,14 +1050,18 @@ public abstract partial class SharedMapSystem public bool IsAnchored(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid) { var tilePos = TileIndicesFor(uid, grid, coords); - var (chunk, chunkTile) = ChunkAndOffsetForTile(uid, grid, tilePos); + + if (!TryChunkAndOffsetForTile(uid, grid, tilePos, out var chunk, out var chunkTile)) + return false; + var snapgrid = chunk.GetSnapGrid((ushort)chunkTile.X, (ushort)chunkTile.Y); return snapgrid?.Contains(euid) == true; } public bool AddToSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid) { - var (chunk, chunkTile) = ChunkAndOffsetForTile(gridUid, grid, pos); + if (!TryChunkAndOffsetForTile(gridUid, grid, pos, out var chunk, out var chunkTile)) + return false; if (chunk.GetTile((ushort)chunkTile.X, (ushort)chunkTile.Y).IsEmpty) return false; @@ -1011,7 +1077,12 @@ public abstract partial class SharedMapSystem public void RemoveFromSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid) { - var (chunk, chunkTile) = ChunkAndOffsetForTile(gridUid, grid, pos); + var gridChunkIndices = GridTileToChunkIndices(gridUid, grid, pos); + + if (!grid.Chunks.TryGetValue(gridChunkIndices, out var chunk)) + return; + + var chunkTile = chunk.GridTileToChunkTile(pos); chunk.RemoveFromSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, euid); } @@ -1020,12 +1091,18 @@ public abstract partial class SharedMapSystem RemoveFromSnapGridCell(gridUid, grid, TileIndicesFor(gridUid, grid, coords), euid); } - private (MapChunk, Vector2i) ChunkAndOffsetForTile(EntityUid uid, MapGridComponent grid, Vector2i pos) + private bool TryChunkAndOffsetForTile(EntityUid uid, MapGridComponent grid, Vector2i pos, + [NotNullWhen(true)]out MapChunk? chunk, out Vector2i offset) { var gridChunkIndices = GridTileToChunkIndices(uid, grid, pos); - var chunk = GetOrAddChunk(uid, grid, gridChunkIndices); - var chunkTile = chunk.GridTileToChunkTile(pos); - return (chunk, chunkTile); + if (!grid.Chunks.TryGetValue(gridChunkIndices, out chunk)) + { + offset = default; + return false; + } + + offset = chunk.GridTileToChunkTile(pos); + return true; } public IEnumerable GetInDir(EntityUid uid, MapGridComponent grid, EntityCoordinates position, Direction dir) @@ -1264,7 +1341,7 @@ public abstract partial class SharedMapSystem var gridTile = mapChunk.ChunkTileToGridTile(tileIndices); mapChunk.LastTileModifiedTick = _timing.CurTick; grid.LastTileModifiedTick = _timing.CurTick; - Dirty(grid); + Dirty(uid, grid); // The map serializer currently sets tiles of unbound grids as part of the deserialization process // It properly sets SuppressOnTileChanged so that the event isn't spammed for every tile on the grid. @@ -1272,7 +1349,7 @@ public abstract partial class SharedMapSystem if (!MapManager.SuppressOnTileChanged) { var newTileRef = new TileRef(uid, gridTile, newTile); - _mapInternal.RaiseOnTileChanged(newTileRef, oldTile); + _mapInternal.RaiseOnTileChanged(newTileRef, oldTile, mapChunk.Indices); } if (shapeChanged && !mapChunk.SuppressCollisionRegeneration) diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.GridChunk.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.GridChunk.cs index a72c64b036..51122b46d3 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.GridChunk.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.GridChunk.cs @@ -14,12 +14,13 @@ public abstract partial class SharedMapSystem /// The X tile index relative to the chunk. /// The Y tile index relative to the chunk. /// The new tile to insert. - internal void SetChunkTile(EntityUid uid, MapGridComponent grid, MapChunk chunk, ushort xIndex, ushort yIndex, Tile tile) + internal bool SetChunkTile(EntityUid uid, MapGridComponent grid, MapChunk chunk, ushort xIndex, ushort yIndex, Tile tile) { if (!chunk.TrySetTile(xIndex, yIndex, tile, out var oldTile, out var shapeChanged)) - return; + return false; var tileIndices = new Vector2i(xIndex, yIndex); OnTileModified(uid, grid, chunk, tileIndices, tile, oldTile, shapeChanged); + return true; } } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 5e79bb7c3f..002235c59d 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -126,11 +126,12 @@ namespace Robust.Shared.GameObjects /// /// Creates a new instance of this class. /// - public TileChangedEvent(EntityUid uid, TileRef newTile, Tile oldTile) + public TileChangedEvent(EntityUid uid, TileRef newTile, Tile oldTile, Vector2i chunkIndex) { Entity = uid; NewTile = newTile; OldTile = oldTile; + ChunkIndex = chunkIndex; } /// @@ -147,36 +148,10 @@ namespace Robust.Shared.GameObjects /// Old tile that was replaced. /// public readonly Tile OldTile; - } - - /// - /// Arguments for when a one or more tiles on a grid are modified at once. - /// - public sealed class GridModifiedEvent : EntityEventArgs - { - /// - /// The id of the grid being changed. - /// - public EntityUid GridEnt { get; } /// - /// Grid being changed. + /// The index of the grid-chunk that this tile belongs to. /// - public MapGridComponent Grid { get; } - - /// - /// Set of tiles that were modified. - /// - public IReadOnlyCollection<(Vector2i position, Tile tile)> Modified { get; } - - /// - /// Creates a new instance of this class. - /// - public GridModifiedEvent(EntityUid gridEnt, MapGridComponent grid, IReadOnlyCollection<(Vector2i position, Tile tile)> modified) - { - GridEnt = gridEnt; - Grid = grid; - Modified = modified; - } + public readonly Vector2i ChunkIndex; } } diff --git a/Robust.Shared/GameStates/GameStateMapData.cs b/Robust.Shared/GameStates/GameStateMapData.cs index dc6e50fbfd..3fb09a608d 100644 --- a/Robust.Shared/GameStates/GameStateMapData.cs +++ b/Robust.Shared/GameStates/GameStateMapData.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization; @@ -13,11 +14,12 @@ namespace Robust.Shared.GameStates // Definitely wasteful to send EVERY tile. // Optimize away future coder. // Also it's stored row-major. - public readonly Tile[] TileData; + public readonly Tile[]? TileData; + [MemberNotNullWhen(false, nameof(TileData))] public bool IsDeleted() { - return TileData == default; + return TileData == null; } private ChunkDatum(Vector2i index, Tile[] tileData) @@ -33,7 +35,7 @@ namespace Robust.Shared.GameStates public static ChunkDatum CreateDeleted(Vector2i index) { - return new ChunkDatum(index, default!); + return new ChunkDatum(index, null!); } } } diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs index 86cb9242b6..0e42ea41b0 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 @@ -13,7 +14,7 @@ namespace Robust.Shared.Map /// /// A reference to the new tile. /// The old tile that got replaced. - void RaiseOnTileChanged(TileRef tileRef, Tile oldTile); + void RaiseOnTileChanged(TileRef tileRef, Tile oldTile, Vector2i chunk); MapId CreateMap(MapId? mapId, EntityUid euid); diff --git a/Robust.Shared/Map/MapChunk.cs b/Robust.Shared/Map/MapChunk.cs index 8160eddc9f..98cc4d4d3b 100644 --- a/Robust.Shared/Map/MapChunk.cs +++ b/Robust.Shared/Map/MapChunk.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Maths; @@ -227,35 +229,48 @@ namespace Robust.Shared.Map if (yIndex >= Tiles.Length) throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds."); - // same tile, no point to continue - if (Tiles[xIndex, yIndex] == tile) + shapeChanged = false; + + ref var tileRef = ref Tiles[xIndex, yIndex]; + if (tileRef == tile) { - oldTile = Tile.Empty; - shapeChanged = false; + oldTile = default; return false; } - oldTile = Tiles[xIndex, yIndex]; - var oldFilledTiles = FilledTiles; - - if (oldTile.IsEmpty != tile.IsEmpty) + if (tileRef.IsEmpty) { - if (oldTile.IsEmpty) + if (!tile.IsEmpty) { FilledTiles += 1; - } - else - { - FilledTiles -= 1; + shapeChanged = true; } } + else if (tile.IsEmpty) + { + FilledTiles -= 1; + shapeChanged = true; + } - shapeChanged = oldFilledTiles != FilledTiles; DebugTools.Assert(FilledTiles >= 0); - Tiles[xIndex, yIndex] = tile; + oldTile = tileRef; + tileRef = tile; + ValidateChunk(); return true; } + + [Conditional("DEBUG")] + public void ValidateChunk() + { + var totalFilled = 0; + foreach (var t in Tiles) + { + if (!t.IsEmpty) + totalFilled += 1; + } + DebugTools.Assert(totalFilled == FilledTiles); + } } /// diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs index 0b2b501264..a34ef91c8e 100644 --- a/Robust.Shared/Map/MapManager.GridCollection.cs +++ b/Robust.Shared/Map/MapManager.GridCollection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; +using Robust.Shared.Maths; using Robust.Shared.Utility; // All the obsolete warnings about GridId are probably useless here. @@ -138,7 +139,7 @@ internal partial class MapManager /// /// A reference to the new tile. /// The old tile that got replaced. - public void RaiseOnTileChanged(TileRef tileRef, Tile oldTile) + public void RaiseOnTileChanged(TileRef tileRef, Tile oldTile, Vector2i chunk) { #if DEBUG DebugTools.Assert(_dbgGuardRunning); @@ -148,7 +149,7 @@ internal partial class MapManager return; var euid = tileRef.GridUid; - var ev = new TileChangedEvent(euid, tileRef, oldTile); + var ev = new TileChangedEvent(euid, tileRef, oldTile, chunk); EntityManager.EventBus.RaiseLocalEvent(euid, ref ev, true); } diff --git a/Robust.Shared/Map/MapManager.Queries.cs b/Robust.Shared/Map/MapManager.Queries.cs index 35cc744da4..389feec5ae 100644 --- a/Robust.Shared/Map/MapManager.Queries.cs +++ b/Robust.Shared/Map/MapManager.Queries.cs @@ -215,14 +215,14 @@ internal partial class MapManager // you account for the fact that fixtures are shrunk slightly! var chunkIndices = SharedMapSystem.GetChunkIndices(localPos, iGrid.ChunkSize); - if (!tuple.mapSystem.HasChunk(iUid, iGrid, chunkIndices)) + if (!iGrid.Chunks.TryGetValue(chunkIndices, out var chunk)) return true; - var chunk = tuple.mapSystem.GetOrAddChunk(iUid, iGrid, chunkIndices); var chunkRelative = SharedMapSystem.GetChunkRelative(localPos, iGrid.ChunkSize); var chunkTile = chunk.GetTile(chunkRelative); - if (chunkTile.IsEmpty) return true; + if (chunkTile.IsEmpty) + return true; tuple.uid = iUid; tuple.grid = iGrid;