using System.Diagnostics; using Content.Server.Atmos.Components; using Content.Server.Atmos.Piping.Components; using Content.Server.NodeContainer.NodeGroups; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Reactions; using JetBrains.Annotations; using Robust.Shared.Map.Components; using Robust.Shared.Utility; namespace Content.Server.Atmos.EntitySystems; public partial class AtmosphereSystem { /* General API for interacting with AtmosphereSystem. If you feel like you're stepping on eggshells because you can't access things in AtmosphereSystem, consider adding a method here instead of making your own way to work around it. */ /// /// Gets the that an entity is contained within. /// /// The entity to get the mixture for. /// If true, will ignore mixtures that the entity is contained in /// (ex. lockers and cryopods) and just get the tile mixture. /// If true, will mark the tile as active for atmosphere processing. /// A if one could be found, null otherwise. [PublicAPI] public GasMixture? GetContainingMixture(Entity ent, bool ignoreExposed = false, bool excite = false) { if (!Resolve(ent, ref ent.Comp)) return null; return GetContainingMixture(ent, ent.Comp.GridUid, ent.Comp.MapUid, ignoreExposed, excite); } /// /// Gets the that an entity is contained within. /// /// The entity to get the mixture for. /// The grid that the entity may be on. /// The map that the entity may be on. /// If true, will ignore mixtures that the entity is contained in /// (ex. lockers and cryopods) and just get the tile mixture. /// If true, will mark the tile as active for atmosphere processing. /// A if one could be found, null otherwise. [PublicAPI] public GasMixture? GetContainingMixture( Entity ent, Entity? grid, Entity? map, bool ignoreExposed = false, bool excite = false) { if (!Resolve(ent, ref ent.Comp)) return null; if (!ignoreExposed && !ent.Comp.Anchored) { // Used for things like disposals/cryo to change which air people are exposed to. var ev = new AtmosExposedGetAirEvent((ent, ent.Comp), excite); RaiseLocalEvent(ent, ref ev); if (ev.Handled) return ev.Gas; // TODO ATMOS: recursively iterate up through parents // This really needs recursive InContainer metadata flag for performance // And ideally some fast way to get the innermost airtight container. } var position = _transformSystem.GetGridTilePositionOrDefault((ent, ent.Comp)); return GetTileMixture(grid, map, position, excite); } /// /// Checks if a grid has an atmosphere. /// /// The grid to check. /// True if the grid has an atmosphere, false otherwise. [PublicAPI] public bool HasAtmosphere(EntityUid gridUid) { return _atmosQuery.HasComponent(gridUid); } /// /// Sets whether a grid is simulated by Atmospherics. /// /// The grid to set. /// Whether the grid should be simulated. /// >True if the grid's simulated state was changed, false otherwise. [PublicAPI] public bool SetSimulatedGrid(EntityUid gridUid, bool simulated) { // TODO ATMOS this event literally has no subscribers. Did this just get silently refactored out? var ev = new SetSimulatedGridMethodEvent(gridUid, simulated); RaiseLocalEvent(gridUid, ref ev); return ev.Handled; } /// /// Checks whether a grid is simulated by Atmospherics. /// /// The grid to check. /// >True if the grid is simulated, false otherwise. public bool IsSimulatedGrid(EntityUid gridUid) { var ev = new IsSimulatedGridMethodEvent(gridUid); RaiseLocalEvent(gridUid, ref ev); return ev.Simulated; } /// /// Gets all s on a grid. /// /// The grid to get mixtures for. /// Whether to mark all tiles as active for atmosphere processing. /// An enumerable of all gas mixtures on the grid. [PublicAPI] public IEnumerable GetAllMixtures(EntityUid gridUid, bool excite = false) { var ev = new GetAllMixturesMethodEvent(gridUid, excite); RaiseLocalEvent(gridUid, ref ev); if (!ev.Handled) return []; DebugTools.AssertNotNull(ev.Mixtures); return ev.Mixtures!; } /// /// Invalidates a tile on a grid, marking it for revalidation. /// /// Frequently used tile data like are determined once and cached. /// If this tile's state changes, ex. being added or removed, then this position in the map needs to /// be updated. /// /// Tiles that need to be updated are marked as invalid and revalidated before all other /// processing stages. /// /// The grid entity. /// The tile to invalidate. [PublicAPI] public void InvalidateTile(Entity entity, Vector2i tile) { if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false)) entity.Comp.InvalidatedCoords.Add(tile); } /// /// Gets the gas mixtures for a list of tiles on a grid or map. /// /// The grid to get mixtures from. /// The map to get mixtures from. /// The list of tiles to get mixtures for. /// Whether to mark the tiles as active for atmosphere processing. /// >An array of gas mixtures corresponding to the input tiles. [PublicAPI] public GasMixture?[]? GetTileMixtures( Entity? grid, Entity? map, List tiles, bool excite = false) { GasMixture?[]? mixtures = null; var handled = false; // If we've been passed a grid, try to let it handle it. if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1)) { if (excite) Resolve(gridEnt, ref gridEnt.Comp2); handled = true; mixtures = new GasMixture?[tiles.Count]; for (var i = 0; i < tiles.Count; i++) { var tile = tiles[i]; if (!gridEnt.Comp1.Tiles.TryGetValue(tile, out var atmosTile)) { // need to get map atmosphere handled = false; continue; } mixtures[i] = atmosTile.Air; if (excite) { AddActiveTile(gridEnt.Comp1, atmosTile); InvalidateVisuals((gridEnt.Owner, gridEnt.Comp2), tile); } } } if (handled) return mixtures; // We either don't have a grid, or the event wasn't handled. // Let the map handle it instead, and also broadcast the event. if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp)) { mixtures ??= new GasMixture?[tiles.Count]; for (var i = 0; i < tiles.Count; i++) { mixtures[i] ??= mapEnt.Comp.Mixture; } return mixtures; } // Default to a space mixture... This is a space game, after all! mixtures ??= new GasMixture?[tiles.Count]; for (var i = 0; i < tiles.Count; i++) { mixtures[i] ??= GasMixture.SpaceGas; } return mixtures; } /// /// Gets the gas mixture for a specific tile that an entity is on. /// /// The entity to get the tile mixture for. /// Whether to mark the tile as active for atmosphere processing. /// A if one could be found, null otherwise. /// This does not return the that the entity /// may be contained in, ex. if the entity is currently in a locker/crate with its own /// . [PublicAPI] public GasMixture? GetTileMixture(Entity entity, bool excite = false) { if (!Resolve(entity.Owner, ref entity.Comp)) return null; var indices = _transformSystem.GetGridTilePositionOrDefault(entity); return GetTileMixture(entity.Comp.GridUid, entity.Comp.MapUid, indices, excite); } /// /// Gets the gas mixture for a specific tile on a grid or map. /// /// The grid to get the mixture from. /// The map to get the mixture from. /// The tile to get the mixture from. /// Whether to mark the tile as active for atmosphere processing. /// >A if one could be found, null otherwise. [PublicAPI] public GasMixture? GetTileMixture( Entity? grid, Entity? map, Vector2i gridTile, bool excite = false) { // If we've been passed a grid, try to let it handle it. if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1, false) && gridEnt.Comp1.Tiles.TryGetValue(gridTile, out var tile)) { if (excite) { AddActiveTile(gridEnt.Comp1, tile); InvalidateVisuals((grid.Value.Owner, grid.Value.Comp2), gridTile); } return tile.Air; } if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) return mapEnt.Comp.Mixture; // Default to a space mixture... This is a space game, after all! return GasMixture.SpaceGas; } /// /// Triggers a tile's to react. /// /// The grid to react the tile on. /// The tile to react. /// The result of the reaction. [PublicAPI] public ReactionResult ReactTile(EntityUid gridId, Vector2i tile) { var ev = new ReactTileMethodEvent(gridId, tile); RaiseLocalEvent(gridId, ref ev); ev.Handled = true; return ev.Result; } /// /// Checks if a tile on a grid is air-blocked in the specified directions. /// /// The grid to check. /// The tile on the grid to check. /// The directions to check for air-blockage. /// Optional map grid component associated with the grid. /// True if the tile is air-blocked in the specified directions, false otherwise. [PublicAPI] public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, MapGridComponent? mapGridComp = null) { if (!Resolve(gridUid, ref mapGridComp, false)) return false; // TODO ATMOS: This reconstructs the data instead of getting the cached version. Might want to include a method to get the cached version later. var data = GetAirtightData(gridUid, mapGridComp, tile); return data.BlockedDirections.IsFlagSet(directions); } /// /// Checks if a tile on a grid or map is space as defined by a tile's definition of space. /// Some tiles can hold back space and others cannot - for example, plating can hold /// back space, whereas scaffolding cannot, exposing the map atmosphere beneath. /// /// This does not check if the on the tile is space, /// it only checks the current tile's ability to hold back space. /// The grid to check. /// The map to check. /// The tile to check. /// True if the tile is space, false otherwise. [PublicAPI] public bool IsTileSpace(Entity? grid, Entity? map, Vector2i tile) { if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false) && gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos)) { return tileAtmos.Space; } if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) return mapEnt.Comp.Space; // If nothing handled the event, it'll default to true. // Oh well, this is a space game after all, deal with it! return true; } /// /// Checks if the gas mixture on a tile is "probably safe". /// Probably safe is defined as having at least air alarm-grade safe pressure and temperature. /// (more than 260K, less than 360K, and between safe low and high pressure as defined in /// and ) /// /// The grid to check. /// The map to check. /// The tile to check. /// True if the tile's mixture is probably safe, false otherwise. [PublicAPI] public bool IsTileMixtureProbablySafe(Entity? grid, Entity map, Vector2i tile) { return IsMixtureProbablySafe(GetTileMixture(grid, map, tile)); } /// /// Gets the heat capacity of the gas mixture on a tile. /// /// The grid to check. /// The map to check. /// The tile on the grid/map to check. /// >The heat capacity of the tile's mixture, or the heat capacity of space if a mixture could not be found. [PublicAPI] public float GetTileHeatCapacity(Entity? grid, Entity map, Vector2i tile) { return GetHeatCapacity(GetTileMixture(grid, map, tile) ?? GasMixture.SpaceGas); } /// /// Gets an enumerator for the adjacent tile mixtures of a tile on a grid. /// /// The grid to get adjacent tile mixtures from. /// The tile to get adjacent mixtures for. /// Whether to include blocked adjacent tiles. /// Whether to mark the adjacent tiles as active for atmosphere processing. /// An enumerator for the adjacent tile mixtures. [PublicAPI] public TileMixtureEnumerator GetAdjacentTileMixtures(Entity grid, Vector2i tile, bool includeBlocked = false, bool excite = false) { // TODO ATMOS includeBlocked and excite parameters are unhandled currently. if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return TileMixtureEnumerator.Empty; return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile) ? TileMixtureEnumerator.Empty : new TileMixtureEnumerator(atmosTile.AdjacentTiles); } /// /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met. /// /// The grid to expose the tile on. /// The tile to expose. /// The temperature of the hotspot to expose. /// You can think of this as exposing a temperature of a flame. /// The volume of the hotspot to expose. /// You can think of this as how big the flame is initially. /// Bigger flames will ramp a fire faster. /// Whether to "boost" a fire that's currently on the tile already. /// Does nothing if the tile isn't already a hotspot. /// This clamps the temperature and volume of the hotspot to the maximum /// of the provided parameters and whatever's on the tile. /// Entity that started the exposure for admin logging. [PublicAPI] public void HotspotExpose(Entity grid, Vector2i tile, float exposedTemperature, float exposedVolume, EntityUid? sparkSourceUid = null, bool soh = false) { if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return; if (grid.Comp.Tiles.TryGetValue(tile, out var atmosTile)) HotspotExpose(grid.Comp, atmosTile, exposedTemperature, exposedVolume, soh, sparkSourceUid); } /// /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met. /// /// The to expose. /// The temperature of the hotspot to expose. /// You can think of this as exposing a temperature of a flame. /// The volume of the hotspot to expose. /// You can think of this as how big the flame is initially. /// Bigger flames will ramp a fire faster. /// Whether to "boost" a fire that's currently on the tile already. /// Does nothing if the tile isn't already a hotspot. /// This clamps the temperature and volume of the hotspot to the maximum /// of the provided parameters and whatever's on the tile. /// Entity that started the exposure for admin logging. [PublicAPI] public void HotspotExpose(TileAtmosphere tile, float exposedTemperature, float exposedVolume, EntityUid? sparkSourceUid = null, bool soh = false) { if (!_atmosQuery.TryGetComponent(tile.GridIndex, out var atmos)) return; DebugTools.Assert(atmos.Tiles.TryGetValue(tile.GridIndices, out var tmp) && tmp == tile); HotspotExpose(atmos, tile, exposedTemperature, exposedVolume, soh, sparkSourceUid); } /// /// Extinguishes a hotspot on a tile. /// /// The grid to extinguish the hotspot on. /// The tile on the grid to extinguish the hotspot on. [PublicAPI] public void HotspotExtinguish(EntityUid gridUid, Vector2i tile) { var ev = new HotspotExtinguishMethodEvent(gridUid, tile); RaiseLocalEvent(gridUid, ref ev); } /// /// Checks if a hotspot is active on a tile. /// /// The grid to check. /// The tile on the grid to check. /// True if a hotspot is active on the tile, false otherwise. [PublicAPI] public bool IsHotspotActive(EntityUid gridUid, Vector2i tile) { var ev = new IsHotspotActiveMethodEvent(gridUid, tile); RaiseLocalEvent(gridUid, ref ev); // If not handled, this will be false. Just like in space! return ev.Result; } /// /// Adds a to a grid. /// /// The grid to add the pipe net to. /// The pipe net to add. /// True if the pipe net was added, false otherwise. [PublicAPI] public bool AddPipeNet(Entity grid, PipeNet pipeNet) { return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Add(pipeNet); } /// /// Removes a from a grid. /// /// The grid to remove the pipe net from. /// The pipe net to remove. /// True if the pipe net was removed, false otherwise. [PublicAPI] public bool RemovePipeNet(Entity grid, PipeNet pipeNet) { // Technically this event can be fired even on grids that don't // actually have grid atmospheres. if (pipeNet.Grid is not null) { var ev = new PipeNodeGroupRemovedEvent(grid, pipeNet.NetId); RaiseLocalEvent(ref ev); } return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet); } /// /// Adds an entity with an to a grid's list of atmos devices. /// /// The grid to add the device to. /// The device to add. /// True if the device was added, false otherwise. [PublicAPI] public bool AddAtmosDevice(Entity grid, Entity device) { DebugTools.Assert(device.Comp.JoinedGrid == null); DebugTools.Assert(Transform(device).GridUid == grid); if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return false; if (!grid.Comp.AtmosDevices.Add(device)) return false; device.Comp.JoinedGrid = grid; return true; } /// /// Removes an entity with an from a grid's list of atmos devices. /// /// The grid to remove the device from. /// The device to remove. /// True if the device was removed, false otherwise. public bool RemoveAtmosDevice(Entity grid, Entity device) { DebugTools.Assert(device.Comp.JoinedGrid == grid); if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return false; if (!grid.Comp.AtmosDevices.Remove(device)) return false; device.Comp.JoinedGrid = null; return true; } /// /// Adds an entity with a DeltaPressureComponent to the DeltaPressure processing list. /// Also fills in important information on the component itself. /// /// The grid to add the entity to. /// The entity to add. /// True if the entity was added to the list, false if it could not be added or /// if the entity was already present in the list. [PublicAPI] public bool TryAddDeltaPressureEntity(Entity grid, Entity ent) { // The entity needs to be part of a grid, and it should be the right one :) var xform = Transform(ent); // The entity is not on a grid, so it cannot possibly have an atmosphere that affects it. if (xform.GridUid == null) { return false; } // Entity should be on the grid it's being added to. Debug.Assert(xform.GridUid == grid.Owner); if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return false; if (grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner)) { return false; } grid.Comp.DeltaPressureEntityLookup[ent.Owner] = grid.Comp.DeltaPressureEntities.Count; grid.Comp.DeltaPressureEntities.Add(ent); ent.Comp.GridUid = grid.Owner; ent.Comp.InProcessingList = true; return true; } /// /// Removes an entity with a DeltaPressureComponent from the DeltaPressure processing list. /// /// The grid to remove the entity from. /// The entity to remove. /// True if the entity was removed from the list, false if it could not be removed or /// if the entity was not present in the list. [PublicAPI] public bool TryRemoveDeltaPressureEntity(Entity grid, Entity ent) { if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return false; if (!grid.Comp.DeltaPressureEntityLookup.TryGetValue(ent.Owner, out var index)) return false; var lastIndex = grid.Comp.DeltaPressureEntities.Count - 1; if (lastIndex < 0) return false; if (index != lastIndex) { var lastEnt = grid.Comp.DeltaPressureEntities[lastIndex]; grid.Comp.DeltaPressureEntities[index] = lastEnt; grid.Comp.DeltaPressureEntityLookup[lastEnt.Owner] = index; } grid.Comp.DeltaPressureEntities.RemoveAt(lastIndex); grid.Comp.DeltaPressureEntityLookup.Remove(ent.Owner); if (grid.Comp.DeltaPressureCursor > grid.Comp.DeltaPressureEntities.Count) grid.Comp.DeltaPressureCursor = grid.Comp.DeltaPressureEntities.Count; ent.Comp.InProcessingList = false; ent.Comp.GridUid = null; return true; } /// /// Checks if a DeltaPressureComponent is currently considered for processing on a grid. /// /// The grid that the entity may belong to. /// The entity to check. /// True if the entity is part of the processing list, false otherwise. [PublicAPI] public bool IsDeltaPressureEntityInList(Entity grid, Entity ent) { // Dict and list must be in sync - deep-fried if we aren't. if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return false; var contains = grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner); Debug.Assert(contains == grid.Comp.DeltaPressureEntities.Contains(ent)); return contains; } [ByRefEvent] private record struct SetSimulatedGridMethodEvent( EntityUid Grid, bool Simulated, bool Handled = false); [ByRefEvent] private record struct IsSimulatedGridMethodEvent( EntityUid Grid, bool Simulated = false, bool Handled = false); [ByRefEvent] private record struct GetAllMixturesMethodEvent( EntityUid Grid, bool Excite = false, IEnumerable? Mixtures = null, bool Handled = false); [ByRefEvent] private record struct ReactTileMethodEvent( EntityUid GridId, Vector2i Tile, ReactionResult Result = default, bool Handled = false); [ByRefEvent] private record struct HotspotExtinguishMethodEvent( EntityUid Grid, Vector2i Tile, bool Handled = false); [ByRefEvent] private record struct IsHotspotActiveMethodEvent( EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false); } /// /// Raised broadcasted when a pipe node group within a grid has been removed. /// /// The grid with the removed node group. /// The net id of the removed node group. [ByRefEvent] public record struct PipeNodeGroupRemovedEvent(EntityUid Grid, int NetId);