diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 898e63e3c..91ce909b9 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -43,9 +43,9 @@ namespace Robust.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs b/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs index f65b769ac..fae20b6e4 100644 --- a/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/ClientOccluderSystem.cs @@ -72,7 +72,7 @@ namespace Robust.Client.GameObjects var sender = ev.Sender; if (EntityManager.EntityExists(sender) && EntityManager.TryGetComponent(sender, out ClientOccluderComponent? iconSmooth) - && iconSmooth.Running) + && iconSmooth.Initialized) { var grid1 = _mapManager.GetGrid(EntityManager.GetComponent(sender).GridID); var coords = EntityManager.GetComponent(sender).Coordinates; @@ -85,7 +85,7 @@ namespace Robust.Client.GameObjects } // Entity is no longer valid, update around the last position it was at. - if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out var grid)) + else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out var grid)) { var pos = ev.LastPosition.Value.pos; diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index 446c5f79a..7456f40f3 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -7,7 +7,6 @@ using System.Linq; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Input; -using Robust.Client.Map; using Robust.Client.Player; using Robust.Client.Timing; using Robust.Shared; @@ -50,7 +49,7 @@ namespace Robust.Client.GameStates [Dependency] private readonly IPlayerManager _players = default!; [Dependency] private readonly IClientNetManager _network = default!; [Dependency] private readonly IBaseClient _client = default!; - [Dependency] private readonly IClientMapManager _mapManager = default!; + [Dependency] private readonly INetworkedMapManager _mapManager = default!; [Dependency] private readonly IClientGameTiming _timing = default!; [Dependency] private readonly INetConfigurationManager _config = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 5aec20203..80393973e 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; diff --git a/Robust.Client/Map/ClientMapManager.cs b/Robust.Client/Map/ClientMapManager.cs deleted file mode 100644 index ce409db5e..000000000 --- a/Robust.Client/Map/ClientMapManager.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -namespace Robust.Client.Map -{ - internal class ClientMapManager : MapManager, IClientMapManager - { - public void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan entityStates) - { - // There was no map data this tick, so nothing to do. - if(data == null) - return; - - // First we need to figure out all the NEW MAPS. - if(data.CreatedMaps != null) - { - DebugTools.Assert(!entityStates.IsEmpty, "Received new maps, but no entity state."); - - foreach (var mapId in data.CreatedMaps) - { - // map already exists from a previous state. - if (_maps.Contains(mapId)) - continue; - - EntityUid mapEuid = default; - - //get shared euid of map comp entity - foreach (var entityState in entityStates!) - { - foreach (var compChange in entityState.ComponentChanges.Span) - { - if (compChange.State is not MapComponentState mapCompState || mapCompState.MapId != mapId) - continue; - - mapEuid = entityState.Uid; - goto BreakMapEntSearch; - } - } - BreakMapEntSearch: - - DebugTools.Assert(mapEuid != default, $"Could not find corresponding entity state for new map {mapId}."); - - CreateMap(mapId, mapEuid); - } - } - - // Then make all the grids. - if(data.CreatedGrids != null) - { - DebugTools.Assert(data.GridData is not null, "Received new grids, but GridData was null."); - - foreach (var (gridId, creationDatum) in data.CreatedGrids) - { - if (_grids.ContainsKey(gridId)) - continue; - - EntityUid gridEuid = default; - - //get shared euid of map comp entity - foreach (var entityState in entityStates!) - { - foreach (var compState in entityState.ComponentChanges.Span) - { - if (compState.State is not MapGridComponentState gridCompState || gridCompState.GridIndex != gridId) - continue; - - gridEuid = entityState.Uid; - goto BreakGridEntSearch; - } - } - BreakGridEntSearch: - - DebugTools.Assert(gridEuid != default, $"Could not find corresponding entity state for new grid {gridId}."); - - MapId gridMapId = default; - foreach (var kvData in data.GridData!) - { - if (kvData.Key != gridId) - continue; - - gridMapId = kvData.Value.Coordinates.MapId; - break; - } - - DebugTools.Assert(gridMapId != default, $"Could not find corresponding gridData for new grid {gridId}."); - - CreateGrid(gridMapId, gridId, creationDatum.ChunkSize, gridEuid); - } - } - - // Process all grid updates. - if(data.GridData != null) - { - SuppressOnTileChanged = true; - // Ok good all the grids and maps exist now. - foreach (var (gridId, gridDatum) in data.GridData) - { - var grid = _grids[gridId]; - if (grid.ParentMapId != gridDatum.Coordinates.MapId) - { - throw new NotImplementedException("Moving grids between maps is not yet implemented"); - } - - // I love mapmanager!!! - grid.WorldPosition = gridDatum.Coordinates.Position; - grid.WorldRotation = gridDatum.Angle; - - var modified = new List<(Vector2i position, Tile tile)>(); - foreach (var chunkData in gridDatum.ChunkData) - { - var chunk = grid.GetChunk(chunkData.Index); - chunk.SuppressCollisionRegeneration = true; - DebugTools.Assert(chunkData.TileData.Length == grid.ChunkSize * grid.ChunkSize); - - var counter = 0; - for (ushort x = 0; x < grid.ChunkSize; x++) - { - for (ushort y = 0; y < grid.ChunkSize; y++) - { - var tile = chunkData.TileData[counter++]; - if (chunk.GetTileRef(x, y).Tile != tile) - { - chunk.SetTile(x, y, tile); - modified.Add((new Vector2i(chunk.X * grid.ChunkSize + x, chunk.Y * grid.ChunkSize + y), tile)); - } - } - } - } - - 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; - } - } - - public void ApplyGameStatePost(GameStateMapData? data) - { - if(data == null) // if there is no data, there is nothing to do! - return; - - if(data.DeletedGrids != null) - { - foreach (var grid in data.DeletedGrids) - { - if (_grids.ContainsKey(grid)) - { - DeleteGrid(grid); - } - } - } - - if(data.DeletedMaps != null) - { - foreach (var map in data.DeletedMaps) - { - if (_maps.Contains(map)) - { - DeleteMap(map); - } - } - } - } - } -} diff --git a/Robust.Client/Map/IClientMapManager.cs b/Robust.Client/Map/IClientMapManager.cs deleted file mode 100644 index b1b40a4b2..000000000 --- a/Robust.Client/Map/IClientMapManager.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.Map; - -namespace Robust.Client.Map -{ - internal interface IClientMapManager : IMapManagerInternal - { - // Two methods here, so that new grids etc can be made BEFORE entities get states applied, - // but old ones can be deleted after. - void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan entityStates); - void ApplyGameStatePost(GameStateMapData? data); - } -} diff --git a/Robust.Server/GameStates/PVSSystem.cs b/Robust.Server/GameStates/PVSSystem.cs index b081820d2..59c24e667 100644 --- a/Robust.Server/GameStates/PVSSystem.cs +++ b/Robust.Server/GameStates/PVSSystem.cs @@ -232,8 +232,8 @@ internal partial class PVSSystem : EntitySystem pvsCollection.AddGrid(gridId); } - var uid = _mapManager.GetGrid(gridId).GridEntityId; - _entityPvsCollection.UpdateIndex(uid); + var euid = _mapManager.GetGridEuid(gridId); + _entityPvsCollection.UpdateIndex(euid); } private void OnMapDestroyed(object? sender, MapEventArgs e) diff --git a/Robust.Server/GameStates/ServerGameStateManager.cs b/Robust.Server/GameStates/ServerGameStateManager.cs index ce9598f9d..ff82ccb49 100644 --- a/Robust.Server/GameStates/ServerGameStateManager.cs +++ b/Robust.Server/GameStates/ServerGameStateManager.cs @@ -1,19 +1,16 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Robust.Server.GameObjects; -using Robust.Server.Map; using Robust.Server.Player; -using Robust.Shared; -using Robust.Shared.Configuration; using Robust.Shared.Enums; 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.Network.Messages; using Robust.Shared.Timing; @@ -35,7 +32,7 @@ namespace Robust.Server.GameStates [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IServerNetManager _networkManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IServerMapManager _mapManager = default!; + [Dependency] private readonly INetworkedMapManager _mapManager = default!; [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IServerEntityNetworkManager _entityNetworkManager = default!; diff --git a/Robust.Server/Map/IServerMapManager.cs b/Robust.Server/Map/IServerMapManager.cs deleted file mode 100644 index dd649c9c3..000000000 --- a/Robust.Server/Map/IServerMapManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Map; -using Robust.Shared.Timing; - -namespace Robust.Server.Map -{ - internal interface IServerMapManager : IMapManagerInternal - { - GameStateMapData? GetStateData(GameTick fromTick); - void CullDeletionHistory(GameTick uptoTick); - } -} diff --git a/Robust.Server/Map/ServerMapManager.cs b/Robust.Server/Map/ServerMapManager.cs deleted file mode 100644 index 839fcd45a..000000000 --- a/Robust.Server/Map/ServerMapManager.cs +++ /dev/null @@ -1,180 +0,0 @@ -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.Map; -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(); - - public override void DeleteMap(MapId mapID) - { - base.DeleteMap(mapID); - _mapDeletionHistory.Add((GameTiming.CurTick, mapID)); - } - - public override void DeleteGrid(GridId gridID) - { - 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) || - !EntityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) || - chunk.Fixtures.Count == 0) 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? - var fixtureSystem = EntitySystem.Get(); - - foreach (var fixture in chunk.Fixtures) - { - fixtureSystem.DestroyFixture(body, fixture); - } - } - - public GameStateMapData? GetStateData(GameTick fromTick) - { - var gridDatums = new Dictionary(); - foreach (var grid in _grids.Values) - { - if (grid.LastTileModifiedTick < fromTick) - { - 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.LastTileModifiedTick < fromTick) - { - continue; - } - - var tileBuffer = new Tile[grid.ChunkSize * (uint) grid.ChunkSize]; - - // Flatten the tile array. - // NetSerializer doesn't do multi-dimensional arrays. - // This is probably really expensive. - for (var x = 0; x < grid.ChunkSize; x++) - { - for (var y = 0; y < grid.ChunkSize; y++) - { - tileBuffer[x * grid.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y); - } - } - - chunkData.Add(new GameStateMapData.ChunkDatum(index, tileBuffer)); - } - - var gridDatum = new GameStateMapData.GridDatum( - chunkData.ToArray(), - deletedChunkData.ToArray(), - new MapCoordinates(grid.WorldPosition, grid.ParentMapId), - grid.WorldRotation); - - gridDatums.Add(grid.Index, gridDatum); - } - - // -- Map Deletion Data -- - var mapDeletionsData = new List(); - - foreach (var (tick, mapId) in _mapDeletionHistory) - { - if (tick < fromTick) continue; - mapDeletionsData.Add(mapId); - } - - // -- Grid Deletion Data - var gridDeletionsData = new List(); - - foreach (var (tick, gridId) in _gridDeletionHistory) - { - if (tick < fromTick) continue; - gridDeletionsData.Add(gridId); - } - - // -- Map Creations -- - var mapCreations = new List(); - - foreach (var (mapId, tick) in _mapCreationTick) - { - if (tick < fromTick || mapId == MapId.Nullspace) continue; - mapCreations.Add(mapId); - } - - // - Grid Creation data -- - var gridCreations = new Dictionary(); - - foreach (var (gridId, grid) in _grids) - { - if (grid.CreatedTick < fromTick || grid.ParentMapId == MapId.Nullspace) continue; - gridCreations.Add(gridId, new GameStateMapData.GridCreationDatum(grid.ChunkSize)); - } - - // no point sending empty collections - if (gridDatums.Count == 0) gridDatums = default; - if (gridDeletionsData.Count == 0) gridDeletionsData = default; - if (mapDeletionsData.Count == 0) mapDeletionsData = default; - if (mapCreations.Count == 0) mapCreations = default; - if (gridCreations.Count == 0) gridCreations = default; - - // no point even creating an empty map state if no data - 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>()); - } - - 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/Maps/MapLoader.cs b/Robust.Server/Maps/MapLoader.cs index e35887a86..0fda7b981 100644 --- a/Robust.Server/Maps/MapLoader.cs +++ b/Robust.Server/Maps/MapLoader.cs @@ -231,7 +231,7 @@ namespace Robust.Server.Maps private readonly MapLoadOptions? _loadOptions; private readonly Dictionary GridIDMap = new(); - public readonly List Grids = new(); + public readonly List Grids = new(); private readonly Dictionary EntityUidMap = new(); private readonly Dictionary UidEntityMap = new(); @@ -330,6 +330,9 @@ namespace Robust.Server.Maps // Actually instance components and run ExposeData on them. FinishEntitiesLoad(); + // Finish binding MapGrids to their respective MapGridComponents. + BindGridComponents(); + // Clear the net tick numbers so that components from prototypes (not modified by map) // aren't sent over the wire initially. ResetNetTicks(); @@ -473,6 +476,43 @@ namespace Robust.Server.Maps } } + private void BindGridComponents() + { + // There were no new grids, nothing to do here. + if(Grids.Count == 0) + return; + + // MapGrids already contain their assigned GridId from their ctor, and the MapComponents just got deserialized. + // Now we need to actually bind the MapGrids to their components so that you can resolve GridId -> EntityUid + // After doing this, it should be 100% safe to use the MapManager API like normal. + + // get ents that the grids will bind to + var gridComps = new Dictionary(Grids.Count); + + // linear search for new grid comps + foreach (var tuple in _entitiesToDeserialize) + { + if (!_serverEntityManager.TryGetComponent(tuple.Item1, out MapGridComponent gridComp)) + continue; + + // These should actually be new, pre-init + DebugTools.Assert(gridComp.LifeStage == ComponentLifeStage.Added); + + // yaml deserializer turns "null" into Invalid, this has been encountered by a customer from failed serialization. + DebugTools.Assert(gridComp.GridIndex != GridId.Invalid); + + gridComps[gridComp.GridIndex] = gridComp; + } + + // Actually bind them + foreach (var mapGrid in Grids) + { + // designed to throw if something is broken, every grid must map to an ent + var gridComp = gridComps[mapGrid.Index]; + _mapManager.BindGrid(gridComp, mapGrid); + } + } + private void AttachMapEntities() { var mapEntity = _mapManager.GetMapEntityIdOrThrow(TargetMap); @@ -496,8 +536,6 @@ namespace Robust.Server.Maps { if (_serverEntityManager.TryGetComponent(entity, out IMapGridComponent? grid)) { - var castGrid = (MapGrid) grid.Grid; - castGrid.GridEntityId = entity; pvs?.EntityPVSCollection.UpdateIndex(entity); // The problem here is that the grid is initialising at the same time as everything else which // is bad for slothcoin because a bunch of components are only added @@ -543,23 +581,19 @@ namespace Robust.Server.Maps private void ReadGridSection() { - var grids = RootNode.GetNode("grids"); + var yamlGrids = RootNode.GetNode("grids"); - foreach (var grid in grids) + foreach (var yamlGrid in yamlGrids) { - var newId = new GridId?(); - YamlGridSerializer.DeserializeGrid( - _mapManager, TargetMap, ref newId, - (YamlMappingNode) grid["settings"], - (YamlSequenceNode) grid["chunks"], + var grid = YamlGridSerializer.DeserializeGrid( + _mapManager, TargetMap, + (YamlMappingNode) yamlGrid["settings"], + (YamlSequenceNode) yamlGrid["chunks"], _tileMap!, _tileDefinitionManager ); - if (newId != null) - { - Grids.Add(_mapManager.GetGrid(newId.Value)); - } + Grids.Add(grid); } } @@ -654,7 +688,7 @@ namespace Robust.Server.Maps throw new InvalidOperationException(); } - Grids.Add(grid); + Grids.Add((MapGrid) grid); GridIDMap.Add(grid.Index, GridIDMap.Count); } @@ -896,6 +930,9 @@ namespace Robust.Server.Maps bool skipHook, ISerializationContext? context = null) { + // This is the code that deserializes the Grids index into the GridId. This has to happen between Grid allocation + // and when grids are bound to their entities. + if (node.Value == "null") return new DeserializedValue(GridId.Invalid); var val = int.Parse(node.Value); diff --git a/Robust.Server/Maps/YamlGridSerializer.cs b/Robust.Server/Maps/YamlGridSerializer.cs index 3d699df5d..6c458dfa2 100644 --- a/Robust.Server/Maps/YamlGridSerializer.cs +++ b/Robust.Server/Maps/YamlGridSerializer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Utility; using YamlDotNet.Core; @@ -77,7 +76,7 @@ namespace Robust.Server.Maps return Convert.ToBase64String(barr); } - public static void DeserializeGrid(IMapManagerInternal mapMan, MapId mapId, ref GridId? gridId, YamlMappingNode info, + public static MapGrid DeserializeGrid(IMapManagerInternal mapMan, MapId mapId, YamlMappingNode info, YamlSequenceNode chunks, IReadOnlyDictionary tileDefMapping, ITileDefinitionManager tileDefinitionManager) { @@ -97,14 +96,15 @@ namespace Robust.Server.Maps sgsz = float.Parse(val, CultureInfo.InvariantCulture); } - var grid = mapMan.CreateGridNoEntity(mapId, gridId); - - gridId = grid.Index; + //TODO: Pass in options + var grid = mapMan.CreateUnboundGrid(mapId); foreach (var chunkNode in chunks.Cast()) { DeserializeChunk(mapMan, grid, chunkNode, tileDefMapping, tileDefinitionManager); } + + return grid; } private static void DeserializeChunk(IMapManager mapMan, IMapGridInternal grid, YamlMappingNode chunkData, IReadOnlyDictionary tileDefMapping, ITileDefinitionManager tileDefinitionManager) diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index 4322ae2f6..239efb66e 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -4,7 +4,6 @@ using Robust.Server.DataMetrics; using Robust.Server.Debugging; using Robust.Server.GameObjects; using Robust.Server.GameStates; -using Robust.Server.Map; using Robust.Server.Maps; using Robust.Server.Placement; using Robust.Server.Player; @@ -47,9 +46,9 @@ namespace Robust.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs b/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs index 24ac2118c..da0c73381 100644 --- a/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs +++ b/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs @@ -16,7 +16,6 @@ namespace Robust.Shared.GameObjects { GridId GridIndex { get; } IMapGrid Grid { get; } - void ClearGridId(); bool AnchorEntity(TransformComponent transform); void UnanchorEntity(TransformComponent transform); @@ -24,16 +23,19 @@ namespace Robust.Shared.GameObjects /// [ComponentReference(typeof(IMapGridComponent))] - [NetworkedComponent()] + [NetworkedComponent] internal class MapGridComponent : Component, IMapGridComponent { - [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IMapManagerInternal _mapManager = default!; [Dependency] private readonly IEntityManager _entMan = default!; + // This field is used for deserialization internally in the map loader. [ViewVariables(VVAccess.ReadOnly)] [DataField("index")] private GridId _gridIndex = GridId.Invalid; + private IMapGrid? _mapGrid; + /// public GridId GridIndex { @@ -43,12 +45,10 @@ namespace Robust.Shared.GameObjects /// [ViewVariables] - public IMapGrid Grid => _mapManager.GetGrid(_gridIndex); - - /// - public void ClearGridId() + public IMapGrid Grid { - _gridIndex = GridId.Invalid; + get => _mapGrid ?? throw new InvalidOperationException(); + set => _mapGrid = value; } protected override void Initialize() @@ -62,6 +62,13 @@ namespace Robust.Shared.GameObjects } } + protected override void OnRemove() + { + _mapManager.TrueGridDelete((MapGrid)_mapGrid!); + + base.OnRemove(); + } + /// public bool AnchorEntity(TransformComponent transform) { diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 072ca6232..db954b459 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -1,4 +1,4 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -16,7 +16,7 @@ namespace Robust.Shared.GameObjects SubscribeLocalEvent(OnGridAdd); SubscribeLocalEvent(OnGridInit); SubscribeLocalEvent(OnGridStartup); - SubscribeLocalEvent(OnGridRemove); + SubscribeLocalEvent(OnGridRemove); } private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args) @@ -38,7 +38,7 @@ namespace Robust.Shared.GameObjects EntityManager.EventBus.RaiseLocalEvent(uid, msg); } - private void OnGridRemove(EntityUid uid, MapGridComponent component, ComponentRemove args) + private void OnGridRemove(EntityUid uid, MapGridComponent component, ComponentShutdown args) { EntityManager.EventBus.RaiseLocalEvent(uid, new GridRemovalEvent(uid, component.GridIndex)); MapManager.OnComponentRemoved(component); diff --git a/Robust.Shared/Map/FindGridsEnumerator.cs b/Robust.Shared/Map/FindGridsEnumerator.cs index 03d3aa4c4..c7dfbc14a 100644 --- a/Robust.Shared/Map/FindGridsEnumerator.cs +++ b/Robust.Shared/Map/FindGridsEnumerator.cs @@ -9,15 +9,16 @@ namespace Robust.Shared.Map { public struct FindGridsEnumerator : IDisposable { - private IEntityManager _entityManager; + private readonly IEntityManager _entityManager; - private Dictionary.Enumerator _enumerator; + // ReSharper disable once FieldCanBeMadeReadOnly.Local + private IEnumerator _enumerator; private MapId _mapId; private Box2 _worldAABB; private bool _approx; - internal FindGridsEnumerator(IEntityManager entityManager, Dictionary.Enumerator enumerator, MapId mapId, Box2 worldAABB, bool approx) + internal FindGridsEnumerator(IEntityManager entityManager, IEnumerator enumerator, MapId mapId, Box2 worldAABB, bool approx) { _entityManager = entityManager; _enumerator = enumerator; @@ -36,7 +37,7 @@ namespace Robust.Shared.Map return false; } - var (_, nextGrid) = _enumerator.Current; + var nextGrid = _enumerator.Current; if (nextGrid.ParentMapId != _mapId) { diff --git a/Robust.Shared/Map/GridEventHandler.cs b/Robust.Shared/Map/GridEventHandler.cs index dd222500b..40830c119 100644 --- a/Robust.Shared/Map/GridEventHandler.cs +++ b/Robust.Shared/Map/GridEventHandler.cs @@ -1,4 +1,6 @@ -namespace Robust.Shared.Map +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Map { /// /// Invoked when a grid is altered. diff --git a/Robust.Shared/Map/IMapGrid.cs b/Robust.Shared/Map/IMapGrid.cs index 53f2759b6..49abf1ee6 100644 --- a/Robust.Shared/Map/IMapGrid.cs +++ b/Robust.Shared/Map/IMapGrid.cs @@ -10,7 +10,7 @@ namespace Robust.Shared.Map /// This is a collection of tiles in a grid format. /// [PublicAPI] - public interface IMapGrid : IDisposable + public interface IMapGrid { /// /// The integer ID of the map this grid is currently located within. diff --git a/Robust.Shared/Map/IMapManager.cs b/Robust.Shared/Map/IMapManager.cs index dc816f510..dead9d039 100644 --- a/Robust.Shared/Map/IMapManager.cs +++ b/Robust.Shared/Map/IMapManager.cs @@ -47,21 +47,21 @@ namespace Robust.Shared.Map /// /// Creates a new map. /// - /// + /// /// If provided, the new map will use this ID. If not provided, a new ID will be selected automatically. /// /// The new map. /// /// Throw if an explicit ID for the map or default grid is passed and a map or grid with the specified ID already exists, respectively. /// - MapId CreateMap(MapId? mapID = null); + MapId CreateMap(MapId? mapId = null); /// /// Check whether a map with specified ID exists. /// - /// The map ID to check existance of. + /// The map ID to check existence of. /// True if the map exists, false otherwise. - bool MapExists(MapId mapID); + bool MapExists(MapId mapId); /// /// Creates a new entity, then sets it as the map entity. @@ -87,12 +87,12 @@ namespace Robust.Shared.Map IEnumerable GetAllMapIds(); - void DeleteMap(MapId mapID); + void DeleteMap(MapId mapId); - IMapGrid CreateGrid(MapId currentMapID, GridId? gridID = null, ushort chunkSize = 16); - IMapGrid GetGrid(GridId gridID); + IMapGrid CreateGrid(MapId currentMapId, GridId? gridId = null, ushort chunkSize = 16); + IMapGrid GetGrid(GridId gridId); bool TryGetGrid(GridId gridId, [NotNullWhen(true)] out IMapGrid? grid); - bool GridExists(GridId gridID); + bool GridExists(GridId gridId); IEnumerable GetAllMapGrids(MapId mapId); /// @@ -118,16 +118,16 @@ namespace Robust.Shared.Map /// Returns true when a grid was found under the location. bool TryFindGridAt(MapCoordinates mapCoordinates, [NotNullWhen(true)] out IMapGrid? grid); - void FindGridsIntersectingEnumerator(MapId mapId, Box2 worldAABB, out FindGridsEnumerator enumerator, bool approx = false); + void FindGridsIntersectingEnumerator(MapId mapId, Box2 worldAabb, out FindGridsEnumerator enumerator, bool approx = false); /// /// Returns the grids intersecting this AABB. /// /// The relevant MapID - /// The AABB to intersect + /// The AABB to intersect /// Set to false if you wish to accurately get the grid bounds per-tile. /// - IEnumerable FindGridsIntersecting(MapId mapId, Box2 worldAABB, bool approx = false); + IEnumerable FindGridsIntersecting(MapId mapId, Box2 worldAabb, bool approx = false); /// /// @@ -147,7 +147,7 @@ namespace Robust.Shared.Map /// Set to false if you wish to accurately get the grid bounds per-tile. IEnumerable FindGridsIntersecting(MapId mapId, Box2Rotated worldArea, bool approx = false); - void DeleteGrid(GridId gridID); + void DeleteGrid(GridId gridId); /// /// A tile is being modified. @@ -180,5 +180,11 @@ namespace Robust.Shared.Map MapId NextMapId(); GridId NextGridId(); + EntityUid GetGridEuid(GridId id); + IMapGridComponent GetGridComp(GridId id); + IMapGridComponent GetGridComp(EntityUid euid); + IMapGrid GetGrid(EntityUid euid); + bool TryGetGrid(EntityUid euid, [NotNullWhen(true)] out IMapGrid? grid); + bool GridExists(EntityUid euid); } } diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs index 090be542e..0ecf9ba41 100644 --- a/Robust.Shared/Map/IMapManagerInternal.cs +++ b/Robust.Shared/Map/IMapManagerInternal.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Timing; namespace Robust.Shared.Map @@ -21,6 +23,10 @@ namespace Robust.Shared.Map /// The old tile that got replaced. void RaiseOnTileChanged(TileRef tileRef, Tile oldTile); - IMapGridInternal CreateGridNoEntity(MapId currentMapID, GridId? gridID = null, ushort chunkSize = 16); + bool TryGetGridComp(GridId id, [MaybeNullWhen(false)]out IMapGridComponent comp); + bool TryGetGridEuid(GridId id, [MaybeNullWhen(false)]out EntityUid euid); + void TrueGridDelete(MapGrid grid); + MapGrid CreateUnboundGrid(MapId mapId); + void BindGrid(MapGridComponent gridComponent, MapGrid mapGrid); } } diff --git a/Robust.Shared/Map/MapGrid.cs b/Robust.Shared/Map/MapGrid.cs index 70abbaff2..30137cb57 100644 --- a/Robust.Shared/Map/MapGrid.cs +++ b/Robust.Shared/Map/MapGrid.cs @@ -71,14 +71,6 @@ namespace Robust.Shared.Map LastTileModifiedTick = CreatedTick = _mapManager.GameTiming.CurTick; } - /// - /// Disposes the grid. - /// - public void Dispose() - { - // Nothing for now. - } - /// [ViewVariables] public Box2 WorldBounds => diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs new file mode 100644 index 000000000..d969cffcc --- /dev/null +++ b/Robust.Shared/Map/MapManager.GridCollection.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Utility; + +namespace Robust.Shared.Map; + +/// +/// Arguments for when a one or more tiles on a grid is changed at once. +/// +public class GridChangedEventArgs : EventArgs +{ + /// + /// Creates a new instance of this class. + /// + public GridChangedEventArgs(IMapGrid grid, IReadOnlyCollection<(Vector2i position, Tile tile)> modified) + { + Grid = grid; + Modified = modified; + } + + /// + /// Grid being changed. + /// + public IMapGrid Grid { get; } + + public IReadOnlyCollection<(Vector2i position, Tile tile)> Modified { get; } +} + +/// +/// Arguments for when a single tile on a grid is changed locally or remotely. +/// +public class TileChangedEventArgs : EventArgs +{ + /// + /// Creates a new instance of this class. + /// + public TileChangedEventArgs(TileRef newTile, Tile oldTile) + { + NewTile = newTile; + OldTile = oldTile; + } + + /// + /// New tile that replaced the old one. + /// + public TileRef NewTile { get; } + + /// + /// Old tile that was replaced. + /// + public Tile OldTile { get; } +} + +internal partial class MapManager +{ + private readonly Dictionary _grids = new(); + + private GridId _highestGridId = GridId.Invalid; + + public virtual void ChunkRemoved(MapChunk chunk) { } + + public EntityUid GetGridEuid(GridId id) + { + DebugTools.Assert(id != GridId.Invalid); + + //This turns into a linear search with EntityQuery without the _grids mapping + return _grids[id]; + } + + public bool TryGetGridEuid(GridId id, [MaybeNullWhen(false)] out EntityUid euid) + { + DebugTools.Assert(id != GridId.Invalid); + + if (_grids.TryGetValue(id, out euid)) + return true; + + euid = default; + return false; + } + + public IMapGridComponent GetGridComp(GridId id) + { + DebugTools.Assert(id != GridId.Invalid); + + var euid = GetGridEuid(id); + return GetGridComp(euid); + } + + public IMapGridComponent GetGridComp(EntityUid euid) + { + return EntityManager.GetComponent(euid); + } + + public bool TryGetGridComp(GridId id, [MaybeNullWhen(false)] out IMapGridComponent comp) + { + DebugTools.Assert(id != GridId.Invalid); + + var euid = GetGridEuid(id); + if (EntityManager.TryGetComponent(euid, out comp)) + return true; + + comp = default; + return false; + } + + public IMapGridInternal CreateBoundGrid(MapId mapId, MapGridComponent gridComponent) + { + var mapGrid = CreateUnboundGrid(mapId); + + BindGrid(gridComponent, mapGrid); + return mapGrid; + } + + public void BindGrid(MapGridComponent gridComponent, MapGrid mapGrid) + { + gridComponent.Grid = mapGrid; + gridComponent.GridIndex = mapGrid.Index; + mapGrid.GridEntityId = gridComponent.Owner; + + _grids.Add(mapGrid.Index, mapGrid.GridEntityId); + Logger.InfoS("map", $"Binding grid {mapGrid.Index} to entity {gridComponent.Owner}"); + OnGridCreated?.Invoke(mapGrid.ParentMapId, mapGrid.Index); + } + + public MapGrid CreateUnboundGrid(MapId mapId) + { + var newGrid = CreateGridImpl(mapId, null, 16, false, default); + Logger.InfoS("map", $"Creating unbound grid {newGrid.Index}"); + return (MapGrid) newGrid; + } + + public IEnumerable GetAllGrids() + { + return EntityManager.EntityQuery(true).Select(c => c.Grid); + } + + public IMapGrid CreateGrid(MapId currentMapId, GridId? gridId = null, ushort chunkSize = 16) + { + DebugTools.Assert(gridId != GridId.Invalid); + + var mapGrid = CreateGridImpl(currentMapId, gridId, chunkSize, true, default); + _grids.Add(mapGrid.Index, mapGrid.GridEntityId); + Logger.InfoS("map", $"Creating new grid {mapGrid.Index}"); + + EntityManager.InitializeComponents(mapGrid.GridEntityId); + EntityManager.StartComponents(mapGrid.GridEntityId); + OnGridCreated?.Invoke(currentMapId, mapGrid.Index); + return mapGrid; + } + + public IMapGrid GetGrid(EntityUid euid) + { + return GetGridComp(euid).Grid; + } + + public IMapGrid GetGrid(GridId gridId) + { + DebugTools.Assert(gridId != GridId.Invalid); + + var euid = GetGridEuid(gridId); + return GetGridComp(euid).Grid; + } + + public bool IsGrid(EntityUid uid) + { + return EntityManager.HasComponent(uid); + } + + public bool TryGetGrid(EntityUid euid, [MaybeNullWhen(false)] out IMapGrid grid) + { + if (EntityManager.TryGetComponent(euid, out IMapGridComponent comp)) + { + grid = comp.Grid; + return true; + } + + grid = default; + return false; + } + + public bool TryGetGrid(GridId gridId, [MaybeNullWhen(false)] out IMapGrid grid) + { + // grid 0 compatibility + if (gridId == GridId.Invalid) + { + grid = default; + return false; + } + + if (!TryGetGridEuid(gridId, out var euid)) + { + grid = default; + return false; + } + + return TryGetGrid(euid, out grid); + } + + public bool GridExists(GridId gridId) + { + // grid 0 compatibility + return gridId != GridId.Invalid && TryGetGridEuid(gridId, out var euid) && GridExists(euid); + } + + public bool GridExists(EntityUid euid) + { + return EntityManager.EntityExists(euid) && EntityManager.HasComponent(euid); + } + + public IEnumerable GetAllMapGrids(MapId mapId) + { + return EntityManager.EntityQuery(true) + .Where(c => c.Grid.ParentMapId == mapId) + .Select(c => c.Grid); + } + + public void FindGridsIntersectingEnumerator(MapId mapId, Box2 worldAabb, out FindGridsEnumerator enumerator, bool approx = false) + { + enumerator = new FindGridsEnumerator(EntityManager, GetAllGrids().Cast().GetEnumerator(), mapId, worldAabb, approx); + } + + public virtual void DeleteGrid(GridId gridId) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + // Possible the grid was already deleted / is invalid + if (!TryGetGrid(gridId, out var iGrid)) + { + DebugTools.Assert($"Calling {nameof(DeleteGrid)} with unknown id {gridId}."); + return; // Silently fail on release + } + + var grid = (MapGrid)iGrid; + if (grid.Deleting) + { + DebugTools.Assert($"Calling {nameof(DeleteGrid)} multiple times for grid {gridId}."); + return; // Silently fail on release + } + + var entityId = grid.GridEntityId; + if (!EntityManager.TryGetComponent(entityId, out MetaDataComponent metaComp)) + { + DebugTools.Assert($"Calling {nameof(DeleteGrid)} with {gridId}, but there was no allocated entity."); + return; // Silently fail on release + } + + // DeleteGrid may be triggered by the entity being deleted, + // so make sure that's not the case. + if (metaComp.EntityLifeStage < EntityLifeStage.Terminating) + EntityManager.DeleteEntity(entityId); + } + + public void TrueGridDelete(MapGrid grid) + { + grid.Deleting = true; + + var mapId = grid.ParentMapId; + var gridId = grid.Index; + + _grids.Remove(grid.Index); + + Logger.DebugS("map", $"Deleted grid {gridId}"); + + // TODO: Remove this trash + OnGridRemoved?.Invoke(mapId, gridId); + } + + public GridId NextGridId() + { + return _highestGridId = new GridId(_highestGridId.Value + 1); + } + + /// + public event EventHandler? TileChanged; + + public event GridEventHandler? OnGridCreated; + public event GridEventHandler? OnGridRemoved; + + /// + /// Should the OnTileChanged event be suppressed? This is useful for initially loading the map + /// so that you don't spam an event for each of the million station tiles. + /// + /// + public event EventHandler? GridChanged; + + /// + public bool SuppressOnTileChanged { get; set; } + + public void OnComponentRemoved(MapGridComponent comp) + { + var gridIndex = comp.GridIndex; + if (gridIndex == GridId.Invalid) + return; + + if (!GridExists(gridIndex)) + return; + + Logger.DebugS("map", $"Entity {comp.Owner} removed grid component, removing bound grid {gridIndex}"); + DeleteGrid(gridIndex); + } + + /// + /// Raises the OnTileChanged event. + /// + /// A reference to the new tile. + /// The old tile that got replaced. + public void RaiseOnTileChanged(TileRef tileRef, Tile oldTile) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + if (SuppressOnTileChanged) + return; + + TileChanged?.Invoke(this, new TileChangedEventArgs(tileRef, oldTile)); + } + + protected IMapGrid CreateGrid(MapId currentMapId, GridId gridId, ushort chunkSize, EntityUid euid) + { + var mapGrid = CreateGridImpl(currentMapId, gridId, chunkSize, true, euid); + _grids.Add(mapGrid.Index, mapGrid.GridEntityId); + Logger.InfoS("map", $"Creating new grid {mapGrid.Index}"); + + EntityManager.InitializeComponents(mapGrid.GridEntityId); + EntityManager.StartComponents(mapGrid.GridEntityId); + OnGridCreated?.Invoke(currentMapId, mapGrid.Index); + return mapGrid; + } + + protected void InvokeGridChanged(object? sender, GridChangedEventArgs ev) + { + GridChanged?.Invoke(sender, ev); + } + + private IMapGridInternal CreateGridImpl(MapId currentMapId, GridId? gridId, ushort chunkSize, bool createEntity, + EntityUid euid) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + var actualId = gridId ?? new GridId(_highestGridId.Value + 1); + + DebugTools.Assert(actualId != GridId.Invalid); + + if (GridExists(actualId)) + throw new InvalidOperationException($"A grid with ID {actualId} already exists"); + + if (_highestGridId.Value < actualId.Value) + _highestGridId = actualId; + + var grid = new MapGrid(this, EntityManager, actualId, chunkSize, currentMapId); + + if (actualId != GridId.Invalid && createEntity) // nullspace default grid is not bound to an entity + { + // the entity may already exist from map deserialization + IMapGridComponent? result = null; + foreach (var comp in EntityManager.EntityQuery(true)) + { + if (comp.GridIndex != actualId) + continue; + + result = comp; + break; + } + + if (result != null) + { + grid.GridEntityId = result.Owner; + ((MapGridComponent)result).Grid = grid; + Logger.DebugS("map", $"Rebinding grid {actualId} to entity {grid.GridEntityId}"); + } + else + { + var gridEnt = EntityManager.CreateEntityUninitialized(null, euid); + + grid.GridEntityId = gridEnt; + + Logger.DebugS("map", $"Binding grid {actualId} to entity {grid.GridEntityId}"); + + var gridComp = EntityManager.AddComponent(gridEnt); + gridComp.GridIndex = grid.Index; + gridComp.Grid = grid; + + //TODO: This is a hack to get TransformComponent.MapId working before entity states + //are applied. After they are applied the parent may be different, but the MapId will + //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to + //use in transform states anytime before the state parent is properly set. + EntityManager.GetComponent(gridEnt).AttachParent(GetMapEntityIdOrThrow(currentMapId)); + } + } + else + Logger.DebugS("map", $"Skipping entity binding for gridId {actualId}"); + + return grid; + } +} diff --git a/Robust.Shared/Map/MapManager.GridTrees.cs b/Robust.Shared/Map/MapManager.GridTrees.cs index c1db5bc86..3bbd253f8 100644 --- a/Robust.Shared/Map/MapManager.GridTrees.cs +++ b/Robust.Shared/Map/MapManager.GridTrees.cs @@ -26,12 +26,12 @@ internal partial class MapManager private void InitializeGridTrees() { - _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnGridInit); - _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnGridRemove); - _entityManager.EventBus.SubscribeLocalEvent(OnGridMove); - _entityManager.EventBus.SubscribeLocalEvent(OnGridRotate); - _entityManager.EventBus.SubscribeLocalEvent(OnGridMapChange); - _entityManager.EventBus.SubscribeLocalEvent(OnGridBoundsChange); + EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnGridInit); + EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnGridRemove); + EntityManager.EventBus.SubscribeLocalEvent(OnGridMove); + EntityManager.EventBus.SubscribeLocalEvent(OnGridRotate); + EntityManager.EventBus.SubscribeLocalEvent(OnGridMapChange); + EntityManager.EventBus.SubscribeLocalEvent(OnGridBoundsChange); } private void OnMapCreatedGridTree(MapEventArgs e) diff --git a/Robust.Shared/Map/MapManager.MapCollection.cs b/Robust.Shared/Map/MapManager.MapCollection.cs new file mode 100644 index 000000000..30e3f44eb --- /dev/null +++ b/Robust.Shared/Map/MapManager.MapCollection.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Robust.Shared.Map; + +/// +/// Arguments for when a map is created or deleted locally ore remotely. +/// +public class MapEventArgs : EventArgs +{ + /// + /// Creates a new instance of this class. + /// + public MapEventArgs(MapId map) + { + Map = map; + } + + /// + /// Map that is being modified. + /// + public MapId Map { get; } +} + +internal partial class MapManager +{ + private readonly Dictionary _mapEntities = new(); + private readonly HashSet _maps = new(); + protected readonly Dictionary MapCreationTick = new(); //TODO: MapComponent.CreationTick? + private MapId _highestMapId = MapId.Nullspace; + + /// + public virtual void DeleteMap(MapId mapId) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + if (!_maps.Contains(mapId)) + throw new InvalidOperationException($"Attempted to delete nonexistant map '{mapId}'"); + + // grids are cached because Delete modifies collection + foreach (var grid in GetAllMapGrids(mapId).ToList()) + { + DeleteGrid(grid.Index); + } + + if (mapId != MapId.Nullspace) + { + var args = new MapEventArgs(mapId); + OnMapDestroyedGridTree(args); + MapDestroyed?.Invoke(this, args); + _maps.Remove(mapId); + MapCreationTick.Remove(mapId); + } + + if (_mapEntities.TryGetValue(mapId, out var ent)) + { + EntityManager.DeleteEntity(ent); + _mapEntities.Remove(mapId); + } + + Logger.InfoS("map", $"Deleting map {mapId}"); + } + + public MapId CreateMap(MapId? mapId = null) + { + return CreateMap(mapId, default); + } + + /// + public bool MapExists(MapId mapId) + { + return _maps.Contains(mapId); + } + + public EntityUid CreateNewMapEntity(MapId mapId) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + var newEntity = EntityManager.CreateEntityUninitialized(null); + SetMapEntity(mapId, newEntity); + + EntityManager.InitializeComponents(newEntity); + EntityManager.StartComponents(newEntity); + + return newEntity; + } + + /// + public void SetMapEntity(MapId mapId, EntityUid newMapEntity) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + if (!_maps.Contains(mapId)) + throw new InvalidOperationException($"Map {mapId} does not exist."); + + foreach (var kvEntity in _mapEntities) + { + if (kvEntity.Value == newMapEntity) + { + throw new InvalidOperationException( + $"Entity {newMapEntity} is already the root node of map {kvEntity.Key}."); + } + } + + // remove existing graph + if (_mapEntities.TryGetValue(mapId, out var oldEntId)) + { + //Note: This prevents setting a subgraph as the root, since the subgraph will be deleted + EntityManager.DeleteEntity(oldEntId); + } + else + _mapEntities.Add(mapId, EntityUid.Invalid); + + // re-use or add map component + if (!EntityManager.TryGetComponent(newMapEntity, out MapComponent? mapComp)) + mapComp = EntityManager.AddComponent(newMapEntity); + else + { + if (mapComp.WorldMap != mapId) + { + Logger.WarningS("map", + $"Setting map {mapId} root to entity {newMapEntity}, but entity thinks it is root node of map {mapComp.WorldMap}."); + } + } + + Logger.DebugS("map", $"Setting map {mapId} entity to {newMapEntity}"); + + // set as new map entity + mapComp.WorldMap = mapId; + _mapEntities[mapId] = newMapEntity; + } + + public EntityUid GetMapEntityId(MapId mapId) + { + if (_mapEntities.TryGetValue(mapId, out var entId)) + return entId; + + return EntityUid.Invalid; + } + + /// + /// Replaces GetMapEntity()'s throw-on-failure semantics. + /// + public EntityUid GetMapEntityIdOrThrow(MapId mapId) + { + return _mapEntities[mapId]; + } + + public bool HasMapEntity(MapId mapId) + { + return _mapEntities.ContainsKey(mapId); + } + + public IEnumerable GetAllMapIds() + { + return _maps; + } + + public bool IsMap(EntityUid uid) + { + return EntityManager.HasComponent(uid); + } + + public MapId NextMapId() + { + return _highestMapId = new MapId(_highestMapId.Value + 1); + } + + /// + public MapId DefaultMap => MapId.Nullspace; + + /// + public event EventHandler? MapCreated; + + /// + public event EventHandler? MapDestroyed; + + protected MapId CreateMap(MapId? mapId, EntityUid entityUid) + { +#if DEBUG + DebugTools.Assert(_dbgGuardRunning); +#endif + + var actualId = mapId ?? new MapId(_highestMapId.Value + 1); + + if (MapExists(actualId)) + throw new InvalidOperationException($"A map with ID {actualId} already exists"); + + if (_highestMapId.Value < actualId.Value) + _highestMapId = actualId; + + _maps.Add(actualId); + MapCreationTick.Add(actualId, GameTiming.CurTick); + Logger.InfoS("map", $"Creating new map {actualId}"); + + if (actualId != MapId.Nullspace) // nullspace isn't bound to an entity + { + var mapComps = EntityManager.EntityQuery(true); + + IMapComponent? result = null; + foreach (var mapComp in mapComps) + { + if (mapComp.WorldMap != actualId) + continue; + + result = mapComp; + break; + } + + if (result != null) + { + _mapEntities.Add(actualId, result.Owner); + Logger.DebugS("map", $"Rebinding map {actualId} to entity {result.Owner}"); + } + else + { + var newEnt = EntityManager.CreateEntityUninitialized(null, entityUid); + _mapEntities.Add(actualId, newEnt); + + var mapComp = EntityManager.AddComponent(newEnt); + mapComp.WorldMap = actualId; + EntityManager.InitializeComponents(newEnt); + EntityManager.StartComponents(newEnt); + Logger.DebugS("map", $"Binding map {actualId} to entity {newEnt}"); + } + } + + var args = new MapEventArgs(actualId); + OnMapCreatedGridTree(args); + MapCreated?.Invoke(this, args); + + return actualId; + } + + private void EnsureNullspaceExistsAndClear() + { + if (!_maps.Contains(MapId.Nullspace)) + CreateMap(MapId.Nullspace); + else + { + if (!_mapEntities.TryGetValue(MapId.Nullspace, out var mapEntId)) + return; + + // Notice we do not clear off any added comps to the nullspace map entity, would it be better to just delete + // and recreate the entity, letting recursive entity deletion perform this foreach loop? + + foreach (var childEuid in EntityManager.GetComponent(mapEntId).ChildEntities) + { + EntityManager.DeleteEntity(childEuid); + } + } + } + + private void DeleteAllMaps() + { + foreach (var map in _maps.ToArray()) + { + if (map != MapId.Nullspace) + DeleteMap(map); + } + + if (_mapEntities.TryGetValue(MapId.Nullspace, out var entId)) + { + Logger.InfoS("map", $"Deleting map entity {entId}"); + EntityManager.DeleteEntity(entId); + + if (_mapEntities.Remove(MapId.Nullspace)) + Logger.InfoS("map", "Removing nullspace map entity."); + } + } +} diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs index 1ecf5775f..a0a613bde 100644 --- a/Robust.Shared/Map/MapManager.cs +++ b/Robust.Shared/Map/MapManager.cs @@ -1,642 +1,72 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Physics; using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Robust.Shared.Map +namespace Robust.Shared.Map; + +/// +internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber { - /// - internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber + [field: Dependency] public IGameTiming GameTiming { get; } = default!; + [field: Dependency] public IEntityManager EntityManager { get; } = default!; + + /// + public void Initialize() { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - public IGameTiming GameTiming => _gameTiming; - - public IEntityManager EntityManager => _entityManager; - - /// - public MapId DefaultMap => MapId.Nullspace; - - /// - public event EventHandler? TileChanged; - - public event GridEventHandler? OnGridCreated; - - public event GridEventHandler? OnGridRemoved; - - /// - /// Should the OnTileChanged event be suppressed? This is useful for initially loading the map - /// so that you don't spam an event for each of the million station tiles. - /// - /// - public event EventHandler? GridChanged; - - /// - public event EventHandler? MapCreated; - - /// - public event EventHandler? MapDestroyed; - - /// - public bool SuppressOnTileChanged { get; set; } - - private MapId HighestMapID = MapId.Nullspace; - private GridId HighestGridID = GridId.Invalid; - - private protected readonly HashSet _maps = new(); - private protected readonly Dictionary _mapCreationTick = new(); - - private protected readonly Dictionary _grids = new(); - private protected readonly Dictionary _mapEntities = new(); - + InitializeGridTrees(); #if DEBUG - private bool _dbgGuardInit = false; - private bool _dbgGuardRunning = false; + DebugTools.Assert(!_dbgGuardInit); + DebugTools.Assert(!_dbgGuardRunning); + _dbgGuardInit = true; #endif - - /// - public void Initialize() - { - InitializeGridTrees(); - -#if DEBUG - DebugTools.Assert(!_dbgGuardInit); - DebugTools.Assert(!_dbgGuardRunning); - _dbgGuardInit = true; -#endif - } - - /// - public void Startup() - { -#if DEBUG - DebugTools.Assert(_dbgGuardInit); - _dbgGuardRunning = true; -#endif - - Logger.DebugS("map", "Starting..."); - - if (!_maps.Contains(MapId.Nullspace)) - { - CreateMap(MapId.Nullspace); - } - else if (_mapEntities.TryGetValue(MapId.Nullspace, out var mapEntId)) - { - var mapEnt = mapEntId; - - foreach (var childTransform in _entityManager.GetComponent(mapEnt).Children.ToArray()) - { - _entityManager.DeleteEntity(childTransform.Owner); - } - } - - DebugTools.Assert(_grids.Count == 0); - DebugTools.Assert(!GridExists(GridId.Invalid)); - } - - public void OnComponentRemoved(MapGridComponent comp) - { - var gridIndex = comp.GridIndex; - if (gridIndex != GridId.Invalid) - { - if (GridExists(gridIndex)) - { - Logger.DebugS("map", - $"Entity {comp.Owner} removed grid component, removing bound grid {gridIndex}"); - DeleteGrid(gridIndex); - } - } - } - - public virtual void ChunkRemoved(MapChunk chunk) - { - return; - } - - /// - public void Shutdown() - { -#if DEBUG - DebugTools.Assert(_dbgGuardInit); -#endif - Logger.DebugS("map", "Stopping..."); - - foreach (var map in _maps.ToArray()) - { - if (map != MapId.Nullspace) - { - DeleteMap(map); - } - } - - if (_mapEntities.TryGetValue(MapId.Nullspace, out var entId)) - { - Logger.InfoS("map", $"Deleting map entity {entId}"); - _entityManager.DeleteEntity(entId); - - if (_mapEntities.Remove(MapId.Nullspace)) - Logger.InfoS("map", "Removing nullspace map entity."); - } - -#if DEBUG - DebugTools.Assert(_grids.Count == 0); - DebugTools.Assert(!GridExists(GridId.Invalid)); - _dbgGuardRunning = false; -#endif - } - - /// - public void Restart() - { - Logger.DebugS("map", "Restarting..."); - - Shutdown(); - Startup(); - } - - /// - /// Raises the OnTileChanged event. - /// - /// A reference to the new tile. - /// The old tile that got replaced. - public void RaiseOnTileChanged(TileRef tileRef, Tile oldTile) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - if (SuppressOnTileChanged) - return; - - TileChanged?.Invoke(this, new TileChangedEventArgs(tileRef, oldTile)); - } - - /// - public virtual void DeleteMap(MapId mapID) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - if (!_maps.Contains(mapID)) - { - throw new InvalidOperationException($"Attempted to delete nonexistant map '{mapID}'"); - } - - // grids are cached because Delete modifies collection - foreach (var grid in GetAllMapGrids(mapID).ToList()) - { - DeleteGrid(grid.Index); - } - - if (mapID != MapId.Nullspace) - { - var args = new MapEventArgs(mapID); - OnMapDestroyedGridTree(args); - MapDestroyed?.Invoke(this, args); - _maps.Remove(mapID); - _mapCreationTick.Remove(mapID); - } - - if (_mapEntities.TryGetValue(mapID, out var ent)) - { - _entityManager.DeleteEntity(ent); - _mapEntities.Remove(mapID); - } - - Logger.InfoS("map", $"Deleting map {mapID}"); - } - - public MapId CreateMap(MapId? mapID = null) - { - return CreateMap(mapID, default); - } - - public MapId CreateMap(MapId? mapID, EntityUid entityUid) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - MapId actualID; - if (mapID != null) - { - actualID = mapID.Value; - } - else - { - actualID = new MapId(HighestMapID.Value + 1); - } - - if (MapExists(actualID)) - { - throw new InvalidOperationException($"A map with ID {actualID} already exists"); - } - - if (HighestMapID.Value < actualID.Value) - { - HighestMapID = actualID; - } - - _maps.Add(actualID); - _mapCreationTick.Add(actualID, _gameTiming.CurTick); - Logger.InfoS("map", $"Creating new map {actualID}"); - - if (actualID != MapId.Nullspace) // nullspace isn't bound to an entity - { - var mapComps = _entityManager.EntityQuery(true); - - IMapComponent? result = null; - foreach (var mapComp in mapComps) - { - if (mapComp.WorldMap != actualID) - continue; - - result = mapComp; - break; - } - - if (result != null) - { - _mapEntities.Add(actualID, result.Owner); - Logger.DebugS("map", $"Rebinding map {actualID} to entity {result.Owner}"); - } - else - { - var newEnt = _entityManager.CreateEntityUninitialized(null, entityUid); - _mapEntities.Add(actualID, newEnt); - - var mapComp = _entityManager.AddComponent(newEnt); - mapComp.WorldMap = actualID; - _entityManager.InitializeComponents(newEnt); - _entityManager.StartComponents(newEnt); - Logger.DebugS("map", $"Binding map {actualID} to entity {newEnt}"); - } - } - - var args = new MapEventArgs(actualID); - OnMapCreatedGridTree(args); - MapCreated?.Invoke(this, args); - - return actualID; - } - - /// - public bool MapExists(MapId mapID) - { - return _maps.Contains(mapID); - } - - public EntityUid CreateNewMapEntity(MapId mapId) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - var newEntity = _entityManager.CreateEntityUninitialized(null); - SetMapEntity(mapId, newEntity); - - _entityManager.InitializeComponents(newEntity); - _entityManager.StartComponents(newEntity); - - return newEntity; - } - - /// - public void SetMapEntity(MapId mapId, EntityUid newMapEntity) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - if (!_maps.Contains(mapId)) - throw new InvalidOperationException($"Map {mapId} does not exist."); - - foreach (var kvEntity in _mapEntities) - { - if (kvEntity.Value == newMapEntity) - { - throw new InvalidOperationException( - $"Entity {newMapEntity} is already the root node of map {kvEntity.Key}."); - } - } - - // remove existing graph - if (_mapEntities.TryGetValue(mapId, out var oldEntId)) - { - //Note: This prevents setting a subgraph as the root, since the subgraph will be deleted - _entityManager.DeleteEntity(oldEntId); - } - else - { - _mapEntities.Add(mapId, EntityUid.Invalid); - } - - // re-use or add map component - if (!_entityManager.TryGetComponent(newMapEntity, out MapComponent? mapComp)) - { - mapComp = _entityManager.AddComponent(newMapEntity); - } - else - { - if (mapComp.WorldMap != mapId) - Logger.WarningS("map", - $"Setting map {mapId} root to entity {newMapEntity}, but entity thinks it is root node of map {mapComp.WorldMap}."); - } - - Logger.DebugS("map", $"Setting map {mapId} entity to {newMapEntity}"); - - // set as new map entity - mapComp.WorldMap = mapId; - _mapEntities[mapId] = newMapEntity; - } - - public EntityUid GetMapEntityId(MapId mapId) - { - if (_mapEntities.TryGetValue(mapId, out var entId)) - return entId; - - return EntityUid.Invalid; - } - - /// - /// Replaces GetMapEntity()'s throw-on-failure semantics. - /// - public EntityUid GetMapEntityIdOrThrow(MapId mapId) - { - return _mapEntities[mapId]; - } - - public bool HasMapEntity(MapId mapId) - { - return _mapEntities.ContainsKey(mapId); - } - - public IEnumerable GetAllMapIds() - { - return _maps; - } - - public IEnumerable GetAllGrids() - { - return _grids.Values; - } - - public IMapGrid CreateGrid(MapId currentMapID, GridId? gridID = null, ushort chunkSize = 16) - { - return CreateGridImpl(currentMapID, gridID, chunkSize, true, default); - } - - public IMapGrid CreateGrid(MapId currentMapID, GridId gridID, ushort chunkSize, EntityUid euid) - { - return CreateGridImpl(currentMapID, gridID, chunkSize, true, euid); - } - - private IMapGridInternal CreateGridImpl(MapId currentMapID, GridId? gridID, ushort chunkSize, bool createEntity, - EntityUid euid) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - - GridId actualID; - if (gridID != null) - { - actualID = gridID.Value; - } - else - { - actualID = new GridId(HighestGridID.Value + 1); - } - - DebugTools.Assert(actualID != GridId.Invalid); - - if (GridExists(actualID)) - { - throw new InvalidOperationException($"A grid with ID {actualID} already exists"); - } - - if (HighestGridID.Value < actualID.Value) - { - HighestGridID = actualID; - } - - var grid = new MapGrid(this, _entityManager, actualID, chunkSize, currentMapID); - _grids.Add(actualID, grid); - Logger.InfoS("map", $"Creating new grid {actualID}"); - - if (actualID != GridId.Invalid && createEntity) // nullspace default grid is not bound to an entity - { - // the entity may already exist from map deserialization - IMapGridComponent? result = null; - foreach (var comp in _entityManager.EntityQuery(true)) - { - if (comp.GridIndex != actualID) - continue; - - result = comp; - break; - } - - if (result != null) - { - grid.GridEntityId = result.Owner; - Logger.DebugS("map", $"Rebinding grid {actualID} to entity {grid.GridEntityId}"); - } - else - { - var gridEnt = EntityManager.CreateEntityUninitialized(null, euid); - - grid.GridEntityId = gridEnt; - - Logger.DebugS("map", $"Binding grid {actualID} to entity {grid.GridEntityId}"); - - var gridComp = _entityManager.AddComponent(gridEnt); - gridComp.GridIndex = grid.Index; - - //TODO: This is a hack to get TransformComponent.MapId working before entity states - //are applied. After they are applied the parent may be different, but the MapId will - //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to - //use in transform states anytime before the state parent is properly set. - _entityManager.GetComponent(gridEnt).AttachParent(GetMapEntityIdOrThrow(currentMapID)); - - _entityManager.InitializeComponents(gridEnt); - _entityManager.StartComponents(gridEnt); - } - } - else - { - Logger.DebugS("map", $"Skipping entity binding for gridId {actualID}"); - } - - OnGridCreated?.Invoke(currentMapID, actualID); - return grid; - } - - public IMapGridInternal CreateGridNoEntity(MapId currentMapID, GridId? gridID = null, ushort chunkSize = 16) - { - return CreateGridImpl(currentMapID, gridID, chunkSize, false, default); - } - - public IMapGrid GetGrid(GridId gridID) - { - return _grids[gridID]; - } - - public bool IsGrid(EntityUid uid) - { - return _grids.Any(x => x.Value.GridEntityId == uid); - } - - public bool IsMap(EntityUid uid) - { - return _mapEntities.Any(x => x.Value == uid); - } - - public bool TryGetGrid(GridId gridId, [NotNullWhen(true)] out IMapGrid? grid) - { - if (_grids.TryGetValue(gridId, out var gridinterface)) - { - grid = gridinterface; - return true; - } - - grid = null; - return false; - } - - public bool GridExists(GridId gridID) - { - return _grids.ContainsKey(gridID); - } - - public IEnumerable GetAllMapGrids(MapId mapId) - { - return _grids.Values.Where(m => m.ParentMapId == mapId); - } - - public void FindGridsIntersectingEnumerator(MapId mapId, Box2 worldAABB, out FindGridsEnumerator enumerator, bool approx = false) - { - enumerator = new FindGridsEnumerator(_entityManager, _grids.GetEnumerator(), mapId, worldAABB, approx); - } - - public virtual void DeleteGrid(GridId gridID) - { -#if DEBUG - DebugTools.Assert(_dbgGuardRunning); -#endif - // Possible the grid was already deleted / is invalid - if (!_grids.TryGetValue(gridID, out var grid) || grid.Deleting) - return; - - grid.Deleting = true; - - var mapId = grid.ParentMapId; - - var entityId = grid.GridEntityId; - - if (_entityManager.EntityExists(entityId)) - { - // DeleteGrid may be triggered by the entity being deleted, - // so make sure that's not the case. - if (_entityManager.GetComponent(entityId).EntityLifeStage <= EntityLifeStage.MapInitialized) - _entityManager.DeleteEntity(entityId); - } - - grid.Dispose(); - _grids.Remove(grid.Index); - - Logger.DebugS("map", $"Deleted grid {gridID}"); - OnGridRemoved?.Invoke(mapId, gridID); - } - - public MapId NextMapId() - { - return HighestMapID = new MapId(HighestMapID.Value + 1); - } - - public GridId NextGridId() - { - return HighestGridID = new GridId(HighestGridID.Value + 1); - } - - protected void InvokeGridChanged(object? sender, GridChangedEventArgs ev) - { - GridChanged?.Invoke(sender, ev); - } } - /// - /// Arguments for when a map is created or deleted locally ore remotely. - /// - public class MapEventArgs : EventArgs + /// + public void Startup() { - /// - /// Map that is being modified. - /// - public MapId Map { get; } +#if DEBUG + DebugTools.Assert(_dbgGuardInit); + _dbgGuardRunning = true; +#endif - /// - /// Creates a new instance of this class. - /// - public MapEventArgs(MapId map) - { - Map = map; - } + Logger.DebugS("map", "Starting..."); + + EnsureNullspaceExistsAndClear(); + + DebugTools.Assert(_grids.Count == 0); + DebugTools.Assert(!GridExists(GridId.Invalid)); } - /// - /// Arguments for when a single tile on a grid is changed locally or remotely. - /// - public class TileChangedEventArgs : EventArgs + /// + public void Shutdown() { - /// - /// New tile that replaced the old one. - /// - public TileRef NewTile { get; } +#if DEBUG + DebugTools.Assert(_dbgGuardInit); +#endif + Logger.DebugS("map", "Stopping..."); - /// - /// Old tile that was replaced. - /// - public Tile OldTile { get; } + DeleteAllMaps(); - /// - /// Creates a new instance of this class. - /// - public TileChangedEventArgs(TileRef newTile, Tile oldTile) - { - NewTile = newTile; - OldTile = oldTile; - } +#if DEBUG + DebugTools.Assert(_grids.Count == 0); + DebugTools.Assert(!GridExists(GridId.Invalid)); + _dbgGuardRunning = false; +#endif } - /// - /// Arguments for when a one or more tiles on a grid is changed at once. - /// - public class GridChangedEventArgs : EventArgs + /// + public void Restart() { - /// - /// Grid being changed. - /// - public IMapGrid Grid { get; } + Logger.DebugS("map", "Restarting..."); - public IReadOnlyCollection<(Vector2i position, Tile tile)> Modified { get; } - - /// - /// Creates a new instance of this class. - /// - public GridChangedEventArgs(IMapGrid grid, IReadOnlyCollection<(Vector2i position, Tile tile)> modified) - { - Grid = grid; - Modified = modified; - } + Shutdown(); + Startup(); } + +#if DEBUG + private bool _dbgGuardInit; + private bool _dbgGuardRunning; +#endif } diff --git a/Robust.Shared/Map/NetworkedMapManager.cs b/Robust.Shared/Map/NetworkedMapManager.cs new file mode 100644 index 000000000..1323315b5 --- /dev/null +++ b/Robust.Shared/Map/NetworkedMapManager.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Robust.Shared.Map; + +internal interface INetworkedMapManager : IMapManagerInternal +{ + GameStateMapData? GetStateData(GameTick fromTick); + void CullDeletionHistory(GameTick upToTick); + + // Two methods here, so that new grids etc can be made BEFORE entities get states applied, + // but old ones can be deleted after. + void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan entityStates); + void ApplyGameStatePost(GameStateMapData? data); +} + +internal class NetworkedMapManager : MapManager, INetworkedMapManager +{ + private readonly Dictionary> _chunkDeletionHistory = new(); + private readonly List<(GameTick tick, GridId gridId)> _gridDeletionHistory = new(); + private readonly List<(GameTick tick, MapId mapId)> _mapDeletionHistory = new(); + + public override void DeleteMap(MapId mapId) + { + base.DeleteMap(mapId); + _mapDeletionHistory.Add((GameTiming.CurTick, mapId)); + } + + public override void DeleteGrid(GridId gridId) + { + 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) || + !EntityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) || + chunk.Fixtures.Count == 0) + 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? + var fixtureSystem = EntitySystem.Get(); + + foreach (var fixture in chunk.Fixtures) + { + fixtureSystem.DestroyFixture(body, fixture); + } + } + + public GameStateMapData? GetStateData(GameTick fromTick) + { + var gridDatums = new Dictionary(); + foreach (MapGrid grid in GetAllGrids()) + { + if (grid.LastTileModifiedTick < fromTick) + 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.LastTileModifiedTick < fromTick) + continue; + + var tileBuffer = new Tile[grid.ChunkSize * (uint)grid.ChunkSize]; + + // Flatten the tile array. + // NetSerializer doesn't do multi-dimensional arrays. + // This is probably really expensive. + for (var x = 0; x < grid.ChunkSize; x++) + { + for (var y = 0; y < grid.ChunkSize; y++) + { + tileBuffer[x * grid.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y); + } + } + + chunkData.Add(new GameStateMapData.ChunkDatum(index, tileBuffer)); + } + + var gridDatum = new GameStateMapData.GridDatum( + chunkData.ToArray(), + deletedChunkData.ToArray(), + new MapCoordinates(grid.WorldPosition, grid.ParentMapId), + grid.WorldRotation); + + gridDatums.Add(grid.Index, gridDatum); + } + + // -- Map Deletion Data -- + var mapDeletionsData = new List(); + + foreach (var (tick, mapId) in _mapDeletionHistory) + { + if (tick < fromTick) + continue; + mapDeletionsData.Add(mapId); + } + + // -- Grid Deletion Data + var gridDeletionsData = new List(); + + foreach (var (tick, gridId) in _gridDeletionHistory) + { + if (tick < fromTick) + continue; + gridDeletionsData.Add(gridId); + } + + // -- Map Creations -- + var mapCreations = new List(); + + foreach (var (mapId, tick) in MapCreationTick) + { + if (tick < fromTick || mapId == MapId.Nullspace) + continue; + mapCreations.Add(mapId); + } + + // - Grid Creation data -- + var gridCreations = new Dictionary(); + + foreach (MapGrid grid in GetAllGrids()) + { + if (grid.CreatedTick < fromTick || grid.ParentMapId == MapId.Nullspace) + continue; + gridCreations.Add(grid.Index, new GameStateMapData.GridCreationDatum(grid.ChunkSize)); + } + + // no point sending empty collections + if (gridDatums.Count == 0) + gridDatums = default; + if (gridDeletionsData.Count == 0) + gridDeletionsData = default; + if (mapDeletionsData.Count == 0) + mapDeletionsData = default; + if (mapCreations.Count == 0) + mapCreations = default; + if (gridCreations.Count == 0) + gridCreations = default; + + // no point even creating an empty map state if no data + 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>()); + } + + 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); + } + + public void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan entityStates) + { + // There was no map data this tick, so nothing to do. + if (data == null) + return; + + // First we need to figure out all the NEW MAPS. + if (data.CreatedMaps != null) + { + DebugTools.Assert(!entityStates.IsEmpty, "Received new maps, but no entity state."); + + foreach (var mapId in data.CreatedMaps) + { + // map already exists from a previous state. + if (MapExists(mapId)) + continue; + + EntityUid mapEuid = default; + + //get shared euid of map comp entity + foreach (var entityState in entityStates) + { + foreach (var compChange in entityState.ComponentChanges.Span) + { + if (compChange.State is not MapComponentState mapCompState || mapCompState.MapId != mapId) + continue; + + mapEuid = entityState.Uid; + goto BreakMapEntSearch; + } + } + + BreakMapEntSearch: + + DebugTools.Assert(mapEuid != default, $"Could not find corresponding entity state for new map {mapId}."); + + CreateMap(mapId, mapEuid); + } + } + + // Then make all the grids. + if (data.CreatedGrids != null) + { + DebugTools.Assert(data.GridData is not null, "Received new grids, but GridData was null."); + + foreach (var (gridId, creationDatum) in data.CreatedGrids) + { + if (GridExists(gridId)) + continue; + + EntityUid gridEuid = default; + + //get shared euid of map comp entity + foreach (var entityState in entityStates) + { + foreach (var compState in entityState.ComponentChanges.Span) + { + if (compState.State is not MapGridComponentState gridCompState || gridCompState.GridIndex != gridId) + continue; + + gridEuid = entityState.Uid; + goto BreakGridEntSearch; + } + } + + BreakGridEntSearch: + + DebugTools.Assert(gridEuid != default, $"Could not find corresponding entity state for new grid {gridId}."); + + MapId gridMapId = default; + foreach (var kvData in data.GridData!) + { + if (kvData.Key != gridId) + continue; + + gridMapId = kvData.Value.Coordinates.MapId; + break; + } + + DebugTools.Assert(gridMapId != default, $"Could not find corresponding gridData for new grid {gridId}."); + + CreateGrid(gridMapId, gridId, creationDatum.ChunkSize, gridEuid); + } + } + + // Process all grid updates. + if (data.GridData != null) + { + SuppressOnTileChanged = true; + // Ok good all the grids and maps exist now. + foreach (var (gridId, gridDatum) in data.GridData) + { + var grid = (MapGrid)GetGrid(gridId); + if (grid.ParentMapId != gridDatum.Coordinates.MapId) + throw new NotImplementedException("Moving grids between maps is not yet implemented"); + + // I love mapmanager!!! + grid.WorldPosition = gridDatum.Coordinates.Position; + grid.WorldRotation = gridDatum.Angle; + + var modified = new List<(Vector2i position, Tile tile)>(); + foreach (var chunkData in gridDatum.ChunkData) + { + var chunk = grid.GetChunk(chunkData.Index); + chunk.SuppressCollisionRegeneration = true; + DebugTools.Assert(chunkData.TileData.Length == grid.ChunkSize * grid.ChunkSize); + + var counter = 0; + for (ushort x = 0; x < grid.ChunkSize; x++) + { + for (ushort y = 0; y < grid.ChunkSize; y++) + { + var tile = chunkData.TileData[counter++]; + if (chunk.GetTileRef(x, y).Tile != tile) + { + chunk.SetTile(x, y, tile); + modified.Add((new Vector2i(chunk.X * grid.ChunkSize + x, chunk.Y * grid.ChunkSize + y), tile)); + } + } + } + } + + 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; + } + } + + public void ApplyGameStatePost(GameStateMapData? data) + { + if (data == null) // if there is no data, there is nothing to do! + return; + + if (data.DeletedGrids != null) + { + foreach (var grid in data.DeletedGrids) + { + if (GridExists(grid)) + DeleteGrid(grid); + } + } + + if (data.DeletedMaps != null) + { + foreach (var map in data.DeletedMaps) + { + if (MapExists(map)) + DeleteMap(map); + } + } + } +}