From 1b3cc8aba6b82c3f17801f65e33a47ba961c8ec7 Mon Sep 17 00:00:00 2001 From: Acruid Date: Mon, 29 Apr 2019 03:43:20 -0700 Subject: [PATCH] Grid Bounds (#800) * Added centered unit box static field to Box2. * MapGrid is more testable. * Added some unit tests for MapGrid. Fixed bug in MapChunk.GridTileToLocal(). MapGrid.UpdateAABB() actually expands properly now. * Moved IMapChunk to Robust.Shared.Map. Moved Chunk class out of MapManager class. * Added unit tests for MapChunk. * Bounds reduce by 1, so almost working. * Now bound shrinking works :D * Moved MapGrid out of MapManager. Moved IMapGrid into the Shared/Map folder. Replaced all calls to IMapGrid.ParentMap.Index with IMapGrid.ParentMapId. * Added more MapGrid unit tests. Fixed a bug in TryGetTileRef. --- .../GameObjects/ClientEntityManager.cs | 2 +- .../GameObjects/EntitySystems/EffectSystem.cs | 2 +- .../Graphics/Clyde/Clyde.GridRendering.cs | 24 +- Robust.Client/Placement/Modes/AlignSimilar.cs | 4 +- Robust.Client/Placement/Modes/AlignTileAny.cs | 2 +- .../Placement/Modes/AlignTileDense.cs | 2 +- .../Placement/Modes/AlignTileEmpty.cs | 4 +- .../Placement/Modes/AlignTileNonDense.cs | 2 +- Robust.Client/Placement/Modes/AlignWall.cs | 2 +- Robust.Client/Placement/Modes/PlaceFree.cs | 2 +- Robust.Client/Placement/Modes/PlaceNearby.cs | 2 +- Robust.Client/Placement/PlacementManager.cs | 4 +- Robust.Client/Placement/PlacementMode.cs | 2 +- Robust.Client/State/States/GameScreen.cs | 2 +- .../CustomControls/DebugCoordsPanel.cs | 4 +- Robust.Server/Console/Commands/MapCommands.cs | 2 +- .../EntitySystems/PhysicsSystem.cs | 2 +- .../GameObjects/ServerEntityManager.cs | 4 +- Robust.Server/Maps/YamlGridSerializer.cs | 6 +- Robust.Server/Placement/PlacementManager.cs | 2 +- Robust.Shared.Maths/Box2.cs | 5 + Robust.Shared.Maths/Box2i.cs | 18 +- Robust.Shared/Interfaces/Map/IMapChunk.cs | 65 --- Robust.Shared/Map/IMapChunk.cs | 87 ++++ Robust.Shared/Map/IMapChunkInternal.cs | 13 + .../{Interfaces => }/Map/IMapGrid.cs | 79 ++- Robust.Shared/Map/IMapGridInternal.cs | 42 ++ Robust.Shared/Map/IMapManagerInternal.cs | 18 + Robust.Shared/Map/MapChunk.cs | 365 ++++++++++++++ Robust.Shared/Map/MapGrid.cs | 419 ++++++++++++++++ Robust.Shared/Map/MapIndices.cs | 13 +- Robust.Shared/Map/MapManager.Chunk.cs | 231 --------- Robust.Shared/Map/MapManager.Map.cs | 4 +- Robust.Shared/Map/MapManager.MapGrid.cs | 418 ---------------- Robust.Shared/Map/MapManager.Network.cs | 6 +- Robust.Shared/Map/MapManager.cs | 8 +- Robust.Shared/Map/TileRef.cs | 2 +- Robust.Shared/Prototypes/EntityPrototype.cs | 1 + Robust.Shared/Robust.Shared.csproj | 11 +- .../GameObjects/Components/Transform_Test.cs | 1 + Robust.UnitTesting/Robust.UnitTesting.csproj | 9 +- .../Shared/Map/MapChunk_Tests.cs | 455 ++++++++++++++++++ .../Shared/Map/MapGrid_Tests.cs | 133 +++++ Robust.UnitTesting/Shared/Maths/Box2i_Test.cs | 23 + 44 files changed, 1685 insertions(+), 817 deletions(-) delete mode 100644 Robust.Shared/Interfaces/Map/IMapChunk.cs create mode 100644 Robust.Shared/Map/IMapChunk.cs create mode 100644 Robust.Shared/Map/IMapChunkInternal.cs rename Robust.Shared/{Interfaces => }/Map/IMapGrid.cs (75%) create mode 100644 Robust.Shared/Map/IMapGridInternal.cs create mode 100644 Robust.Shared/Map/IMapManagerInternal.cs create mode 100644 Robust.Shared/Map/MapChunk.cs create mode 100644 Robust.Shared/Map/MapGrid.cs delete mode 100644 Robust.Shared/Map/MapManager.Chunk.cs delete mode 100644 Robust.Shared/Map/MapManager.MapGrid.cs create mode 100644 Robust.UnitTesting/Shared/Map/MapChunk_Tests.cs create mode 100644 Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs create mode 100644 Robust.UnitTesting/Shared/Maths/Box2i_Test.cs diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index dd7ecaba4c..e1a2ee8b44 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -23,7 +23,7 @@ namespace Robust.Client.GameObjects public IEnumerable GetEntitiesInRange(GridCoordinates position, float Range) { var AABB = new Box2(position.Position - new Vector2(Range / 2, Range / 2), position.Position + new Vector2(Range / 2, Range / 2)); - return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMap.Index, AABB); + return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMapId, AABB); } public IEnumerable GetEntitiesIntersecting(MapId mapId, Box2 position) diff --git a/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs b/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs index dd2381b0aa..ee3873e5b8 100644 --- a/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs @@ -307,7 +307,7 @@ namespace Robust.Client.GameObjects foreach (var effect in _owner._Effects) { - if (_mapManager.GetGrid(effect.Coordinates.GridID).ParentMap.Index != map) + if (_mapManager.GetGrid(effect.Coordinates.GridID).ParentMapId != map) { continue; } diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index c9427430b1..40eb447be5 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -46,8 +46,10 @@ namespace Robust.Client.Graphics.Clyde gridProgram.SetUniform(UniModUV, new Vector4(0, 0, 1, 1)); gridProgram.SetUniform(UniModulate, Color.White); - foreach (var grid in _mapManager.GetMap(map).GetAllGrids()) + foreach (var mapGrid in _mapManager.GetMap(map).GetAllGrids()) { + var grid = (IMapGridInternal) mapGrid; + if (!_mapChunkData.ContainsKey(grid.Index)) { continue; @@ -57,14 +59,14 @@ namespace Robust.Client.Graphics.Clyde model.R1C2 = grid.WorldPosition.Y; gridProgram.SetUniform(UniModelMatrix, model); - foreach (var chunk in grid.GetMapChunks()) + foreach (var (index, chunk) in grid.GetMapChunks()) { if (_isChunkDirty(grid, chunk)) { _updateChunkMesh(grid, chunk); } - var datum = _mapChunkData[grid.Index][chunk.Index]; + var datum = _mapChunkData[grid.Index][chunk.Indices]; if (datum.TileCount == 0) { @@ -86,7 +88,7 @@ namespace Robust.Client.Graphics.Clyde { var data = _mapChunkData[grid.Index]; - if (!data.TryGetValue(chunk.Index, out var datum)) + if (!data.TryGetValue(chunk.Indices, out var datum)) { datum = _initChunkBuffers(grid, chunk); } @@ -149,11 +151,11 @@ namespace Robust.Client.Graphics.Clyde var eboSize = _indicesPerChunk(chunk) * sizeof(ushort); var vbo = new Buffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw, - vboSize, $"Grid {grid.Index} chunk {chunk.Index} VBO"); + vboSize, $"Grid {grid.Index} chunk {chunk.Indices} VBO"); var ebo = new Buffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw, - eboSize, $"Grid {grid.Index} chunk {chunk.Index} EBO"); + eboSize, $"Grid {grid.Index} chunk {chunk.Indices} EBO"); - _objectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Index} chunk {chunk.Index} VAO"); + _objectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Index} chunk {chunk.Indices} VAO"); // Vertex Coords GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0); GL.EnableVertexAttribArray(0); @@ -174,14 +176,14 @@ namespace Robust.Client.Graphics.Clyde VBO = vbo }; - _mapChunkData[grid.Index].Add(chunk.Index, datum); + _mapChunkData[grid.Index].Add(chunk.Indices, datum); return datum; } private bool _isChunkDirty(IMapGrid grid, IMapChunk chunk) { var data = _mapChunkData[grid.Index]; - return !data.TryGetValue(chunk.Index, out var datum) || datum.Dirty; + return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty; } public void _setChunkDirty(IMapGrid grid, MapIndices chunk) @@ -199,7 +201,7 @@ namespace Robust.Client.Graphics.Clyde foreach (var (pos, _) in args.Modified) { var grid = args.Grid; - var chunk = grid.GridTileToGridChunk(pos); + var chunk = grid.GridTileToChunkIndices(pos); _setChunkDirty(grid, chunk); } } @@ -207,7 +209,7 @@ namespace Robust.Client.Graphics.Clyde private void _updateTileMapOnUpdate(object sender, TileChangedEventArgs args) { var grid = _mapManager.GetGrid(args.NewTile.GridIndex); - var chunk = grid.GridTileToGridChunk(new MapIndices(args.NewTile.X, args.NewTile.Y)); + var chunk = grid.GridTileToChunkIndices(new MapIndices(args.NewTile.X, args.NewTile.Y)); _setChunkDirty(grid, chunk); } diff --git a/Robust.Client/Placement/Modes/AlignSimilar.cs b/Robust.Client/Placement/Modes/AlignSimilar.cs index 91f7d4fa0a..924c5fafcb 100644 --- a/Robust.Client/Placement/Modes/AlignSimilar.cs +++ b/Robust.Client/Placement/Modes/AlignSimilar.cs @@ -17,7 +17,7 @@ namespace Robust.Client.Placement.Modes { MouseCoords = ScreenToPlayerGrid(mouseScreen); var mapGrid = pManager.MapManager.GetGrid(MouseCoords.GridID); - CurrentTile = mapGrid.GetTile(MouseCoords); + CurrentTile = mapGrid.GetTileRef(MouseCoords); if (pManager.CurrentPermission.IsTile) { @@ -32,7 +32,7 @@ namespace Robust.Client.Placement.Modes var manager = IoCManager.Resolve(); var snapToEntities = manager.GetEntitiesInRange(MouseCoords, SnapToRange) - .Where(entity => entity.Prototype == pManager.CurrentPrototype && entity.Transform.MapID == mapGrid.ParentMap.Index) + .Where(entity => entity.Prototype == pManager.CurrentPrototype && entity.Transform.MapID == mapGrid.ParentMapId) .OrderBy(entity => (entity.Transform.WorldPosition - MouseCoords.ToWorld(pManager.MapManager).Position).LengthSquared) .ToList(); diff --git a/Robust.Client/Placement/Modes/AlignTileAny.cs b/Robust.Client/Placement/Modes/AlignTileAny.cs index 76d0fc7beb..db9ef56d9d 100644 --- a/Robust.Client/Placement/Modes/AlignTileAny.cs +++ b/Robust.Client/Placement/Modes/AlignTileAny.cs @@ -14,7 +14,7 @@ namespace Robust.Client.Placement.Modes MouseCoords = ScreenToPlayerGrid(mouseScreen); var mapGrid = pManager.MapManager.GetGrid(MouseCoords.GridID); - CurrentTile = mapGrid.GetTile(MouseCoords); + CurrentTile = mapGrid.GetTileRef(MouseCoords); float tileSize = mapGrid.TileSize; //convert from ushort to float GridDistancing = tileSize; diff --git a/Robust.Client/Placement/Modes/AlignTileDense.cs b/Robust.Client/Placement/Modes/AlignTileDense.cs index fb33a371c8..df27837125 100644 --- a/Robust.Client/Placement/Modes/AlignTileDense.cs +++ b/Robust.Client/Placement/Modes/AlignTileDense.cs @@ -14,7 +14,7 @@ namespace Robust.Client.Placement.Modes MouseCoords = ScreenToPlayerGrid(mouseScreen); var mapGrid = pManager.MapManager.GetGrid(MouseCoords.GridID); - CurrentTile = mapGrid.GetTile(MouseCoords); + CurrentTile = mapGrid.GetTileRef(MouseCoords); float tileSize = mapGrid.TileSize; //convert from ushort to float GridDistancing = tileSize; diff --git a/Robust.Client/Placement/Modes/AlignTileEmpty.cs b/Robust.Client/Placement/Modes/AlignTileEmpty.cs index ebe20e000a..db81561842 100644 --- a/Robust.Client/Placement/Modes/AlignTileEmpty.cs +++ b/Robust.Client/Placement/Modes/AlignTileEmpty.cs @@ -17,7 +17,7 @@ namespace Robust.Client.Placement.Modes MouseCoords = ScreenToPlayerGrid(mouseScreen); var mapGrid = pManager.MapManager.GetGrid(MouseCoords.GridID); - CurrentTile = mapGrid.GetTile(MouseCoords); + CurrentTile = mapGrid.GetTileRef(MouseCoords); float tileSize = mapGrid.TileSize; //convert from ushort to float GridDistancing = tileSize; @@ -43,7 +43,7 @@ namespace Robust.Client.Placement.Modes } var entitymanager = IoCManager.Resolve(); - return !(entitymanager.AnyEntitiesIntersecting(pManager.MapManager.GetGrid(MouseCoords.GridID).ParentMap.Index, + return !(entitymanager.AnyEntitiesIntersecting(pManager.MapManager.GetGrid(MouseCoords.GridID).ParentMapId, new Box2(new Vector2(CurrentTile.X, CurrentTile.Y), new Vector2(CurrentTile.X + 0.99f, CurrentTile.Y + 0.99f)))); } } diff --git a/Robust.Client/Placement/Modes/AlignTileNonDense.cs b/Robust.Client/Placement/Modes/AlignTileNonDense.cs index cbc1dd0cfb..1e97bb52ad 100644 --- a/Robust.Client/Placement/Modes/AlignTileNonDense.cs +++ b/Robust.Client/Placement/Modes/AlignTileNonDense.cs @@ -14,7 +14,7 @@ namespace Robust.Client.Placement.Modes MouseCoords = ScreenToPlayerGrid(mouseScreen); var mapGrid = pManager.MapManager.GetGrid(MouseCoords.GridID); - CurrentTile = mapGrid.GetTile(MouseCoords); + CurrentTile = mapGrid.GetTileRef(MouseCoords); float tileSize = mapGrid.TileSize; //convert from ushort to float GridDistancing = tileSize; diff --git a/Robust.Client/Placement/Modes/AlignWall.cs b/Robust.Client/Placement/Modes/AlignWall.cs index 0dd8dda59b..a5a7d6cac7 100644 --- a/Robust.Client/Placement/Modes/AlignWall.cs +++ b/Robust.Client/Placement/Modes/AlignWall.cs @@ -12,7 +12,7 @@ namespace Robust.Client.Placement.Modes public override void AlignPlacementMode(ScreenCoordinates mouseScreen) { MouseCoords = ScreenToPlayerGrid(mouseScreen); - CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTile(MouseCoords); + CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTileRef(MouseCoords); if (pManager.CurrentPermission.IsTile) { diff --git a/Robust.Client/Placement/Modes/PlaceFree.cs b/Robust.Client/Placement/Modes/PlaceFree.cs index 0a9efdd1c6..1e165a53cf 100644 --- a/Robust.Client/Placement/Modes/PlaceFree.cs +++ b/Robust.Client/Placement/Modes/PlaceFree.cs @@ -9,7 +9,7 @@ namespace Robust.Client.Placement.Modes public override void AlignPlacementMode(ScreenCoordinates mouseScreen) { MouseCoords = ScreenToPlayerGrid(mouseScreen); - CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTile(MouseCoords); + CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTileRef(MouseCoords); } public override bool IsValidPosition(GridCoordinates position) diff --git a/Robust.Client/Placement/Modes/PlaceNearby.cs b/Robust.Client/Placement/Modes/PlaceNearby.cs index e4b3cae4fe..43f07dd6cd 100644 --- a/Robust.Client/Placement/Modes/PlaceNearby.cs +++ b/Robust.Client/Placement/Modes/PlaceNearby.cs @@ -11,7 +11,7 @@ namespace Robust.Client.Placement.Modes public override void AlignPlacementMode(ScreenCoordinates mouseScreen) { MouseCoords = ScreenToPlayerGrid(mouseScreen); - CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTile(MouseCoords); + CurrentTile = pManager.MapManager.GetGrid(MouseCoords.GridID).GetTileRef(MouseCoords); } public override bool IsValidPosition(GridCoordinates position) diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 034ebf5d1b..5429e66858 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -573,7 +573,7 @@ namespace Robust.Client.Placement private void RequestPlacement(GridCoordinates coordinates) { - if (MapManager.GetGrid(coordinates.GridID).ParentMap.Index == MapId.Nullspace) return; + if (MapManager.GetGrid(coordinates.GridID).ParentMapId == MapId.Nullspace) return; if (CurrentPermission == null) return; if (!CurrentMode.IsValidPosition(coordinates)) return; if (Hijack != null && Hijack.HijackPlacementRequest(coordinates)) return; @@ -583,7 +583,7 @@ namespace Robust.Client.Placement var grid = MapManager.GetGrid(coordinates.GridID); // no point changing the tile to the same thing. - if (grid.GetTile(coordinates).Tile.TypeId == CurrentPermission.TileType) + if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType) return; foreach (var tileChange in _pendingTileChanges) diff --git a/Robust.Client/Placement/PlacementMode.cs b/Robust.Client/Placement/PlacementMode.cs index d4e4e3b67a..2be9b15652 100644 --- a/Robust.Client/Placement/PlacementMode.cs +++ b/Robust.Client/Placement/PlacementMode.cs @@ -198,7 +198,7 @@ namespace Robust.Client.Placement bounds.Width, bounds.Height); - if (pManager.PhysicsManager.IsColliding(collisionbox, pManager.MapManager.GetGrid(coordinates.GridID).ParentMap.Index)) + if (pManager.PhysicsManager.IsColliding(collisionbox, pManager.MapManager.GetGrid(coordinates.GridID).ParentMapId)) return true; return false; diff --git a/Robust.Client/State/States/GameScreen.cs b/Robust.Client/State/States/GameScreen.cs index 382feae131..92ca7d2716 100644 --- a/Robust.Client/State/States/GameScreen.cs +++ b/Robust.Client/State/States/GameScreen.cs @@ -162,7 +162,7 @@ namespace Robust.Client.State.States public IList GetEntitiesUnderPosition(GridCoordinates coordinates) { // Find all the entities intersecting our click - var entities = _entityManager.GetEntitiesIntersecting(_mapManager.GetGrid(coordinates.GridID).ParentMap.Index, coordinates.Position); + var entities = _entityManager.GetEntitiesIntersecting(_mapManager.GetGrid(coordinates.GridID).ParentMapId, coordinates.Position); // Check the entities against whether or not we can click them var foundEntities = new List<(IEntity clicked, int drawDepth)>(); diff --git a/Robust.Client/UserInterface/CustomControls/DebugCoordsPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugCoordsPanel.cs index 683b046487..448dc297b2 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugCoordsPanel.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugCoordsPanel.cs @@ -100,7 +100,7 @@ namespace Robust.Client.UserInterface.CustomControls try { var coords = eyeManager.ScreenToWorld(new ScreenCoordinates(mouseScreenPos)); - mouseWorldMap = (int) _mapManager.GetGrid(coords.GridID).ParentMap.Index; + mouseWorldMap = (int) _mapManager.GetGrid(coords.GridID).ParentMapId; mouseWorldGrid = (int) coords.GridID; mouseWorldPos = coords; worldToScreen = eyeManager.WorldToScreen(coords); @@ -109,7 +109,7 @@ namespace Robust.Client.UserInterface.CustomControls mouseEntity = gameScreen.GetEntityUnderPosition(coords); } - tile = _mapManager.GetGrid(coords.GridID).GetTile(coords); + tile = _mapManager.GetGrid(coords.GridID).GetTileRef(coords); } catch { diff --git a/Robust.Server/Console/Commands/MapCommands.cs b/Robust.Server/Console/Commands/MapCommands.cs index 7ac8fd4837..9a9f175a57 100644 --- a/Robust.Server/Console/Commands/MapCommands.cs +++ b/Robust.Server/Console/Commands/MapCommands.cs @@ -161,7 +161,7 @@ namespace Robust.Server.Console.Commands var pos = player.AttachedEntity.Transform.GridPosition; - shell.SendText(player, $"MapID:{IoCManager.Resolve().GetGrid(pos.GridID).ParentMap.Index} GridID:{pos.GridID} X:{pos.X:N2} Y:{pos.Y:N2}"); + shell.SendText(player, $"MapID:{IoCManager.Resolve().GetGrid(pos.GridID).ParentMapId} GridID:{pos.GridID} X:{pos.X:N2} Y:{pos.Y:N2}"); } } diff --git a/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs b/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs index e684c0eca1..f367b8abbe 100644 --- a/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs @@ -145,7 +145,7 @@ namespace Robust.Server.GameObjects.EntitySystems { var location = entity.Transform; var grid = _mapManager.GetGrid(location.GridPosition.GridID); - var tile = grid.GetTile(location.GridPosition); + var tile = grid.GetTileRef(location.GridPosition); var tileDef = _tileDefinitionManager[tile.Tile.TypeId]; if (tileDef.Friction != 0) { diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index a3da8c5902..5873e3ef68 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -173,7 +173,7 @@ namespace Robust.Server.GameObjects /// public IEnumerable GetEntitiesIntersecting(GridCoordinates position) { - return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMap.Index, position.ToWorld(_mapManager).Position); + return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMapId, position.ToWorld(_mapManager).Position); } /// @@ -191,7 +191,7 @@ namespace Robust.Server.GameObjects public IEnumerable GetEntitiesInRange(GridCoordinates position, float range) { var aabb = new Box2(position.Position - new Vector2(range / 2, range / 2), position.Position + new Vector2(range / 2, range / 2)); - return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMap.Index, aabb); + return GetEntitiesIntersecting(_mapManager.GetGrid(position.GridID).ParentMapId, aabb); } /// diff --git a/Robust.Server/Maps/YamlGridSerializer.cs b/Robust.Server/Maps/YamlGridSerializer.cs index 796931bb35..00732db099 100644 --- a/Robust.Server/Maps/YamlGridSerializer.cs +++ b/Robust.Server/Maps/YamlGridSerializer.cs @@ -15,8 +15,10 @@ namespace Robust.Server.Maps { public static class YamlGridSerializer { - public static YamlMappingNode SerializeGrid(IMapGrid grid) + public static YamlMappingNode SerializeGrid(IMapGrid mapGrid) { + var grid = (IMapGridInternal) mapGrid; + var gridn = new YamlMappingNode(); var info = new YamlMappingNode(); var chunkSeq = new YamlSequenceNode(); @@ -32,7 +34,7 @@ namespace Robust.Server.Maps var chunks = grid.GetMapChunks(); foreach (var chunk in chunks) { - var chunkNode = SerializeChunk(chunk); + var chunkNode = SerializeChunk(chunk.Value); chunkSeq.Add(chunkNode); } diff --git a/Robust.Server/Placement/PlacementManager.cs b/Robust.Server/Placement/PlacementManager.cs index 59f5ce1474..737756841e 100644 --- a/Robust.Server/Placement/PlacementManager.cs +++ b/Robust.Server/Placement/PlacementManager.cs @@ -130,7 +130,7 @@ namespace Robust.Server.Placement foreach (var grid in gridsInArea) { // figure out closest intersect - var gridIntersect = gridSearchBox.Intersect(grid.AABBWorld); + var gridIntersect = gridSearchBox.Intersect(grid.WorldBounds); var gridDist = (gridIntersect.Center - position).LengthSquared; if (gridDist >= distance) diff --git a/Robust.Shared.Maths/Box2.cs b/Robust.Shared.Maths/Box2.cs index 6828b7581b..ae8d8741c2 100644 --- a/Robust.Shared.Maths/Box2.cs +++ b/Robust.Shared.Maths/Box2.cs @@ -38,6 +38,11 @@ namespace Robust.Shared.Maths public Vector2 Size => new Vector2(Width, Height); public Vector2 Center => BottomLeft + Size / 2; + /// + /// A 1x1 unit box with the origin centered. + /// + public static readonly Box2 UnitCentered = new Box2(-0.5f, -0.5f, 0.5f, 0.5f); + public Box2(Vector2 bottomLeft, Vector2 topRight) : this(bottomLeft.X, bottomLeft.Y, topRight.X, topRight.Y) { } diff --git a/Robust.Shared.Maths/Box2i.cs b/Robust.Shared.Maths/Box2i.cs index 21172cb07b..1c0551a95a 100644 --- a/Robust.Shared.Maths/Box2i.cs +++ b/Robust.Shared.Maths/Box2i.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Robust.Shared.Maths { @@ -62,6 +62,22 @@ namespace Robust.Shared.Maths return new Box2i(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); } + /// + /// Returns the smallest rectangle that contains both of the rectangles. + /// + public Box2i Union(in Box2i other) + { + var left = Math.Min(Left, other.Left); + var right = Math.Max(Right, other.Right); + var bottom = Math.Min(Bottom, other.Bottom); + var top = Math.Max(Top, other.Top); + + if (left <= right && bottom <= top) + return new Box2i(left, bottom, right, top); + + return new Box2i(); + } + // override object.Equals public override bool Equals(object obj) { diff --git a/Robust.Shared/Interfaces/Map/IMapChunk.cs b/Robust.Shared/Interfaces/Map/IMapChunk.cs deleted file mode 100644 index 6abcd5965a..0000000000 --- a/Robust.Shared/Interfaces/Map/IMapChunk.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using Robust.Shared.GameObjects.Components.Transform; -using Robust.Shared.Map; - -namespace Robust.Shared.Interfaces.Map -{ - /// - /// This is a square 'chunk' of a MapGrid. - /// - public interface IMapChunk : IEnumerable - { - /// - /// The number of tiles per side of the square chunk. - /// - ushort ChunkSize { get; } - - /// - /// The X index of this chunk. - /// - int X { get; } - - /// - /// The Y index of this chunk. - /// - int Y { get; } - - MapIndices Index { get; } - - /// - /// Returns the tile at the given indices. The tile indices are relative locations to the - /// chunk origin, NOT the grid origin. - /// - /// The X tile index relative to the chunk. - /// The Y tile index relative to the chunk. - /// A reference to a tile. - TileRef GetTile(ushort xTile, ushort yTile); - - /// - /// Returns all of the tiles in the chunk. - /// - /// Will empty (space) tiles be added to the collection? - /// - IEnumerable GetAllTiles(bool ignoreEmpty = true); - - /// - /// Replaces a single tile inside of the chunk. - /// - /// The X tile index relative to the chunk. - /// The Y tile index relative to the chunk. - /// The new tile to insert. - void SetTile(ushort xChunkTile, ushort yChunkTile, Tile tile); - - /// - /// Transforms Tile indices relative to the grid into tile indices relative to this chunk. - /// - /// Tile indices relative to the grid. - /// Tile indices relative to this chunk. - MapIndices GridTileToChunkTile(MapIndices gridTile); - - IEnumerable GetSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset); - - void AddToSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap); - void RemoveFromSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap); - } -} diff --git a/Robust.Shared/Map/IMapChunk.cs b/Robust.Shared/Map/IMapChunk.cs new file mode 100644 index 0000000000..83b58ad384 --- /dev/null +++ b/Robust.Shared/Map/IMapChunk.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Maths; + +namespace Robust.Shared.Map +{ + /// + /// A square section of a . + /// + public interface IMapChunk : IEnumerable + { + /// + /// The number of tiles per side of the square chunk. + /// + ushort ChunkSize { get; } + + /// + /// The X index of this chunk inside the . + /// + int X { get; } + + /// + /// The Y index of this chunk inside the . + /// + int Y { get; } + + /// + /// The positional indices of this chunk in the . + /// + MapIndices Indices { get; } + + /// + /// Returns the tile at the given indices. + /// + /// The X tile index relative to the chunk origin. + /// The Y tile index relative to the chunk origin. + /// A reference to a tile. + TileRef GetTileRef(ushort xIndex, ushort yIndex); + + /// + /// Returns the tile reference at the given indices. + /// + /// The tile indices relative to the chunk origin. + /// A reference to a tile. + TileRef GetTileRef(MapIndices indices); + + Tile GetTile(ushort xIndex, ushort yIndex); + + /// + /// Returns all of the tiles in the chunk, while optionally filtering empty files. + /// Returned order is guaranteed to be row-major. + /// + /// Will empty (space) tiles be added to the collection? + /// + IEnumerable GetAllTiles(bool ignoreEmpty = true); + + /// + /// Replaces a single tile inside of the chunk. + /// + /// The X tile index relative to the chunk. + /// The Y tile index relative to the chunk. + /// The new tile to insert. + void SetTile(ushort xIndex, ushort yIndex, Tile tile); + + /// + /// Transforms Tile indices relative to the grid into tile indices relative to this chunk. + /// + /// Tile indices relative to the grid. + /// Tile indices relative to this chunk. + MapIndices GridTileToChunkTile(MapIndices gridTile); + + /// + /// Translates chunk tile indices to grid tile indices. + /// + /// The indices relative to the chunk origin. + /// The indices relative to the grid origin. + MapIndices ChunkTileToGridTile(MapIndices chunkTile); + + IEnumerable GetSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset); + + void AddToSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap); + void RemoveFromSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap); + + Box2i CalcLocalBounds(); + } +} diff --git a/Robust.Shared/Map/IMapChunkInternal.cs b/Robust.Shared/Map/IMapChunkInternal.cs new file mode 100644 index 0000000000..5365a0aef6 --- /dev/null +++ b/Robust.Shared/Map/IMapChunkInternal.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Timing; + +namespace Robust.Shared.Map +{ + /// + internal interface IMapChunkInternal : IMapChunk + { + /// + /// The last game simulation tick that this chunk was modified. + /// + GameTick LastModifiedTick { get; } + } +} diff --git a/Robust.Shared/Interfaces/Map/IMapGrid.cs b/Robust.Shared/Map/IMapGrid.cs similarity index 75% rename from Robust.Shared/Interfaces/Map/IMapGrid.cs rename to Robust.Shared/Map/IMapGrid.cs index 9f3ec806fd..19611f08ff 100644 --- a/Robust.Shared/Interfaces/Map/IMapGrid.cs +++ b/Robust.Shared/Map/IMapGrid.cs @@ -1,20 +1,26 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using Robust.Shared.GameObjects.Components.Transform; -using Robust.Shared.Map; +using Robust.Shared.Interfaces.Map; using Robust.Shared.Maths; -namespace Robust.Shared.Interfaces.Map +namespace Robust.Shared.Map { /// /// This is a collection of tiles in a grid format. /// + [PublicAPI] public interface IMapGrid : IDisposable { /// /// True if we are the default grid of our map. /// bool IsDefaultGrid { get; } + + /// + /// The map that this grid exists inside of. + /// IMap ParentMap { get; } /// @@ -22,6 +28,9 @@ namespace Robust.Shared.Interfaces.Map /// MapId ParentMapId { get; set; } + /// + /// The identifier of this grid. + /// GridId Index { get; } /// @@ -32,7 +41,7 @@ namespace Robust.Shared.Interfaces.Map /// /// The bounding box of the grid in world coordinates. /// - Box2 AABBWorld { get; } + Box2 WorldBounds { get; } /// /// The length of a side of the square chunk in number of tiles. @@ -66,14 +75,14 @@ namespace Robust.Shared.Interfaces.Map /// /// The location of the tile in coordinates. /// The tile at the world coordinates. - TileRef GetTile(GridCoordinates worldPos); + TileRef GetTileRef(GridCoordinates worldPos); /// - /// Gets a tile a the given tile coordinates. This will not create a new chunk. + /// Gets a tile a the given grid indices. This will not create a new chunk. /// /// The location of the tile in coordinates. /// The tile at the tile coordinates. - TileRef GetTile(MapIndices tileCoordinates); + TileRef GetTileRef(MapIndices tileCoordinates); /// /// Returns all tiles in the grid, in row-major order [xTileIndex, yTileIndex]. @@ -91,11 +100,8 @@ namespace Robust.Shared.Interfaces.Map /// /// Modifies a single tile inside of the chunk. /// - /// - /// The new internal ID of the tile. - /// The new data of the tile. - void SetTile(GridCoordinates worldPos, ushort tileId, ushort tileData = 0); - + /// + /// The tile to insert at the coordinates. void SetTile(MapIndices gridIndices, Tile tile); /// @@ -122,39 +128,7 @@ namespace Robust.Shared.Interfaces.Map void RemoveFromSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset, SnapGridComponent snap); #endregion SnapGridAccess - - #region ChunkAccess - - /// - /// The total number of chunks contained on this grid. - /// - int ChunkCount { get; } - - /// - /// Returns the chunk at the given indices. If the chunk does not exist, - /// then a new one is generated that is filled with empty space. - /// - /// The X index of the chunk in this grid. - /// The Y index of the chunk in this grid. - /// The existing or new chunk. - IMapChunk GetChunk(int xIndex, int yIndex); - - /// - /// Returns the chunk at the given indices. If the chunk does not exist, - /// then a new one is generated that is filled with empty space. - /// - /// The indices of the chunk in this grid. - /// The existing or new chunk. - IMapChunk GetChunk(MapIndices chunkIndices); - - /// - /// Returns all chunks in this grid. This will not generate new chunks. - /// - /// All chunks in the grid. - IEnumerable GetMapChunks(); - - #endregion ChunkAccess - + #region Transforms /// @@ -187,22 +161,29 @@ namespace Robust.Shared.Interfaces.Map /// /// Transforms grid-space tile indices to local coordinates. + /// The resulting coordinates are centered on the tile. /// /// /// GridCoordinates GridTileToLocal(MapIndices gridTile); /// - /// Transforms grid indices into an outvar tile, returns false if no tile is found + /// Transforms grid indices into a tile reference, returns false if no tile is found. /// - /// The Grid Tile indices. + /// The Grid Tile indices. + /// /// - bool IndicesToTile(MapIndices indices, out TileRef tile); + bool TryGetTileRef(MapIndices indices, out TileRef tile); /// - /// Transforms grid tile indices to grid chunk indices. + /// Transforms grid tile indices to chunk indices. /// - MapIndices GridTileToGridChunk(MapIndices gridTile); + MapIndices GridTileToChunkIndices(MapIndices gridTile); + + /// + /// Transforms local grid coordinates to chunk indices. + /// + MapIndices LocalToChunkIndices(GridCoordinates posWorld); #endregion Transforms } diff --git a/Robust.Shared/Map/IMapGridInternal.cs b/Robust.Shared/Map/IMapGridInternal.cs new file mode 100644 index 0000000000..76a4b8bf2c --- /dev/null +++ b/Robust.Shared/Map/IMapGridInternal.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Robust.Shared.Timing; + +namespace Robust.Shared.Map +{ + internal interface IMapGridInternal : IMapGrid + { + GameTick LastModifiedTick { get; } + + GameTick CurTick { get; } + + /// + /// The total number of chunks contained on this grid. + /// + int ChunkCount { get; } + + void NotifyTileChanged(in TileRef tileRef, in Tile oldTile); + + /// + /// Returns the chunk at the given indices. If the chunk does not exist, + /// then a new one is generated that is filled with empty space. + /// + /// The X index of the chunk in this grid. + /// The Y index of the chunk in this grid. + /// The existing or new chunk. + IMapChunkInternal GetChunk(int xIndex, int yIndex); + + /// + /// Returns the chunk at the given indices. If the chunk does not exist, + /// then a new one is generated that is filled with empty space. + /// + /// The indices of the chunk in this grid. + /// The existing or new chunk. + IMapChunkInternal GetChunk(MapIndices chunkIndices); + + /// + /// Returns all chunks in this grid. This will not generate new chunks. + /// + /// All chunks in the grid. + IReadOnlyDictionary GetMapChunks(); + } +} diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs new file mode 100644 index 0000000000..da890b5426 --- /dev/null +++ b/Robust.Shared/Map/IMapManagerInternal.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Timing; + +namespace Robust.Shared.Map +{ + /// + internal interface IMapManagerInternal : IMapManager + { + IGameTiming GameTiming { get; } + + /// + /// Raises the OnTileChanged event. + /// + /// A reference to the new tile. + /// The old tile that got replaced. + void RaiseOnTileChanged(TileRef tileRef, Tile oldTile); + } +} diff --git a/Robust.Shared/Map/MapChunk.cs b/Robust.Shared/Map/MapChunk.cs new file mode 100644 index 0000000000..ada10a5ba3 --- /dev/null +++ b/Robust.Shared/Map/MapChunk.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.Maths; +using Robust.Shared.Timing; + +namespace Robust.Shared.Map +{ + /// + internal class MapChunk : IMapChunkInternal + { + private readonly IMapGridInternal _grid; + private readonly MapIndices _gridIndices; + + private readonly Tile[,] _tiles; + private readonly SnapGridCell[,] _snapGrid; + + private Box2i _cachedBounds; + + /// + public GameTick LastModifiedTick { get; private set; } + + /// + /// Constructs an instance of a MapGrid chunk. + /// + /// + /// + /// + /// + public MapChunk(IMapGridInternal grid, int x, int y, ushort chunkSize) + { + _grid = grid; + LastModifiedTick = grid.CurTick; + _gridIndices = new MapIndices(x, y); + ChunkSize = chunkSize; + + _tiles = new Tile[ChunkSize, ChunkSize]; + _snapGrid = new SnapGridCell[ChunkSize, ChunkSize]; + } + + /// + public ushort ChunkSize { get; } + + /// + public int X => _gridIndices.X; + + /// + public int Y => _gridIndices.Y; + + /// + public MapIndices Indices => _gridIndices; + + /// + public TileRef GetTileRef(ushort xIndex, ushort yIndex) + { + if (xIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds."); + + if (yIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds."); + + var indices = ChunkTileToGridTile(new MapIndices(xIndex, yIndex)); + return new TileRef(_grid.ParentMapId, _grid.Index, indices, _tiles[xIndex, yIndex]); + } + + /// + public TileRef GetTileRef(MapIndices indices) + { + if (indices.X >= ChunkSize || indices.X < 0 || indices.Y >= ChunkSize || indices.Y < 0) + throw new ArgumentOutOfRangeException(nameof(indices), "Tile indices out of bounds."); + + var chunkIndices = ChunkTileToGridTile(indices); + return new TileRef(_grid.ParentMapId, _grid.Index, chunkIndices, _tiles[indices.X, indices.Y]); + } + + /// + public Tile GetTile(ushort xIndex, ushort yIndex) + { + if (xIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds."); + + if (yIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds."); + + return _tiles[xIndex, yIndex]; + } + + /// + public IEnumerable GetAllTiles(bool ignoreEmpty = true) + { + for (var x = 0; x < ChunkSize; x++) + for (var y = 0; y < ChunkSize; y++) + { + if (ignoreEmpty && _tiles[x, y].IsEmpty) + continue; + + var indices = ChunkTileToGridTile(new MapIndices(x, y)); + yield return new TileRef(_grid.ParentMapId, _grid.Index, indices.X, indices.Y, _tiles[x, y]); + } + } + + /// + public void SetTile(ushort xIndex, ushort yIndex, Tile tile) + { + if (xIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds."); + + if (yIndex >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds."); + + // same tile, no point to continue + if (_tiles[xIndex, yIndex].TypeId == tile.TypeId) + return; + + var gridTile = ChunkTileToGridTile(new MapIndices(xIndex, yIndex)); + var newTileRef = new TileRef(_grid.ParentMapId, _grid.Index, gridTile, tile); + var oldTile = _tiles[xIndex, yIndex]; + LastModifiedTick = _grid.CurTick; + + _tiles[xIndex, yIndex] = tile; + CheckBounds(new Vector2i(xIndex, yIndex), tile.IsEmpty); + + _grid.NotifyTileChanged(newTileRef, oldTile); + } + + /// + /// Returns an enumerator that iterates through all grid tiles. + /// + /// + public IEnumerator GetEnumerator() + { + for (var x = 0; x < ChunkSize; x++) + for (var y = 0; y < ChunkSize; y++) + { + if (_tiles[x, y].IsEmpty) + continue; + + var gridTile = ChunkTileToGridTile(new MapIndices(x, y)); + yield return new TileRef(_grid.ParentMapId, _grid.Index, gridTile.X, gridTile.Y, _tiles[x, y]); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public MapIndices GridTileToChunkTile(MapIndices gridTile) + { + var size = ChunkSize; + var x = MathHelper.Mod(gridTile.X, size); + var y = MathHelper.Mod(gridTile.Y, size); + return new MapIndices(x, y); + } + + /// + public MapIndices ChunkTileToGridTile(MapIndices chunkTile) + { + return chunkTile + _gridIndices * ChunkSize; + } + + /// + public IEnumerable GetSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset) + { + if (xCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xCell), "Tile indices out of bounds."); + + if (yCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yCell), "Tile indices out of bounds."); + + var cell = _snapGrid[xCell, yCell]; + var list = offset == SnapGridOffset.Center ? cell.Center : cell.Edge; + + if (list == null) + yield break; + + foreach (var element in list) + { + yield return element; + } + } + + /// + public void AddToSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap) + { + if (xCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xCell), "Tile indices out of bounds."); + + if (yCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yCell), "Tile indices out of bounds."); + + ref var cell = ref _snapGrid[xCell, yCell]; + if (offset == SnapGridOffset.Center) + { + if (cell.Center == null) + { + cell.Center = new List(1); + } + cell.Center.Add(snap); + } + else + { + if (cell.Edge == null) + { + cell.Edge = new List(1); + } + cell.Edge.Add(snap); + } + } + + /// + public void RemoveFromSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap) + { + if (xCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(xCell), "Tile indices out of bounds."); + + if (yCell >= ChunkSize) + throw new ArgumentOutOfRangeException(nameof(yCell), "Tile indices out of bounds."); + + ref var cell = ref _snapGrid[xCell, yCell]; + if (offset == SnapGridOffset.Center) + { + cell.Center?.Remove(snap); + } + else + { + cell.Edge?.Remove(snap); + } + } + + private void CheckBounds(in Vector2i indices, bool empty) + { + var tileBounds = new Box2i(0,0,1, 1).Translated(indices); + + if (!empty) + { + // placing tiles can only expand the bounds, it is easier to just blind union + _cachedBounds = _cachedBounds.Size.Equals(Vector2i.Zero) ? tileBounds : _cachedBounds.Union(tileBounds); + } + else + { + // removing tiles can only shrink the bounds, this is not an easy thing to detect + ReduceBoundsX(_tiles, tileBounds, ref _cachedBounds); + ReduceBoundsY(_tiles, tileBounds, ref _cachedBounds); + } + } + + private static void ReduceBoundsX(in Tile[,] tiles, Box2i tBounds, ref Box2i cBounds) + { + if (tBounds.Left != cBounds.Left && tBounds.Right != cBounds.Right) + return; // nothing to do, we are not on an edge + + var left = tBounds.Left == cBounds.Left; + + // removing a tile can shrink the side more than one tile + while (cBounds.Width > 0) + { + // check if we are the only tile holding the side out + if(!AnyTileOnY(tiles, new Vector2i(cBounds.Bottom, cBounds.Top), tBounds.BottomLeft)) + return; // our removal does not modify the edge, we are done here + + // shrink the chunk bounds + int newLeft; + int newRight; + if (left) + { + newLeft = cBounds.Left + 1; + newRight = cBounds.Right; + tBounds = tBounds.Translated(new Vector2i(1, 0)); + } + else + { + newLeft = cBounds.Left; + newRight = cBounds.Right - 1; + tBounds = tBounds.Translated(new Vector2i(-1, 0)); + } + + cBounds = new Box2i(newLeft, cBounds.Bottom, newRight, cBounds.Top); + } + } + + private static void ReduceBoundsY(in Tile[,] tiles, Box2i tBounds, ref Box2i cBounds) + { + if (tBounds.Bottom != cBounds.Bottom && tBounds.Top != cBounds.Top) + return; // nothing to do, we are not on an edge + + var bottom = tBounds.Bottom == cBounds.Bottom; // which side we are moving + + // removing a tile can shrink the side more than one tile + while (cBounds.Height > 0) + { + // check if we are the only tile holding the side out + if (!AnyTileOnX(tiles, new Vector2i(cBounds.Left, cBounds.Right), tBounds.BottomLeft)) + return; // our removal does not modify the edge + + // shrink the chunk bounds + int newBottom; + int newTop; + if (bottom) + { + newBottom = cBounds.Bottom + 1; + newTop = cBounds.Top; + tBounds = tBounds.Translated(new Vector2i(0, 1)); + } + else + { + newBottom = cBounds.Bottom; + newTop = cBounds.Top - 1; + tBounds = tBounds.Translated(new Vector2i(0, -1)); + } + + cBounds = new Box2i(cBounds.Left, newBottom, cBounds.Right, newTop); + } + } + + private static bool AnyTileOnX(in Tile[,] tiles, Vector2i extents, in Vector2i indices) + { + var y = indices.Y; + for (var x = extents.X; x < extents.Y; x++) + { + if(tiles[x, y].IsEmpty) + continue; + + return false; + } + + return true; + } + + private static bool AnyTileOnY(in Tile[,] tiles, Vector2i extents, in Vector2i indices) + { + var x = indices.X; + for (var y = extents.X; y < extents.Y; y++) + { + if(tiles[x, y].IsEmpty) + continue; + + return false; + } + + return true; + } + + /// + public Box2i CalcLocalBounds() + { + return _cachedBounds; + } + + + /// + public override string ToString() + { + return $"Chunk {_gridIndices}"; + } + + private struct SnapGridCell + { + public List Center; + public List Edge; + } + } +} diff --git a/Robust.Shared/Map/MapGrid.cs b/Robust.Shared/Map/MapGrid.cs new file mode 100644 index 0000000000..4c7e039949 --- /dev/null +++ b/Robust.Shared/Map/MapGrid.cs @@ -0,0 +1,419 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Maths; +using Robust.Shared.Timing; + +namespace Robust.Shared.Map +{ + /// + internal class MapGrid : IMapGridInternal + { + /// + /// Game tick that the map was created. + /// + public GameTick CreatedTick { get; } + + /// + /// Last game tick that the map was modified. + /// + public GameTick LastModifiedTick { get; private set; } + + /// + public GameTick CurTick => _mapManager.GameTiming.CurTick; + + /// + public bool IsDefaultGrid => ParentMap.DefaultGrid == this; + + /// + public IMap ParentMap => _mapManager.GetMap(ParentMapId); + + /// + public MapId ParentMapId { get; set; } + + /// + /// Grid chunks than make up this grid. + /// + private readonly Dictionary _chunks = new Dictionary(); + + private readonly IMapManagerInternal _mapManager; + private Vector2 _worldPosition; + + /// + /// Initializes a new instance of the class. + /// + /// Reference to the that will manage this grid. + /// Index identifier of this grid. + /// The dimension of this square chunk. + /// Distance in world units between the lines on the conceptual snap grid. + /// Parent map identifier. + internal MapGrid(IMapManagerInternal mapManager, GridId gridIndex, ushort chunkSize, float snapSize, + MapId parentMapId) + { + _mapManager = mapManager; + Index = gridIndex; + ChunkSize = chunkSize; + SnapSize = snapSize; + ParentMapId = parentMapId; + LastModifiedTick = CreatedTick = _mapManager.GameTiming.CurTick; + } + + /// + /// Disposes the grid. + /// + public void Dispose() + { + // Nothing for now. + } + + /// + public Box2 WorldBounds => LocalBounds.Translated(WorldPosition); + + public Box2 LocalBounds { get; private set; } + + /// + public ushort ChunkSize { get; } + + /// + public float SnapSize { get; } + + /// + public GridId Index { get; } + + /// + /// The length of the side of a square tile in world units. + /// + public ushort TileSize { get; } = 1; + + /// + public Vector2 WorldPosition + { + get => _worldPosition; + set + { + _worldPosition = value; + LastModifiedTick = _mapManager.GameTiming.CurTick; + } + } + + /// + /// Expands the AABB for this grid when a new tile is added. If the tile is already inside the existing AABB, + /// nothing happens. If it is outside, the AABB is expanded to fit the new tile. + /// + /// The new tile to check. + /// + private void UpdateAABB(MapIndices gridTile, bool empty) + { + LocalBounds = new Box2(); + foreach (var chunk in _chunks.Values) + { + var chunkBounds = chunk.CalcLocalBounds(); + + if(chunkBounds.Size.Equals(Vector2i.Zero)) + continue; + + if (LocalBounds.Size == Vector2.Zero) + { + var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize); + LocalBounds = gridBounds; + } + else + { + var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize); + LocalBounds = LocalBounds.Union(gridBounds); + } + } + } + + /// + public void NotifyTileChanged(in TileRef tileRef, in Tile oldTile) + { + LastModifiedTick = _mapManager.GameTiming.CurTick; + UpdateAABB(tileRef.GridIndices, tileRef.Tile.IsEmpty); + _mapManager.RaiseOnTileChanged(tileRef, oldTile); + } + + /// + public bool OnSnapCenter(Vector2 position) + { + return (FloatMath.CloseTo(position.X % SnapSize, 0) && FloatMath.CloseTo(position.Y % SnapSize, 0)); + } + + /// + public bool OnSnapBorder(Vector2 position) + { + return (FloatMath.CloseTo(position.X % SnapSize, SnapSize / 2) && FloatMath.CloseTo(position.Y % SnapSize, SnapSize / 2)); + } + + #region TileAccess + + /// + public TileRef GetTileRef(GridCoordinates worldPos) + { + return GetTileRef(WorldToTile(worldPos)); + } + + /// + public TileRef GetTileRef(MapIndices tileCoordinates) + { + var chunkIndices = GridTileToChunkIndices(tileCoordinates); + + if (!_chunks.TryGetValue(chunkIndices, out var output)) + { + // Chunk doesn't exist, return a tileRef to an empty (space) tile. + return new TileRef(ParentMapId, Index, tileCoordinates.X, tileCoordinates.Y, default); + } + + var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates); + return output.GetTileRef((ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y); + } + + /// + public IEnumerable GetAllTiles(bool ignoreSpace = true) + { + foreach (var kvChunk in _chunks) + { + foreach (var tileRef in kvChunk.Value) + { + if (!tileRef.Tile.IsEmpty) + yield return tileRef; + } + } + } + + /// + public void SetTile(GridCoordinates worldPos, Tile tile) + { + var localTile = WorldToTile(worldPos); + SetTile(new MapIndices(localTile.X, localTile.Y), tile); + } + + /// + public void SetTile(MapIndices gridIndices, Tile tile) + { + var (chunk, chunkTile) = ChunkAndOffsetForTile(gridIndices); + chunk.SetTile((ushort)chunkTile.X, (ushort)chunkTile.Y, tile); + } + + /// + public IEnumerable GetTilesIntersecting(Box2 worldArea, bool ignoreEmpty = true, Predicate predicate = null) + { + //TODO: needs world -> local -> tile translations. + var gridTileLb = new MapIndices((int)Math.Floor(worldArea.Left), (int)Math.Floor(worldArea.Bottom)); + var gridTileRt = new MapIndices((int)Math.Floor(worldArea.Right), (int)Math.Floor(worldArea.Top)); + + var tiles = new List(); + + for (var x = gridTileLb.X; x <= gridTileRt.X; x++) + { + for (var y = gridTileLb.Y; y <= gridTileRt.Y; y++) + { + var gridChunk = GridTileToChunkIndices(new MapIndices(x, y)); + + if (_chunks.TryGetValue(gridChunk, out var chunk)) + { + var chunkTile = chunk.GridTileToChunkTile(new MapIndices(x, y)); + var tile = chunk.GetTileRef((ushort)chunkTile.X, (ushort)chunkTile.Y); + + if (ignoreEmpty && tile.Tile.IsEmpty) + continue; + + if (predicate == null || predicate(tile)) + { + tiles.Add(tile); + } + } + else if (!ignoreEmpty) + { + var tile = new TileRef(ParentMapId, Index, x, y, new Tile()); + + if (predicate == null || predicate(tile)) + { + tiles.Add(tile); + } + } + } + } + return tiles; + } + + #endregion TileAccess + + #region ChunkAccess + + /// + /// The total number of allocated chunks in the grid. + /// + public int ChunkCount => _chunks.Count; + + /// + public IMapChunkInternal GetChunk(int xIndex, int yIndex) + { + return GetChunk(new MapIndices(xIndex, yIndex)); + } + + /// + public IMapChunkInternal GetChunk(MapIndices chunkIndices) + { + if (_chunks.TryGetValue(chunkIndices, out var output)) + return output; + + return _chunks[chunkIndices] = new MapChunk(this, chunkIndices.X, chunkIndices.Y, ChunkSize); + } + + /// + public IReadOnlyDictionary GetMapChunks() + { + return _chunks; + } + + #endregion ChunkAccess + + #region SnapGridAccess + + /// + public IEnumerable GetSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset) + { + return GetSnapGridCell(SnapGridCellFor(worldPos, offset), offset); + } + + /// + public IEnumerable GetSnapGridCell(MapIndices pos, SnapGridOffset offset) + { + var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); + return chunk.GetSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset); + } + + /// + public MapIndices SnapGridCellFor(GridCoordinates worldPos, SnapGridOffset offset) + { + var local = worldPos.ConvertToGrid(_mapManager, this); + if (offset == SnapGridOffset.Edge) + { + local = local.Offset(new Vector2(TileSize / 2f, TileSize / 2f)); + } + var x = (int)Math.Floor(local.X / TileSize); + var y = (int)Math.Floor(local.Y / TileSize); + return new MapIndices(x, y); + } + + /// + public void AddToSnapGridCell(MapIndices pos, SnapGridOffset offset, SnapGridComponent snap) + { + var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); + chunk.AddToSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset, snap); + } + + /// + public void AddToSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset, SnapGridComponent snap) + { + AddToSnapGridCell(SnapGridCellFor(worldPos, offset), offset, snap); + } + + /// + public void RemoveFromSnapGridCell(MapIndices pos, SnapGridOffset offset, SnapGridComponent snap) + { + var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); + chunk.RemoveFromSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset, snap); + } + + /// + public void RemoveFromSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset, SnapGridComponent snap) + { + RemoveFromSnapGridCell(SnapGridCellFor(worldPos, offset), offset, snap); + } + + private (IMapChunk, MapIndices) ChunkAndOffsetForTile(MapIndices pos) + { + var gridChunkIndices = GridTileToChunkIndices(pos); + var chunk = GetChunk(gridChunkIndices); + var chunkTile = chunk.GridTileToChunkTile(pos); + return (chunk, chunkTile); + } + + #endregion + + #region Transforms + + /// + public Vector2 WorldToLocal(Vector2 posWorld) + { + return posWorld - WorldPosition; + } + + /// + public GridCoordinates LocalToWorld(GridCoordinates posLocal) + { + return new GridCoordinates(posLocal.Position + WorldPosition, _mapManager.GetGrid(posLocal.GridID).ParentMap); + } + + /// + public Vector2 ConvertToWorld(Vector2 posLocal) + { + return posLocal + WorldPosition; + } + + public MapIndices WorldToTile(Vector2 posWorld) + { + var local = WorldToLocal(posWorld); + var x = (int)Math.Floor(local.X / TileSize); + var y = (int)Math.Floor(local.Y / TileSize); + return new MapIndices(x, y); + } + + /// + /// Transforms global world coordinates to tile indices relative to grid origin. + /// + public MapIndices WorldToTile(GridCoordinates posWorld) + { + var local = posWorld.ConvertToGrid(_mapManager, this); + var x = (int)Math.Floor(local.X / TileSize); + var y = (int)Math.Floor(local.Y / TileSize); + return new MapIndices(x, y); + } + + /// + /// Transforms global world coordinates to chunk indices relative to grid origin. + /// + public MapIndices LocalToChunkIndices(GridCoordinates posWorld) + { + var local = posWorld.ConvertToGrid(_mapManager, this); + var x = (int)Math.Floor(local.X / (TileSize * ChunkSize)); + var y = (int)Math.Floor(local.Y / (TileSize * ChunkSize)); + return new MapIndices(x, y); + } + + /// + public MapIndices GridTileToChunkIndices(MapIndices gridTile) + { + var x = (int)Math.Floor(gridTile.X / (float)ChunkSize); + var y = (int)Math.Floor(gridTile.Y / (float)ChunkSize); + + return new MapIndices(x, y); + } + + /// + public GridCoordinates GridTileToLocal(MapIndices gridTile) + { + return new GridCoordinates(gridTile.X * TileSize + (TileSize / 2f), gridTile.Y * TileSize + (TileSize / 2f), this); + } + + /// + public bool TryGetTileRef(MapIndices indices, out TileRef tile) + { + var chunkIndices = GridTileToChunkIndices(indices); + if (!_chunks.TryGetValue(chunkIndices, out var chunk)) + { + tile = default; + return false; + } + + var cTileIndices = chunk.GridTileToChunkTile(indices); + tile = chunk.GetTileRef(cTileIndices); + return true; + } + + #endregion Transforms + } +} diff --git a/Robust.Shared/Map/MapIndices.cs b/Robust.Shared/Map/MapIndices.cs index 757e21156c..e6136ec912 100644 --- a/Robust.Shared/Map/MapIndices.cs +++ b/Robust.Shared/Map/MapIndices.cs @@ -1,5 +1,6 @@ -using System; +using System; using JetBrains.Annotations; +using Robust.Shared.Maths; using Robust.Shared.Serialization; namespace Robust.Shared.Map @@ -87,5 +88,15 @@ namespace Robust.Shared.Map { return X ^ (Y * 23011); } + + public static implicit operator Vector2i(in MapIndices indices) + { + return new Vector2i(indices.X, indices.Y); + } + + public static implicit operator MapIndices(in Vector2i indices) + { + return new MapIndices(indices.X, indices.Y); + } } } diff --git a/Robust.Shared/Map/MapManager.Chunk.cs b/Robust.Shared/Map/MapManager.Chunk.cs deleted file mode 100644 index 0a1351eb91..0000000000 --- a/Robust.Shared/Map/MapManager.Chunk.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Robust.Shared.GameObjects.Components.Transform; -using Robust.Shared.Interfaces.Map; -using Robust.Shared.Maths; -using Robust.Shared.Timing; - -namespace Robust.Shared.Map -{ - public partial class MapManager - { - /// - /// A square section of the map. - /// - internal class Chunk : IMapChunk - { - internal GameTick LastModifiedTick { get; private set; } - private readonly MapGrid _grid; - private readonly MapIndices _gridIndices; - private readonly MapManager _mapManager; - - internal readonly Tile[,] _tiles; - private readonly SnapGridCell[,] _snapGrid; - - /// - /// Constructs an instance of a MapGrid chunk. - /// - /// - /// - /// - /// - /// - public Chunk(MapManager manager, MapGrid grid, int x, int y, ushort chunkSize) - { - _mapManager = manager; - LastModifiedTick = _mapManager._gameTiming.CurTick; - ChunkSize = chunkSize; - _grid = grid; - _gridIndices = new MapIndices(x, y); - - _tiles = new Tile[ChunkSize, ChunkSize]; - _snapGrid = new SnapGridCell[ChunkSize, ChunkSize]; - } - - /// - public ushort ChunkSize { get; } - - /// - public int X => _gridIndices.X; - - /// - public int Y => _gridIndices.Y; - - public MapIndices Index => new MapIndices(X, Y); - - /// - /// Returns the tile at the given indices. The tile indices are relative locations to the chunk origin, - /// NOT local to the grid. - /// - /// The X tile index relative to the chunk origin. - /// The Y tile index relative to the chunk origin. - /// A reference to a tile. - public TileRef GetTile(ushort xTile, ushort yTile) - { - // array out of bounds - if (xTile >= ChunkSize || yTile >= ChunkSize) - throw new ArgumentOutOfRangeException("Tile indices out of bounds."); - - var indices = ChunkTileToGridTile(new MapIndices(xTile, yTile)); - return new TileRef(_grid.ParentMapId, _grid.Index, indices.X, indices.Y, _tiles[xTile, yTile]); - } - public TileRef GetTile(MapIndices indices) - { - // array out of bounds - if (indices.X >= ChunkSize || indices.X < 0 || indices.Y >= ChunkSize || indices.Y < 0) - throw new ArgumentOutOfRangeException("Tile indices out of bounds."); - - return new TileRef(_grid.ParentMapId, _grid.Index, indices.X, indices.Y, _tiles[indices.X, indices.Y]); - } - - /// - [Obsolete("Enumerate over the chunk instead.")] - public IEnumerable GetAllTiles(bool ignoreEmpty = true) - { - for (var x = 0; x < ChunkSize; x++) - for (var y = 0; y < ChunkSize; y++) - { - if (_tiles[x, y].IsEmpty) - continue; - - var indices = ChunkTileToGridTile(new MapIndices(x, y)); - yield return new TileRef(_grid.ParentMapId, _grid.Index, indices.X, indices.Y, _tiles[x, y]); - } - } - - /// - public void SetTile(ushort xChunkTile, ushort yChunkTile, Tile tile) - { - if (xChunkTile >= ChunkSize || yChunkTile >= ChunkSize) - throw new ArgumentException("Tile indices out of bounds."); - - // same tile, no point to continue - if (_tiles[xChunkTile, yChunkTile].TypeId == tile.TypeId) - return; - - var gridTile = ChunkTileToGridTile(new MapIndices(xChunkTile, yChunkTile)); - var newTileRef = new TileRef(_grid.ParentMapId, _grid.Index, gridTile.X, gridTile.Y, tile); - var oldTile = _tiles[xChunkTile, yChunkTile]; - _grid.LastModifiedTick = LastModifiedTick = _mapManager._gameTiming.CurTick; - _mapManager.RaiseOnTileChanged(newTileRef, oldTile); - _grid.UpdateAABB(gridTile); - - _tiles[xChunkTile, yChunkTile] = tile; - } - - /// - /// Returns an enumerator that iterates through all grid tiles. - /// - /// - public IEnumerator GetEnumerator() - { - for (var x = 0; x < ChunkSize; x++) - for (var y = 0; y < ChunkSize; y++) - { - var gridTile = ChunkTileToGridTile(new MapIndices(x, y)); - yield return new TileRef(_grid.ParentMapId, _grid.Index, gridTile.X, gridTile.Y, _tiles[x, y]); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - public MapIndices GridTileToChunkTile(MapIndices gridTile) - { - var size = ChunkSize; - var x = MathHelper.Mod(gridTile.X, size); - var y = MathHelper.Mod(gridTile.Y, size); - return new MapIndices(x, y); - } - - /// - public void SetTile(ushort xChunkTile, ushort yChunkTile, ushort tileId, ushort tileData = 0) - { - SetTile(xChunkTile, yChunkTile, new Tile(tileId, tileData)); - } - - - public IEnumerable GetSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset) - { - var cell = _snapGrid[xCell, yCell]; - List list; - if (offset == SnapGridOffset.Center) - { - list = cell.Center; - } - else - { - list = cell.Edge; - } - - if (list != null) - { - foreach (var element in list) - { - yield return element; - } - } - } - - public void AddToSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap) - { - ref var cell = ref _snapGrid[xCell, yCell]; - if (offset == SnapGridOffset.Center) - { - if (cell.Center == null) - { - cell.Center = new List(1); - } - cell.Center.Add(snap); - } - else - { - if (cell.Edge == null) - { - cell.Edge = new List(1); - } - cell.Edge.Add(snap); - } - } - - public void RemoveFromSnapGridCell(ushort xCell, ushort yCell, SnapGridOffset offset, SnapGridComponent snap) - { - ref var cell = ref _snapGrid[xCell, yCell]; - if (offset == SnapGridOffset.Center) - { - cell.Center?.Remove(snap); - } - else - { - cell.Edge?.Remove(snap); - } - } - - /// - /// Translates chunk tile indices to grid tile indices. - /// - /// The indices relative to the chunk origin. - /// The indices relative to the grid origin. - private MapIndices ChunkTileToGridTile(MapIndices chunkTile) - { - return chunkTile + _gridIndices * ChunkSize; - } - - /// - public override string ToString() - { - return $"Chunk {_gridIndices}, {ChunkSize}"; - } - - private struct SnapGridCell - { - public List Center; - public List Edge; - } - } - } -} diff --git a/Robust.Shared/Map/MapManager.Map.cs b/Robust.Shared/Map/MapManager.Map.cs index a814aec01b..d9ac829f56 100644 --- a/Robust.Shared/Map/MapManager.Map.cs +++ b/Robust.Shared/Map/MapManager.Map.cs @@ -75,7 +75,7 @@ namespace Robust.Shared.Map public IMapGrid FindGridAt(Vector2 worldPos) { foreach (var kvGrid in _grids) - if (kvGrid.Value.AABBWorld.Contains(worldPos) && kvGrid.Value != DefaultGrid) + if (kvGrid.Value.WorldBounds.Contains(worldPos) && kvGrid.Value != DefaultGrid) return kvGrid.Value; return DefaultGrid; } @@ -89,7 +89,7 @@ namespace Robust.Shared.Map { var gridList = new List(); foreach (var kvGrid in _grids) - if (kvGrid.Value.AABBWorld.Intersects(worldArea)) + if (kvGrid.Value.WorldBounds.Intersects(worldArea)) gridList.Add(kvGrid.Value); return gridList; } diff --git a/Robust.Shared/Map/MapManager.MapGrid.cs b/Robust.Shared/Map/MapManager.MapGrid.cs deleted file mode 100644 index 336a259782..0000000000 --- a/Robust.Shared/Map/MapManager.MapGrid.cs +++ /dev/null @@ -1,418 +0,0 @@ -using System; -using System.Collections.Generic; -using Robust.Shared.GameObjects.Components.Transform; -using Robust.Shared.Interfaces.Map; -using Robust.Shared.Maths; -using Robust.Shared.Timing; - -namespace Robust.Shared.Map -{ - /// - public partial class MapManager - { - /// - internal class MapGrid : IMapGrid - { - /// - /// Game tick that the map was created. - /// - public GameTick CreatedTick { get; } - - /// - /// Last game tick that the map was modified. - /// - public GameTick LastModifiedTick { get; internal set; } - - /// - public bool IsDefaultGrid => ParentMap.DefaultGrid == this; - - /// - public IMap ParentMap => _mapManager.GetMap(ParentMapId); - - /// - public MapId ParentMapId { get; set; } - - /// - /// Grid chunks than make up this grid. - /// - internal readonly Dictionary _chunks = new Dictionary(); - - private readonly MapManager _mapManager; - private Vector2 _worldPosition; - - /// - /// Initializes a new instance of the class. - /// - /// Reference to the that will manage this grid. - /// Index identifier of this grid. - /// The dimension of this square chunk. - /// Distance in world units between the lines on the conceptual snap grid. - /// Parent map identifier. - internal MapGrid(MapManager mapManager, GridId gridIndex, ushort chunkSize, float snapSize, MapId parentMapId) - { - _mapManager = mapManager; - Index = gridIndex; - ChunkSize = chunkSize; - SnapSize = snapSize; - ParentMapId = parentMapId; - LastModifiedTick = CreatedTick = _mapManager._gameTiming.CurTick; - } - - /// - /// Disposes the grid. - /// - public void Dispose() - { - // Nothing for now. - } - - /// - public Box2 AABBWorld { get; private set; } - - /// - public ushort ChunkSize { get; } - - /// - public float SnapSize { get; } - - /// - public GridId Index { get; } - - /// - /// The length of the side of a square tile in world units. - /// - public ushort TileSize { get; } = 1; - - /// - public Vector2 WorldPosition - { - get => _worldPosition; - set - { - _worldPosition = value; - LastModifiedTick = _mapManager._gameTiming.CurTick; - } - } - - /// - /// Expands the AABB for this grid when a new tile is added. If the tile is already inside the existing AABB, - /// nothing happens. If it is outside, the AABB is expanded to fit the new tile. - /// - /// The new tile to check. - public void UpdateAABB(MapIndices gridTile) - { - var worldPos = GridTileToLocal(gridTile).ToWorld(_mapManager); - - if (AABBWorld.Contains(worldPos.Position)) - return; - - // rect union - var a = AABBWorld; - var b = worldPos; - - var minX = Math.Min(a.Left, b.X); - var maxX = Math.Max(a.Right, b.X); - - var minY = Math.Min(a.Bottom, b.Y); - var maxY = Math.Max(a.Top, b.Y); - - AABBWorld = Box2.FromDimensions(minX, minY, maxX - minX, maxY - minY); - } - - /// - public bool OnSnapCenter(Vector2 position) - { - return (FloatMath.CloseTo(position.X % SnapSize, 0) && FloatMath.CloseTo(position.Y % SnapSize, 0)); - } - - /// - public bool OnSnapBorder(Vector2 position) - { - return (FloatMath.CloseTo(position.X % SnapSize, SnapSize / 2) && FloatMath.CloseTo(position.Y % SnapSize, SnapSize / 2)); - } - - #region TileAccess - - /// - public TileRef GetTile(GridCoordinates worldPos) - { - return GetTile(WorldToTile(worldPos)); - } - - /// - public TileRef GetTile(MapIndices tileCoordinates) - { - var chunkIndices = GridTileToGridChunk(tileCoordinates); - - if (!_chunks.TryGetValue(chunkIndices, out var output)) - { - // Chunk doesn't exist, return a tileRef to an empty (space) tile. - return new TileRef(ParentMapId, Index, tileCoordinates.X, tileCoordinates.Y, default); - } - - var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates); - return output.GetTile((ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y); - } - - /// - public IEnumerable GetAllTiles(bool ignoreSpace = true) - { - foreach (var kvChunk in _chunks) - { - foreach (var tileRef in kvChunk.Value) - { - if (!tileRef.Tile.IsEmpty) - yield return tileRef; - } - } - } - - /// - public void SetTile(GridCoordinates worldPos, Tile tile) - { - var localTile = WorldToTile(worldPos); - SetTile(localTile.X, localTile.Y, tile); - } - - private void SetTile(int xIndex, int yIndex, Tile tile) - { - var (chunk, chunkTile) = ChunkAndOffsetForTile(new MapIndices(xIndex, yIndex)); - chunk.SetTile((ushort)chunkTile.X, (ushort)chunkTile.Y, tile); - } - - /// - public void SetTile(GridCoordinates worldPos, ushort tileId, ushort tileData = 0) - { - SetTile(worldPos, new Tile(tileId, tileData)); - } - - public void SetTile(MapIndices gridIndices, Tile tile) - { - var (chunk, chunkTile) = ChunkAndOffsetForTile(gridIndices); - chunk.SetTile((ushort)chunkTile.X, (ushort)chunkTile.Y, tile); - } - - /// - public IEnumerable GetTilesIntersecting(Box2 worldArea, bool ignoreEmpty = true, Predicate predicate = null) - { - //TODO: needs world -> local -> tile translations. - var gridTileLb = new MapIndices((int)Math.Floor(worldArea.Left), (int)Math.Floor(worldArea.Bottom)); - var gridTileRt = new MapIndices((int)Math.Floor(worldArea.Right), (int)Math.Floor(worldArea.Top)); - - var tiles = new List(); - - for (var x = gridTileLb.X; x <= gridTileRt.X; x++) - { - for (var y = gridTileLb.Y; y <= gridTileRt.Y; y++) - { - var gridChunk = GridTileToGridChunk(new MapIndices(x, y)); - - if (_chunks.TryGetValue(gridChunk, out var chunk)) - { - var chunkTile = chunk.GridTileToChunkTile(new MapIndices(x, y)); - var tile = chunk.GetTile((ushort)chunkTile.X, (ushort)chunkTile.Y); - - if (ignoreEmpty && tile.Tile.IsEmpty) - continue; - - if (predicate == null || predicate(tile)) - { - tiles.Add(tile); - } - } - else if (!ignoreEmpty) - { - var tile = new TileRef(ParentMapId, Index, x, y, new Tile()); - - if (predicate == null || predicate(tile)) - { - tiles.Add(tile); - } - } - } - } - return tiles; - } - - #endregion TileAccess - - #region ChunkAccess - - /// - /// The total number of allocated chunks in the grid. - /// - public int ChunkCount => _chunks.Count; - - /// - public IMapChunk GetChunk(int xIndex, int yIndex) - { - return GetChunk(new MapIndices(xIndex, yIndex)); - } - - /// - public IMapChunk GetChunk(MapIndices chunkIndices) - { - if (_chunks.TryGetValue(chunkIndices, out var output)) - return output; - - return _chunks[chunkIndices] = new Chunk(_mapManager, this, chunkIndices.X, chunkIndices.Y, ChunkSize); - } - - /// - public IEnumerable GetMapChunks() - { - foreach (var kvChunk in _chunks) - { - yield return kvChunk.Value; - } - } - - #endregion ChunkAccess - - #region SnapGridAccess - - /// - public IEnumerable GetSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset) - { - return GetSnapGridCell(SnapGridCellFor(worldPos, offset), offset); - } - - /// - public IEnumerable GetSnapGridCell(MapIndices pos, SnapGridOffset offset) - { - var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); - return chunk.GetSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset); - } - - /// - public MapIndices SnapGridCellFor(GridCoordinates worldPos, SnapGridOffset offset) - { - var local = worldPos.ConvertToGrid(_mapManager, this); - if (offset == SnapGridOffset.Edge) - { - local = local.Offset(new Vector2(TileSize / 2f, TileSize / 2f)); - } - var x = (int)Math.Floor(local.X / TileSize); - var y = (int)Math.Floor(local.Y / TileSize); - return new MapIndices(x, y); - } - - /// - public void AddToSnapGridCell(MapIndices pos, SnapGridOffset offset, SnapGridComponent snap) - { - var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); - chunk.AddToSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset, snap); - } - - /// - public void AddToSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset, SnapGridComponent snap) - { - AddToSnapGridCell(SnapGridCellFor(worldPos, offset), offset, snap); - } - - /// - public void RemoveFromSnapGridCell(MapIndices pos, SnapGridOffset offset, SnapGridComponent snap) - { - var (chunk, chunkTile) = ChunkAndOffsetForTile(pos); - chunk.RemoveFromSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, offset, snap); - } - - /// - public void RemoveFromSnapGridCell(GridCoordinates worldPos, SnapGridOffset offset, SnapGridComponent snap) - { - RemoveFromSnapGridCell(SnapGridCellFor(worldPos, offset), offset, snap); - } - - private (IMapChunk, MapIndices) ChunkAndOffsetForTile(MapIndices pos) - { - var gridChunkIndices = GridTileToGridChunk(pos); - var chunk = GetChunk(gridChunkIndices); - var chunkTile = chunk.GridTileToChunkTile(pos); - return (chunk, chunkTile); - } - - #endregion - - #region Transforms - - /// - public Vector2 WorldToLocal(Vector2 posWorld) - { - return posWorld - WorldPosition; - } - - /// - public GridCoordinates LocalToWorld(GridCoordinates posLocal) - { - return new GridCoordinates(posLocal.Position + WorldPosition, _mapManager.GetGrid(posLocal.GridID).ParentMap); - } - - /// - public Vector2 ConvertToWorld(Vector2 posLocal) - { - return posLocal + WorldPosition; - } - - public MapIndices WorldToTile(Vector2 posWorld) - { - var local = WorldToLocal(posWorld); - var x = (int)Math.Floor(local.X / TileSize); - var y = (int)Math.Floor(local.Y / TileSize); - return new MapIndices(x, y); - } - - /// - /// Transforms global world coordinates to tile indices relative to grid origin. - /// - public MapIndices WorldToTile(GridCoordinates posWorld) - { - var local = posWorld.ConvertToGrid(_mapManager, this); - var x = (int)Math.Floor(local.X / TileSize); - var y = (int)Math.Floor(local.Y / TileSize); - return new MapIndices(x, y); - } - - /// - /// Transforms global world coordinates to chunk indices relative to grid origin. - /// - public MapIndices WorldToChunk(GridCoordinates posWorld) - { - var local = posWorld.ConvertToGrid(_mapManager, this); - var x = (int)Math.Floor(local.X / (TileSize * ChunkSize)); - var y = (int)Math.Floor(local.Y / (TileSize * ChunkSize)); - return new MapIndices(x, y); - } - - /// - public MapIndices GridTileToGridChunk(MapIndices gridTile) - { - var x = (int)Math.Floor(gridTile.X / (float)ChunkSize); - var y = (int)Math.Floor(gridTile.Y / (float)ChunkSize); - - return new MapIndices(x, y); - } - - /// - public GridCoordinates GridTileToLocal(MapIndices gridTile) - { - return new GridCoordinates(gridTile.X * TileSize + (TileSize / 2), gridTile.Y * TileSize + (TileSize / 2), this); - } - - /// - public bool IndicesToTile(MapIndices indices, out TileRef tile) - { - var chunkIndices = new MapIndices(indices.X / ChunkSize, indices.Y / ChunkSize); - if (!_chunks.ContainsKey(chunkIndices)) - { - tile = new TileRef(); //Nothing should ever use or access this, bool check should occur first - return false; - } - var chunk = _chunks[chunkIndices]; - tile = chunk.GetTile(new MapIndices(indices.X % ChunkSize, indices.Y % ChunkSize)); - return true; - } - - #endregion Transforms - } - } -} diff --git a/Robust.Shared/Map/MapManager.Network.cs b/Robust.Shared/Map/MapManager.Network.cs index 98e4b5a2f5..c3dabe29aa 100644 --- a/Robust.Shared/Map/MapManager.Network.cs +++ b/Robust.Shared/Map/MapManager.Network.cs @@ -30,7 +30,7 @@ namespace Robust.Shared.Map } var chunkData = new List(); - foreach (var (index, chunk) in grid._chunks) + foreach (var (index, chunk) in grid.GetMapChunks()) { if (chunk.LastModifiedTick < fromTick) { @@ -45,7 +45,7 @@ namespace Robust.Shared.Map for (var x = 0; x < grid.ChunkSize; x++) for (var y = 0; y < grid.ChunkSize; y++) { - tileBuffer[x * grid.ChunkSize + y] = chunk._tiles[x, y]; + tileBuffer[x * grid.ChunkSize + y] = chunk.GetTile((ushort) x, (ushort) y); } chunkData.Add(new GameStateMapData.ChunkDatum(index, tileBuffer)); @@ -153,7 +153,7 @@ namespace Robust.Shared.Map for (ushort y = 0; y < grid.ChunkSize; y++) { var tile = chunkData.TileData[counter++]; - if (chunk.GetTile(x, y).Tile != tile) + if (chunk.GetTileRef(x, y).Tile != tile) { chunk.SetTile(x, y, tile); modified.Add((new MapIndices(chunk.X * grid.ChunkSize + x, chunk.Y * grid.ChunkSize + y), tile)); diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs index a16eab1eae..845b0c100e 100644 --- a/Robust.Shared/Map/MapManager.cs +++ b/Robust.Shared/Map/MapManager.cs @@ -9,10 +9,12 @@ using Robust.Shared.Utility; namespace Robust.Shared.Map { - /// - public partial class MapManager : IMapManager, IPostInjectInit + /// + public partial class MapManager : IMapManagerInternal, IPostInjectInit { - [Dependency] private protected readonly IGameTiming _gameTiming; + [Dependency] private readonly IGameTiming _gameTiming; + + public IGameTiming GameTiming => _gameTiming; /// public IMap DefaultMap => GetMap(MapId.Nullspace); diff --git a/Robust.Shared/Map/TileRef.cs b/Robust.Shared/Map/TileRef.cs index 22fb8a6ba2..2cef09073e 100644 --- a/Robust.Shared/Map/TileRef.cs +++ b/Robust.Shared/Map/TileRef.cs @@ -15,7 +15,7 @@ namespace Robust.Shared.Map public readonly MapId MapIndex; /// - /// Identifier of the this Tile belongs to. + /// Identifier of the this Tile belongs to. /// public readonly GridId GridIndex; diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 2eec7008c7..c00bd3d367 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using Robust.Shared.Log; +using Robust.Shared.Map; using Robust.Shared.Maths; using YamlDotNet.RepresentationModel; using Robust.Shared.Serialization; diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index e3bcd16811..e6374c5e88 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -171,8 +171,7 @@ - - + @@ -194,6 +193,10 @@ + + + + @@ -304,7 +307,7 @@ - + @@ -374,7 +377,7 @@ False - + diff --git a/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs b/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs index 3811b64cd4..d9fdaf3c35 100644 --- a/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs +++ b/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs @@ -7,6 +7,7 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects.Components; using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; +using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Prototypes; diff --git a/Robust.UnitTesting/Robust.UnitTesting.csproj b/Robust.UnitTesting/Robust.UnitTesting.csproj index fce7a6ec70..8085febac9 100644 --- a/Robust.UnitTesting/Robust.UnitTesting.csproj +++ b/Robust.UnitTesting/Robust.UnitTesting.csproj @@ -58,9 +58,9 @@ - - - + + + 0.7.5-beta2 @@ -110,7 +110,10 @@ + + + diff --git a/Robust.UnitTesting/Shared/Map/MapChunk_Tests.cs b/Robust.UnitTesting/Shared/Map/MapChunk_Tests.cs new file mode 100644 index 0000000000..f0fc22f725 --- /dev/null +++ b/Robust.UnitTesting/Shared/Map/MapChunk_Tests.cs @@ -0,0 +1,455 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NUnit.Framework; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Robust.UnitTesting.Shared.Map +{ + [TestFixture, Parallelizable, TestOf(typeof(MapChunk))] + class MapChunk_Tests + { + [Test] + public void GetChunkSize() + { + var chunk = MapChunkFactory(7, 9); + + Assert.That(chunk.ChunkSize, Is.EqualTo((ushort)8)); + } + + [Test] + public void GetIndices() + { + var chunk = MapChunkFactory(7, 9); + + Assert.That(chunk.X, Is.EqualTo(7)); + Assert.That(chunk.Y, Is.EqualTo(9)); + Assert.That(chunk.Indices, Is.EqualTo(new MapIndices(7,9))); + } + + [Test] + public void ConstructorSetsLastTick() + { + var chunk = MapChunkFactory(7, 9); + + Assert.That(chunk.LastModifiedTick, Is.EqualTo(new GameTick(11))); + } + + [Test] + public void SetTileThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.SetTile(8,0, new Tile()))); + Assert.Throws((() => chunk.SetTile(0, 8, new Tile()))); + } + + [Test] + public void SetTileModifiesLastTick() + { + var curTick = new GameTick(11); + var mapGrid = new Mock(); + mapGrid.SetupGet(f => f.CurTick).Returns((() => curTick)); + mapGrid.SetupGet(f => f.ParentMapId).Returns(new MapId(11)); + mapGrid.SetupGet(f => f.Index).Returns(new GridId(13)); + + var chunk = new MapChunk(mapGrid.Object, 7, 9, 8); + + curTick = new GameTick(13); + chunk.SetTile(3, 5, new Tile(1, 3)); + + Assert.That(chunk.LastModifiedTick, Is.EqualTo(new GameTick(13))); + } + + [Test] + public void SetTileDuplicateDoesNotModifyLastTick() + { + var curTick = new GameTick(11); + var mapGrid = new Mock(); + mapGrid.SetupGet(f => f.CurTick).Returns((() => curTick)); + mapGrid.SetupGet(f => f.ParentMapId).Returns(new MapId(11)); + mapGrid.SetupGet(f => f.Index).Returns(new GridId(13)); + + var chunk = new MapChunk(mapGrid.Object, 7, 9, 8); + + curTick = new GameTick(13); + chunk.SetTile(3, 5, new Tile(1, 3)); + curTick = new GameTick(14); + chunk.SetTile(3, 5, new Tile(1, 3)); + + Assert.That(chunk.LastModifiedTick, Is.EqualTo(new GameTick(13))); + } + + [Test] + public void GetTileRefByIndex() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var result = chunk.GetTileRef(3, 5); + + Assert.That(result.X, Is.EqualTo(8 * 7 + 3)); + Assert.That(result.Y, Is.EqualTo(8 * 9 + 5)); + Assert.That(result.Tile.TypeId, Is.EqualTo(1)); + Assert.That(result.Tile.Data, Is.EqualTo((ushort) 3)); + Assert.That(result.GridIndex, Is.EqualTo(new GridId(13))); + Assert.That(result.MapIndex, Is.EqualTo(new MapId(11))); + } + + [Test] + public void GetTileRefByIndices() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var result = chunk.GetTileRef(new MapIndices(3, 5)); + + Assert.That(result.X, Is.EqualTo(8 * 7 + 3)); + Assert.That(result.Y, Is.EqualTo(8 * 9 + 5)); + Assert.That(result.Tile.TypeId, Is.EqualTo(1)); + Assert.That(result.Tile.Data, Is.EqualTo((ushort)3)); + Assert.That(result.GridIndex, Is.EqualTo(new GridId(13))); + Assert.That(result.MapIndex, Is.EqualTo(new MapId(11))); + } + + [Test] + public void GetTileRefThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.GetTileRef(8, 0))); + Assert.Throws((() => chunk.GetTileRef(0, 8))); + Assert.Throws((() => chunk.GetTileRef(new MapIndices(8,0)))); + Assert.Throws((() => chunk.GetTileRef(new MapIndices(0, 8)))); + } + + [Test] + public void GetTileByIndex() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var result = chunk.GetTile(3, 5); + + Assert.That(result.TypeId, Is.EqualTo(1)); + Assert.That(result.Data, Is.EqualTo((ushort)3)); + } + + [Test] + public void GetTileByIndexThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.GetTile(8, 0))); + Assert.Throws((() => chunk.GetTile(0, 8))); + } + + [Test] + public void GetAllTilesNotEmpty() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(5, 4, new Tile(5, 7)); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var tiles = chunk.GetAllTiles().ToList(); + + Assert.That(tiles.Count, Is.EqualTo(2)); + + // Order is guaranteed to be row-major (because C# is), the Y value is contiguous in memory + Assert.That(tiles[0], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 3, 8 * 9 + 5), + new Tile(1, 3)))); + + Assert.That(tiles[1], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 5, 8 * 9 + 4), + new Tile(5, 7)))); + } + + [Test] + public void GetAllTilesEmpty() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(5, 4, new Tile(5, 7)); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var tiles = chunk.GetAllTiles(false).ToList(); + + Assert.That(tiles.Count, Is.EqualTo(8*8)); + + // Order is guaranteed to be row-major (because C# is), the Y value is contiguous in memory + Assert.That(tiles[8*3+5], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 3, 8 * 9 + 5), + new Tile(1, 3)))); + + Assert.That(tiles[8*5+4], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 5, 8 * 9 + 4), + new Tile(5, 7)))); + } + + [Test] + public void SetTileNotifiesGrid() + { + var mapGrid = new Mock(); + mapGrid.SetupGet(f => f.CurTick).Returns(new GameTick(11)); + mapGrid.SetupGet(f => f.ParentMapId).Returns(new MapId(11)); + mapGrid.SetupGet(f => f.Index).Returns(new GridId(13)); + mapGrid.Setup(f => f.NotifyTileChanged(It.Ref.IsAny, It.Ref.IsAny)).Verifiable(); + + var chunk = new MapChunk(mapGrid.Object, 7, 9, 8); + chunk.SetTile(3, 5, new Tile(1, 3)); + + mapGrid.Verify(f => f.NotifyTileChanged(It.Ref.IsAny, It.Ref.IsAny), Times.Once); + } + + [Test] + public void SetTileDuplicateNotifiesOnce() + { + var mapGrid = new Mock(); + mapGrid.SetupGet(f => f.CurTick).Returns(new GameTick(11)); + mapGrid.SetupGet(f => f.ParentMapId).Returns(new MapId(11)); + mapGrid.SetupGet(f => f.Index).Returns(new GridId(13)); + mapGrid.Setup(f => f.NotifyTileChanged(It.Ref.IsAny, It.Ref.IsAny)).Verifiable(); + + var chunk = new MapChunk(mapGrid.Object, 7, 9, 8); + chunk.SetTile(3, 5, new Tile(1, 3)); + chunk.SetTile(3, 5, new Tile(1, 3)); + + mapGrid.Verify(f => f.NotifyTileChanged(It.Ref.IsAny, It.Ref.IsAny), Times.Once); + } + + [Test] + public void EnumerateTiles() + { + var chunk = MapChunkFactory(7, 9); + chunk.SetTile(5, 4, new Tile(5, 7)); + chunk.SetTile(3, 5, new Tile(1, 3)); + + var tiles = new List(); + foreach (var tileRef in ((IEnumerable)chunk)) + { + tiles.Add((TileRef)tileRef); + } + + Assert.That(tiles.Count, Is.EqualTo(2)); + + // Order is guaranteed to be row-major (because C# is), the Y value is contiguous in memory + Assert.That(tiles[0], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 3, 8 * 9 + 5), + new Tile(1, 3)))); + + Assert.That(tiles[1], + Is.EqualTo(new TileRef(new MapId(11), + new GridId(13), + new MapIndices(8 * 7 + 5, 8 * 9 + 4), + new Tile(5, 7)))); + } + + [Test] + public void GridToChunkIndices() + { + // chunk indices are relative to the lowest coordinates of the chunk + // EX 8x8 chunk (0,0) occupies tiles 0 to 8 on each axis + // 8x8 chunk (-1,-1) occupies tiles -8 to -1 on each axis + var chunk = MapChunkFactory(-1, -1); + + var indices = chunk.GridTileToChunkTile(new MapIndices(-3, -5)); + + // drawing this out helps a ton + // grid tile -1,-1 is chunk tile 7,7 + // grid tile -8,-8 is chunk tile 0,0 + Assert.That(indices, Is.EqualTo(new MapIndices(5, 3))); + } + + [Test] + public void GetToString() + { + var chunk = MapChunkFactory(7, 9); + + var result = chunk.ToString(); + + Assert.That(result, Is.EqualTo("Chunk {7,9}")); + } + + [Test] + public void GetEmptySnapGrid() + { + var chunk = MapChunkFactory(7, 9); + + Assert.That(chunk.GetSnapGridCell(0,0, SnapGridOffset.Center).ToList().Count, Is.EqualTo(0)); + Assert.That(chunk.GetSnapGridCell(0, 0, SnapGridOffset.Edge).ToList().Count, Is.EqualTo(0)); + } + + [Test] + public void GetSnapGridThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.GetSnapGridCell(8,0, SnapGridOffset.Center).ToList())); + Assert.Throws((() => chunk.GetSnapGridCell(0, 8, SnapGridOffset.Center).ToList())); + Assert.Throws((() => chunk.GetSnapGridCell(8,0,SnapGridOffset.Edge).ToList())); + Assert.Throws((() => chunk.GetSnapGridCell(0, 8, SnapGridOffset.Edge).ToList())); + } + + [Test] + public void AddSnapGridCellCenter() + { + var chunk = MapChunkFactory(7, 9); + + var snapGridComponent = new SnapGridComponent(); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Center, snapGridComponent); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Edge, new SnapGridComponent()); + chunk.AddToSnapGridCell(3, 6, SnapGridOffset.Center, new SnapGridComponent()); + + var result = chunk.GetSnapGridCell(3, 5, SnapGridOffset.Center).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(snapGridComponent)); + } + + [Test] + public void AddSnapGridCellEdge() + { + var chunk = MapChunkFactory(7, 9); + + var snapGridComponent = new SnapGridComponent(); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Edge, snapGridComponent); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Center, new SnapGridComponent()); + chunk.AddToSnapGridCell(3, 6, SnapGridOffset.Edge, new SnapGridComponent()); + + var result = chunk.GetSnapGridCell(3, 5, SnapGridOffset.Edge).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(snapGridComponent)); + } + + [Test] + public void AddSnapGridThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.AddToSnapGridCell(8, 0, SnapGridOffset.Center, new SnapGridComponent()))); + Assert.Throws((() => chunk.AddToSnapGridCell(0, 8, SnapGridOffset.Center, new SnapGridComponent()))); + Assert.Throws((() => chunk.AddToSnapGridCell(8, 0, SnapGridOffset.Edge, new SnapGridComponent()))); + Assert.Throws((() => chunk.AddToSnapGridCell(0, 8, SnapGridOffset.Edge, new SnapGridComponent()))); + } + + [Test] + public void RemoveSnapGridCellCenter() + { + var chunk = MapChunkFactory(7, 9); + + var snapGridComponent = new SnapGridComponent(); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Center, snapGridComponent); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Edge, new SnapGridComponent()); + chunk.AddToSnapGridCell(3, 6, SnapGridOffset.Center, new SnapGridComponent()); + + chunk.RemoveFromSnapGridCell(3, 5, SnapGridOffset.Center, snapGridComponent); + + var result = chunk.GetSnapGridCell(3, 5, SnapGridOffset.Center).ToList(); + Assert.That(result.Count, Is.EqualTo(0)); + } + + [Test] + public void RemoveSnapGridCellEdge() + { + var chunk = MapChunkFactory(7, 9); + + var snapGridComponent = new SnapGridComponent(); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Edge, snapGridComponent); + chunk.AddToSnapGridCell(3, 5, SnapGridOffset.Center, new SnapGridComponent()); + chunk.AddToSnapGridCell(3, 6, SnapGridOffset.Edge, new SnapGridComponent()); + + chunk.RemoveFromSnapGridCell(3, 5, SnapGridOffset.Edge, snapGridComponent); + + var result = chunk.GetSnapGridCell(3, 5, SnapGridOffset.Edge).ToList(); + Assert.That(result.Count, Is.EqualTo(0)); + } + + [Test] + public void RemoveSnapGridThrowsOutOfRange() + { + var chunk = MapChunkFactory(7, 9); + + Assert.Throws((() => chunk.RemoveFromSnapGridCell(8, 0, SnapGridOffset.Center, new SnapGridComponent()))); + Assert.Throws((() => chunk.RemoveFromSnapGridCell(0, 8, SnapGridOffset.Center, new SnapGridComponent()))); + Assert.Throws((() => chunk.RemoveFromSnapGridCell(8, 0, SnapGridOffset.Edge, new SnapGridComponent()))); + Assert.Throws((() => chunk.RemoveFromSnapGridCell(0, 8, SnapGridOffset.Edge, new SnapGridComponent()))); + } + + [Test] + public void BoundsExpandWhenTileAdded() + { + var chunk = MapChunkFactory(7, 9); + + chunk.SetTile(3, 5, new Tile(1)); + chunk.SetTile(5, 7, new Tile(1)); + + var bounds = chunk.CalcLocalBounds(); + + Assert.That(bounds.Left, Is.EqualTo(3)); + Assert.That(bounds.Bottom, Is.EqualTo(5)); + Assert.That(bounds.Right, Is.EqualTo(6)); + Assert.That(bounds.Top, Is.EqualTo(8)); + } + + [Test] + public void BoundsContractWhenTileRemovedBL() + { + var chunk = MapChunkFactory(7, 9); + + chunk.SetTile(3, 5, new Tile(1)); + chunk.SetTile(5, 7, new Tile(1)); + + chunk.SetTile(3, 5, Tile.Empty); + + var bounds = chunk.CalcLocalBounds(); + + Assert.That(bounds.Left, Is.EqualTo(5)); + Assert.That(bounds.Bottom, Is.EqualTo(7)); + Assert.That(bounds.Right, Is.EqualTo(6)); + Assert.That(bounds.Top, Is.EqualTo(8)); + } + + [Test] + public void BoundsContractWhenTileRemovedTR() + { + var chunk = MapChunkFactory(7, 9); + + chunk.SetTile(3, 5, new Tile(1)); + chunk.SetTile(5, 7, new Tile(1)); + + chunk.SetTile(5, 7, Tile.Empty); + + var bounds = chunk.CalcLocalBounds(); + + Assert.That(bounds.Left, Is.EqualTo(3)); + Assert.That(bounds.Bottom, Is.EqualTo(5)); + Assert.That(bounds.Right, Is.EqualTo(4)); + Assert.That(bounds.Top, Is.EqualTo(6)); + } + + public IMapChunkInternal MapChunkFactory(int xChunkIndex, int yChunkIndex) + { + var mapGrid = new Mock(); + mapGrid.SetupGet(f=>f.CurTick).Returns(new GameTick(11)); + mapGrid.SetupGet(f => f.ParentMapId).Returns(new MapId(11)); + mapGrid.SetupGet(f => f.Index).Returns(new GridId(13)); + + return new MapChunk(mapGrid.Object, xChunkIndex, yChunkIndex, 8); + } + } +} diff --git a/Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs b/Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs new file mode 100644 index 0000000000..04cc3638a3 --- /dev/null +++ b/Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using MapGrid = Robust.Shared.Map.MapGrid; + +namespace Robust.UnitTesting.Shared.Map +{ + [TestFixture, Parallelizable, TestOf(typeof(MapGrid))] + class MapGrid_Tests + { + [Test] + public void GetTileRefCoords() + { + var grid = MapGridFactory(new GridId(1)); + grid.SetTile(new MapIndices(-9, -1), new Tile(1, 2)); + + var result = grid.GetTileRef(new MapIndices(-9, -1)); + + Assert.That(grid.ChunkCount, Is.EqualTo(1)); + Assert.That(grid.GetMapChunks().Keys.ToList()[0], Is.EqualTo(new MapIndices(-2, -1))); + Assert.That(result, Is.EqualTo(new TileRef(new MapId(3), new GridId(1), new MapIndices(-9,-1), new Tile(1, 2)))); + } + + /// + /// Verifies that the world Bounds of the grid properly expand when tiles are placed. + /// + [Test] + public void BoundsExpansion() + { + var grid = MapGridFactory(new GridId(1)); + + grid.SetTile(new MapIndices(-1, -2), new Tile(1)); + grid.SetTile(new MapIndices(1, 2), new Tile(1)); + + var bounds = grid.WorldBounds; + + // this is world, so add the grid world pos + Assert.That(bounds.Bottom, Is.EqualTo(-2+5)); + Assert.That(bounds.Left, Is.EqualTo(-1+3)); + Assert.That(bounds.Top, Is.EqualTo(3+5)); + Assert.That(bounds.Right, Is.EqualTo(2+3)); + } + + /// + /// Verifies that the world bounds of the grid properly contract when a tile is removed. + /// + [Test] + public void BoundsContract() + { + var grid = MapGridFactory(new GridId(1)); + + grid.SetTile(new MapIndices(-1, -2), new Tile(1)); + grid.SetTile(new MapIndices(1, 2), new Tile(1)); + + grid.SetTile(new MapIndices(1, 2), Tile.Empty); + + var bounds = grid.WorldBounds; + + // this is world, so add the grid world pos + Assert.That(bounds.Bottom, Is.EqualTo(-2+5)); + Assert.That(bounds.Left, Is.EqualTo(-1+3)); + Assert.That(bounds.Top, Is.EqualTo(-1+5)); + Assert.That(bounds.Right, Is.EqualTo(0+3)); + } + + [Test] + public void GridTileToChunkIndices() + { + var grid = MapGridFactory(new GridId(1)); + + var result = grid.GridTileToChunkIndices(new MapIndices(-9, -1)); + + Assert.That(result, Is.EqualTo(new MapIndices(-2, -1))); + } + + /// + /// Verifies that the local position is centered on the tile, instead of bottom left. + /// + [Test] + public void ToLocalCentered() + { + var grid = MapGridFactory(new GridId(1)); + + var result = grid.GridTileToLocal(new MapIndices(0, 0)).Position; + + Assert.That(result.X, Is.EqualTo(0.5f)); + Assert.That(result.Y, Is.EqualTo(0.5f)); + } + + [Test] + public void TryGetTileRefNoTile() + { + var grid = MapGridFactory(new GridId(1)); + + var foundTile = grid.TryGetTileRef(new MapIndices(-9, -1), out var tileRef); + + Assert.That(foundTile, Is.False); + Assert.That(tileRef, Is.EqualTo(new TileRef())); + Assert.That(grid.ChunkCount, Is.EqualTo(0)); + } + + [Test] + public void TryGetTileRefTileExists() + { + var grid = MapGridFactory(new GridId(1)); + grid.SetTile(new MapIndices(-9, -1), new Tile(1, 2)); + + var foundTile = grid.TryGetTileRef(new MapIndices(-9, -1), out var tileRef); + + Assert.That(foundTile, Is.True); + Assert.That(grid.ChunkCount, Is.EqualTo(1)); + Assert.That(grid.GetMapChunks().Keys.ToList()[0], Is.EqualTo(new MapIndices(-2, -1))); + Assert.That(tileRef, Is.EqualTo(new TileRef(new MapId(3), new GridId(1), new MapIndices(-9, -1), new Tile(1, 2)))); + } + + private static IMapGridInternal MapGridFactory(GridId id) + { + var timing = new Mock(); + var mapMan = new Mock(); + mapMan.SetupGet(p => p.GameTiming).Returns(timing.Object); + + var newGrid = new MapGrid(mapMan.Object, id, 8, 1, new MapId(3)) + { + WorldPosition = new Vector2(3, 5) + }; + + return newGrid; + } + } +} diff --git a/Robust.UnitTesting/Shared/Maths/Box2i_Test.cs b/Robust.UnitTesting/Shared/Maths/Box2i_Test.cs new file mode 100644 index 0000000000..250ff1c36b --- /dev/null +++ b/Robust.UnitTesting/Shared/Maths/Box2i_Test.cs @@ -0,0 +1,23 @@ +using NUnit.Framework; +using Robust.Shared.Maths; + +namespace Robust.UnitTesting.Shared.Maths +{ + [TestFixture, Parallelizable, TestOf(typeof(Box2i))] + class Box2i_Test + { + [Test] + public void Box2iUnion() + { + var boxOne = new Box2i(-1, -1, 1, 1); + var boxTwo = new Box2i(0, 0, 2, 2); + + var result = boxOne.Union(boxTwo); + + Assert.That(result.Left, Is.EqualTo(-1)); + Assert.That(result.Bottom, Is.EqualTo(-1)); + Assert.That(result.Right, Is.EqualTo(2)); + Assert.That(result.Top, Is.EqualTo(2)); + } + } +}