using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Content.Server.Atmos.Components; using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Reactions; using Content.Server.NodeContainer.NodeGroups; using Content.Shared.Atmos; using Content.Shared.Maps; using Robust.Shared.GameObjects; // ReSharper disable once RedundantUsingDirective using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Utility; using Dependency = Robust.Shared.IoC.DependencyAttribute; namespace Content.Server.Atmos.EntitySystems { public partial class AtmosphereSystem { [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!; private void InitializeGrid() { SubscribeLocalEvent(OnGridAtmosphereInit); } private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent gridAtmosphere, ComponentInit args) { base.Initialize(); gridAtmosphere.Tiles.Clear(); if (!ComponentManager.TryGetComponent(uid, out IMapGridComponent? mapGrid)) return; if (gridAtmosphere.TilesUniqueMixes != null) { foreach (var (indices, mix) in gridAtmosphere.TilesUniqueMixes) { try { gridAtmosphere.Tiles.Add(indices, new TileAtmosphere(mapGrid.GridIndex, indices, (GasMixture) gridAtmosphere.UniqueMixes![mix].Clone())); } catch (ArgumentOutOfRangeException) { Logger.Error($"Error during atmos serialization! Tile at {indices} points to an unique mix ({mix}) out of range!"); throw; } InvalidateTile(gridAtmosphere, indices); } } GridRepopulateTiles(mapGrid.Grid, gridAtmosphere); } #region Grid Is Simulated /// /// Returns whether a grid has a simulated atmosphere. /// /// Coordinates to be checked. /// Whether the grid has a simulated atmosphere. public bool IsSimulatedGrid(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return IsSimulatedGrid(tuple.Value.Grid); return false; } /// /// Returns whether a grid has a simulated atmosphere. /// /// Grid to be checked. /// Whether the grid has a simulated atmosphere. public bool IsSimulatedGrid(GridId grid) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return false; if (ComponentManager.HasComponent(mapGrid.GridEntityId)) return true; return false; } #endregion #region Grid Get All Mixtures /// /// Gets all tile mixtures within a grid atmosphere, optionally invalidating them all. /// /// Coordinates where to get the grid to get all tile mixtures from. /// Whether to invalidate all tiles. /// All tile mixtures in a grid. public IEnumerable GetAllTileMixtures(EntityCoordinates coordinates, bool invalidate = false) { if (TryGetGridAndTile(coordinates, out var tuple)) return GetAllTileMixtures(tuple.Value.Grid, invalidate); return Enumerable.Empty(); } /// /// Gets all tile mixtures within a grid atmosphere, optionally invalidating them all. /// /// Grid where to get all tile mixtures from. /// Whether to invalidate all tiles. /// All tile mixtures in a grid. public IEnumerable GetAllTileMixtures(GridId grid, bool invalidate = false) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return Enumerable.Empty(); if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetAllTileMixtures(gridAtmosphere, invalidate); } return Enumerable.Empty(); } /// /// Gets all tile mixtures within a grid atmosphere, optionally invalidating them all. /// /// Grid Atmosphere to get all mixtures from. /// Whether to invalidate all mixtures. /// All the tile mixtures in a grid. public IEnumerable GetAllTileMixtures(GridAtmosphereComponent gridAtmosphere, bool invalidate = false) { foreach (var (indices, tile) in gridAtmosphere.Tiles) { if (tile.Air == null) continue; if (invalidate) InvalidateTile(gridAtmosphere, indices); yield return tile.Air; } } #endregion #region Grid Cell Volume /// /// Gets the volume in liters for a number of tiles, on a specific grid. /// /// The grid in question. /// The amount of tiles. /// The volume in liters that the tiles occupy. public float GetVolumeForTiles(GridId grid, int tiles = 1) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return Atmospherics.CellVolume * tiles; return GetVolumeForTiles(mapGrid, tiles); } /// /// Gets the volume in liters for a number of tiles, on a specific grid. /// /// The grid in question. /// The amount of tiles. /// The volume in liters that the tiles occupy. public float GetVolumeForTiles(IMapGrid mapGrid, int tiles = 1) { return Atmospherics.CellVolume * mapGrid.TileSize * tiles; } #endregion #region Grid Get Obstructing /// /// Gets all obstructing AirtightComponent instances in a specific tile. /// /// The grid where to get the tile. /// The indices of the tile. /// public virtual IEnumerable GetObstructingComponents(IMapGrid mapGrid, Vector2i tile) { foreach (var uid in mapGrid.GetAnchoredEntities(tile)) { if (ComponentManager.TryGetComponent(uid, out var ac)) yield return ac; } } private AtmosDirection GetBlockedDirections(IMapGrid mapGrid, Vector2i indices) { var value = AtmosDirection.Invalid; foreach (var airtightComponent in GetObstructingComponents(mapGrid, indices)) { if(airtightComponent.AirBlocked) value |= airtightComponent.AirBlockedDirection; } return value; } #endregion #region Grid Revalidate /// /// Revalidates all invalid coordinates in a grid atmosphere. /// /// The grid in question. /// The grid atmosphere in question. /// Whether the process succeeded or got paused due to time constrains. private bool GridRevalidate(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere) { var volume = GetVolumeForTiles(mapGrid, 1); if (!gridAtmosphere.RevalidatePaused) gridAtmosphere.CurrentRunInvalidatedCoordinates = new Queue(gridAtmosphere.InvalidatedCoords); gridAtmosphere.InvalidatedCoords.Clear(); var number = 0; while (gridAtmosphere.CurrentRunInvalidatedCoordinates.TryDequeue(out var indices)) { var tile = GetTileAtmosphere(gridAtmosphere, indices); if (tile == null) { tile = new TileAtmosphere(mapGrid.Index, indices, new GasMixture(volume){Temperature = Atmospherics.T20C}); gridAtmosphere.Tiles[indices] = tile; } var isAirBlocked = IsTileAirBlocked(mapGrid, indices); tile.BlockedAirflow = GetBlockedDirections(mapGrid, indices); UpdateAdjacent(mapGrid, gridAtmosphere, tile); if (IsTileSpace(mapGrid, indices) && !isAirBlocked) { tile.Air = new GasMixture(volume); tile.Air.MarkImmutable(); gridAtmosphere.Tiles[indices] = tile; } else if (isAirBlocked) { var nullAir = false; foreach (var airtight in GetObstructingComponents(mapGrid, indices)) { if (!airtight.NoAirWhenFullyAirBlocked) continue; nullAir = true; break; } if (nullAir) { tile.Air = null; tile.Hotspot = new Hotspot(); } } else { if (tile.Air == null && NeedsVacuumFixing(mapGrid, indices)) { FixVacuum(gridAtmosphere, tile.GridIndices); } // Tile used to be space, but isn't anymore. if (tile.Air?.Immutable ?? false) { tile.Air = null; } tile.Air ??= new GasMixture(volume){Temperature = Atmospherics.T20C}; } // By removing the active tile, we effectively remove its excited group, if any. RemoveActiveTile(gridAtmosphere, tile); // Then we activate the tile again. AddActiveTile(gridAtmosphere, tile); // TODO ATMOS: Query all the contents of this tile (like walls) and calculate the correct thermal conductivity tile.ThermalConductivity = tile.Tile?.Tile.GetContentTileDefinition().ThermalConductivity ?? 0.5f; InvalidateVisuals(mapGrid.Index, indices); for (var i = 0; i < Atmospherics.Directions; i++) { var direction = (AtmosDirection) (1 << i); var otherIndices = indices.Offset(direction.ToDirection()); var otherTile = GetTileAtmosphereOrCreateSpace(mapGrid, gridAtmosphere, otherIndices); if (otherTile != null) AddActiveTile(gridAtmosphere, otherTile); } if (number++ < InvalidCoordinatesLagCheckIterations) continue; number = 0; // Process the rest next time. if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime) { return false; } } return true; } #endregion #region Grid Repopulate /// /// Repopulates all tiles on a grid atmosphere. /// /// The grid where to get all valid tiles from. /// The grid atmosphere where the tiles will be repopulated. public void GridRepopulateTiles(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere) { var volume = GetVolumeForTiles(mapGrid, 1); foreach (var tile in mapGrid.GetAllTiles()) { if(!gridAtmosphere.Tiles.ContainsKey(tile.GridIndices)) gridAtmosphere.Tiles[tile.GridIndices] = new TileAtmosphere(tile.GridIndex, tile.GridIndices, new GasMixture(volume){Temperature = Atmospherics.T20C}); InvalidateTile(gridAtmosphere, tile.GridIndices); } foreach (var (position, tile) in gridAtmosphere.Tiles.ToArray()) { UpdateAdjacent(mapGrid, gridAtmosphere, tile); InvalidateVisuals(mapGrid.Index, position); } } #endregion #region Tile Pry /// /// Pries a tile in a grid. /// /// The grid in question. /// The indices of the tile. private void PryTile(IMapGrid mapGrid, Vector2i tile) { if (!mapGrid.TryGetTileRef(tile, out var tileRef)) return; tileRef.PryTile(_mapManager, _tileDefinitionManager, EntityManager); } #endregion #region Tile Invalidate /// /// Invalidates a tile at a certain position. /// /// Coordinates of the tile. public void InvalidateTile(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) InvalidateTile(tuple.Value.Grid, tuple.Value.Tile); } /// /// Invalidates a tile at a certain position. /// /// Grid where to invalidate the tile. /// The indices of the tile. public void InvalidateTile(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { InvalidateTile(gridAtmosphere, tile); return; } } /// /// Invalidates a tile at a certain position. /// /// Grid Atmosphere where to invalidate the tile. /// The tile's indices. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InvalidateTile(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { gridAtmosphere.InvalidatedCoords.Add(tile); } #endregion #region Tile Invalidate Visuals public void InvalidateVisuals(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) InvalidateVisuals(tuple.Value.Grid, tuple.Value.Tile); } public void InvalidateVisuals(GridId grid, Vector2i tile) { _gasTileOverlaySystem.Invalidate(grid, tile); } #endregion #region Tile Atmosphere Get /// /// Gets the tile atmosphere in a position, or null. /// /// Coordinates where to get the tile. /// Do NOT use this outside of atmos internals. /// The Tile Atmosphere in the position, or null if not on a grid. public TileAtmosphere? GetTileAtmosphere(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return GetTileAtmosphere(tuple.Value.Grid, tuple.Value.Tile); return null; } /// /// Gets the tile atmosphere in a position, or null. /// /// Grid where to get the tile. /// Indices of the tile. /// Do NOT use this outside of atmos internals. /// The Tile Atmosphere in the position, or null. public TileAtmosphere? GetTileAtmosphere(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return null; if(ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetTileAtmosphere(gridAtmosphere, tile); } return null; } /// /// Gets the tile atmosphere in a position, or null. /// /// Grid atmosphere where to get the tile. /// Indices of the tile. /// Do NOT use this outside of atmos internals. /// The Tile Atmosphere in the position, or null. public TileAtmosphere? GetTileAtmosphere(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return tileAtmosphere; return null; } /// /// Gets the tile atmosphere in a position and if not possible returns a space tile or null. /// /// Coordinates of the tile. /// Do NOT use this outside of atmos internals. /// The tile atmosphere of a specific position in a grid, a space tile atmosphere if the tile is space or null if not on a grid. public TileAtmosphere? GetTileAtmosphereOrCreateSpace(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return GetTileAtmosphereOrCreateSpace(tuple.Value.Grid, tuple.Value.Tile); return null; } /// /// Gets the tile atmosphere in a position and if not possible returns a space tile or null. /// /// Grid where to get the tile. /// Indices of the tile. /// Do NOT use this outside of atmos internals. /// The tile atmosphere of a specific position in a grid, a space tile atmosphere if the tile is space or null if the grid doesn't exist. public TileAtmosphere? GetTileAtmosphereOrCreateSpace(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return null; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetTileAtmosphereOrCreateSpace(mapGrid, gridAtmosphere, tile); } return null; } /// /// Gets the tile atmosphere in a position and if not possible returns a space tile or null. /// /// Grid where to get the tile. /// Grid Atmosphere where to get the tile. /// Indices of the tile. /// Do NOT use this outside of atmos internals. /// The tile atmosphere of a specific position in a grid or a space tile atmosphere if the tile is space. public TileAtmosphere GetTileAtmosphereOrCreateSpace(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere, Vector2i tile) { var tileAtmosphere = GetTileAtmosphere(gridAtmosphere, tile); if (tileAtmosphere != null) return tileAtmosphere; // That tile must be space, or something has gone horribly wrong! DebugTools.Assert(IsTileSpace(mapGrid, tile)); return new TileAtmosphere(mapGrid.Index, tile, new GasMixture(Atmospherics.CellVolume) {Temperature = Atmospherics.TCMB}, true); } #endregion #region Tile Active Add /// /// Makes a tile become active and start processing. /// /// Coordinates where to get the tile. public void AddActiveTile(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) AddActiveTile(tuple.Value.Grid, tuple.Value.Tile); } /// /// Makes a tile become active and start processing. /// /// Grid where to get the tile. /// Indices of the tile to be activated. public void AddActiveTile(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { AddActiveTile(gridAtmosphere, tile); return; } } /// /// Makes a tile become active and start processing. /// /// Grid Atmosphere where to get the tile. /// Indices of the tile to be activated. public void AddActiveTile(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; AddActiveTile(gridAtmosphere, tileAtmosphere); } /// /// Makes a tile become active and start processing. Does NOT check if the tile belongs to the grid atmos. /// /// Grid Atmosphere where to get the tile. /// Tile Atmosphere to be activated. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddActiveTile(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) { if (tile.Air == null) return; tile.Excited = true; gridAtmosphere.ActiveTiles.Add(tile); } #endregion #region Tile Active Remove /// /// Makes a tile become inactive and stop processing. /// /// Coordinates where to get the tile. /// Whether to dispose of the tile's public void RemoveActiveTile(EntityCoordinates coordinates, bool disposeExcitedGroup = true) { if(TryGetGridAndTile(coordinates, out var tuple)) RemoveActiveTile(tuple.Value.Grid, tuple.Value.Tile, disposeExcitedGroup); } /// /// Makes a tile become inactive and stop processing. /// /// Grid where to get the tile. /// Indices of the tile to be deactivated. /// Whether to dispose of the tile's public void RemoveActiveTile(GridId grid, Vector2i tile, bool disposeExcitedGroup = true) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { RemoveActiveTile(gridAtmosphere, tile); return; } } /// /// Makes a tile become inactive and stop processing. /// /// Grid Atmosphere where to get the tile. /// Indices of the tile to be deactivated. /// Whether to dispose of the tile's public void RemoveActiveTile(GridAtmosphereComponent gridAtmosphere, Vector2i tile, bool disposeExcitedGroup = true) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; RemoveActiveTile(gridAtmosphere, tileAtmosphere, disposeExcitedGroup); } /// /// Makes a tile become inactive and stop processing. /// /// Grid Atmosphere where to get the tile. /// Tile Atmosphere to be deactivated. /// Whether to dispose of the tile's private void RemoveActiveTile(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, bool disposeExcitedGroup = true) { tile.Excited = false; gridAtmosphere.ActiveTiles.Remove(tile); if (tile.ExcitedGroup == null) return; if (disposeExcitedGroup) ExcitedGroupDispose(gridAtmosphere, tile.ExcitedGroup); else ExcitedGroupRemoveTile(tile.ExcitedGroup, tile); } #endregion #region Tile Mixture /// /// Returns a reference to the gas mixture on a tile, or null. /// /// Coordinates where to get the tile. /// Whether to invalidate the tile. /// The tile mixture, or null public GasMixture? GetTileMixture(EntityCoordinates coordinates, bool invalidate = false) { return TryGetGridAndTile(coordinates, out var tuple) ? GetTileMixture(tuple.Value.Grid, tuple.Value.Tile, invalidate) : null; } /// /// Returns a reference to the gas mixture on a tile, or null. /// /// Grid where to get the tile air. /// Indices of the tile. /// Whether to invalidate the tile. /// The tile mixture, or null public GasMixture? GetTileMixture(GridId grid, Vector2i tile, bool invalidate = false) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return null; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetTileMixture(gridAtmosphere, tile, invalidate); } if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out SpaceGridAtmosphereComponent? spaceAtmosphere)) { // Always return a new space gas mixture in this case. return GasMixture.SpaceGas; } return null; } /// /// Returns a reference to the gas mixture on a tile, or null. /// /// Grid Atmosphere where to get the tile air. /// Indices of the tile. /// Whether to invalidate the tile. /// The tile mixture, or null [MethodImpl(MethodImplOptions.AggressiveInlining)] public GasMixture? GetTileMixture(GridAtmosphereComponent gridAtmosphere, Vector2i tile, bool invalidate = false) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return null; // Invalidate the tile if needed. if (invalidate) InvalidateTile(gridAtmosphere, tile); // Return actual tile air or null. return tileAtmosphere.Air; } #endregion #region Tile React /// /// Causes a gas mixture reaction on a specific tile. /// /// Coordinates where to get the tile. /// Reaction results. public ReactionResult React(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return React(tuple.Value.Grid, tuple.Value.Tile); return ReactionResult.NoReaction; } /// /// Causes a gas mixture reaction on a specific tile. /// /// Grid where to get the tile. /// Indices of the tile. /// Reaction results. public ReactionResult React(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return ReactionResult.NoReaction; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return React(gridAtmosphere, tile); } return ReactionResult.NoReaction; } /// /// Causes a gas mixture reaction on a specific tile. /// /// Grid Atmosphere where to get the tile. /// Indices of the tile. /// Reaction results. public ReactionResult React(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere) || tileAtmosphere.Air == null) return ReactionResult.NoReaction; InvalidateTile(gridAtmosphere, tile); return React(tileAtmosphere.Air, tileAtmosphere); } #endregion #region Tile Air-blocked /// /// Returns if the tile in question is "air-blocked" in a certain direction or not. /// This could be due to a number of reasons, such as walls, doors, etc. /// /// Coordinates where to get the tile. /// Directions to check. /// Whether the tile is blocked in the directions specified. public bool IsTileAirBlocked(EntityCoordinates coordinates, AtmosDirection direction = AtmosDirection.All) { if (TryGetGridAndTile(coordinates, out var tuple)) return IsTileAirBlocked(tuple.Value.Grid, tuple.Value.Tile, direction); return false; } /// /// Returns if the tile in question is "air-blocked" in a certain direction or not. /// This could be due to a number of reasons, such as walls, doors, etc. /// /// Grid where to get the tile. /// Indices of the tile. /// Directions to check. /// Whether the tile is blocked in the directions specified. public bool IsTileAirBlocked(GridId grid, Vector2i tile, AtmosDirection direction = AtmosDirection.All) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return false; return IsTileAirBlocked(mapGrid, tile, direction); } /// /// Returns if the tile in question is "air-blocked" in a certain direction or not. /// This could be due to a number of reasons, such as walls, doors, etc. /// /// Grid where to get the tile. /// Indices of the tile. /// Directions to check. /// Whether the tile is blocked in the directions specified. public bool IsTileAirBlocked(IMapGrid mapGrid, Vector2i tile, AtmosDirection direction = AtmosDirection.All) { var directions = AtmosDirection.Invalid; foreach (var obstructingComponent in GetObstructingComponents(mapGrid, tile)) { if (!obstructingComponent.AirBlocked) continue; // We set the directions that are air-blocked so far, // as you could have a full obstruction with only 4 directional air blockers. directions |= obstructingComponent.AirBlockedDirection; if (directions.IsFlagSet(direction)) return true; } return false; } #endregion #region Tile Space /// /// Returns whether the specified tile is a space tile or not. /// /// Coordinates where to check the tile. /// Whether the tile is space or not. public bool IsTileSpace(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return IsTileSpace(tuple.Value.Grid, tuple.Value.Tile); return true; } /// /// Returns whether the specified tile is a space tile or not. /// /// Grid where to check the tile. /// Indices of the tile. /// Whether the tile is space or not. public bool IsTileSpace(GridId grid, Vector2i tile) { return !_mapManager.TryGetGrid(grid, out var mapGrid) || IsTileSpace(mapGrid, tile); } public bool IsTileSpace(IMapGrid mapGrid, Vector2i tile) { if (!mapGrid.TryGetTileRef(tile, out var tileRef)) return true; return ((ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId]).IsSpace; } #endregion #region Adjacent Get Positions /// /// Gets all the positions adjacent to a tile. Can include air-blocked directions. /// /// Coordinates where to get the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// The positions adjacent to the tile. public IEnumerable GetAdjacentTiles(EntityCoordinates coordinates, bool includeBlocked = false) { if (TryGetGridAndTile(coordinates, out var tuple)) return GetAdjacentTiles(tuple.Value.Grid, tuple.Value.Tile, includeBlocked); return Enumerable.Empty(); } /// /// Gets all the positions adjacent to a tile. Can include air-blocked directions. /// /// Grid where to get the tiles. /// Indices of the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// The positions adjacent to the tile. public IEnumerable GetAdjacentTiles(GridId grid, Vector2i tile, bool includeBlocked = false) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return Enumerable.Empty(); if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetAdjacentTiles(gridAtmosphere, tile, includeBlocked); } return Enumerable.Empty(); } /// /// Gets all the positions adjacent to a tile. Can include air-blocked directions. /// /// Grid Atmosphere where to get the tiles. /// Indices of the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// The positions adjacent to the tile. public IEnumerable GetAdjacentTiles(GridAtmosphereComponent gridAtmosphere, Vector2i tile, bool includeBlocked = false) { if(!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) yield break; for (var i = 0; i < tileAtmosphere.AdjacentTiles.Length; i++) { var adjacentTile = tileAtmosphere.AdjacentTiles[i]; // TileAtmosphere has nullable disabled, so just in case... // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (adjacentTile?.Air == null) continue; if (!includeBlocked) { var direction = (AtmosDirection) (1 << i); if (tileAtmosphere.BlockedAirflow.IsFlagSet(direction)) continue; } yield return adjacentTile.GridIndices; } } #endregion #region Adjacent Get Mixture /// /// Gets all tile gas mixtures adjacent to a specific tile, and optionally invalidates them. /// Does not return the tile in question, only the adjacent ones. /// /// Coordinates where to get the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// Whether to invalidate all adjacent tiles. /// All adjacent tile gas mixtures to the tile in question public IEnumerable GetAdjacentTileMixtures(EntityCoordinates coordinates, bool includeBlocked = false, bool invalidate = false) { if (TryGetGridAndTile(coordinates, out var tuple)) return GetAdjacentTileMixtures(tuple.Value.Grid, tuple.Value.Tile); return Enumerable.Empty(); } /// /// Gets all tile gas mixtures adjacent to a specific tile, and optionally invalidates them. /// Does not return the tile in question, only the adjacent ones. /// /// Grid where to get the tile. /// Indices of the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// Whether to invalidate all adjacent tiles. /// All adjacent tile gas mixtures to the tile in question public IEnumerable GetAdjacentTileMixtures(GridId grid, Vector2i tile, bool includeBlocked = false, bool invalidate = false) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return Enumerable.Empty(); if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return GetAdjacentTileMixtures(gridAtmosphere, tile, includeBlocked, invalidate); } return Enumerable.Empty(); } /// /// Gets all tile gas mixtures adjacent to a specific tile, and optionally invalidates them. /// Does not return the tile in question, only the adjacent ones. /// /// Grid Atmosphere where to get the tile. /// Indices of the tile. /// Whether to include tiles in directions the tile is air-blocked in. /// Whether to invalidate all adjacent tiles. /// All adjacent tile gas mixtures to the tile in question public IEnumerable GetAdjacentTileMixtures(GridAtmosphereComponent gridAtmosphere, Vector2i tile, bool includeBlocked = false, bool invalidate = false) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return Enumerable.Empty(); return GetAdjacentTileMixtures(gridAtmosphere, tileAtmosphere, includeBlocked, invalidate); } /// /// Gets all tile gas mixtures adjacent to a specific tile, and optionally invalidates them. /// Does not return the tile in question, only the adjacent ones. /// /// Grid Atmosphere where the tile is. /// Tile Atmosphere in question. /// Whether to include tiles in directions the tile is air-blocked in. /// Whether to invalidate all adjacent tiles. /// All adjacent tile gas mixtures to the tile in question private IEnumerable GetAdjacentTileMixtures(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, bool includeBlocked = false, bool invalidate = false) { for (var i = 0; i < tile.AdjacentTiles.Length; i++) { var adjacentTile = tile.AdjacentTiles[i]; // TileAtmosphere has nullable disabled, so just in case... // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (adjacentTile?.Air == null) continue; if (!includeBlocked) { var direction = (AtmosDirection) (1 << i); if (tile.BlockedAirflow.IsFlagSet(direction)) continue; } if (invalidate) InvalidateTile(gridAtmosphere, adjacentTile.GridIndices); yield return adjacentTile.Air; } } #endregion #region Adjacent Update /// /// Immediately updates a tile's blocked air directions. /// /// Coordinates where to get the tile. public void UpdateAdjacent(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) UpdateAdjacent(tuple.Value.Grid, tuple.Value.Tile); } /// /// Immediately updates a tile's blocked air directions. /// /// Grid where to get the tile. /// Indices of the tile. public void UpdateAdjacent(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { UpdateAdjacent(mapGrid, gridAtmosphere, tile); return; } } /// /// Immediately updates a tile's blocked air directions. /// /// Grid where to get the tile. /// Grid Atmosphere where to get the tile. /// Indices of the tile. public void UpdateAdjacent(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; UpdateAdjacent(mapGrid, gridAtmosphere, tileAtmosphere); } /// /// Immediately updates a tile's blocked air directions. /// /// Grid where to get the tile. /// Grid Atmosphere of the tile. /// Tile Atmosphere to be updated. private void UpdateAdjacent(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere, TileAtmosphere tileAtmosphere) { tileAtmosphere.AdjacentBits = AtmosDirection.Invalid; for (var i = 0; i < Atmospherics.Directions; i++) { var direction = (AtmosDirection) (1 << i); var otherIndices = tileAtmosphere.GridIndices.Offset(direction.ToDirection()); var adjacent = GetTileAtmosphereOrCreateSpace(mapGrid, gridAtmosphere, otherIndices); tileAtmosphere.AdjacentTiles[direction.ToIndex()] = adjacent; UpdateAdjacent(mapGrid, gridAtmosphere, adjacent, direction.GetOpposite()); if (!tileAtmosphere.BlockedAirflow.IsFlagSet(direction) && !IsTileAirBlocked(mapGrid, adjacent.GridIndices, direction.GetOpposite())) { tileAtmosphere.AdjacentBits |= direction; } } } /// /// Immediately updates a tile's single blocked air direction. /// /// Coordinates where to get the tile. /// Direction to be updated. public void UpdateAdjacent(EntityCoordinates coordinates, AtmosDirection direction) { if(TryGetGridAndTile(coordinates, out var tuple)) UpdateAdjacent(tuple.Value.Grid, tuple.Value.Tile, direction); } /// /// Immediately updates a tile's single blocked air direction. /// /// Grid where to get the tile. /// Indices of the tile. /// Direction to be updated. public void UpdateAdjacent(GridId grid, Vector2i tile, AtmosDirection direction) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { UpdateAdjacent(mapGrid, gridAtmosphere, tile, direction); return; } } /// /// Immediately updates a tile's single blocked air direction. /// /// Grid where to get the tile. /// Grid Atmosphere where to get the tile. /// Indices of the tile. /// Direction to be updated. public void UpdateAdjacent(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere, Vector2i tile, AtmosDirection direction) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; UpdateAdjacent(mapGrid, gridAtmosphere, tileAtmosphere, direction); } /// /// Immediately updates a tile's single blocked air direction. /// /// Grid where to get the tile. /// Grid where to get the tile. /// Tile Atmosphere to be updated. /// Direction to be updated. private void UpdateAdjacent(IMapGrid mapGrid, GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, AtmosDirection direction) { tile.AdjacentTiles[direction.ToIndex()] = GetTileAtmosphereOrCreateSpace(mapGrid, gridAtmosphere, tile.GridIndices.Offset(direction.ToDirection())); if (!tile.BlockedAirflow.IsFlagSet(direction) && !IsTileAirBlocked(mapGrid, tile.GridIndices.Offset(direction.ToDirection()), direction.GetOpposite())) { tile.AdjacentBits |= direction; } else { tile.AdjacentBits &= ~direction; } } #endregion #region Hotspot Expose /// /// Exposes temperature to a tile, creating a hotspot (fire) if the conditions are ideal. /// Can also be used to make an existing hotspot hotter/bigger. Also invalidates the tile. /// /// Coordinates where to get the tile. /// Temperature to expose to the tile. /// Volume of the exposed temperature. /// If true, the existing hotspot values will be set to the exposed values, but only if they're smaller. public void HotspotExpose(EntityCoordinates coordinates, float exposedTemperature, float exposedVolume, bool soh = false) { if(TryGetGridAndTile(coordinates, out var tuple)) HotspotExpose(tuple.Value.Grid, tuple.Value.Tile, exposedTemperature, exposedVolume, soh); } /// /// Exposes temperature to a tile, creating a hotspot (fire) if the conditions are ideal. /// Can also be used to make an existing hotspot hotter/bigger. Also invalidates the tile. /// /// Grid where to get the tile. /// Indices of the tile. /// Temperature to expose to the tile. /// Volume of the exposed temperature. /// If true, the existing hotspot values will be set to the exposed values, but only if they're smaller. public void HotspotExpose(GridId grid, Vector2i tile, float exposedTemperature, float exposedVolume, bool soh = false) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { var tileAtmosphere = GetTileAtmosphere(gridAtmosphere, tile); if (tileAtmosphere == null) return; HotspotExpose(gridAtmosphere, tileAtmosphere, exposedTemperature, exposedVolume, soh); InvalidateTile(gridAtmosphere, tile); return; } } #endregion #region Hotspot Extinguish /// /// Extinguishes a hotspot (fire) on a certain tile, if any. Also invalidates the tile. /// /// Coordinates where to get the tile. public void HotspotExtinguish(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) HotspotExtinguish(tuple.Value.Grid, tuple.Value.Tile); } /// /// Extinguishes a hotspot (fire) on a certain tile, if any. Also invalidates the tile. /// /// Grid where to get the tile. /// Indices of the tile. public void HotspotExtinguish(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { HotspotExtinguish(gridAtmosphere, tile); return; } } /// /// Extinguishes a hotspot (fire) on a certain tile, if any. Also invalidates the tile. /// /// Grid Atmosphere where to get the tile. /// Indices of the tile. public void HotspotExtinguish(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; tileAtmosphere.Hotspot = new Hotspot(); InvalidateTile(gridAtmosphere, tile); } #endregion #region Hotspot Active /// /// Returns whether there's an active hotspot (fire) on a certain tile. /// /// Position where to get the tile. /// Whether the hotspot is active or not. public bool IsHotspotActive(EntityCoordinates coordinates) { if (TryGetGridAndTile(coordinates, out var tuple)) return IsHotspotActive(tuple.Value.Grid, tuple.Value.Tile); return false; } /// /// Returns whether there's an active hotspot (fire) on a certain tile. /// /// Grid where to get the tile /// Indices for the tile /// Whether the hotspot is active or not. public bool IsHotspotActive(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return false; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { return IsHotspotActive(gridAtmosphere, tile); } return false; } /// /// Returns whether there's an active hotspot (fire) on a certain tile. /// /// Grid Atmosphere where to get the tile /// Indices for the tile /// Whether the hotspot is active or not. public bool IsHotspotActive(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return false; return tileAtmosphere.Hotspot.Valid; } #endregion #region PipeNet Add public void AddPipeNet(PipeNet pipeNet) { if (!_mapManager.TryGetGrid(pipeNet.Grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { gridAtmosphere.PipeNets.Add(pipeNet); } } #endregion #region PipeNet Remove public void RemovePipeNet(PipeNet pipeNet) { if (!_mapManager.TryGetGrid(pipeNet.Grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { gridAtmosphere.PipeNets.Remove(pipeNet); } } #endregion #region AtmosDevice Add public bool AddAtmosDevice(AtmosDeviceComponent atmosDevice) { var grid = atmosDevice.Owner.Transform.GridID; if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return false; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { atmosDevice.JoinedGrid = grid; gridAtmosphere.AtmosDevices.Add(atmosDevice); return true; } return false; } #endregion #region AtmosDevice Remove public bool RemoveAtmosDevice(AtmosDeviceComponent atmosDevice) { if (atmosDevice.JoinedGrid == null) return false; var grid = atmosDevice.JoinedGrid.Value; if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return false; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere) && gridAtmosphere.AtmosDevices.Contains(atmosDevice)) { atmosDevice.JoinedGrid = null; gridAtmosphere.AtmosDevices.Remove(atmosDevice); return true; } return false; } #endregion #region Mixture Safety /// /// Checks whether a tile's gas mixture is probably safe. /// This only checks temperature and pressure, not gas composition. /// /// Coordinates where to get the tile. /// Whether the tile's gas mixture is probably safe. public bool IsTileMixtureProbablySafe(EntityCoordinates coordinates) { return IsMixtureProbablySafe(GetTileMixture(coordinates)); } /// /// Checks whether a tile's gas mixture is probably safe. /// This only checks temperature and pressure, not gas composition. /// /// Grid where to get the tile. /// Indices of the tile. /// Whether the tile's gas mixture is probably safe. public bool IsTileMixtureProbablySafe(GridId grid, Vector2i tile) { return IsMixtureProbablySafe(GetTileMixture(grid, tile)); } /// /// Checks whether a gas mixture is probably safe. /// This only checks temperature and pressure, not gas composition. /// /// Mixture to be checked. /// Whether the mixture is probably safe. public bool IsMixtureProbablySafe(GasMixture? air) { // Note that oxygen mix isn't checked, but survival boxes make that not necessary. if (air == null) return false; switch (air.Pressure) { case <= Atmospherics.WarningLowPressure: case >= Atmospherics.WarningHighPressure: return false; } switch (air.Temperature) { case <= 260: case >= 360: return false; } return true; } #endregion #region Fix Vacuum /// /// Attempts to fix a sudden vacuum by creating gas based on adjacent tiles. /// /// Coordinates where to get the tile. public void FixVacuum(EntityCoordinates coordinates) { if(TryGetGridAndTile(coordinates, out var tuple)) FixVacuum(tuple.Value.Grid, tuple.Value.Tile); } /// /// Attempts to fix a sudden vacuum by creating gas based on adjacent tiles. /// /// Grid where to get the tile. /// Indices of the tile. public void FixVacuum(GridId grid, Vector2i tile) { if (!_mapManager.TryGetGrid(grid, out var mapGrid)) return; if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { FixVacuum(gridAtmosphere, tile); return; } } public void FixVacuum(GridAtmosphereComponent gridAtmosphere, Vector2i tile) { if (!gridAtmosphere.Tiles.TryGetValue(tile, out var tileAtmosphere)) return; var adjacent = GetAdjacentTileMixtures(gridAtmosphere, tileAtmosphere, false, true).ToArray(); tileAtmosphere.Air = new GasMixture(GetVolumeForTiles(tileAtmosphere.GridIndex, 1)) {Temperature = Atmospherics.T20C}; // Return early, let's not cause any funny NaNs. if (adjacent.Length == 0) return; var ratio = 1f / adjacent.Length; var totalTemperature = 0f; foreach (var adj in adjacent) { totalTemperature += adj.Temperature; // Remove a bit of gas from the adjacent ratio... var mix = adj.RemoveRatio(ratio); // And merge it to the new tile air. Merge(tileAtmosphere.Air, mix); // Return removed gas to its original mixture. Merge(adj, mix); } // New temperature is the arithmetic mean of the sum of the adjacent temperatures... tileAtmosphere.Air.Temperature = totalTemperature / adjacent.Length; } public bool NeedsVacuumFixing(IMapGrid mapGrid, Vector2i indices) { var value = false; foreach (var airtightComponent in GetObstructingComponents(mapGrid, indices)) { value |= airtightComponent.FixVacuum; } return value; } #endregion #region Position Helpers public bool TryGetGridAndTile(MapCoordinates coordinates, [NotNullWhen(true)] out (GridId Grid, Vector2i Tile)? tuple) { if (!_mapManager.TryFindGridAt(coordinates, out var grid)) { tuple = null; return false; } tuple = (grid.Index, grid.TileIndicesFor(coordinates)); return true; } public bool TryGetGridAndTile(EntityCoordinates coordinates, [NotNullWhen(true)] out (GridId Grid, Vector2i Tile)? tuple) { if (!coordinates.IsValid(EntityManager)) { tuple = null; return false; } var gridId = coordinates.GetGridId(EntityManager); if (!_mapManager.TryGetGrid(gridId, out var grid)) { tuple = null; return false; } tuple = (gridId, grid.TileIndicesFor(coordinates)); return true; } public bool TryGetMapGrid(GridAtmosphereComponent gridAtmosphere, [NotNullWhen(true)] out IMapGrid? mapGrid) { if (gridAtmosphere.Owner.TryGetComponent(out IMapGridComponent? mapGridComponent)) { mapGrid = mapGridComponent.Grid; return true; } mapGrid = null; return false; } #endregion } }