[AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state",
};
- private Dictionary<Gas, string> _gasShorthands = new Dictionary<Gas, string>()
- {
- [Gas.Ammonia] = "NH₃",
- [Gas.CarbonDioxide] = "CO₂",
- [Gas.Frezon] = "F",
- [Gas.Nitrogen] = "N₂",
- [Gas.NitrousOxide] = "N₂O",
- [Gas.Oxygen] = "O₂",
- [Gas.Plasma] = "P",
- [Gas.Tritium] = "T",
- [Gas.WaterVapor] = "H₂O",
- };
-
public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates)
{
RobustXamlLoader.Load(this);
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
FixedPoint2 gasPercent = percent * 100f;
-
- var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X");
+ var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasLabel = new Label()
{
- Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
FontOverride = normalFont,
FontColorOverride = GetAlarmStateColor(alert),
HorizontalAlignment = HAlignment.Center,
--- /dev/null
+using Content.Shared.Atmos.Components;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed class AtmosMonitoringConsoleBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AtmosMonitoringConsoleWindow? _menu;
+
+ public AtmosMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new AtmosMonitoringConsoleWindow(this, Owner);
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not AtmosMonitoringConsoleBoundInterfaceState castState)
+ return;
+
+ EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
+ _menu?.UpdateUI(xform?.Coordinates, castState.AtmosNetworks);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
--- /dev/null
+using Content.Client.Pinpointer.UI;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Pinpointer;
+using Robust.Client.Graphics;
+using Robust.Shared.Collections;
+using Robust.Shared.Map.Components;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ public bool ShowPipeNetwork = true;
+ public int? FocusNetId = null;
+
+ private const int ChunkSize = 4;
+
+ private readonly Color _basePipeNetColor = Color.LightGray;
+ private readonly Color _unfocusedPipeNetColor = Color.DimGray;
+
+ private List<AtmosMonitoringConsoleLine> _atmosPipeNetwork = new();
+ private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
+
+ // Look up tables for merging continuous lines. Indexed by line color
+ private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLines = new();
+ private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLinesReversed = new();
+ private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLines = new();
+ private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLinesReversed = new();
+
+ public AtmosMonitoringConsoleNavMapControl() : base()
+ {
+ PostWallDrawingAction += DrawAllPipeNetworks;
+ }
+
+ protected override void UpdateNavMap()
+ {
+ base.UpdateNavMap();
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(Owner, out var console))
+ return;
+
+ if (!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
+ return;
+
+ _atmosPipeNetwork = GetDecodedAtmosPipeChunks(console.AtmosPipeChunks, grid);
+ }
+
+ private void DrawAllPipeNetworks(DrawingHandleScreen handle)
+ {
+ if (!ShowPipeNetwork)
+ return;
+
+ // Draw networks
+ if (_atmosPipeNetwork != null && _atmosPipeNetwork.Any())
+ DrawPipeNetwork(handle, _atmosPipeNetwork);
+ }
+
+ private void DrawPipeNetwork(DrawingHandleScreen handle, List<AtmosMonitoringConsoleLine> atmosPipeNetwork)
+ {
+ var offset = GetOffset();
+ offset = offset with { Y = -offset.Y };
+
+ if (WorldRange / WorldMaxRange > 0.5f)
+ {
+ var pipeNetworks = new Dictionary<Color, ValueList<Vector2>>();
+
+ foreach (var chunkedLine in atmosPipeNetwork)
+ {
+ var start = ScalePosition(chunkedLine.Origin - offset);
+ var end = ScalePosition(chunkedLine.Terminus - offset);
+
+ if (!pipeNetworks.TryGetValue(chunkedLine.Color, out var subNetwork))
+ subNetwork = new ValueList<Vector2>();
+
+ subNetwork.Add(start);
+ subNetwork.Add(end);
+
+ pipeNetworks[chunkedLine.Color] = subNetwork;
+ }
+
+ foreach ((var color, var subNetwork) in pipeNetworks)
+ {
+ if (subNetwork.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, subNetwork.Span, color);
+ }
+ }
+
+ else
+ {
+ var pipeVertexUVs = new Dictionary<Color, ValueList<Vector2>>();
+
+ foreach (var chunkedLine in atmosPipeNetwork)
+ {
+ var leftTop = ScalePosition(new Vector2
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ - offset);
+
+ var rightTop = ScalePosition(new Vector2
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ - offset);
+
+ var leftBottom = ScalePosition(new Vector2
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ - offset);
+
+ var rightBottom = ScalePosition(new Vector2
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ - offset);
+
+ if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
+ pipeVertexUV = new ValueList<Vector2>();
+
+ pipeVertexUV.Add(leftBottom);
+ pipeVertexUV.Add(leftTop);
+ pipeVertexUV.Add(rightBottom);
+ pipeVertexUV.Add(leftTop);
+ pipeVertexUV.Add(rightBottom);
+ pipeVertexUV.Add(rightTop);
+
+ pipeVertexUVs[chunkedLine.Color] = pipeVertexUV;
+ }
+
+ foreach ((var color, var pipeVertexUV) in pipeVertexUVs)
+ {
+ if (pipeVertexUV.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, pipeVertexUV.Span, color);
+ }
+ }
+ }
+
+ private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Vector2i, AtmosPipeChunk>? chunks, MapGridComponent? grid)
+ {
+ var decodedOutput = new List<AtmosMonitoringConsoleLine>();
+
+ if (chunks == null || grid == null)
+ return decodedOutput;
+
+ // Clear stale look up table values
+ _horizLines.Clear();
+ _horizLinesReversed.Clear();
+ _vertLines.Clear();
+ _vertLinesReversed.Clear();
+
+ // Generate masks
+ var northMask = (ulong)1 << 0;
+ var southMask = (ulong)1 << 1;
+ var westMask = (ulong)1 << 2;
+ var eastMask = (ulong)1 << 3;
+
+ foreach ((var chunkOrigin, var chunk) in chunks)
+ {
+ var list = new List<AtmosMonitoringConsoleLine>();
+
+ foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
+ {
+ // Determine the correct coloration for the pipe
+ var color = Color.FromHex(hexColor) * _basePipeNetColor;
+
+ if (FocusNetId != null && FocusNetId != netId)
+ color *= _unfocusedPipeNetColor;
+
+ // Get the associated line look up tables
+ if (!_horizLines.TryGetValue(color, out var horizLines))
+ {
+ horizLines = new();
+ _horizLines[color] = horizLines;
+ }
+
+ if (!_horizLinesReversed.TryGetValue(color, out var horizLinesReversed))
+ {
+ horizLinesReversed = new();
+ _horizLinesReversed[color] = horizLinesReversed;
+ }
+
+ if (!_vertLines.TryGetValue(color, out var vertLines))
+ {
+ vertLines = new();
+ _vertLines[color] = vertLines;
+ }
+
+ if (!_vertLinesReversed.TryGetValue(color, out var vertLinesReversed))
+ {
+ vertLinesReversed = new();
+ _vertLinesReversed[color] = vertLinesReversed;
+ }
+
+ // Loop over the chunk
+ for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
+ {
+ if (atmosPipeData == 0)
+ continue;
+
+ var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
+
+ if ((atmosPipeData & mask) == 0)
+ continue;
+
+ var relativeTile = GetTileFromIndex(tileIdx);
+ var tile = (chunk.Origin * ChunkSize + relativeTile) * grid.TileSize;
+ tile = tile with { Y = -tile.Y };
+
+ // Calculate the draw point offsets
+ var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
+ new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+
+ var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
+ new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+
+ var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
+ new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+
+ var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
+ new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+
+ // Since we can have pipe lines that have a length of a half tile,
+ // double the vectors and convert to vector2i so we can merge them
+ AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
+ AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
+ }
+ }
+ }
+
+ // Scale the vector2is back down and convert to vector2
+ foreach (var (color, horizLines) in _horizLines)
+ {
+ // Get the corresponding sRBG color
+ var sRGB = GetsRGBColor(color);
+
+ foreach (var (origin, terminal) in horizLines)
+ decodedOutput.Add(new AtmosMonitoringConsoleLine
+ (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
+ }
+
+ foreach (var (color, vertLines) in _vertLines)
+ {
+ // Get the corresponding sRBG color
+ var sRGB = GetsRGBColor(color);
+
+ foreach (var (origin, terminal) in vertLines)
+ decodedOutput.Add(new AtmosMonitoringConsoleLine
+ (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
+ }
+
+ return decodedOutput;
+ }
+
+ private Vector2 ConvertVector2iToVector2(Vector2i vector, float scale = 1f)
+ {
+ return new Vector2(vector.X * scale, vector.Y * scale);
+ }
+
+ private Vector2i ConvertVector2ToVector2i(Vector2 vector, float scale = 1f)
+ {
+ return new Vector2i((int)MathF.Round(vector.X * scale), (int)MathF.Round(vector.Y * scale));
+ }
+
+ private Vector2i GetTileFromIndex(int index)
+ {
+ var x = index / ChunkSize;
+ var y = index % ChunkSize;
+ return new Vector2i(x, y);
+ }
+
+ private Color GetsRGBColor(Color color)
+ {
+ if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
+ {
+ sRGB = Color.ToSrgb(color);
+ _sRGBLookUp[color] = sRGB;
+ }
+
+ return sRGB;
+ }
+}
+
+public struct AtmosMonitoringConsoleLine
+{
+ public readonly Vector2 Origin;
+ public readonly Vector2 Terminus;
+ public readonly Color Color;
+
+ public AtmosMonitoringConsoleLine(Vector2 origin, Vector2 terminus, Color color)
+ {
+ Origin = origin;
+ Terminus = terminus;
+ Color = color;
+ }
+}
--- /dev/null
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Consoles;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentHandleState>(OnHandleState);
+ }
+
+ private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
+ {
+ Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
+ Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
+
+ switch (args.Current)
+ {
+ case AtmosMonitoringConsoleDeltaState delta:
+ {
+ modifiedChunks = delta.ModifiedChunks;
+ atmosDevices = delta.AtmosDevices;
+
+ foreach (var index in component.AtmosPipeChunks.Keys)
+ {
+ if (!delta.AllChunks!.Contains(index))
+ component.AtmosPipeChunks.Remove(index);
+ }
+
+ break;
+ }
+
+ case AtmosMonitoringConsoleState state:
+ {
+ modifiedChunks = state.Chunks;
+ atmosDevices = state.AtmosDevices;
+
+ foreach (var index in component.AtmosPipeChunks.Keys)
+ {
+ if (!state.Chunks.ContainsKey(index))
+ component.AtmosPipeChunks.Remove(index);
+ }
+
+ break;
+ }
+ default:
+ return;
+ }
+
+ foreach (var (origin, chunk) in modifiedChunks)
+ {
+ var newChunk = new AtmosPipeChunk(origin);
+ newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
+
+ component.AtmosPipeChunks[origin] = newChunk;
+ }
+
+ component.AtmosDevices.Clear();
+
+ foreach (var (nuid, atmosDevice) in atmosDevices)
+ {
+ component.AtmosDevices[nuid] = atmosDevice;
+ }
+ }
+}
--- /dev/null
+<controls:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:ui="clr-namespace:Content.Client.Atmos.Consoles"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ Title="{Loc 'atmos-monitoring-window-title'}"
+ Resizable="False"
+ SetSize="1120 750"
+ MinSize="1120 750">
+ <BoxContainer Orientation="Vertical">
+ <!-- Main display -->
+ <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
+ <!-- Nav map -->
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
+ <ui:AtmosMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
+
+ <!-- System warning -->
+ <PanelContainer Name="SystemWarningPanel"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Top"
+ HorizontalExpand="True"
+ Margin="0 48 0 0"
+ Visible="False">
+ <RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
+ </PanelContainer>
+
+ </ui:AtmosMonitoringConsoleNavMapControl>
+
+ <!-- Nav map legend -->
+ <BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_square.png"
+ Modulate="#a9a9a9"
+ SetSize="16 16"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'atmos-monitoring-window-label-gas-opening'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
+ SetSize="16 16"
+ Modulate="#a9a9a9"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'atmos-monitoring-window-label-gas-scrubber'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_arrow_east.png"
+ SetSize="16 16"
+ Modulate="#a9a9a9"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'atmos-monitoring-window-label-gas-flow-regulator'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
+ SetSize="16 16"
+ Modulate="#a9a9a9"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'atmos-monitoring-window-label-thermoregulator'}"/>
+ </BoxContainer>
+ </BoxContainer>
+
+ <!-- Atmosphere status -->
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
+
+ <!-- Station name -->
+ <controls:StripeBack>
+ <PanelContainer>
+ <RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
+ </PanelContainer>
+ </controls:StripeBack>
+
+ <!-- Alarm status (entries added by C# code) -->
+ <TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
+ <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+ <BoxContainer Name="AtmosNetworksTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
+ </ScrollContainer>
+ </TabContainer>
+
+ <!-- Overlay toggles -->
+ <BoxContainer Orientation="Vertical" Margin="0 10 0 0">
+ <Label Text="{Loc 'atmos-monitoring-window-toggle-overlays'}" Margin="0 0 0 5"/>
+ <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+ <CheckBox Name="ShowPipeNetwork" Text="{Loc 'atmos-monitoring-window-show-pipe-network'}" Pressed="True" HorizontalExpand="True"/>
+ <CheckBox Name="ShowGasPipeSensors" Text="{Loc 'atmos-monitoring-window-show-gas-pipe-sensors'}" Pressed="False" HorizontalExpand="True"/>
+ </BoxContainer>
+ </BoxContainer>
+
+ </BoxContainer>
+
+ </BoxContainer>
+
+ <!-- Footer -->
+ <BoxContainer Orientation="Vertical">
+ <PanelContainer StyleClasses="LowDivider" />
+ <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+ <Label Text="{Loc 'atmos-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
+ <Label Text="{Loc 'atmos-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
+ HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
+ <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+ VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+</controls:FancyWindow>
--- /dev/null
+using Content.Client.Pinpointer.UI;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
+{
+ private readonly IEntityManager _entManager;
+ private readonly IPrototypeManager _protoManager;
+ private readonly SpriteSystem _spriteSystem;
+
+ private EntityUid? _owner;
+ private NetEntity? _focusEntity;
+ private int? _focusNetId;
+
+ private bool _autoScrollActive = false;
+
+ private readonly Color _unfocusedDeviceColor = Color.DimGray;
+ private ProtoId<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
+ private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
+
+ public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve<IEntityManager>();
+ _protoManager = IoCManager.Resolve<IPrototypeManager>();
+ _spriteSystem = _entManager.System<SpriteSystem>();
+
+ // Pass the owner to nav map
+ _owner = owner;
+ NavMap.Owner = _owner;
+
+ // Set nav map grid uid
+ var stationName = Loc.GetString("atmos-monitoring-window-unknown-location");
+ EntityCoordinates? consoleCoords = null;
+
+ if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
+ {
+ consoleCoords = xform.Coordinates;
+ NavMap.MapUid = xform.GridUid;
+
+ // Assign station name
+ if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
+ stationName = stationMetaData.EntityName;
+
+ var msg = new FormattedMessage();
+ msg.TryAddMarkup(Loc.GetString("atmos-monitoring-window-station-name", ("stationName", stationName)), out _);
+
+ StationName.SetMessage(msg);
+ }
+
+ else
+ {
+ StationName.SetMessage(stationName);
+ NavMap.Visible = false;
+ }
+
+ // Set trackable entity selected action
+ NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
+
+ // Update nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Set tab container headers
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-monitoring-window-tab-networks"));
+
+ // Set UI toggles
+ ShowPipeNetwork.OnToggled += _ => OnShowPipeNetworkToggled();
+ ShowGasPipeSensors.OnToggled += _ => OnShowGasPipeSensors();
+
+ // Set nav map colors
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
+ return;
+
+ NavMap.TileColor = console.NavMapTileColor;
+ NavMap.WallColor = console.NavMapWallColor;
+
+ // Initalize
+ UpdateUI(consoleCoords, Array.Empty<AtmosMonitoringConsoleEntry>());
+ }
+
+ #region Toggle handling
+
+ private void OnShowPipeNetworkToggled()
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
+ return;
+
+ NavMap.ShowPipeNetwork = ShowPipeNetwork.Pressed;
+
+ foreach (var (netEnt, device) in console.AtmosDevices)
+ {
+ if (device.NavMapBlip == _gasPipeSensorProtoId)
+ continue;
+
+ if (ShowPipeNetwork.Pressed)
+ AddTrackedEntityToNavMap(device);
+
+ else
+ NavMap.TrackedEntities.Remove(netEnt);
+ }
+ }
+
+ private void OnShowGasPipeSensors()
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
+ return;
+
+ foreach (var (netEnt, device) in console.AtmosDevices)
+ {
+ if (device.NavMapBlip != _gasPipeSensorProtoId)
+ continue;
+
+ if (ShowGasPipeSensors.Pressed)
+ AddTrackedEntityToNavMap(device, true);
+
+ else
+ NavMap.TrackedEntities.Remove(netEnt);
+ }
+ }
+
+ #endregion
+
+ public void UpdateUI
+ (EntityCoordinates? consoleCoords,
+ AtmosMonitoringConsoleEntry[] atmosNetworks)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
+ return;
+
+ // Reset nav map values
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ if (_focusEntity != null && !console.AtmosDevices.Any(x => x.Key == _focusEntity))
+ ClearFocus();
+
+ // Add tracked entities to the nav map
+ UpdateNavMapBlips();
+
+ // Show the monitor location
+ var consoleNetEnt = _entManager.GetNetEntity(_owner);
+
+ if (consoleCoords != null && consoleNetEnt != null)
+ {
+ var proto = _protoManager.Index(_navMapConsoleProtoId);
+
+ if (proto.TexturePaths != null && proto.TexturePaths.Length != 0)
+ {
+ var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(proto.TexturePaths[0]));
+ var blip = new NavMapBlip(consoleCoords.Value, texture, proto.Color, proto.Blinks, proto.Selectable);
+ NavMap.TrackedEntities[consoleNetEnt.Value] = blip;
+ }
+ }
+
+ // Update the nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Clear excess children from the tables
+ while (AtmosNetworksTable.ChildCount > atmosNetworks.Length)
+ AtmosNetworksTable.RemoveChild(AtmosNetworksTable.GetChild(AtmosNetworksTable.ChildCount - 1));
+
+ // Update all entries in each table
+ for (int index = 0; index < atmosNetworks.Length; index++)
+ {
+ var entry = atmosNetworks.ElementAt(index);
+ UpdateUIEntry(entry, index, AtmosNetworksTable, console);
+ }
+ }
+
+ private void UpdateNavMapBlips()
+ {
+ if (_owner == null || !_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
+ return;
+
+ if (NavMap.Visible)
+ {
+ foreach (var (netEnt, device) in console.AtmosDevices)
+ {
+ // Update the focus network ID, incase it has changed
+ if (_focusEntity == netEnt)
+ {
+ _focusNetId = device.NetId;
+ NavMap.FocusNetId = _focusNetId;
+ }
+
+ var isSensor = device.NavMapBlip == _gasPipeSensorProtoId;
+
+ // Skip network devices if the toggled is off
+ if (!ShowPipeNetwork.Pressed && !isSensor)
+ continue;
+
+ // Skip gas pipe sensors if the toggle is off
+ if (!ShowGasPipeSensors.Pressed && isSensor)
+ continue;
+
+ AddTrackedEntityToNavMap(device, isSensor);
+ }
+ }
+ }
+
+ private void AddTrackedEntityToNavMap(AtmosDeviceNavMapData metaData, bool isSensor = false)
+ {
+ var proto = _protoManager.Index(metaData.NavMapBlip);
+
+ if (proto.TexturePaths == null || proto.TexturePaths.Length == 0)
+ return;
+
+ var idx = Math.Clamp((int)metaData.Direction / 2, 0, proto.TexturePaths.Length - 1);
+ var texture = proto.TexturePaths.Length > 0 ? proto.TexturePaths[idx] : proto.TexturePaths[0];
+ var color = isSensor ? proto.Color : proto.Color * metaData.PipeColor;
+
+ if (_focusNetId != null && metaData.NetId != _focusNetId)
+ color *= _unfocusedDeviceColor;
+
+ var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
+ var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+ var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
+ NavMap.TrackedEntities[metaData.NetEntity] = blip;
+ }
+
+ private void UpdateUIEntry(AtmosMonitoringConsoleEntry data, int index, Control table, AtmosMonitoringConsoleComponent console)
+ {
+ // Make new UI entry if required
+ if (index >= table.ChildCount)
+ {
+ var newEntryContainer = new AtmosMonitoringEntryContainer(data);
+
+ // On click
+ newEntryContainer.FocusButton.OnButtonUp += args =>
+ {
+ if (_focusEntity == newEntryContainer.Data.NetEntity)
+ {
+ ClearFocus();
+ }
+
+ else
+ {
+ SetFocus(newEntryContainer.Data.NetEntity, newEntryContainer.Data.NetId);
+
+ var coords = _entManager.GetCoordinates(newEntryContainer.Data.Coordinates);
+ NavMap.CenterToCoordinates(coords);
+ }
+
+ // Update affected UI elements across all tables
+ UpdateConsoleTable(console, AtmosNetworksTable, _focusEntity);
+ };
+
+ // Add the entry to the current table
+ table.AddChild(newEntryContainer);
+ }
+
+ // Update values and UI elements
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not AtmosMonitoringEntryContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateUIEntry(data, index, table, console);
+
+ return;
+ }
+
+ var entryContainer = (AtmosMonitoringEntryContainer)tableChild;
+ entryContainer.UpdateEntry(data, data.NetEntity == _focusEntity);
+ }
+
+ private void UpdateConsoleTable(AtmosMonitoringConsoleComponent console, Control table, NetEntity? currTrackedEntity)
+ {
+ foreach (var tableChild in table.Children)
+ {
+ if (tableChild is not AtmosAlarmEntryContainer)
+ continue;
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ if (entryContainer.NetEntity != currTrackedEntity)
+ entryContainer.RemoveAsFocus();
+
+ else if (entryContainer.NetEntity == currTrackedEntity)
+ entryContainer.SetAsFocus();
+ }
+ }
+
+ private void SetTrackedEntityFromNavMap(NetEntity? focusEntity)
+ {
+ if (focusEntity == null)
+ return;
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
+ return;
+
+ foreach (var (netEnt, device) in console.AtmosDevices)
+ {
+ if (netEnt != focusEntity)
+ continue;
+
+ if (device.NavMapBlip != _gasPipeSensorProtoId)
+ return;
+
+ // Set new focus
+ SetFocus(focusEntity.Value, device.NetId);
+
+ // Get the scroll position of the selected entity on the selected button the UI
+ ActivateAutoScrollToFocus();
+
+ break;
+ }
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ AutoScrollToFocus();
+ }
+
+ private void ActivateAutoScrollToFocus()
+ {
+ _autoScrollActive = true;
+ }
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = AtmosNetworksTable.Parent as ScrollContainer;
+ if (scroll == null)
+ return;
+
+ if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
+ return;
+
+ if (!TryGetNextScrollPosition(out float? nextScrollPosition))
+ return;
+
+ vScrollbar.ValueTarget = nextScrollPosition.Value;
+
+ if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
+ _autoScrollActive = false;
+ }
+
+ private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
+ {
+ vScrollBar = null;
+
+ foreach (var control in scroll.Children)
+ {
+ if (control is not VScrollBar)
+ continue;
+
+ vScrollBar = (VScrollBar)control;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = AtmosNetworksTable.Parent as ScrollContainer;
+ if (scroll == null)
+ return false;
+
+ var container = scroll.Children.ElementAt(0) as BoxContainer;
+ if (container == null || container.Children.Count() == 0)
+ return false;
+
+ // Exit if the heights of the children haven't been initialized yet
+ if (!container.Children.Any(x => x.Height > 0))
+ return false;
+
+ nextScrollPosition = 0;
+
+ foreach (var control in container.Children)
+ {
+ if (control is not AtmosMonitoringEntryContainer)
+ continue;
+
+ var entry = (AtmosMonitoringEntryContainer)control;
+
+ if (entry.Data.NetEntity == _focusEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ // Failed to find control
+ nextScrollPosition = null;
+
+ return false;
+ }
+
+ private void SetFocus(NetEntity focusEntity, int focusNetId)
+ {
+ _focusEntity = focusEntity;
+ _focusNetId = focusNetId;
+ NavMap.FocusNetId = focusNetId;
+
+ OnFocusChanged();
+ }
+
+ private void ClearFocus()
+ {
+ _focusEntity = null;
+ _focusNetId = null;
+ NavMap.FocusNetId = null;
+
+ OnFocusChanged();
+ }
+
+ private void OnFocusChanged()
+ {
+ UpdateNavMapBlips();
+ NavMap.ForceNavMapUpdate();
+
+ if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
+ return;
+
+ for (int index = 0; index < AtmosNetworksTable.ChildCount; index++)
+ {
+ var entry = (AtmosMonitoringEntryContainer)AtmosNetworksTable.GetChild(index);
+
+ if (entry == null)
+ continue;
+
+ UpdateUIEntry(entry.Data, index, AtmosNetworksTable, console);
+ }
+ }
+}
--- /dev/null
+<BoxContainer xmlns="https://spacestation14.io"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:s="clr-namespace:Content.Client.Stylesheets"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ Orientation="Vertical" HorizontalExpand ="True" Margin="0 0 0 3">
+
+ <!-- Network selection button -->
+ <Button Name="FocusButton" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 6 8" StyleClasses="OpenLeft" Access="Public">
+ <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+ <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal" SetHeight="32">
+ <PanelContainer Name="NetworkColorStripe" HorizontalAlignment="Left" SetWidth="8" VerticalExpand="True" Margin="-8 -2 0 0">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#d7d7d7"/>
+ </PanelContainer.PanelOverride>
+ </PanelContainer>
+ <Label Name="NetworkNameLabel" Text="???" HorizontalExpand="True" HorizontalAlignment="Center"/>
+ </BoxContainer>
+
+ <!-- Panel that appears on selecting the device -->
+
+ <PanelContainer HorizontalExpand="True" Margin="-8 0 -14 -4" Access="Public">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#25252a"/>
+ </PanelContainer.PanelOverride>
+ <BoxContainer Name="MainDataContainer" HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+ <Control>
+ <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+ <BoxContainer HorizontalExpand="True" Orientation="Horizontal">
+ <Label Name="TemperatureHeaderLabel" Text="{Loc 'atmos-alerts-window-temperature-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ <Label Name="PressureHeaderLabel" Text="{Loc 'atmos-alerts-window-pressure-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ <Label Name="TotalMolHeaderLabel" Text="{Loc 'atmos-alerts-window-total-mol-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ </BoxContainer>
+ <PanelContainer HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#202023"/>
+ </PanelContainer.PanelOverride>
+ <BoxContainer HorizontalExpand="True" Orientation="Horizontal">
+ <Label Name="TemperatureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ <Label Name="PressureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ <Label Name="TotalMolLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+ </BoxContainer>
+ </PanelContainer>
+ <BoxContainer HorizontalExpand="True" Orientation="Horizontal" Margin="8 0">
+ <TextureRect Name="ArrowTexture" VerticalAlignment="Center" SetSize="12 12" Stretch="KeepAspectCentered" Margin="3 0" TexturePath="/Textures/Interface/Nano/triangle_right.png"></TextureRect>
+ <Label Name="GasesHeaderLabel" Text="{Loc 'atmos-monitoring-window-label-gases'}" HorizontalAlignment="Left" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="4 0 0 0" SetHeight="24"></Label>
+ </BoxContainer>
+
+ </BoxContainer>
+ </Control>
+
+ <!-- Atmosphere status -->
+ <Control Name="FocusContainer" ReservesSpace="False" Visible="False">
+ <!-- Main container for displaying atmospheric data -->
+ <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+ <PanelContainer HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#202023"/>
+ </PanelContainer.PanelOverride>
+
+ <!-- Gas entries added via C# code -->
+ <GridContainer Name="GasGridContainer" HorizontalExpand="True" Columns = "4"></GridContainer>
+ </PanelContainer>
+ </BoxContainer>
+ </Control>
+ </BoxContainer>
+
+ <!-- If the alarm is inactive, this is label is displayed instead -->
+ <Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
+
+ </PanelContainer>
+ </BoxContainer>
+ </Button>
+</BoxContainer>
--- /dev/null
+using Content.Client.Stylesheets;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.FixedPoint;
+using Content.Shared.Temperature;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
+{
+ public AtmosMonitoringConsoleEntry Data;
+
+ private readonly IEntityManager _entManager;
+ private readonly IResourceCache _cache;
+
+ public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve<IEntityManager>();
+ _cache = IoCManager.Resolve<IResourceCache>();
+
+ Data = data;
+
+ // Modulate colored stripe
+ NetworkColorStripe.Modulate = data.Color;
+
+ // Load fonts
+ var headerFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11);
+ var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+
+ // Set fonts
+ TemperatureHeaderLabel.FontOverride = headerFont;
+ PressureHeaderLabel.FontOverride = headerFont;
+ TotalMolHeaderLabel.FontOverride = headerFont;
+ GasesHeaderLabel.FontOverride = headerFont;
+
+ TemperatureLabel.FontOverride = normalFont;
+ PressureLabel.FontOverride = normalFont;
+ TotalMolLabel.FontOverride = normalFont;
+
+ NoDataLabel.FontOverride = headerFont;
+ }
+
+ public void UpdateEntry(AtmosMonitoringConsoleEntry updatedData, bool isFocus)
+ {
+ // Load fonts
+ var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+
+ // Update name and values
+ if (!string.IsNullOrEmpty(updatedData.Address))
+ NetworkNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", updatedData.EntityName), ("address", updatedData.Address));
+
+ else
+ NetworkNameLabel.Text = Loc.GetString(updatedData.EntityName);
+
+ Data = updatedData;
+
+ // Modulate colored stripe
+ NetworkColorStripe.Modulate = Data.Color;
+
+ // Focus updates
+ if (isFocus)
+ SetAsFocus();
+ else
+ RemoveAsFocus();
+
+ // Check if powered
+ if (!updatedData.IsPowered)
+ {
+ MainDataContainer.Visible = false;
+ NoDataLabel.Visible = true;
+
+ return;
+ }
+
+ // Set container visibility
+ MainDataContainer.Visible = true;
+ NoDataLabel.Visible = false;
+
+ // Update temperature
+ var isNotVacuum = updatedData.TotalMolData > 1e-6f;
+ var tempK = (FixedPoint2)updatedData.TemperatureData;
+ var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float());
+
+ TemperatureLabel.Text = isNotVacuum ?
+ Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)) :
+ Loc.GetString("atmos-alerts-window-invalid-value");
+
+ TemperatureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
+
+ // Update pressure
+ PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)updatedData.PressureData));
+ PressureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
+
+ // Update total mol
+ TotalMolLabel.Text = Loc.GetString("atmos-alerts-window-total-mol-value", ("value", (FixedPoint2)updatedData.TotalMolData));
+ TotalMolLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
+
+ // Update other present gases
+ GasGridContainer.RemoveAllChildren();
+
+ if (updatedData.GasData.Count() == 0)
+ {
+ // No gases
+ var gasLabel = new Label()
+ {
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"),
+ FontOverride = normalFont,
+ FontColorOverride = StyleNano.DisabledFore,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(0, 2, 0, 0),
+ SetHeight = 24f,
+ };
+
+ GasGridContainer.AddChild(gasLabel);
+ }
+
+ else
+ {
+ // Add an entry for each gas
+ foreach (var (gas, percent) in updatedData.GasData)
+ {
+ var gasPercent = (FixedPoint2)0f;
+ gasPercent = percent * 100f;
+
+ var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
+
+ var gasLabel = new Label()
+ {
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
+ FontOverride = normalFont,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(0, 2, 0, 0),
+ SetHeight = 24f,
+ };
+
+ GasGridContainer.AddChild(gasLabel);
+ }
+ }
+ }
+
+ public void SetAsFocus()
+ {
+ FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
+ ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png";
+ FocusContainer.Visible = true;
+ }
+
+ public void RemoveAsFocus()
+ {
+ FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+ ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
+ FocusContainer.Visible = false;
+ }
+}
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
- // Beacons
- if (_beacons.Pressed)
- {
- var rectBuffer = new Vector2(5f, 3f);
-
- // Calculate font size for current zoom level
- var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
- var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
-
- foreach (var beacon in _navMap.Beacons.Values)
- {
- var position = beacon.Position - offset;
- position = ScalePosition(position with { Y = -position.Y });
-
- var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
- handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
- handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
- }
- }
-
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
position = ScalePosition(new Vector2(position.X, -position.Y));
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
- var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
+ var positionOffset = new Vector2(scalingCoefficient * blip.Scale * blip.Texture.Width, scalingCoefficient * blip.Scale * blip.Texture.Height);
handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
}
+
+ // Beacons
+ if (_beacons.Pressed)
+ {
+ var rectBuffer = new Vector2(5f, 3f);
+
+ // Calculate font size for current zoom level
+ var fontSize = (int)Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
+ var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
+
+ foreach (var beacon in _navMap.Beacons.Values)
+ {
+ var position = beacon.Position - offset;
+ position = ScalePosition(position with { Y = -position.Y });
+
+ var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
+ handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
+ handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
+ }
+ }
}
protected override void FrameUpdate(FrameEventArgs args)
Vector2i foundTermius;
Vector2i foundOrigin;
+ if (origin == terminus)
+ return;
+
// Does our new line end at the beginning of an existing line?
if (lookup.Remove(terminus, out foundTermius))
{
public Color Color;
public bool Blinks;
public bool Selectable;
+ public float Scale;
- public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
+ public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true, float scale = 1f)
{
Coordinates = coordinates;
Texture = texture;
Color = color;
Blinks = blinks;
Selectable = selectable;
+ Scale = scale;
}
}
--- /dev/null
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.Piping.Components;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.EntitySystems;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Consoles;
+using Content.Shared.Labels.Components;
+using Content.Shared.Pinpointer;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Timing;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Server.Atmos.Consoles;
+
+public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+ [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ // Private variables
+ // Note: this data does not need to be saved
+ private Dictionary<EntityUid, Dictionary<Vector2i, AtmosPipeChunk>> _gridAtmosPipeChunks = new();
+ private float _updateTimer = 1.0f;
+
+ // Constants
+ private const float UpdateTime = 1.0f;
+ private const int ChunkSize = 4;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // Console events
+ SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
+ SubscribeLocalEvent<AtmosMonitoringConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChanged);
+ SubscribeLocalEvent<AtmosMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
+
+ // Tracked device events
+ SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, NodeGroupsRebuilt>(OnEntityNodeGroupsRebuilt);
+ SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, AtmosPipeColorChangedEvent>(OnEntityPipeColorChanged);
+ SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, EntityTerminatingEvent>(OnEntityShutdown);
+
+ // Grid events
+ SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
+ }
+
+ #region Event handling
+
+ private void OnConsoleInit(EntityUid uid, AtmosMonitoringConsoleComponent component, ComponentInit args)
+ {
+ InitializeAtmosMonitoringConsole(uid, component);
+ }
+
+ private void OnConsoleAnchorChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, AnchorStateChangedEvent args)
+ {
+ InitializeAtmosMonitoringConsole(uid, component);
+ }
+
+ private void OnConsoleParentChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, EntParentChangedMessage args)
+ {
+ component.ForceFullUpdate = true;
+ InitializeAtmosMonitoringConsole(uid, component);
+ }
+
+ private void OnEntityNodeGroupsRebuilt(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, NodeGroupsRebuilt args)
+ {
+ InitializeAtmosMonitoringDevice(uid, component);
+ }
+
+ private void OnEntityPipeColorChanged(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, AtmosPipeColorChangedEvent args)
+ {
+ InitializeAtmosMonitoringDevice(uid, component);
+ }
+
+ private void OnEntityShutdown(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, EntityTerminatingEvent args)
+ {
+ ShutDownAtmosMonitoringEntity(uid, component);
+ }
+
+ private void OnGridSplit(ref GridSplitEvent args)
+ {
+ // Collect grids
+ var allGrids = args.NewGrids.ToList();
+
+ if (!allGrids.Contains(args.Grid))
+ allGrids.Add(args.Grid);
+
+ // Rebuild the pipe networks on the affected grids
+ foreach (var ent in allGrids)
+ {
+ if (!TryComp<MapGridComponent>(ent, out var grid))
+ continue;
+
+ RebuildAtmosPipeGrid(ent, grid);
+ }
+
+ // Update atmos monitoring consoles that stand upon an updated grid
+ var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform.GridUid == null)
+ continue;
+
+ if (!allGrids.Contains(entXform.GridUid.Value))
+ continue;
+
+ InitializeAtmosMonitoringConsole(ent, entConsole);
+ }
+ }
+
+ #endregion
+
+ #region UI updates
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ _updateTimer += frameTime;
+
+ if (_updateTimer >= UpdateTime)
+ {
+ _updateTimer -= UpdateTime;
+
+ var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform?.GridUid == null)
+ continue;
+
+ UpdateUIState(ent, entConsole, entXform);
+ }
+ }
+ }
+
+ public void UpdateUIState
+ (EntityUid uid,
+ AtmosMonitoringConsoleComponent component,
+ TransformComponent xform)
+ {
+ if (!_userInterfaceSystem.IsUiOpen(uid, AtmosMonitoringConsoleUiKey.Key))
+ return;
+
+ var gridUid = xform.GridUid!.Value;
+
+ if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
+ return;
+
+ if (!TryComp<GridAtmosphereComponent>(gridUid, out var atmosphere))
+ return;
+
+ // The grid must have a NavMapComponent to visualize the map in the UI
+ EnsureComp<NavMapComponent>(gridUid);
+
+ // Gathering data to be send to the client
+ var atmosNetworks = new List<AtmosMonitoringConsoleEntry>();
+ var query = AllEntityQuery<GasPipeSensorComponent, TransformComponent>();
+
+ while (query.MoveNext(out var ent, out var entSensor, out var entXform))
+ {
+ if (entXform?.GridUid != xform.GridUid)
+ continue;
+
+ if (!entXform.Anchored)
+ continue;
+
+ var entry = CreateAtmosMonitoringConsoleEntry(ent, entXform);
+
+ if (entry != null)
+ atmosNetworks.Add(entry.Value);
+ }
+
+ // Set the UI state
+ _userInterfaceSystem.SetUiState(uid, AtmosMonitoringConsoleUiKey.Key,
+ new AtmosMonitoringConsoleBoundInterfaceState(atmosNetworks.ToArray()));
+ }
+
+ private AtmosMonitoringConsoleEntry? CreateAtmosMonitoringConsoleEntry(EntityUid uid, TransformComponent xform)
+ {
+ AtmosMonitoringConsoleEntry? entry = null;
+
+ var netEnt = GetNetEntity(uid);
+ var name = MetaData(uid).EntityName;
+ var address = string.Empty;
+
+ if (xform.GridUid == null)
+ return null;
+
+ if (!TryGettingFirstPipeNode(uid, out var pipeNode, out var netId) ||
+ pipeNode == null ||
+ netId == null)
+ return null;
+
+ var pipeColor = TryComp<AtmosPipeColorComponent>(uid, out var colorComponent) ? colorComponent.Color : Color.White;
+
+ // Name the entity based on its label, if available
+ if (TryComp<LabelComponent>(uid, out var label) && label.CurrentLabel != null)
+ name = label.CurrentLabel;
+
+ // Otherwise use its base name and network address
+ else if (TryComp<DeviceNetworkComponent>(uid, out var deviceNet))
+ address = deviceNet.Address;
+
+ // Entry for unpowered devices
+ if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver) && !apcPowerReceiver.Powered)
+ {
+ entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
+ {
+ IsPowered = false,
+ Color = pipeColor
+ };
+
+ return entry;
+ }
+
+ // Entry for powered devices
+ var gasData = new Dictionary<Gas, float>();
+ var isAirPresent = pipeNode.Air.TotalMoles > 0;
+
+ if (isAirPresent)
+ {
+ foreach (var gas in Enum.GetValues<Gas>())
+ {
+ if (pipeNode.Air[(int)gas] > 0)
+ gasData.Add(gas, pipeNode.Air[(int)gas] / pipeNode.Air.TotalMoles);
+ }
+ }
+
+ entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
+ {
+ TemperatureData = isAirPresent ? pipeNode.Air.Temperature : 0f,
+ PressureData = pipeNode.Air.Pressure,
+ TotalMolData = pipeNode.Air.TotalMoles,
+ GasData = gasData,
+ Color = pipeColor
+ };
+
+ return entry;
+ }
+
+ private Dictionary<NetEntity, AtmosDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
+ {
+ var atmosDeviceNavMapData = new Dictionary<NetEntity, AtmosDeviceNavMapData>();
+
+ var query = AllEntityQuery<AtmosMonitoringConsoleDeviceComponent, TransformComponent>();
+ while (query.MoveNext(out var ent, out var entComponent, out var entXform))
+ {
+ if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
+ atmosDeviceNavMapData.Add(data.Value.NetEntity, data.Value);
+ }
+
+ return atmosDeviceNavMapData;
+ }
+
+ private bool TryGetAtmosDeviceNavMapData
+ (EntityUid uid,
+ AtmosMonitoringConsoleDeviceComponent component,
+ TransformComponent xform,
+ EntityUid gridUid,
+ [NotNullWhen(true)] out AtmosDeviceNavMapData? device)
+ {
+ device = null;
+
+ if (component.NavMapBlip == null)
+ return false;
+
+ if (xform.GridUid != gridUid)
+ return false;
+
+ if (!xform.Anchored)
+ return false;
+
+ var direction = xform.LocalRotation.GetCardinalDir();
+
+ if (!TryGettingFirstPipeNode(uid, out var _, out var netId))
+ netId = -1;
+
+ var color = Color.White;
+
+ if (TryComp<AtmosPipeColorComponent>(uid, out var atmosPipeColor))
+ color = atmosPipeColor.Color;
+
+ device = new AtmosDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), netId.Value, component.NavMapBlip.Value, direction, color);
+
+ return true;
+ }
+
+ #endregion
+
+ #region Pipe net functions
+
+ private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
+ {
+ var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
+
+ // Adds all atmos pipes to the nav map via bit mask chunks
+ var queryPipes = AllEntityQuery<AtmosPipeColorComponent, NodeContainerComponent, TransformComponent>();
+ while (queryPipes.MoveNext(out var ent, out var entAtmosPipeColor, out var entNodeContainer, out var entXform))
+ {
+ if (entXform.GridUid != gridUid)
+ continue;
+
+ if (!entXform.Anchored)
+ continue;
+
+ var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates);
+ var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
+ var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
+
+ if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
+ {
+ chunk = new AtmosPipeChunk(chunkOrigin);
+ allChunks[chunkOrigin] = chunk;
+ }
+
+ UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, GetTileIndex(relative), ref chunk);
+ }
+
+ // Add or update the chunks on the associated grid
+ _gridAtmosPipeChunks[gridUid] = allChunks;
+
+ // Update the consoles that are on the same grid
+ var queryConsoles = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+ while (queryConsoles.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (gridUid != entXform.GridUid)
+ continue;
+
+ entConsole.AtmosPipeChunks = allChunks;
+ Dirty(ent, entConsole);
+ }
+ }
+
+ private void RebuildSingleTileOfPipeNetwork(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords)
+ {
+ if (!_gridAtmosPipeChunks.TryGetValue(gridUid, out var allChunks))
+ allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
+
+ var tile = _sharedMapSystem.GetTileRef(gridUid, grid, coords);
+ var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
+ var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
+ var tileIdx = GetTileIndex(relative);
+
+ if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
+ chunk = new AtmosPipeChunk(chunkOrigin);
+
+ // Remove all stale values for the tile
+ foreach (var (index, atmosPipeData) in chunk.AtmosPipeData)
+ {
+ var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
+ chunk.AtmosPipeData[index] = atmosPipeData & ~mask;
+ }
+
+ // Rebuild the tile's pipe data
+ foreach (var ent in _sharedMapSystem.GetAnchoredEntities(gridUid, grid, coords))
+ {
+ if (!TryComp<AtmosPipeColorComponent>(ent, out var entAtmosPipeColor))
+ continue;
+
+ if (!TryComp<NodeContainerComponent>(ent, out var entNodeContainer))
+ continue;
+
+ UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, tileIdx, ref chunk);
+ }
+
+ // Add or update the chunk on the associated grid
+ // Only the modified chunk will be sent to the client
+ chunk.LastUpdate = _gameTiming.CurTick;
+ allChunks[chunkOrigin] = chunk;
+ _gridAtmosPipeChunks[gridUid] = allChunks;
+
+ // Update the components of the monitoring consoles that are attached to the same grid
+ var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (gridUid != entXform.GridUid)
+ continue;
+
+ entConsole.AtmosPipeChunks = allChunks;
+ Dirty(ent, entConsole);
+ }
+ }
+
+ private void UpdateAtmosPipeChunk(EntityUid uid, NodeContainerComponent nodeContainer, AtmosPipeColorComponent pipeColor, int tileIdx, ref AtmosPipeChunk chunk)
+ {
+ // Entities that are actively being deleted are not to be drawn
+ if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
+ return;
+
+ foreach ((var id, var node) in nodeContainer.Nodes)
+ {
+ if (node is not PipeNode)
+ continue;
+
+ var pipeNode = (PipeNode)node;
+ var netId = GetPipeNodeNetId(pipeNode);
+ var pipeDirection = pipeNode.CurrentPipeDirection;
+
+ chunk.AtmosPipeData.TryGetValue((netId, pipeColor.Color.ToHex()), out var atmosPipeData);
+ atmosPipeData |= (ulong)pipeDirection << tileIdx * SharedNavMapSystem.Directions;
+ chunk.AtmosPipeData[(netId, pipeColor.Color.ToHex())] = atmosPipeData;
+ }
+ }
+
+ private bool TryGettingFirstPipeNode(EntityUid uid, [NotNullWhen(true)] out PipeNode? pipeNode, [NotNullWhen(true)] out int? netId)
+ {
+ pipeNode = null;
+ netId = null;
+
+ if (!TryComp<NodeContainerComponent>(uid, out var nodeContainer))
+ return false;
+
+ foreach (var node in nodeContainer.Nodes.Values)
+ {
+ if (node is PipeNode)
+ {
+ pipeNode = (PipeNode)node;
+ netId = GetPipeNodeNetId(pipeNode);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private int GetPipeNodeNetId(PipeNode pipeNode)
+ {
+ if (pipeNode.NodeGroup is BaseNodeGroup)
+ {
+ var nodeGroup = (BaseNodeGroup)pipeNode.NodeGroup;
+
+ return nodeGroup.NetId;
+ }
+
+ return -1;
+ }
+
+ #endregion
+
+ #region Initialization functions
+
+ private void InitializeAtmosMonitoringConsole(EntityUid uid, AtmosMonitoringConsoleComponent component)
+ {
+ var xform = Transform(uid);
+
+ if (xform.GridUid == null)
+ return;
+
+ var grid = xform.GridUid.Value;
+
+ if (!TryComp<MapGridComponent>(grid, out var map))
+ return;
+
+ component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
+
+ if (!_gridAtmosPipeChunks.TryGetValue(grid, out var chunks))
+ {
+ RebuildAtmosPipeGrid(grid, map);
+ }
+
+ else
+ {
+ component.AtmosPipeChunks = chunks;
+ Dirty(uid, component);
+ }
+ }
+
+ private void InitializeAtmosMonitoringDevice(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
+ {
+ // Rebuild tile
+ var xform = Transform(uid);
+ var gridUid = xform.GridUid;
+
+ if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
+ RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
+
+ // Update blips on affected consoles
+ if (component.NavMapBlip == null)
+ return;
+
+ var netEntity = EntityManager.GetNetEntity(uid);
+ var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ var isDirty = entConsole.AtmosDevices.Remove(netEntity);
+
+ if (gridUid != null &&
+ gridUid == entXform.GridUid &&
+ xform.Anchored &&
+ TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
+ {
+ entConsole.AtmosDevices.Add(netEntity, data.Value);
+ isDirty = true;
+ }
+
+ if (isDirty)
+ Dirty(ent, entConsole);
+ }
+ }
+
+ private void ShutDownAtmosMonitoringEntity(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
+ {
+ // Rebuild tile
+ var xform = Transform(uid);
+ var gridUid = xform.GridUid;
+
+ if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
+ RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
+
+ // Update blips on affected consoles
+ if (component.NavMapBlip == null)
+ return;
+
+ var netEntity = EntityManager.GetNetEntity(uid);
+ var query = AllEntityQuery<AtmosMonitoringConsoleComponent>();
+
+ while (query.MoveNext(out var ent, out var entConsole))
+ {
+ if (entConsole.AtmosDevices.Remove(netEntity))
+ Dirty(ent, entConsole);
+ }
+ }
+
+ #endregion
+
+ private int GetTileIndex(Vector2i relativeTile)
+ {
+ return relativeTile.X * ChunkSize + relativeTile.Y;
+ }
+}
using Content.Server.Atmos.Piping.EntitySystems;
using JetBrains.Annotations;
-namespace Content.Server.Atmos.Piping.Components
+namespace Content.Server.Atmos.Piping.Components;
+
+[RegisterComponent]
+public sealed partial class AtmosPipeColorComponent : Component
{
- [RegisterComponent]
- public sealed partial class AtmosPipeColorComponent : Component
- {
- [DataField("color")]
- public Color Color { get; set; } = Color.White;
+ [DataField]
+ public Color Color { get; set; } = Color.White;
- [ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
- public Color ColorVV
- {
- get => Color;
- set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
- }
+ [ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
+ public Color ColorVV
+ {
+ get => Color;
+ set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
}
}
+
+[ByRefEvent]
+public record struct AtmosPipeColorChangedEvent(Color color)
+{
+ public Color Color = color;
+}
return;
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
+
+ var ev = new AtmosPipeColorChangedEvent(color);
+ RaiseLocalEvent(uid, ref ev);
}
}
}
/// </summary>
public const float SpaceHeatCapacity = 7000f;
+ /// <summary>
+ /// Dictionary of chemical abbreviations for <see cref="Gas"/>
+ /// </summary>
+ public static Dictionary<Gas, string> GasAbbreviations = new Dictionary<Gas, string>()
+ {
+ [Gas.Ammonia] = Loc.GetString("gas-ammonia-abbreviation"),
+ [Gas.CarbonDioxide] = Loc.GetString("gas-carbon-dioxide-abbreviation"),
+ [Gas.Frezon] = Loc.GetString("gas-frezon-abbreviation"),
+ [Gas.Nitrogen] = Loc.GetString("gas-nitrogen-abbreviation"),
+ [Gas.NitrousOxide] = Loc.GetString("gas-nitrous-oxide-abbreviation"),
+ [Gas.Oxygen] = Loc.GetString("gas-oxygen-abbreviation"),
+ [Gas.Plasma] = Loc.GetString("gas-plasma-abbreviation"),
+ [Gas.Tritium] = Loc.GetString("gas-tritium-abbreviation"),
+ [Gas.WaterVapor] = Loc.GetString("gas-water-vapor-abbreviation"),
+ };
+
#region Excited Groups
/// <summary>
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Entities with component will be queried against for their
+/// atmos monitoring data on atmos monitoring consoles
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class GasPipeSensorComponent : Component;
--- /dev/null
+using Content.Shared.Atmos.Consoles;
+using Content.Shared.Pinpointer;
+using Content.Shared.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Entities capable of opening the atmos monitoring console UI
+/// require this component to function correctly
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedAtmosMonitoringConsoleSystem))]
+public sealed partial class AtmosMonitoringConsoleComponent : Component
+{
+ /*
+ * Don't need DataFields as this can be reconstructed
+ */
+
+ /// <summary>
+ /// A dictionary of the all the nav map chunks that contain anchored atmos pipes
+ /// </summary>
+ [ViewVariables]
+ public Dictionary<Vector2i, AtmosPipeChunk> AtmosPipeChunks = new();
+
+ /// <summary>
+ /// A list of all the atmos devices that will be used to populate the nav map
+ /// </summary>
+ [ViewVariables]
+ public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = new();
+
+ /// <summary>
+ /// Color of the floor tiles on the nav map screen
+ /// </summary>
+ [DataField, ViewVariables]
+ public Color NavMapTileColor;
+
+ /// <summary>
+ /// Color of the wall lines on the nav map screen
+ /// </summary>
+ [DataField, ViewVariables]
+ public Color NavMapWallColor;
+
+ /// <summary>
+ /// The next time this component is dirtied, it will force the full state
+ /// to be sent to the client, instead of just the delta state
+ /// </summary>
+ [ViewVariables]
+ public bool ForceFullUpdate = false;
+}
+
+[Serializable, NetSerializable]
+public struct AtmosPipeChunk(Vector2i origin)
+{
+ /// <summary>
+ /// Chunk position
+ /// </summary>
+ [ViewVariables]
+ public readonly Vector2i Origin = origin;
+
+ /// <summary>
+ /// Bitmask look up for atmos pipes, 1 for occupied and 0 for empty.
+ /// Indexed by the color hexcode of the pipe
+ /// </summary>
+ [ViewVariables]
+ public Dictionary<(int, string), ulong> AtmosPipeData = new();
+
+ /// <summary>
+ /// The last game tick that the chunk was updated
+ /// </summary>
+ [NonSerialized]
+ public GameTick LastUpdate;
+}
+
+[Serializable, NetSerializable]
+public struct AtmosDeviceNavMapData
+{
+ /// <summary>
+ /// The entity in question
+ /// </summary>
+ public NetEntity NetEntity;
+
+ /// <summary>
+ /// Location of the entity
+ /// </summary>
+ public NetCoordinates NetCoordinates;
+
+ /// <summary>
+ /// The associated pipe network ID
+ /// </summary>
+ public int NetId = -1;
+
+ /// <summary>
+ /// Prototype ID for the nav map blip
+ /// </summary>
+ public ProtoId<NavMapBlipPrototype> NavMapBlip;
+
+ /// <summary>
+ /// Direction of the entity
+ /// </summary>
+ public Direction Direction;
+
+ /// <summary>
+ /// Color of the attached pipe
+ /// </summary>
+ public Color PipeColor;
+
+ /// <summary>
+ /// Populate the atmos monitoring console nav map with a single entity
+ /// </summary>
+ public AtmosDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, int netId, ProtoId<NavMapBlipPrototype> navMapBlip, Direction direction, Color pipeColor)
+ {
+ NetEntity = netEntity;
+ NetCoordinates = netCoordinates;
+ NetId = netId;
+ NavMapBlip = navMapBlip;
+ Direction = direction;
+ PipeColor = pipeColor;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class AtmosMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
+{
+ /// <summary>
+ /// A list of all entries to populate the UI with
+ /// </summary>
+ public AtmosMonitoringConsoleEntry[] AtmosNetworks;
+
+ /// <summary>
+ /// Sends data from the server to the client to populate the atmos monitoring console UI
+ /// </summary>
+ public AtmosMonitoringConsoleBoundInterfaceState(AtmosMonitoringConsoleEntry[] atmosNetworks)
+ {
+ AtmosNetworks = atmosNetworks;
+ }
+}
+
+[Serializable, NetSerializable]
+public struct AtmosMonitoringConsoleEntry
+{
+ /// <summary>
+ /// The entity in question
+ /// </summary>
+ public NetEntity NetEntity;
+
+ /// <summary>
+ /// Location of the entity
+ /// </summary>
+ public NetCoordinates Coordinates;
+
+ /// <summary>
+ /// The associated pipe network ID
+ /// </summary>
+ public int NetId = -1;
+
+ /// <summary>
+ /// Localised device name
+ /// </summary>
+ public string EntityName;
+
+ /// <summary>
+ /// Device network address
+ /// </summary>
+ public string Address;
+
+ /// <summary>
+ /// Temperature (K)
+ /// </summary>
+ public float TemperatureData;
+
+ /// <summary>
+ /// Pressure (kPA)
+ /// </summary>
+ public float PressureData;
+
+ /// <summary>
+ /// Total number of mols of gas
+ /// </summary>
+ public float TotalMolData;
+
+ /// <summary>
+ /// Mol and percentage for all detected gases
+ /// </summary>
+ public Dictionary<Gas, float> GasData = new();
+
+ /// <summary>
+ /// The color to be associated with the pipe network
+ /// </summary>
+ public Color Color;
+
+ /// <summary>
+ /// Indicates whether the entity is powered
+ /// </summary>
+ public bool IsPowered = true;
+
+ /// <summary>
+ /// Used to populate the atmos monitoring console UI with data from a single air alarm
+ /// </summary>
+ public AtmosMonitoringConsoleEntry
+ (NetEntity entity,
+ NetCoordinates coordinates,
+ int netId,
+ string entityName,
+ string address)
+ {
+ NetEntity = entity;
+ Coordinates = coordinates;
+ NetId = netId;
+ EntityName = entityName;
+ Address = address;
+ }
+}
+
+public enum AtmosPipeChunkDataFacing : byte
+{
+ // Values represent bit shift offsets when retrieving data in the tile array.
+ North = 0,
+ South = SharedNavMapSystem.ArraySize,
+ East = SharedNavMapSystem.ArraySize * 2,
+ West = SharedNavMapSystem.ArraySize * 3,
+}
+
+/// <summary>
+/// UI key associated with the atmos monitoring console
+/// </summary>
+[Serializable, NetSerializable]
+public enum AtmosMonitoringConsoleUiKey
+{
+ Key
+}
--- /dev/null
+using Content.Shared.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Entities with this component appear on the
+/// nav maps of atmos monitoring consoles
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AtmosMonitoringConsoleDeviceComponent : Component
+{
+ /// <summary>
+ /// Prototype ID for the blip used to represent this
+ /// entity on the atmos monitoring console nav map.
+ /// If null, no blip is drawn (i.e., null for pipes)
+ /// </summary>
+ [DataField, ViewVariables]
+ public ProtoId<NavMapBlipPrototype>? NavMapBlip = null;
+}
--- /dev/null
+using Content.Shared.Atmos.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Atmos.Consoles;
+
+public abstract class SharedAtmosMonitoringConsoleSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentGetState>(OnGetState);
+ }
+
+ private void OnGetState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentGetState args)
+ {
+ Dictionary<Vector2i, Dictionary<(int, string), ulong>> chunks;
+
+ // Should this be a full component state or a delta-state?
+ if (args.FromTick <= component.CreationTick || component.ForceFullUpdate)
+ {
+ component.ForceFullUpdate = false;
+
+ // Full state
+ chunks = new(component.AtmosPipeChunks.Count);
+
+ foreach (var (origin, chunk) in component.AtmosPipeChunks)
+ {
+ chunks.Add(origin, chunk.AtmosPipeData);
+ }
+
+ args.State = new AtmosMonitoringConsoleState(chunks, component.AtmosDevices);
+
+ return;
+ }
+
+ chunks = new();
+
+ foreach (var (origin, chunk) in component.AtmosPipeChunks)
+ {
+ if (chunk.LastUpdate < args.FromTick)
+ continue;
+
+ chunks.Add(origin, chunk.AtmosPipeData);
+ }
+
+ args.State = new AtmosMonitoringConsoleDeltaState(chunks, component.AtmosDevices, new(component.AtmosPipeChunks.Keys));
+ }
+
+ #region: System messages
+
+ [Serializable, NetSerializable]
+ protected sealed class AtmosMonitoringConsoleState(
+ Dictionary<Vector2i, Dictionary<(int, string), ulong>> chunks,
+ Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices)
+ : ComponentState
+ {
+ public Dictionary<Vector2i, Dictionary<(int, string), ulong>> Chunks = chunks;
+ public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = atmosDevices;
+ }
+
+ [Serializable, NetSerializable]
+ protected sealed class AtmosMonitoringConsoleDeltaState(
+ Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks,
+ Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices,
+ HashSet<Vector2i> allChunks)
+ : ComponentState, IComponentDeltaState<AtmosMonitoringConsoleState>
+ {
+ public Dictionary<Vector2i, Dictionary<(int, string), ulong>> ModifiedChunks = modifiedChunks;
+ public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = atmosDevices;
+ public HashSet<Vector2i> AllChunks = allChunks;
+
+ public void ApplyToFullState(AtmosMonitoringConsoleState state)
+ {
+ foreach (var key in state.Chunks.Keys)
+ {
+ if (!AllChunks!.Contains(key))
+ state.Chunks.Remove(key);
+ }
+
+ foreach (var (index, data) in ModifiedChunks)
+ {
+ state.Chunks[index] = new Dictionary<(int, string), ulong>(data);
+ }
+
+ state.AtmosDevices.Clear();
+ foreach (var (nuid, atmosDevice) in AtmosDevices)
+ {
+ state.AtmosDevices.Add(nuid, atmosDevice);
+ }
+ }
+
+ public AtmosMonitoringConsoleState CreateNewFullState(AtmosMonitoringConsoleState state)
+ {
+ var chunks = new Dictionary<Vector2i, Dictionary<(int, string), ulong>>(state.Chunks.Count);
+
+ foreach (var (index, data) in state.Chunks)
+ {
+ if (!AllChunks!.Contains(index))
+ continue;
+
+ if (ModifiedChunks.ContainsKey(index))
+ chunks[index] = new Dictionary<(int, string), ulong>(ModifiedChunks[index]);
+
+ else
+ chunks[index] = new Dictionary<(int, string), ulong>(state.Chunks[index]);
+ }
+
+ return new AtmosMonitoringConsoleState(chunks, new(AtmosDevices));
+ }
+ }
+
+ #endregion
+}
--- /dev/null
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Prototypes;
+
+[Prototype("navMapBlip")]
+public sealed partial class NavMapBlipPrototype : IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ /// <summary>
+ /// Sets whether the associated entity can be selected when the blip is clicked
+ /// </summary>
+ [DataField]
+ public bool Selectable = false;
+
+ /// <summary>
+ /// Sets whether the blips is always blinking
+ /// </summary>
+ [DataField]
+ public bool Blinks = false;
+
+ /// <summary>
+ /// Sets the color of the blip
+ /// </summary>
+ [DataField]
+ public Color Color { get; private set; } = Color.LightGray;
+
+ /// <summary>
+ /// Texture paths associated with the blip
+ /// </summary>
+ [DataField]
+ public ResPath[]? TexturePaths { get; private set; }
+
+ /// <summary>
+ /// Sets the UI scaling of the blip
+ /// </summary>
+ [DataField]
+ public float Scale { get; private set; } = 1f;
+}
atmos-alerts-window-alarm-label = {CAPITALIZE($name)} ({$address})
atmos-alerts-window-temperature-label = Temperature
atmos-alerts-window-temperature-value = {$valueInC} °C ({$valueInK} K)
+atmos-alerts-window-invalid-value = N/A
+atmos-alerts-window-total-mol-label = Total moles
+atmos-alerts-window-total-mol-value = {$value} mol
atmos-alerts-window-pressure-label = Pressure
atmos-alerts-window-pressure-value = {$value} kPa
atmos-alerts-window-oxygenation-label = Oxygenation
--- /dev/null
+gas-ammonia-abbreviation = NH₃
+gas-carbon-dioxide-abbreviation = CO₂
+gas-frezon-abbreviation = F
+gas-nitrogen-abbreviation = N₂
+gas-nitrous-oxide-abbreviation = N₂O
+gas-oxygen-abbreviation = O₂
+gas-plasma-abbreviation = P
+gas-tritium-abbreviation = T
+gas-water-vapor-abbreviation = H₂O
+gas-unknown-abbreviation = X
--- /dev/null
+atmos-monitoring-window-title = Atmospheric Network Monitor
+atmos-monitoring-window-station-name = [color=white][font size=14]{$stationName}[/font][/color]
+atmos-monitoring-window-unknown-location = Unknown location
+atmos-monitoring-window-label-gas-opening = Network opening
+atmos-monitoring-window-label-gas-scrubber = Air scrubber
+atmos-monitoring-window-label-gas-flow-regulator = Flow regulator
+atmos-monitoring-window-label-thermoregulator = Thermoregulator
+atmos-monitoring-window-tab-networks = Atmospheric networks
+atmos-monitoring-window-toggle-overlays = Toggle map overlays
+atmos-monitoring-window-show-pipe-network = Pipe network
+atmos-monitoring-window-show-gas-pipe-sensors = Gas pipe sensors
+atmos-monitoring-window-label-gases = Present gases
+atmos-monitoring-window-flavor-left = Contact an atmospheric technician for assistance
+atmos-monitoring-window-flavor-right = v1.1
\ No newline at end of file
components:
- type: ComputerBoard
prototype: ComputerAlert
+
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: AtmosMonitoringComputerCircuitboard
+ name: atmospheric network monitor board
+ description: A computer printed circuit board for an atmospheric network monitor.
+ components:
+ - type: ComputerBoard
+ prototype: ComputerAtmosMonitoring
- type: entity
parent: BaseComputerCircuitboard
enum.WiresUiKey.Key:
type: WiresBoundUserInterface
+- type: entity
+ parent: BaseComputerAiAccess
+ id: ComputerAtmosMonitoring
+ name: atmospheric network monitor
+ description: Used to monitor the station's atmospheric networks.
+ components:
+ - type: Computer
+ board: AtmosMonitoringComputerCircuitboard
+ - type: Sprite
+ layers:
+ - map: ["computerLayerBody"]
+ state: computer
+ - map: ["computerLayerKeyboard"]
+ state: generic_keyboard
+ - map: ["computerLayerScreen"]
+ state: tank
+ - map: ["computerLayerKeys"]
+ state: atmos_key
+ - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+ state: generic_panel_open
+ - type: AtmosMonitoringConsole
+ navMapTileColor: "#1a1a1a"
+ navMapWallColor: "#404040"
+ - type: ActivatableUI
+ singleUser: true
+ key: enum.AtmosMonitoringConsoleUiKey.Key
+ - type: UserInterface
+ interfaces:
+ enum.AtmosMonitoringConsoleUiKey.Key:
+ type: AtmosMonitoringConsoleBoundUserInterface
+ enum.WiresUiKey.Key:
+ type: WiresBoundUserInterface
+
- type: entity
parent: BaseComputer
id: ComputerEmergencyShuttle
--- /dev/null
+# All consoles
+- type: navMapBlip
+ id: NavMapConsole
+ blinks: true
+ color: Cyan
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_circle.png"
+
+# Atmos monitoring console
+- type: navMapBlip
+ id: GasPipeSensor
+ selectable: true
+ color: "#ffcd00"
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_star.png"
+
+- type: navMapBlip
+ id: GasVentOpening
+ scale: 0.6667
+ color: LightGray
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_square.png"
+
+- type: navMapBlip
+ id: GasVentScrubber
+ scale: 0.6667
+ color: LightGray
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_circle.png"
+
+- type: navMapBlip
+ id: GasFlowRegulator
+ scale: 0.75
+ color: LightGray
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_arrow_south.png"
+ - "/Textures/Interface/NavMap/beveled_arrow_east.png"
+ - "/Textures/Interface/NavMap/beveled_arrow_north.png"
+ - "/Textures/Interface/NavMap/beveled_arrow_west.png"
+
+- type: navMapBlip
+ id: GasValve
+ scale: 0.6667
+ color: LightGray
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_diamond_north_south.png"
+ - "/Textures/Interface/NavMap/beveled_diamond_east_west.png"
+ - "/Textures/Interface/NavMap/beveled_diamond_north_south.png"
+ - "/Textures/Interface/NavMap/beveled_diamond_east_west.png"
+
+- type: navMapBlip
+ id: Thermoregulator
+ scale: 0.6667
+ color: LightGray
+ texturePaths:
+ - "/Textures/Interface/NavMap/beveled_hexagon.png"
\ No newline at end of file
!type:PortablePipeNode
nodeGroupID: Pipe
pipeDirection: South
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: Thermoregulator
- type: ItemSlots
slots:
beakerSlot:
range: 5
sound:
path: /Audio/Ambience/Objects/gas_pump.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasFlowRegulator
+
- type: entity
parent: GasBinaryBase
id: GasVolumePump
examinableAddress: true
prefix: device-address-prefix-volume-pump
- type: WiredNetworkConnection
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasFlowRegulator
+
- type: entity
parent: GasBinaryBase
id: GasPassiveGate
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasValve
+
- type: entity
parent: GasBinaryBase
id: GasValve
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasValve
+
- type: entity
parent: GasBinaryBase
id: SignalControlledValve
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasValve
+
- type: entity
parent: GasBinaryBase
id: GasPort
- type: Construction
graph: GasBinary
node: port
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentOpening
+
- type: entity
parent: GasVentPump
id: GasDualPortVentPump
pipeDirection: South
- type: AmbientSound
enabled: true
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentOpening
+
- type: entity
parent: [ BaseMachine, ConstructibleMachine ]
id: GasRecycler
acts: ["Destruction"]
- type: Machine
board: GasRecyclerMachineCircuitboard
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasValve
+
- type: entity
parent: GasBinaryBase
id: HeatExchanger
- type: Construction
graph: GasBinary
node: radiator
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: Thermoregulator
True: { state: lights }
- type: AtmosMonitor
monitorsPipeNet: true
+ - type: GasPipeSensor
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasPipeSensor
- type: ApcPowerReceiver
- type: ExtensionCableReceiver
- type: Construction
- type: PipeRestrictOverlap
- type: AtmosUnsafeUnanchor
- type: AtmosPipeColor
+ - type: AtmosMonitoringConsoleDevice
- type: Tag
tags:
- Pipe
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasFlowRegulator
+
- type: entity
parent: GasFilter
id: GasFilterFlipped
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasFlowRegulator
+
- type: entity
parent: GasMixer
id: GasMixerFlipped
- type: Construction
graph: GasTrinary
node: pneumaticvalve
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasFlowRegulator
\ No newline at end of file
sound:
path: /Audio/Ambience/Objects/gas_vent.ogg
- type: Weldable
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentOpening
+
- type: entity
parent: GasUnaryBase
id: GasPassiveVent
- type: Construction
graph: GasUnary
node: passivevent
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentOpening
+
- type: entity
parent: [GasUnaryBase, AirSensorBase]
id: GasVentScrubber
sound:
path: /Audio/Ambience/Objects/gas_vent.ogg
- type: Weldable
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentScrubber
+
- type: entity
parent: GasUnaryBase
id: GasOutletInjector
visibleLayers:
- enum.SubfloorLayers.FirstLayer
- enum.LightLayers.Unshaded
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: GasVentOpening
+
- type: entity
parent: [ BaseMachinePowered, ConstructibleMachine ]
id: BaseGasThermoMachine
examinableAddress: true
- type: WiredNetworkConnection
- type: PowerSwitch
-
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: Thermoregulator
+
- type: entity
parent: BaseGasThermoMachine
id: GasThermoMachineFreezer
- type: ExaminableSolution
solution: tank
- type: PowerSwitch
+ - type: AtmosMonitoringConsoleDevice
+ navMapBlip: Thermoregulator
--- /dev/null
+- files: ["beveled_arrow_east.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_arrow_north.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_arrow_south.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_arrow_west.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_circle.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_diamond.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_diamond_east_west.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_diamond_north_south.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_hexagon.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_square.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_star.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
+
+- files: ["beveled_triangle.png"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Created by chromiumboy"
+ source: "https://github.com/chromiumboy"
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true
--- /dev/null
+sample:
+ filter: true