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);