using System.Linq; 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 Robust.Shared.Map.Components; using Robust.Shared.Utility; namespace Content.Server.Atmos.EntitySystems; public partial class AtmosphereSystem { 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); } 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); } public bool HasAtmosphere(EntityUid gridUid) => _atmosQuery.HasComponent(gridUid); public bool SetSimulatedGrid(EntityUid gridUid, bool simulated) { var ev = new SetSimulatedGridMethodEvent(gridUid, simulated); RaiseLocalEvent(gridUid, ref ev); return ev.Handled; } public bool IsSimulatedGrid(EntityUid gridUid) { var ev = new IsSimulatedGridMethodEvent(gridUid); RaiseLocalEvent(gridUid, ref ev); return ev.Simulated; } public IEnumerable GetAllMixtures(EntityUid gridUid, bool excite = false) { var ev = new GetAllMixturesMethodEvent(gridUid, excite); RaiseLocalEvent(gridUid, ref ev); if(!ev.Handled) return Enumerable.Empty(); DebugTools.AssertNotNull(ev.Mixtures); return ev.Mixtures!; } public void InvalidateTile(Entity entity, Vector2i tile) { if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false)) entity.Comp.InvalidatedCoords.Add(tile); } 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 && 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; } 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); } 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 && 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; } public ReactionResult ReactTile(EntityUid gridId, Vector2i tile) { var ev = new ReactTileMethodEvent(gridId, tile); RaiseLocalEvent(gridId, ref ev); ev.Handled = true; return ev.Result; } public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, MapGridComponent? mapGridComp = null) { if (!Resolve(gridUid, ref mapGridComp, false)) return false; var data = GetAirtightData(gridUid, mapGridComp, tile); return data.BlockedDirections.IsFlagSet(directions); } 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; } public bool IsTileMixtureProbablySafe(Entity? grid, Entity map, Vector2i tile) { return IsMixtureProbablySafe(GetTileMixture(grid, map, tile)); } public float GetTileHeatCapacity(Entity? grid, Entity map, Vector2i tile) { return GetHeatCapacity(GetTileMixture(grid, map, tile) ?? GasMixture.SpaceGas); } public TileMixtureEnumerator GetAdjacentTileMixtures(Entity grid, Vector2i tile, bool includeBlocked = false, bool excite = false) { if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return TileMixtureEnumerator.Empty; return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile) ? TileMixtureEnumerator.Empty : new(atmosTile.AdjacentTiles); } 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); } 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); } public void HotspotExtinguish(EntityUid gridUid, Vector2i tile) { var ev = new HotspotExtinguishMethodEvent(gridUid, tile); RaiseLocalEvent(gridUid, ref ev); } 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; } public bool AddPipeNet(Entity grid, PipeNet pipeNet) { return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Add(pipeNet); } 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); } 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; } 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; } [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);