using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Collections; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Map.Enumerators; using Robust.Shared.Map.Events; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public abstract partial class SharedMapSystem { #region CreateGrid /// /// Creates a new grid entity on a given map. /// public Entity CreateGridEntity(MapId mapId, GridCreateOptions? options = null) { return CreateGridEntity(GetMap(mapId), options); } /// /// Creates a new grid entity on a given map. /// public Entity CreateGridEntity(EntityUid mapEnt, GridCreateOptions? options = null) { options ??= GridCreateOptions.Default; return CreateGridInternal(mapEnt, options.Value); } protected Entity CreateGridInternal(EntityUid mapEnt, GridCreateOptions options) { var gridEnt = EntityManager.CreateEntityUninitialized(null); var grid = EnsureComp(gridEnt); grid.ChunkSize = options.ChunkSize; Log.Debug("Binding new grid {gridEnt}"); //TODO: This is a hack to get TransformComponent.MapId working before entity states //are applied. After they are applied the parent may be different, but the MapId will //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to //use in transform states anytime before the state parent is properly set. _transform.SetParent(gridEnt, mapEnt); var meta = _metaQuery.GetComponent(gridEnt); EntityManager.System().SetEntityName(gridEnt, $"grid", meta); EntityManager.InitializeComponents(gridEnt, meta); EntityManager.StartComponents(gridEnt); // Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized. // I don't know whether that is intentional or not. return (gridEnt, grid); } #endregion #region Chunk helpers [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkIndices(Vector2 tile, int chunkSize) { return new Vector2i ((int) Math.Floor(tile.X / chunkSize), (int) Math.Floor(tile.Y / chunkSize)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkIndices(Vector2 tile, byte chunkSize) { return new Vector2i ((int) Math.Floor(tile.X / chunkSize), (int) Math.Floor(tile.Y / chunkSize)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkIndices(Vector2i tile, int chunkSize) { return new Vector2i ((int) Math.Floor(tile.X / (float) chunkSize), (int) Math.Floor(tile.Y / (float) chunkSize)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkIndices(Vector2i tile, byte chunkSize) { return new Vector2i ((int) Math.Floor(tile.X / (float) chunkSize), (int) Math.Floor(tile.Y / (float) chunkSize)); } /// /// Returns the tile offset to a chunk origin based on the provided size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkRelative(Vector2 tile, int chunkSize) { var x = MathHelper.Mod((int) Math.Floor(tile.X), chunkSize); var y = MathHelper.Mod((int) Math.Floor(tile.Y), chunkSize); return new Vector2i(x, y); } /// /// Returns the tile offset to a chunk origin based on the provided size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkRelative(Vector2 tile, byte chunkSize) { var x = MathHelper.Mod((int) Math.Floor(tile.X), chunkSize); var y = MathHelper.Mod((int) Math.Floor(tile.Y), chunkSize); return new Vector2i(x, y); } /// /// Returns the tile offset to a chunk origin based on the provided size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkRelative(Vector2i tile, int chunkSize) { var x = MathHelper.Mod(tile.X, chunkSize); var y = MathHelper.Mod(tile.Y, chunkSize); return new Vector2i(x, y); } /// /// Returns the tile offset to a chunk origin based on the provided size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i GetChunkRelative(Vector2i tile, byte chunkSize) { var x = MathHelper.Mod(tile.X, chunkSize); var y = MathHelper.Mod(tile.Y, chunkSize); return new Vector2i(x, y); } #endregion public static Vector2i GetDirection(Vector2i position, Direction dir, int dist = 1) { switch (dir) { case Direction.East: return position + new Vector2i(dist, 0); case Direction.SouthEast: return position + new Vector2i(dist, -dist); case Direction.South: return position + new Vector2i(0, -dist); case Direction.SouthWest: return position + new Vector2i(-dist, -dist); case Direction.West: return position + new Vector2i(-dist, 0); case Direction.NorthWest: return position + new Vector2i(-dist, dist); case Direction.North: return position + new Vector2i(0, dist); case Direction.NorthEast: return position + new Vector2i(dist, dist); default: throw new NotImplementedException(); } } private void InitializeGrid() { SubscribeLocalEvent(OnGridGetState); SubscribeLocalEvent(OnGridHandleState); SubscribeLocalEvent(OnGridAdd); SubscribeLocalEvent(OnGridInit); SubscribeLocalEvent(OnGridStartup); SubscribeLocalEvent(OnGridRemove); SubscribeLocalEvent(OnGridMove); } /// /// /// public Vector2 GetGridPosition(Entity grid, Vector2 worldPos, Angle worldRot) { if (!Resolve(grid.Owner, ref grid.Comp)) return Vector2.Zero; return worldPos + worldRot.RotateVec(grid.Comp.LocalCenter); } /// /// Gets the mapgrid's position considering its local physics center. /// public Vector2 GetGridPosition(Entity grid) { if (!Resolve(grid.Owner, ref grid.Comp1, ref grid.Comp2)) return Vector2.Zero; var (worldPos, worldRot) = _transform.GetWorldPositionRotation(grid.Comp2); return GetGridPosition((grid.Owner, grid.Comp1), worldPos, worldRot); } private void OnGridBoundsChange(EntityUid uid, MapGridComponent component) { // Just MapLoader things. if (component.MapProxy == DynamicTree.Proxy.Free) return; var xform = Comp(uid); var aabb = GetWorldAABB(uid, component, xform); if (TryComp(xform.MapUid, out var gridTree)) { gridTree.Tree.MoveProxy(component.MapProxy, in aabb); } _physics.MovedGrids.Add(uid); } private void OnGridMove(EntityUid uid, MapGridComponent component, ref MoveEvent args) { if (args.ParentChanged) { OnParentChange(uid, component, ref args); return; } // Just maploader / test things if (component.MapProxy == DynamicTree.Proxy.Free) return; var xform = args.Component; var aabb = GetWorldAABB(uid, component, xform); if (TryComp(xform.MapUid, out var gridTree)) { gridTree.Tree.MoveProxy(component.MapProxy, in aabb); } _physics.MovedGrids.Add(uid); } private void OnParentChange(EntityUid uid, MapGridComponent component, ref MoveEvent args) { UpdatePvsChunks(args.Entity); var (_, xform, meta) = args.Entity; // oh boy // Want gridinit to handle this hence specialcase those situations. // oh boy oh boy, its even worse now. // transform now raises parent change events on startup, because container code is a POS. if (meta.EntityLifeStage < EntityLifeStage.Initialized || args.Component.LifeStage == ComponentLifeStage.Starting) return; Log.Info($"Grid {ToPrettyString(uid, meta)} changed parent. Old parent: {ToPrettyString(args.OldPosition.EntityId)}. New parent: {ToPrettyString(xform.ParentUid)}"); DebugTools.Assert(!_mapQuery.HasComponent(uid)); if (xform.MapUid != null && xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer) { Log.Error($"Grid {ToPrettyString(uid, meta)} is parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}"); return; } if (component.MapProxy != DynamicTree.Proxy.Free) { // Make sure we cleanup old map for moved grid stuff. When returning from nullspace there is no old map // proxy to remove. if (args.OldPosition.EntityId.IsValid()) { var oldMap = _transform.ToMapCoordinates(args.OldPosition); var oldMapUid = GetMapOrInvalid(oldMap.MapId); _physics.MovedGrids.Remove(uid); RemoveGrid(uid, component, oldMapUid); } else { component.MapProxy = DynamicTree.Proxy.Free; } } DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free); if (xform.MapUid != null) { _physics.MovedGrids.Add(uid); AddGrid(uid, component); } } protected virtual void UpdatePvsChunks(Entity grid) { } private void OnGridHandleState(EntityUid uid, MapGridComponent component, ref ComponentHandleState args) { switch (args.Current) { case MapGridComponentDeltaState delta: { DebugTools.Assert(component.ChunkSize == delta.ChunkSize || component.Chunks.Count == 0, "Can't modify chunk size of an existing grid."); component.ChunkSize = delta.ChunkSize; if (delta.ChunkData == null) return; foreach (var (index, chunkData) in delta.ChunkData) { ApplyChunkData(uid, component, index, chunkData); } component.LastTileModifiedTick = delta.LastTileModifiedTick; break; } case MapGridComponentState state: { DebugTools.Assert(component.ChunkSize == state.ChunkSize || component.Chunks.Count == 0, "Can't modify chunk size of an existing grid."); component.LastTileModifiedTick = state.LastTileModifiedTick; component.ChunkSize = state.ChunkSize; foreach (var index in component.Chunks.Keys) { if (!state.FullGridData.ContainsKey(index)) ApplyChunkData(uid, component, index, ChunkDatum.Empty); } foreach (var (index, data) in state.FullGridData) { DebugTools.Assert(!data.IsDeleted()); ApplyChunkData(uid, component, index, data); } break; } default: return; } RegenerateAabb(component); OnGridBoundsChange(uid, component); #if DEBUG foreach (var chunk in component.Chunks.Values) { chunk.ValidateChunk(); DebugTools.Assert(chunk.FilledTiles > 0); } #endif } private void ApplyChunkData( EntityUid uid, MapGridComponent component, Vector2i index, ChunkDatum data) { var counter = 0; var gridEnt = new Entity(uid, component); if (data.IsDeleted()) { if (!component.Chunks.Remove(index, out var deletedChunk)) return; // Deleted chunks still need to raise tile-changed events. deletedChunk.SuppressCollisionRegeneration = true; for (ushort x = 0; x < component.ChunkSize; x++) { for (ushort y = 0; y < component.ChunkSize; y++) { if (!deletedChunk.TrySetTile(x, y, Tile.Empty, out var oldTile, out _)) continue; var gridIndices = deletedChunk.ChunkTileToGridTile((x, y)); var newTileRef = new TileRef(uid, gridIndices, Tile.Empty); RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index); } } deletedChunk.CachedBounds = Box2i.Empty; deletedChunk.SuppressCollisionRegeneration = false; return; } var chunk = GetOrAddChunk(uid, component, index); chunk.Fixtures.Clear(); chunk.Fixtures.UnionWith(data.Fixtures); chunk.SuppressCollisionRegeneration = true; DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty)); DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize); var changedEntry = new ValueList(); for (ushort x = 0; x < component.ChunkSize; x++) { for (ushort y = 0; y < component.ChunkSize; y++) { var tile = data.TileData[counter++]; if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _)) continue; var chunkIndex = new Vector2i(x, y); var gridIndices = chunk.ChunkTileToGridTile(chunkIndex); changedEntry.Add(new TileChangedEntry(tile, oldTile, chunk.Indices, gridIndices)); } } var ev = new TileChangedEvent(gridEnt, changedEntry.ToArray()); EntityManager.EventBus.RaiseLocalEvent(gridEnt.Owner, ref ev, true); DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures)); // These should never refer to the same object DebugTools.AssertNotEqual(chunk.Fixtures, data.Fixtures); chunk.CachedBounds = data.CachedBounds!.Value; chunk.SuppressCollisionRegeneration = false; } private void OnGridGetState(EntityUid uid, MapGridComponent component, ref ComponentGetState args) { if (args.FromTick <= component.CreationTick) { GetFullState(uid, component, ref args); return; } Dictionary? chunkData; var fromTick = args.FromTick; if (component.LastTileModifiedTick < fromTick) { chunkData = null; } else { chunkData = new Dictionary(); foreach (var (tick, indices) in component.ChunkDeletionHistory) { if (tick < fromTick && fromTick != GameTick.Zero) continue; // Chunk may have been re-added sometime after it was deleted, but before deletion history was culled. if (!component.Chunks.TryGetValue(indices, out var chunk)) { chunkData.Add(indices, ChunkDatum.Empty); continue; } if (chunk.LastTileModifiedTick < fromTick) Log.Error($"Encountered un-deleted chunk with an old last-modified tick on grid {ToPrettyString(uid)}"); } foreach (var (index, chunk) in GetMapChunks(uid, component)) { if (chunk.LastTileModifiedTick < fromTick) continue; var tileBuffer = new Tile[component.ChunkSize * (uint) component.ChunkSize]; // Flatten the tile array. // NetSerializer doesn't do multi-dimensional arrays. // This is probably really expensive. for (var x = 0; x < component.ChunkSize; x++) { for (var y = 0; y < component.ChunkSize; y++) { tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y); } } // The client needs to clone the fixture set instead of storing a reference. // TODO Game State // Force the client to serialize & de-serialize implicitly generated component states. var fixtures = chunk.Fixtures; if (_netManager.IsClient) fixtures = new(fixtures); chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds)); } } args.State = new MapGridComponentDeltaState(component.ChunkSize, chunkData, component.LastTileModifiedTick); #if DEBUG if (chunkData == null) return; HashSet keys = new(); foreach (var (index, chunk) in chunkData) { if (chunk.IsDeleted()) continue; DebugTools.Assert(keys.Add(index), "Duplicate chunk"); DebugTools.Assert(chunk.TileData.Any(x => !x.IsEmpty), "Empty non-deleted chunk"); } #endif } private void GetFullState(EntityUid uid, MapGridComponent component, ref ComponentGetState args) { var chunkData = new Dictionary(); foreach (var (index, chunk) in GetMapChunks(uid, component)) { var tileBuffer = new Tile[component.ChunkSize * (uint)component.ChunkSize]; for (var x = 0; x < component.ChunkSize; x++) { for (var y = 0; y < component.ChunkSize; y++) { tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y); } } // The client needs to clone the fixture set instead of storing a reference. // TODO Game State // Force the client to serialize & de-serialize implicitly generated component states. var fixtures = chunk.Fixtures; if (_netManager.IsClient) fixtures = new(fixtures); chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds)); } args.State = new MapGridComponentState(component.ChunkSize, chunkData, component.LastTileModifiedTick); #if DEBUG foreach (var chunk in chunkData.Values) { DebugTools.Assert(chunk.TileData!.Any(x => !x.IsEmpty)); } #endif } /// /// Prunes tracked grid chunk deletions older than some given game tick. /// public void CullDeletionHistory(GameTick upToTick) { var query = AllEntityQuery(); while (query.MoveNext(out var grid)) { var chunks = grid.ChunkDeletionHistory; chunks.RemoveAll(t => t.tick < upToTick); } } private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args) { var msg = new GridAddEvent(uid); RaiseLocalEvent(uid, msg, true); } private void OnGridInit(EntityUid uid, MapGridComponent component, ComponentInit args) { var xform = _xformQuery.GetComponent(uid); // Force networkedmapmanager to send it due to non-ECS legacy code. var curTick = _timing.CurTick; foreach (var chunk in component.Chunks.Values) { chunk.LastTileModifiedTick = curTick; } component.LastTileModifiedTick = curTick; if (xform.MapUid != null && xform.MapUid != uid) _transform.SetParent(uid, xform, xform.MapUid.Value); if (!_mapQuery.HasComponent(uid)) { var aabb = GetWorldAABB(uid, component); if (TryComp(xform.MapUid, out var gridTree)) { var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), component)); DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free); component.MapProxy = proxy; } if (xform.MapUid != null) { _physics.MovedGrids.Add(uid); } } var msg = new GridInitializeEvent(uid, component); RaiseLocalEvent(uid, msg, true); } private void OnGridStartup(EntityUid uid, MapGridComponent component, ComponentStartup args) { var msg = new GridStartupEvent(uid); RaiseLocalEvent(uid, msg, true); } private void OnGridRemove(EntityUid uid, MapGridComponent component, ComponentShutdown args) { Log.Info($"Removing grid {ToPrettyString(uid)}"); if (TryComp(uid, out TransformComponent? xform) && xform.MapUid != null) { RemoveGrid(uid, component, xform.MapUid.Value); } component.MapProxy = DynamicTree.Proxy.Free; RaiseLocalEvent(uid, new GridRemovalEvent(uid), true); } private Box2 GetWorldAABB(EntityUid uid, MapGridComponent grid, TransformComponent? xform = null) { if (!Resolve(uid, ref xform)) return new Box2(); var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); var aabb = grid.LocalAABB.Translated(worldPos); return new Box2Rotated(aabb, worldRot, worldPos).CalcBoundingBox(); } private void AddGrid(EntityUid uid, MapGridComponent grid) { DebugTools.Assert(!_mapQuery.HasComponent(uid)); var aabb = GetWorldAABB(uid, grid); if (!_xformQuery.TryGetComponent(uid, out var xform)) return; if (TryComp(xform.MapUid, out var gridTree)) { var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), grid)); DebugTools.Assert(grid.MapProxy == DynamicTree.Proxy.Free); grid.MapProxy = proxy; } if (xform.MapUid != null) { _physics.MovedGrids.Add(uid); } } private void RemoveGrid(EntityUid uid, MapGridComponent grid, EntityUid mapUid) { if (grid.MapProxy != DynamicTree.Proxy.Free && TryComp(mapUid, out var gridTree)) { gridTree.Tree.DestroyProxy(grid.MapProxy); } grid.MapProxy = DynamicTree.Proxy.Free; if (mapUid.IsValid()) { _physics.MovedGrids.Remove(uid); } } private void RemoveChunk(EntityUid uid, MapGridComponent grid, Vector2i origin) { if (!grid.Chunks.TryGetValue(origin, out var chunk)) return; if (_netManager.IsServer) grid.ChunkDeletionHistory.Add((_timing.CurTick, chunk.Indices)); chunk.Fixtures.Clear(); grid.Chunks.Remove(origin); if (grid.Chunks.Count == 0) RaiseLocalEvent(uid, new EmptyGridEvent { GridId = uid }, true); } /// /// Regenerates the chunk local bounds of this chunk. /// private void RegenerateCollision(EntityUid uid, MapGridComponent grid, MapChunk mapChunk) { RegenerateCollision(uid, grid, new HashSet { mapChunk }); } /// /// Regenerate collision for multiple chunks at once; faster than doing it individually. /// internal void RegenerateCollision(EntityUid uid, MapGridComponent grid, IReadOnlySet chunks) { if (HasComp(uid)) { ClearEmptyMapChunks(uid, grid, chunks); return; } var chunkRectangles = new Dictionary>(chunks.Count); var removedChunks = new List(); foreach (var mapChunk in chunks) { // Even if the chunk is still removed still need to make sure bounds are updated (for now...) // generate collision rectangles for this chunk based on filled tiles. GridChunkPartition.PartitionChunk(mapChunk, out var localBounds, out var rectangles); mapChunk.CachedBounds = localBounds; if (mapChunk.FilledTiles > 0) chunkRectangles.Add(mapChunk, rectangles); else { // Gone. Reduced to atoms // Need to do this before RemoveChunk because it clears fixtures. FixturesComponent? manager = null; PhysicsComponent? body = null; TransformComponent? xform = null; foreach (var id in mapChunk.Fixtures) { mapChunk.Fixtures.Remove(id); _fixtures.DestroyFixture(uid, id, false, manager: manager, body: body, xform: xform); } RemoveChunk(uid, grid, mapChunk.Indices); DebugTools.AssertEqual(mapChunk.Fixtures.Count, 0); removedChunks.Add(mapChunk); } } RegenerateAabb(grid); // May have been deleted from the bulk update above! if (Deleted(uid)) return; _physics.WakeBody(uid); OnGridBoundsChange(uid, grid); var ev = new RegenerateGridBoundsEvent(uid, chunkRectangles, removedChunks, grid); RaiseLocalEvent(ref ev); } private void RegenerateAabb(MapGridComponent grid) { grid.LocalAABB = new Box2(); foreach (var chunk in grid.Chunks.Values) { var chunkBounds = chunk.CachedBounds; if (chunkBounds.Size.Equals(Vector2i.Zero)) continue; if (grid.LocalAABB.Size == Vector2.Zero) { var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize); grid.LocalAABB = gridBounds; } else { var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize); grid.LocalAABB = grid.LocalAABB.Union(gridBounds); } } } /// /// Variation of /// that only simply removes empty chunks. Intended for use with "planet-maps", which have no grid fixtures. /// private void ClearEmptyMapChunks(EntityUid uid, MapGridComponent grid, IReadOnlySet modified) { foreach (var chunk in modified) { DebugTools.Assert(chunk.FilledTiles >= 0); if (chunk.FilledTiles > 0) continue; DebugTools.AssertEqual(chunk.Fixtures.Count, 0, "maps should not have grid-chunk fixtures"); RemoveChunk(uid, grid, chunk.Indices); } } #region TileAccess public TileRef GetTileRef(Entity grid, MapCoordinates coords) { return GetTileRef(grid.Owner, grid.Comp, coords); } public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, MapCoordinates coords) { return GetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords)); } public TileRef GetTileRef(Entity grid, EntityCoordinates coords) { return GetTileRef(grid.Owner, grid.Comp, coords); } public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { return GetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords)); } public TileRef GetTileRef(Entity grid, Vector2i tileCoordinates) { return GetTileRef(grid.Owner, grid.Comp, tileCoordinates); } public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, Vector2i tileCoordinates) { var chunkIndices = GridTileToChunkIndices(uid, grid, tileCoordinates); if (!grid.Chunks.TryGetValue(chunkIndices, out var output)) { // Chunk doesn't exist, return a tileRef to an empty (space) tile. return new TileRef(uid, tileCoordinates.X, tileCoordinates.Y, default); } var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates); return GetTileRef(uid, grid, output, (ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y); } /// /// Returns the tile at the given chunk indices. /// /// /// The X tile index relative to the chunk origin. /// The Y tile index relative to the chunk origin. /// A reference to a tile. internal TileRef GetTileRef(EntityUid uid, MapGridComponent grid, MapChunk mapChunk, ushort xIndex, ushort yIndex) { if (xIndex >= mapChunk.ChunkSize) throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds."); if (yIndex >= mapChunk.ChunkSize) throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds."); var indices = mapChunk.ChunkTileToGridTile(new Vector2i(xIndex, yIndex)); return new TileRef(uid, indices, mapChunk.GetTile(xIndex, yIndex)); } public GridTileEnumerator GetAllTiles(EntityUid uid, MapGridComponent grid, bool ignoreEmpty = true) { return new GridTileEnumerator(uid, grid.Chunks.GetEnumerator(), grid.ChunkSize, ignoreEmpty); } /// /// Returns the total number of tiles on a grid. /// by summing the counts of filled tiles in each chunk. /// /// The target map grid entity /// The total number of filled tiles in . [Pure] public int GetFilledTileCount(Entity ent) { return ent.Comp.Chunks.Values.Sum(chunk => chunk.FilledTiles); } [Obsolete("Use GetAllTiles instead.")] public GridTileEnumerator GetAllTilesEnumerator(EntityUid uid, MapGridComponent grid, bool ignoreEmpty = true) { return GetAllTiles(uid, grid, ignoreEmpty); } public void SetTile(Entity grid, EntityCoordinates coordinates, Tile tile) { SetTile(grid.Owner, grid.Comp, coordinates, tile); } public void SetTile(Entity grid, Vector2i gridIndices, Tile tile) { SetTile(grid.Owner, grid.Comp, gridIndices, tile); } public void SetTiles(Entity grid, List<(Vector2i GridIndices, Tile Tile)> tiles) { SetTiles(grid.Owner, grid.Comp, tiles); } public void SetTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Tile tile) { var localTile = CoordinatesToTile(uid, grid, coords); SetTile(uid, grid, new Vector2i(localTile.X, localTile.Y), tile); } public void SetTile(EntityUid uid, MapGridComponent grid, Vector2i gridIndices, Tile tile) { var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices); if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk)) { if (tile.IsEmpty) return; grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize) { LastTileModifiedTick = _timing.CurTick }; } var offset = chunk.GridTileToChunkTile(gridIndices); SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out _); } public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIndices, Tile Tile)> tiles) { if (tiles.Count == 0) return; var modified = new HashSet(Math.Max(1, tiles.Count / grid.ChunkSize)); var tileChanges = new ValueList(tiles.Count); // Suppress sending out events for each tile changed // We're going to send them all out together at the end SuppressOnTileChanged = true; foreach (var (gridIndices, tile) in tiles) { var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices); if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk)) { if (tile.IsEmpty) continue; grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize) { LastTileModifiedTick = _timing.CurTick }; } var offset = chunk.GridTileToChunkTile(gridIndices); chunk.SuppressCollisionRegeneration = true; var changed = SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out var oldTile, out var shapeChanged); chunk.SuppressCollisionRegeneration = false; if (changed) { if (shapeChanged) modified.Add(chunk); tileChanges.Add(new TileChangedEntry(tile, oldTile, offset, gridIndices)); } } // Notify of all tile changes in one event var ev = new TileChangedEvent((uid, grid), tileChanges.ToArray()); RaiseLocalEvent(uid, ref ev, true); if (modified.Count > 0) RegenerateCollision(uid, grid, modified); // Back to normal SuppressOnTileChanged = false; } [Obsolete("Use GetLocalTilesIntersecting instead.")] public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, bool ignoreEmpty = true, Predicate? predicate = null) { return GetLocalTilesIntersecting(uid, grid, aabb, ignoreEmpty, predicate); } [Obsolete("Use GetTilesIntersecting instead.")] public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, bool ignoreEmpty = true, Predicate? predicate = null) { return GetTilesIntersecting(uid, grid, aabb, ignoreEmpty, predicate); } [Obsolete("Use GetTilesIntersecting instead.")] public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2Rotated bounds, bool ignoreEmpty = true, Predicate? predicate = null) { return GetTilesIntersecting(uid, grid, bounds, ignoreEmpty, predicate); } public TilesEnumerator GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 localAABB, bool ignoreEmpty = true, Predicate? predicate = null) { return new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); } public TilesEnumerator GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated localArea, bool ignoreEmpty = true, Predicate? predicate = null) { var localAABB = localArea.CalcBoundingBox(); return new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB); } public TilesEnumerator GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea, bool ignoreEmpty = true, Predicate? predicate = null) { var matrix = _transform.GetInvWorldMatrix(uid); var localArea = matrix.TransformBox(worldArea); return new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea); } public TilesEnumerator GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 worldArea, bool ignoreEmpty = true, Predicate? predicate = null) { var matrix = _transform.GetInvWorldMatrix(uid); var localArea = matrix.TransformBox(worldArea); return new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea); } public CircleTilesEnumerator GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle localCircle, bool ignoreEmpty = true, Predicate? predicate = null) { var aabb = new Box2(localCircle.Position.X - localCircle.Radius, localCircle.Position.Y - localCircle.Radius, localCircle.Position.X + localCircle.Radius, localCircle.Position.Y + localCircle.Radius); var tileEnumerator = GetLocalTilesIntersecting(uid, grid, aabb, ignoreEmpty, predicate); return new CircleTilesEnumerator(tileEnumerator, grid, localCircle.Position, localCircle.Radius); } public CircleTilesEnumerator GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle worldArea, bool ignoreEmpty = true, Predicate? predicate = null) { var localPosition = WorldToLocal(uid, grid, worldArea.Position); var aabb = new Box2(localPosition.X - worldArea.Radius, localPosition.Y - worldArea.Radius, localPosition.X + worldArea.Radius, localPosition.Y + worldArea.Radius); var tileEnumerator = GetLocalTilesIntersecting(uid, grid, aabb, ignoreEmpty, predicate); return new CircleTilesEnumerator(tileEnumerator, grid, localPosition, worldArea.Radius); } private bool TryGetTile(EntityUid uid, MapGridComponent grid, Vector2i indices, bool ignoreEmpty, [NotNullWhen(true)] out TileRef? tileRef, Predicate? predicate = null) { // Similar to TryGetTileRef but for the tiles intersecting iterators. var gridChunk = GridTileToChunkIndices(uid, grid, indices); if (grid.Chunks.TryGetValue(gridChunk, out var chunk)) { var chunkTile = chunk.GridTileToChunkTile(indices); var tile = GetTileRef(uid, grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); if (ignoreEmpty && tile.Tile.IsEmpty) { tileRef = null; return false; } if (predicate == null || predicate(tile)) { tileRef = tile; return true; } } else if (!ignoreEmpty) { var tile = new TileRef(uid, indices.X, indices.Y, Tile.Empty); if (predicate == null || predicate(tile)) { tileRef = tile; return true; } } tileRef = null; return false; } #endregion TileAccess #region ChunkAccess internal MapChunk GetOrAddChunk(EntityUid uid, MapGridComponent grid, int xIndex, int yIndex) { return GetOrAddChunk(uid, grid, new Vector2i(xIndex, yIndex)); } internal bool TryGetChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices, [NotNullWhen(true)] out MapChunk? chunk) { return grid.Chunks.TryGetValue(chunkIndices, out chunk); } internal MapChunk GetOrAddChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices) { if (grid.Chunks.TryGetValue(chunkIndices, out var output)) return output; var newChunk = new MapChunk(chunkIndices.X, chunkIndices.Y, grid.ChunkSize) { LastTileModifiedTick = _timing.CurTick }; return grid.Chunks[chunkIndices] = newChunk; } public bool HasChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices) { return grid.Chunks.ContainsKey(chunkIndices); } internal IReadOnlyDictionary GetMapChunks(EntityUid uid, MapGridComponent grid) { return grid.Chunks; } internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2 worldAABB) { var localAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldAABB); return GetLocalMapChunks(uid, grid, localAABB); } internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea) { var matrix = _transform.GetInvWorldMatrix(uid); var localArea = matrix.TransformBox(worldArea); return GetLocalMapChunks(uid, grid, localArea); } internal ChunkEnumerator GetLocalMapChunks(EntityUid uid, MapGridComponent grid, Box2 localAABB) { Box2 compAABB; // The entire area intersects. if (_mapQuery.HasComponent(uid)) { compAABB = localAABB; } else { compAABB = grid.LocalAABB.Intersect(localAABB); } return new ChunkEnumerator(grid.Chunks, compAABB, grid.ChunkSize); } #endregion ChunkAccess #region SnapGridAccess public int AnchoredEntityCount(EntityUid uid, MapGridComponent grid, Vector2i pos) { var gridChunkPos = GridTileToChunkIndices(uid, grid, pos); if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk)) return 0; var (x, y) = chunk.GridTileToChunkTile(pos); return chunk.GetSnapGrid((ushort)x, (ushort)y)?.Count ?? 0; // ? } public AnchoredEntitiesEnumerator GetAnchoredEntities(Entity grid, MapCoordinates coords) { return GetAnchoredEntities(grid.Owner, grid.Comp, coords); } public AnchoredEntitiesEnumerator GetAnchoredEntities(EntityUid uid, MapGridComponent grid, MapCoordinates coords) { return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords)); } public AnchoredEntitiesEnumerator GetAnchoredEntities(Entity grid, EntityCoordinates coords) { return GetAnchoredEntities(grid.Owner, grid.Comp, coords); } public AnchoredEntitiesEnumerator GetAnchoredEntities(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords)); } public AnchoredEntitiesEnumerator GetAnchoredEntities(Entity grid, Vector2i pos) { return GetAnchoredEntities(grid.Owner, grid.Comp, pos); } public AnchoredEntitiesEnumerator GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Vector2i pos) { // Because some content stuff checks neighboring tiles (which may not actually exist) we won't just // create an entire chunk for it. var gridChunkPos = GridTileToChunkIndices(uid, grid, pos); if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk)) return AnchoredEntitiesEnumerator.Empty; var chunkTile = chunk.GridTileToChunkTile(pos); var snapgrid = chunk.GetSnapGrid((ushort)chunkTile.X, (ushort)chunkTile.Y); return snapgrid == null ? AnchoredEntitiesEnumerator.Empty : new AnchoredEntitiesEnumerator(snapgrid.GetEnumerator()); } public void GetAnchoredEntities(Entity grid, Vector2i pos, List list) { var gridChunkPos = GridTileToChunkIndices(grid.Owner, grid.Comp, pos); if (!grid.Comp.Chunks.TryGetValue(gridChunkPos, out var chunk)) return; var chunkTile = chunk.GridTileToChunkTile(pos); var anchored = chunk.GetSnapGrid((ushort) chunkTile.X, (ushort) chunkTile.Y); if (anchored != null) list.AddRange(anchored); } [Obsolete("Use GetAnchoredEntities instead.")] public AnchoredEntitiesEnumerator GetAnchoredEntitiesEnumerator(EntityUid uid, MapGridComponent grid, Vector2i pos) { return GetAnchoredEntities(uid, grid, pos); } public AnchoredEntitiesInTilesEnumerator GetLocalAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 localAABB) { var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB); return new AnchoredEntitiesInTilesEnumerator(this, uid, grid, enumerator); } public AnchoredEntitiesInTilesEnumerator GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 worldAABB) { var invWorldMatrix = _transform.GetInvWorldMatrix(uid); var localAABB = invWorldMatrix.TransformBox(worldAABB); var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB); return new AnchoredEntitiesInTilesEnumerator(this, uid, grid, enumerator); } public AnchoredEntitiesInTilesEnumerator GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2Rotated worldBounds) { var tiles = GetTilesIntersecting(uid, grid, worldBounds); return new AnchoredEntitiesInTilesEnumerator(this, uid, grid, tiles); } public Vector2i TileIndicesFor(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { #if DEBUG var mapId = _xformQuery.GetComponent(uid).MapID; DebugTools.Assert(mapId == _transform.GetMapId(coords)); #endif return SnapGridLocalCellFor(uid, grid, LocalToGrid(uid, grid, coords)); } public Vector2i TileIndicesFor(Entity grid, EntityCoordinates coords) { return TileIndicesFor(grid.Owner, grid.Comp, coords); } public Vector2i TileIndicesFor(EntityUid uid, MapGridComponent grid, MapCoordinates worldPos) { #if DEBUG var mapId = _xformQuery.GetComponent(uid).MapID; DebugTools.Assert(mapId == worldPos.MapId); #endif var localPos = WorldToLocal(uid, grid, worldPos.Position); return SnapGridLocalCellFor(uid, grid, localPos); } public Vector2i TileIndicesFor(Entity grid, MapCoordinates coords) { return TileIndicesFor(grid.Owner, grid.Comp, coords); } private Vector2i SnapGridLocalCellFor(EntityUid uid, MapGridComponent grid, Vector2 localPos) { var x = (int)Math.Floor(localPos.X / grid.TileSize); var y = (int)Math.Floor(localPos.Y / grid.TileSize); return new Vector2i(x, y); } public bool IsAnchored(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid) { var tilePos = TileIndicesFor(uid, grid, coords); if (!TryChunkAndOffsetForTile(uid, grid, tilePos, out var chunk, out var chunkTile)) return false; var snapgrid = chunk.GetSnapGrid((ushort)chunkTile.X, (ushort)chunkTile.Y); return snapgrid?.Contains(euid) == true; } public bool AddToSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid) { if (!TryChunkAndOffsetForTile(gridUid, grid, pos, out var chunk, out var chunkTile)) return false; if (chunk.GetTile((ushort)chunkTile.X, (ushort)chunkTile.Y).IsEmpty) return false; chunk.AddToSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, euid); return true; } public bool AddToSnapGridCell(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid) { return AddToSnapGridCell(gridUid, grid, TileIndicesFor(gridUid, grid, coords), euid); } public void RemoveFromSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid) { var gridChunkIndices = GridTileToChunkIndices(gridUid, grid, pos); if (!grid.Chunks.TryGetValue(gridChunkIndices, out var chunk)) return; var chunkTile = chunk.GridTileToChunkTile(pos); chunk.RemoveFromSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, euid); } public void RemoveFromSnapGridCell(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid) { RemoveFromSnapGridCell(gridUid, grid, TileIndicesFor(gridUid, grid, coords), euid); } private bool TryChunkAndOffsetForTile(EntityUid uid, MapGridComponent grid, Vector2i pos, [NotNullWhen(true)]out MapChunk? chunk, out Vector2i offset) { var gridChunkIndices = GridTileToChunkIndices(uid, grid, pos); if (!grid.Chunks.TryGetValue(gridChunkIndices, out chunk)) { offset = default; return false; } offset = chunk.GridTileToChunkTile(pos); return true; } public AnchoredEntitiesEnumerator GetInDir(EntityUid uid, MapGridComponent grid, EntityCoordinates position, Direction dir) { var pos = GetDirection(TileIndicesFor(uid, grid, position), dir); return GetAnchoredEntities(uid, grid, pos); } public AnchoredEntitiesEnumerator GetOffset(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Vector2i offset) { var pos = TileIndicesFor(uid, grid, coords) + offset; return GetAnchoredEntities(uid, grid, pos); } public AnchoredEntitiesEnumerator GetLocal(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords)); } public EntityCoordinates DirectionToGrid(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Direction direction) { return GridTileToLocal(uid, grid, GetDirection(TileIndicesFor(uid, grid, coords), direction)); } public AnchoredEntitiesInTileOffsetsEnumerator GetCardinalNeighborCells(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { var position = TileIndicesFor(uid, grid, coords); return AnchoredEntitiesInTileOffsetsEnumerator.Cardinal(this, uid, grid, position); } public AnchoredEntitiesInTileOffsetsEnumerator GetCellsInSquareArea(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, int n) { var position = TileIndicesFor(uid, grid, coords); return AnchoredEntitiesInTileOffsetsEnumerator.Square(this, uid, grid, position, n); } public struct AnchoredEntitiesInTileOffsetsEnumerator : IEnumerable, IEnumerator { private readonly SharedMapSystem _map; private readonly EntityUid _uid; private readonly MapGridComponent _grid; private readonly Vector2i _position; private readonly int _n; private readonly bool _cardinal; private int _index; private int _x; private int _y; private AnchoredEntitiesEnumerator _anchored; private EntityUid _current; private AnchoredEntitiesInTileOffsetsEnumerator( SharedMapSystem map, EntityUid uid, MapGridComponent grid, Vector2i position, int n, bool cardinal) { _map = map; _uid = uid; _grid = grid; _position = position; _n = n; _cardinal = cardinal; _index = -1; _x = -n - 1; _y = -n; _anchored = default; _current = default; } internal static AnchoredEntitiesInTileOffsetsEnumerator Cardinal( SharedMapSystem map, EntityUid uid, MapGridComponent grid, Vector2i position) { return new AnchoredEntitiesInTileOffsetsEnumerator(map, uid, grid, position, 0, true); } internal static AnchoredEntitiesInTileOffsetsEnumerator Square( SharedMapSystem map, EntityUid uid, MapGridComponent grid, Vector2i position, int n) { return new AnchoredEntitiesInTileOffsetsEnumerator(map, uid, grid, position, n, false); } public readonly AnchoredEntitiesInTileOffsetsEnumerator GetEnumerator() => this; public readonly EntityUid Current => _current; readonly object IEnumerator.Current => Current; public bool MoveNext() { while (true) { if (_anchored.MoveNext(out var current)) { _current = current.Value; return true; } if (!TryGetNextOffset(out var offset)) return false; _anchored = _map.GetAnchoredEntities(_uid, _grid, _position + offset); } } private bool TryGetNextOffset(out Vector2i offset) { if (_cardinal) { _index++; offset = _index switch { 0 => Vector2i.Zero, 1 => new Vector2i(0, 1), 2 => new Vector2i(0, -1), 3 => new Vector2i(1, 0), 4 => new Vector2i(-1, 0), _ => default, }; return _index < 5; } _x++; if (_x > _n) { _x = -_n; _y++; } if (_y > _n) { offset = default; return false; } offset = new Vector2i(_x, _y); return true; } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public readonly void Dispose() { } public void Reset() { throw new NotSupportedException(); } } [Obsolete("Use AnchoredEntitiesInTileOffsetsEnumerator instead.")] public struct CardinalNeighborCellsEnumerator { } [Obsolete("Use AnchoredEntitiesInTileOffsetsEnumerator instead.")] public struct SquareAreaCellsEnumerator { } public struct AnchoredEntitiesInTilesEnumerator : IEnumerable, IEnumerator { private readonly SharedMapSystem _map; private readonly EntityUid _uid; private readonly MapGridComponent _grid; private TilesEnumerator _tiles; private AnchoredEntitiesEnumerator _anchored; private EntityUid _current; internal AnchoredEntitiesInTilesEnumerator(SharedMapSystem map, EntityUid uid, MapGridComponent grid, TilesEnumerator tiles) { _map = map; _uid = uid; _grid = grid; _tiles = tiles; _anchored = default; _current = default; } public readonly AnchoredEntitiesInTilesEnumerator GetEnumerator() => this; public readonly EntityUid Current => _current; readonly object IEnumerator.Current => Current; public bool MoveNext() { while (true) { if (_anchored.MoveNext(out var current)) { _current = current.Value; return true; } if (!_tiles.MoveNext(out var tile)) return false; _anchored = _map.GetAnchoredEntities(_uid, _grid, tile.GridIndices); } } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public readonly void Dispose() { } public void Reset() { throw new NotSupportedException(); } } #endregion #region Transforms public Vector2 WorldToLocal(EntityUid uid, MapGridComponent grid, Vector2 posWorld) { var matrix = _transform.GetInvWorldMatrix(uid); return Vector2.Transform(posWorld, matrix); } public EntityCoordinates MapToGrid(EntityUid uid, MapCoordinates posWorld) { var mapId = _xformQuery.GetComponent(uid).MapID; if (posWorld.MapId != mapId) throw new ArgumentException( $"Grid {uid} is on map {mapId}, but coords are on map {posWorld.MapId}.", nameof(posWorld)); if (!_gridQuery.TryGetComponent(uid, out var grid)) { return new EntityCoordinates(GetMapOrInvalid(posWorld.MapId), new Vector2(posWorld.X, posWorld.Y)); } return new EntityCoordinates(uid, WorldToLocal(uid, grid, posWorld.Position)); } public Vector2 LocalToWorld(EntityUid uid, MapGridComponent grid, Vector2 posLocal) { var matrix = _transform.GetWorldMatrix(uid); return Vector2.Transform(posLocal, matrix); } public Vector2i WorldToTile(EntityUid uid, MapGridComponent grid, Vector2 posWorld) { var local = WorldToLocal(uid, grid, posWorld); var x = (int)Math.Floor(local.X / grid.TileSize); var y = (int)Math.Floor(local.Y / grid.TileSize); return new Vector2i(x, y); } public Vector2i LocalToTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coordinates) { var position = LocalToGrid(uid, grid, coordinates); return new Vector2i((int) Math.Floor(position.X / grid.TileSize), (int) Math.Floor(position.Y / grid.TileSize)); } public Vector2i CoordinatesToTile(EntityUid uid, MapGridComponent grid, MapCoordinates coords) { #if DEBUG var mapId = _xformQuery.GetComponent(uid).MapID; DebugTools.Assert(mapId == coords.MapId); #endif var local = WorldToLocal(uid, grid, coords.Position); var x = (int)Math.Floor(local.X / grid.TileSize); var y = (int)Math.Floor(local.Y / grid.TileSize); return new Vector2i(x, y); } public Vector2i CoordinatesToTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coords) { #if DEBUG var mapId = _xformQuery.GetComponent(uid).MapID; DebugTools.Assert(mapId == _transform.GetMapId(coords)); #endif var local = LocalToGrid(uid, grid, coords); var x = (int)Math.Floor(local.X / grid.TileSize); var y = (int)Math.Floor(local.Y / grid.TileSize); return new Vector2i(x, y); } public Vector2i LocalToChunkIndices(EntityUid uid, MapGridComponent grid, EntityCoordinates gridPos) { var local = LocalToGrid(uid, grid, gridPos); var x = (int)Math.Floor(local.X / (grid.TileSize * grid.ChunkSize)); var y = (int)Math.Floor(local.Y / (grid.TileSize * grid.ChunkSize)); return new Vector2i(x, y); } public Vector2 LocalToGrid(EntityUid uid, MapGridComponent grid, EntityCoordinates position) { return position.EntityId == uid ? position.Position : WorldToLocal(uid, grid, _transform.ToMapCoordinates(position).Position); } public bool CollidesWithGrid(EntityUid uid, MapGridComponent grid, Vector2i indices) { var chunkIndices = GridTileToChunkIndices(uid, grid, indices); if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk)) return false; var cTileIndices = chunk.GridTileToChunkTile(indices); return chunk.GetTile((ushort)cTileIndices.X, (ushort)cTileIndices.Y).TypeId != Tile.Empty.TypeId; } public Vector2i GridTileToChunkIndices(EntityUid uid, MapGridComponent grid, Vector2i gridTile) => GridTileToChunkIndices(grid, gridTile); public Vector2i GridTileToChunkIndices(MapGridComponent grid, Vector2i gridTile) { var x = (int)Math.Floor(gridTile.X / (float) grid.ChunkSize); var y = (int)Math.Floor(gridTile.Y / (float) grid.ChunkSize); return new Vector2i(x, y); } public EntityCoordinates GridTileToLocal(EntityUid uid, MapGridComponent grid, Vector2i gridTile) { var position = TileCenterToVector(uid, grid, gridTile); return new(uid, position); } /// /// Turns a gridtile origin into a Vector2, accounting for tile size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector2 TileToVector(Entity grid, Vector2i gridTile) { return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize); } /// /// Turns a gridtile center into a Vector2, accounting for tile size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector2 TileCenterToVector(EntityUid uid, MapGridComponent grid, Vector2i gridTile) { return TileCenterToVector((uid, grid), gridTile); } /// /// Turns a gridtile center into a Vector2, accounting for tile size. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector2 TileCenterToVector(Entity grid, Vector2i gridTile) { return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize) + grid.Comp.TileSizeHalfVector; } public Vector2 GridTileToWorldPos(EntityUid uid, MapGridComponent grid, Vector2i gridTile) { var locX = gridTile.X * grid.TileSize + (grid.TileSize / 2f); var locY = gridTile.Y * grid.TileSize + (grid.TileSize / 2f); return Vector2.Transform(new Vector2(locX, locY), _transform.GetWorldMatrix(uid)); } public MapCoordinates GridTileToWorld(EntityUid uid, MapGridComponent grid, Vector2i gridTile) { var parentMapId = _xformQuery.GetComponent(uid).MapID; return new(GridTileToWorldPos(uid, grid, gridTile), parentMapId); } public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, Vector2i indices, out TileRef tile) { var chunkIndices = GridTileToChunkIndices(uid, grid, indices); if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk)) { tile = default; return false; } var cTileIndices = chunk.GridTileToChunkTile(indices); tile = GetTileRef(uid, grid, chunk, (ushort)cTileIndices.X, (ushort)cTileIndices.Y); return true; } public bool TryGetTile(MapGridComponent grid, Vector2i indices, out Tile tile) { var chunkIndices = GridTileToChunkIndices(grid, indices); if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk)) { tile = default; return false; } var cTileIndices = chunk.GridTileToChunkTile(indices); tile = chunk.Tiles[cTileIndices.X, cTileIndices.Y]; return true; } /// /// Attempts to get the for the tile at the given grid indices. This will throw an /// exception if the tile at this location has no registered tile definition. /// public bool TryGetTileDef(MapGridComponent grid, Vector2i indices, [NotNullWhen(true)] out ITileDefinition? tileDef) { if (!TryGetTile(grid, indices, out var tile)) { tileDef = null; return false; } tileDef = _tileMan[tile.TypeId]; return true; } public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, out TileRef tile) { return TryGetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords), out tile); } public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, Vector2 worldPos, out TileRef tile) { return TryGetTileRef(uid, grid, WorldToTile(uid, grid, worldPos), out tile); } #endregion Transforms /// /// Calculate the world space AABB for this chunk. /// internal Box2 CalcWorldAABB(EntityUid uid, MapGridComponent grid, MapChunk mapChunk) { var (position, rotation) = _transform.GetWorldPositionRotation(uid); var chunkPosition = mapChunk.Indices; var tileScale = grid.TileSize; var chunkScale = mapChunk.ChunkSize; var worldPos = position + rotation.RotateVec(chunkPosition * tileScale * chunkScale); return new Box2Rotated( ((Box2)mapChunk.CachedBounds .Scale(tileScale)) .Translated(worldPos), rotation, worldPos).CalcBoundingBox(); } private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapChunk, Vector2i tileIndices, Tile newTile, Tile oldTile, bool shapeChanged) { // As the collision regeneration can potentially delete the chunk we'll notify of the tile changed first. var gridTile = mapChunk.ChunkTileToGridTile(tileIndices); mapChunk.LastTileModifiedTick = _timing.CurTick; grid.LastTileModifiedTick = _timing.CurTick; Dirty(uid, grid); // The map serializer currently sets tiles of unbound grids as part of the deserialization process // It properly sets SuppressOnTileChanged so that the event isn't spammed for every tile on the grid. // ParentMapId is not able to be accessed on unbound grids, so we can't even call this function for unbound grids. if (!SuppressOnTileChanged) { var newTileRef = new TileRef(uid, gridTile, newTile); RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices); } if (shapeChanged && !mapChunk.SuppressCollisionRegeneration) { RegenerateCollision(uid, grid, mapChunk); } } /// /// Raises on the provided grid unless is set. /// internal void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) { if (SuppressOnTileChanged) return; var ev = new TileChangedEvent(entity, tileRef, oldTile, chunk); EntityManager.EventBus.RaiseLocalEvent(entity.Owner, ref ev, true); } /// /// Iterates the local tiles of the specified data. /// public struct TilesEnumerator : IEnumerable, IEnumerator { private readonly SharedMapSystem _mapSystem; private readonly EntityUid _uid; private readonly MapGridComponent _grid; private readonly bool _ignoreEmpty; private readonly Predicate? _predicate; private readonly int _lowerY; private readonly int _upperX; private readonly int _upperY; private int _x; private int _y; private TileRef _current; public TilesEnumerator( SharedMapSystem mapSystem, bool ignoreEmpty, Predicate? predicate, EntityUid uid, MapGridComponent grid, Box2 aabb) { _mapSystem = mapSystem; _uid = uid; _grid = grid; _ignoreEmpty = ignoreEmpty; _predicate = predicate; // TODO: Should move the intersecting calls onto mapmanager system and then allow people to pass in xform / xformquery // that way we can avoid the GetComp here. var gridTileLb = new Vector2i((int)Math.Floor(aabb.Left), (int)Math.Floor(aabb.Bottom)); // If we have 20.1 we want to include that tile but if we have 20 then we don't. var gridTileRt = new Vector2i((int)Math.Ceiling(aabb.Right), (int)Math.Ceiling(aabb.Top)); _x = gridTileLb.X; _y = gridTileLb.Y; _lowerY = gridTileLb.Y; _upperX = gridTileRt.X; _upperY = gridTileRt.Y; _current = default; } public readonly TilesEnumerator GetEnumerator() => this; public readonly TileRef Current => _current; readonly object IEnumerator.Current => Current; public bool MoveNext() { return MoveNext(out _); } public bool MoveNext(out TileRef tile) { while (true) { if (_x >= _upperX) { tile = TileRef.Zero; return false; } var gridTile = new Vector2i(_x, _y); _y++; if (_y >= _upperY) { _x++; _y = _lowerY; } var gridChunk = _mapSystem.GridTileToChunkIndices(_uid, _grid, gridTile); if (_grid.Chunks.TryGetValue(gridChunk, out var chunk)) { var chunkTile = chunk.GridTileToChunkTile(gridTile); tile = _mapSystem.GetTileRef(_uid, _grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); if (_ignoreEmpty && tile.Tile.IsEmpty) continue; if (_predicate == null || _predicate(tile)) { _current = tile; return true; } } else if (!_ignoreEmpty) { tile = new TileRef(_uid, gridTile.X, gridTile.Y, Tile.Empty); if (_predicate == null || _predicate(tile)) { _current = tile; return true; } } } } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public readonly void Dispose() { } public void Reset() { throw new NotSupportedException(); } } public struct CircleTilesEnumerator : IEnumerable, IEnumerator { private TilesEnumerator _tiles; private readonly MapGridComponent _grid; private readonly Vector2 _center; private readonly float _radius; private TileRef _current; internal CircleTilesEnumerator(TilesEnumerator tiles, MapGridComponent grid, Vector2 center, float radius) { _tiles = tiles; _grid = grid; _center = center; _radius = radius; _current = default; } public readonly CircleTilesEnumerator GetEnumerator() => this; public readonly TileRef Current => _current; readonly object IEnumerator.Current => Current; public bool MoveNext() { while (_tiles.MoveNext(out var tile)) { var tileCenter = tile.GridIndices + _grid.TileSizeHalfVector; var direction = tileCenter - _center; if (!direction.IsShorterThanOrEqualTo(_radius)) continue; _current = tile; return true; } return false; } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } readonly IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public readonly void Dispose() { } public void Reset() { throw new NotSupportedException(); } } } /// /// Additional parameters used when creating a new grid entity. /// /// The number of tiles long/wide the grids chunks should be. public record struct GridCreateOptions(ushort ChunkSize) { public readonly static GridCreateOptions Default = new(ChunkSize: 16); }