]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmospheric alerts computer (#25938)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Thu, 5 Sep 2024 01:13:17 +0000 (20:13 -0500)
committerGitHub <noreply@github.com>
Thu, 5 Sep 2024 01:13:17 +0000 (21:13 -0400)
* Atmospheric alerts computer

* Moved components, restricted access to them

* Minor tweaks

* The screen will now turn off when the computer is not powered

* Bug fix

* Adjusted label

* Updated to latest master version

16 files changed:
Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs [new file with mode: 0644]
Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs [new file with mode: 0644]
Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/atmos/atmos-alerts-console.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml
Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml
Resources/Textures/Interface/AtmosMonitoring/status_bg.png [new file with mode: 0644]

diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
new file mode 100644 (file)
index 0000000..6bdfb39
--- /dev/null
@@ -0,0 +1,81 @@
+<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">
+
+    <!-- Device selection button -->
+    <Button Name="FocusButton" HorizontalExpand="True" SetHeight="32" Margin="12 0 0 0" StyleClasses="OpenBoth" Access="Public">
+        <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal">
+
+            <!-- Alarm state -->
+            <TextureRect Stretch="Keep" HorizontalAlignment="Left" Margin="-20 -2 0 0" ModulateSelfOverride="#25252a" TexturePath="/Textures/Interface/AtmosMonitoring/status_bg.png">
+                <BoxContainer VerticalExpand="True" 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="AlarmStateLabel" HorizontalExpand="True" HorizontalAlignment="Center" FontColorOverride="#5A5A5A" Text="{Loc 'atmos-alerts-window-invalid-state'}"></Label>
+                </BoxContainer>
+            </TextureRect>
+
+            <!-- Alarm name -->
+            <Label Name="AlarmNameLabel" Text="???" HorizontalExpand="True" HorizontalAlignment="Center" Margin="5 0"></Label>
+        </BoxContainer>
+    </Button>
+
+    <!-- Panel that appears on selecting the device -->
+    <PanelContainer Name="FocusContainer" HorizontalExpand="True" Margin="1 -1 1 0" ReservesSpace="False" Visible="False" Access="Public">
+        <PanelContainer.PanelOverride>
+            <gfx:StyleBoxFlat BackgroundColor="#25252a"/>
+        </PanelContainer.PanelOverride>
+        <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+
+            <!-- Atmosphere status -->
+            <Control>
+
+                <!-- Main container for displaying atmospheric data -->
+                <BoxContainer Name="MainDataContainer" HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical" ReservesSpace="False" Visible="False">
+                    <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="OxygenationHeaderLabel" Text="{Loc 'atmos-alerts-window-oxygenation-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="#5A5A5A" Margin="0 2 0 0" SetHeight="24"></Label>
+                            <Label Name="PressureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#5A5A5A" Margin="0 2 0 0" SetHeight="24"></Label>
+                            <Label Name="OxygenationLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#5A5A5A" Margin="0 2 0 0" SetHeight="24"></Label>
+                        </BoxContainer>
+                    </PanelContainer>
+                    <BoxContainer HorizontalExpand="True" Orientation="Horizontal">
+                        <Label Name="GasesHeaderLabel" Text="{Loc 'atmos-alerts-window-other-gases-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 4 0 0" SetHeight="24"></Label>
+                    </BoxContainer>
+                    <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>
+
+                <!-- If the alarm is inactive, this is label is diplayed instead -->
+                <Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
+
+                <!-- Silencing progress bar -->
+                <controls:StripeBack Name="SilenceAlarmProgressBar" ReservesSpace="False" Visible="False" Access="Public">
+                    <PanelContainer>
+                        <Label Text="{Loc 'atmos-alerts-window-alerts-being-silenced'}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5 5 5 5"/>
+                    </PanelContainer>
+                </controls:StripeBack>
+            </Control>
+
+            <!-- Check box for silencing this alarm -->
+            <CheckBox Name="SilenceCheckBox" Text="{Loc 'atmos-alerts-window-silence-alerts'}" HorizontalAlignment="Left" Margin="5 5 5 5" Access="Public"></CheckBox>
+        </BoxContainer>
+    </PanelContainer>
+
+</BoxContainer>
diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
new file mode 100644 (file)
index 0000000..4900eab
--- /dev/null
@@ -0,0 +1,210 @@
+using Content.Client.Stylesheets;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+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 Robust.Shared.Map;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlarmEntryContainer : BoxContainer
+{
+    public NetEntity NetEntity;
+    public EntityCoordinates? Coordinates;
+
+    private IResourceCache _cache;
+
+    private Dictionary<AtmosAlarmType, string> _alarmStrings = new Dictionary<AtmosAlarmType, string>()
+    {
+        [AtmosAlarmType.Invalid] = "atmos-alerts-window-invalid-state",
+        [AtmosAlarmType.Normal] = "atmos-alerts-window-normal-state",
+        [AtmosAlarmType.Warning] = "atmos-alerts-window-warning-state",
+        [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);
+
+        _cache = IoCManager.Resolve<IResourceCache>();
+
+        NetEntity = uid;
+        Coordinates = coordinates;
+
+        // 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);
+        var smallFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+
+        // Set fonts
+        TemperatureHeaderLabel.FontOverride = headerFont;
+        PressureHeaderLabel.FontOverride = headerFont;
+        OxygenationHeaderLabel.FontOverride = headerFont;
+        GasesHeaderLabel.FontOverride = headerFont;
+
+        TemperatureLabel.FontOverride = normalFont;
+        PressureLabel.FontOverride = normalFont;
+        OxygenationLabel.FontOverride = normalFont;
+
+        NoDataLabel.FontOverride = headerFont;
+
+        SilenceCheckBox.Label.FontOverride = smallFont;
+        SilenceCheckBox.Label.FontColorOverride = Color.DarkGray;
+    }
+
+    public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlertsFocusDeviceData? focusData = null)
+    {
+        // Load fonts
+        var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+
+        // Update alarm state
+        if (!_alarmStrings.TryGetValue(entry.AlarmState, out var alarmString))
+            alarmString = "atmos-alerts-window-invalid-state";
+
+        AlarmStateLabel.Text = Loc.GetString(alarmString);
+        AlarmStateLabel.FontColorOverride = GetAlarmStateColor(entry.AlarmState);
+
+        // Update alarm name
+        AlarmNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", entry.EntityName), ("address", entry.Address));
+
+        // Focus updates
+        FocusContainer.Visible = isFocus;
+
+        if (isFocus)
+            SetAsFocus();
+        else
+            RemoveAsFocus();
+
+        if (isFocus && entry.Group == AtmosAlertsComputerGroup.AirAlarm)
+        {
+            MainDataContainer.Visible = (entry.AlarmState != AtmosAlarmType.Invalid);
+            NoDataLabel.Visible = (entry.AlarmState == AtmosAlarmType.Invalid);
+
+            if (focusData != null)
+            {
+                // Update temperature
+                var tempK = (FixedPoint2) focusData.Value.TemperatureData.Item1;
+                var tempC = (FixedPoint2) TemperatureHelpers.KelvinToCelsius(tempK.Float());
+
+                TemperatureLabel.Text = Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK));
+                TemperatureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.TemperatureData.Item2);
+
+                // Update pressure
+                PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2) focusData.Value.PressureData.Item1));
+                PressureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.PressureData.Item2);
+
+                // Update oxygenation
+                var oxygenPercent = (FixedPoint2) 0f;
+                var oxygenAlert = AtmosAlarmType.Invalid;
+
+                if (focusData.Value.GasData.TryGetValue(Gas.Oxygen, out var oxygenData))
+                {
+                    oxygenPercent = oxygenData.Item2 * 100f;
+                    oxygenAlert = oxygenData.Item3;
+                }
+
+                OxygenationLabel.Text = Loc.GetString("atmos-alerts-window-oxygenation-value", ("value", oxygenPercent));
+                OxygenationLabel.FontColorOverride = GetAlarmStateColor(oxygenAlert);
+
+                // Update other present gases
+                GasGridContainer.RemoveAllChildren();
+
+                var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen);
+
+                if (gasData.Count() == 0)
+                {
+                    // No other 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, (var mol, var percent, var alert)) in gasData)
+                    {
+                        var gasPercent = (FixedPoint2) 0f;
+                        gasPercent = percent * 100f;
+
+                        if (!_gasShorthands.TryGetValue(gas, out var gasShorthand))
+                            gasShorthand = "X";
+
+                        var gasLabel = new Label()
+                        {
+                            Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
+                            FontOverride = normalFont,
+                            FontColorOverride = GetAlarmStateColor(alert),
+                            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";
+    }
+
+    public void RemoveAsFocus()
+    {
+        FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+        ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
+        FocusContainer.Visible = false;
+    }
+
+    private Color GetAlarmStateColor(AtmosAlarmType alarmType)
+    {
+        switch (alarmType)
+        {
+            case AtmosAlarmType.Normal:
+                return StyleNano.GoodGreenFore;
+            case AtmosAlarmType.Warning:
+                return StyleNano.ConcerningOrangeFore;
+            case AtmosAlarmType.Danger:
+                return StyleNano.DangerousRedFore;
+        }
+
+        return StyleNano.DisabledFore;
+    }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..08cae97
--- /dev/null
@@ -0,0 +1,52 @@
+using Content.Shared.Atmos.Components;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
+{
+    [ViewVariables]
+    private AtmosAlertsComputerWindow? _menu;
+
+    public AtmosAlertsComputerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+    protected override void Open()
+    {
+        _menu = new AtmosAlertsComputerWindow(this, Owner);
+        _menu.OpenCentered();
+        _menu.OnClose += Close;
+
+        EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        var castState = (AtmosAlertsComputerBoundInterfaceState) state;
+
+        if (castState == null)
+            return;
+
+        EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
+        _menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData);
+    }
+
+    public void SendFocusChangeMessage(NetEntity? netEntity)
+    {
+        SendMessage(new AtmosAlertsComputerFocusChangeMessage(netEntity));
+    }
+
+    public void SendDeviceSilencedMessage(NetEntity netEntity, bool silenceDevice)
+    {
+        SendMessage(new AtmosAlertsComputerDeviceSilencedMessage(netEntity, silenceDevice));
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        _menu?.Dispose();
+    }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
new file mode 100644 (file)
index 0000000..8824a77
--- /dev/null
@@ -0,0 +1,108 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+               xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
+               xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+               xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+               Title="{Loc 'atmos-alerts-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:NavMapControl 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:NavMapControl>
+
+                <!-- Nav map legend -->
+                <BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
+                    <Label Text="{Loc 'atmos-alerts-window-label-alert-types'}"
+                           Margin="20 0 5 0"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
+                                Modulate="#5A5A5A"
+                                SetSize="16 16"
+                                Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-alerts-window-invalid-state'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
+                                Modulate="#32cd32"
+                                SetSize="16 16"
+                                Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-alerts-window-normal-state'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
+                                SetSize="16 16"
+                                Modulate="#ffb648"
+                                Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-alerts-window-warning-state'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                TexturePath="/Textures/Interface/NavMap/beveled_square.png"
+                                SetSize="16 16"
+                                Modulate="#ff4343"
+                                Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-alerts-window-danger-state'}"/>
+                </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="AlertsTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
+                    </ScrollContainer>
+                    <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+                        <BoxContainer Name="AirAlarmsTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
+                    </ScrollContainer>
+                    <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+                        <BoxContainer Name="FireAlarmsTable" 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-alerts-window-toggle-overlays'}" Margin="0 0 0 5"/>
+                    <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+                        <CheckBox Name="ShowInactiveAlarms" Text="{Loc 'atmos-alerts-window-invalid-state'}" Pressed="False" HorizontalExpand="True"/>
+                        <CheckBox Name="ShowNormalAlarms" Text="{Loc 'atmos-alerts-window-normal-state'}" Pressed="False" HorizontalExpand="True"/>
+                        <CheckBox Name="ShowWarningAlarms" Text="{Loc 'atmos-alerts-window-warning-state'}" Pressed="True" HorizontalExpand="True"/>
+                        <CheckBox Name="ShowDangerAlarms" Text="{Loc 'atmos-alerts-window-danger-state'}" Pressed="True" 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-alerts-window-flavor-left'}" StyleClasses="WindowFooterText" />
+                <Label Text="{Loc 'atmos-alerts-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>
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
new file mode 100644 (file)
index 0000000..3fee5b5
--- /dev/null
@@ -0,0 +1,556 @@
+using Content.Client.Message;
+using Content.Client.Pinpointer.UI;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+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.Timing;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlertsComputerWindow : FancyWindow
+{
+    private readonly IEntityManager _entManager;
+    private readonly SpriteSystem _spriteSystem;
+
+    private EntityUid? _owner;
+    private NetEntity? _trackedEntity;
+
+    private AtmosAlertsComputerEntry[]? _airAlarms = null;
+    private AtmosAlertsComputerEntry[]? _fireAlarms = null;
+    private IEnumerable<AtmosAlertsComputerEntry>? _activeAlarms = null;
+    private Dictionary<NetEntity, float> _deviceSilencingProgress = new();
+
+    public event Action<NetEntity?>? SendFocusChangeMessageAction;
+    public event Action<NetEntity, bool>? SendDeviceSilencedMessageAction;
+
+    private bool _autoScrollActive = false;
+    private bool _autoScrollAwaitsUpdate = false;
+
+    private const float SilencingDuration = 2.5f;
+
+    public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
+    {
+        RobustXamlLoader.Load(this);
+        _entManager = IoCManager.Resolve<IEntityManager>();
+        _spriteSystem = _entManager.System<SpriteSystem>();
+
+        // Pass the owner to nav map
+        _owner = owner;
+        NavMap.Owner = _owner;
+
+        // Set nav map colors
+        NavMap.WallColor = new Color(64, 64, 64);
+        NavMap.TileColor = Color.DimGray * NavMap.WallColor;
+
+        // Set nav map grid uid
+        var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
+
+        if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
+        {
+            NavMap.MapUid = xform.GridUid;
+
+            // Assign station name      
+            if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
+                stationName = stationMetaData.EntityName;
+
+            var msg = new FormattedMessage();
+            msg.AddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)));
+
+            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-alerts-window-tab-no-alerts"));
+        MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms"));
+        MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms"));
+
+        // Set UI toggles
+        ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid);
+        ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal);
+        ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning);
+        ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger);
+
+        // Set atmos monitoring message action
+        SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
+        SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage;
+    }
+
+    #region Toggle handling
+
+    private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState)
+    {
+        if (_owner == null)
+            return;
+
+        if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner.Value, out var console))
+            return;
+
+        foreach (var device in console.AtmosDevices)
+        {
+            var alarmState = GetAlarmState(device.NetEntity, device.Group);
+
+            if (toggledAlarmState != alarmState)
+                continue;
+
+            if (toggle.Pressed)
+                AddTrackedEntityToNavMap(device, alarmState);
+
+            else
+                NavMap.TrackedEntities.Remove(device.NetEntity);
+        }
+    }
+
+    private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState)
+    {
+        if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner, out var console))
+            return;
+
+        if (toggleState)
+            _deviceSilencingProgress[netEntity] = SilencingDuration;
+
+        else
+            _deviceSilencingProgress.Remove(netEntity);
+
+        foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children)
+        {
+            if (entryContainer.NetEntity == netEntity)
+                entryContainer.SilenceAlarmProgressBar.Visible = toggleState;
+        }
+
+        SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState);
+    }
+
+    #endregion
+
+    public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
+    {
+        if (_owner == null)
+            return;
+
+        if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner.Value, out var console))
+            return;
+
+        if (_trackedEntity != focusData?.NetEntity)
+        {
+            SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+            focusData = null;
+        }
+
+        // Retain alarm data for use inbetween updates
+        _airAlarms = airAlarms;
+        _fireAlarms = fireAlarms;
+
+        var allAlarms = airAlarms.Concat(fireAlarms);
+        var silenced = console.SilencedDevices;
+
+        _activeAlarms = allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal &&
+            (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity)));
+
+        // Reset nav map data
+        NavMap.TrackedCoordinates.Clear();
+        NavMap.TrackedEntities.Clear();
+
+        // Add tracked entities to the nav map
+        foreach (var device in console.AtmosDevices)
+        {
+            if (!NavMap.Visible)
+                continue;
+
+            var alarmState = GetAlarmState(device.NetEntity, device.Group);
+
+            if (_trackedEntity != device.NetEntity)
+            {
+                // Skip air alarms if the appropriate overlay is off
+                if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid)
+                    continue;
+
+                if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal)
+                    continue;
+
+                if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning)
+                    continue;
+
+                if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger)
+                    continue;
+            }
+
+            AddTrackedEntityToNavMap(device, alarmState);
+        }
+
+        // Show the monitor location
+        var consoleUid = _entManager.GetNetEntity(_owner);
+
+        if (consoleCoords != null && consoleUid != null)
+        {
+            var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+            var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
+            NavMap.TrackedEntities[consoleUid.Value] = blip;
+        }
+
+        // Update the nav map
+        NavMap.ForceNavMapUpdate();
+
+        // Clear excess children from the tables
+        var activeAlarmCount = _activeAlarms.Count();
+
+        while (AlertsTable.ChildCount > activeAlarmCount)
+            AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1));
+
+        while (AirAlarmsTable.ChildCount > airAlarms.Length)
+            AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1));
+
+        while (FireAlarmsTable.ChildCount > fireAlarms.Length)
+            FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1));
+
+        // Update all entries in each table
+        for (int index = 0; index < _activeAlarms.Count(); index++)
+        {
+            var entry = _activeAlarms.ElementAt(index);
+            UpdateUIEntry(entry, index, AlertsTable, console, focusData);
+        }
+
+        for (int index = 0; index < airAlarms.Count(); index++)
+        {
+            var entry = airAlarms.ElementAt(index);
+            UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData);
+        }
+
+        for (int index = 0; index < fireAlarms.Count(); index++)
+        {
+            var entry = fireAlarms.ElementAt(index);
+            UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData);
+        }
+
+        // If no alerts are active, display a message
+        if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0)
+        {
+            var label = new RichTextLabel()
+            {
+                HorizontalExpand = true,
+                VerticalExpand = true,
+                HorizontalAlignment = HAlignment.Center,
+                VerticalAlignment = VAlignment.Center,
+            };
+
+            label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
+
+            AlertsTable.AddChild(label);
+        }
+
+        // Update the alerts tab with the number of active alerts
+        if (activeAlarmCount == 0)
+            MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+
+        else
+            MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
+
+        // Auto-scroll re-enable
+        if (_autoScrollAwaitsUpdate)
+        {
+            _autoScrollActive = true;
+            _autoScrollAwaitsUpdate = false;
+        }
+    }
+
+    private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState)
+    {
+        var data = GetBlipTexture(alarmState);
+
+        if (data == null)
+            return;
+
+        var texture = data.Value.Item1;
+        var color = data.Value.Item2;
+        var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+
+        if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
+            color *= Color.DimGray;
+
+        var selectable = true;
+        var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
+
+        NavMap.TrackedEntities[metaData.NetEntity] = blip;
+    }
+
+    private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
+    {
+        // Make new UI entry if required
+        if (index >= table.ChildCount)
+        {
+            var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates));
+
+            // On click
+            newEntryContainer.FocusButton.OnButtonUp += args =>
+            {
+                var prevTrackedEntity = _trackedEntity;
+
+                if (_trackedEntity == entry.NetEntity)
+                {
+                    _trackedEntity = null;
+                }
+
+                else
+                {
+                    _trackedEntity = newEntryContainer.NetEntity;
+                    NavMap.CenterToCoordinates(_entManager.GetCoordinates(entry.Coordinates));
+                }
+
+                // Send message to console that the focus has changed
+                SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+
+                // Update affected UI elements across all tables
+                UpdateConsoleTable(console, AlertsTable, _trackedEntity, prevTrackedEntity);
+                UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity, prevTrackedEntity);
+                UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity, prevTrackedEntity);
+            };
+
+            // On toggling the silence check box
+            newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(entry.NetEntity, newEntryContainer.SilenceCheckBox.Pressed);
+
+            // Add the entry to the current table
+            table.AddChild(newEntryContainer);
+        }
+
+        // Update values and UI elements
+        var tableChild = table.GetChild(index);
+
+        if (tableChild is not AtmosAlarmEntryContainer)
+        {
+            table.RemoveChild(tableChild);
+            UpdateUIEntry(entry, index, table, console, focusData);
+
+            return;
+        }
+
+        var entryContainer = tableChild as AtmosAlarmEntryContainer;
+        var silenced = console.SilencedDevices;
+
+        if (entryContainer == null)
+            return;
+
+        entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
+        entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+        entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+    }
+
+    private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity, NetEntity? prevTrackedEntity)
+    {
+        foreach (var child in table.Children)
+        {
+            if (child is not AtmosAlarmEntryContainer)
+                continue;
+
+            var castAlert = (AtmosAlarmEntryContainer) child;
+
+            if (castAlert.NetEntity == prevTrackedEntity)
+                castAlert.RemoveAsFocus();
+
+            else if (castAlert.NetEntity == currTrackedEntity)
+                castAlert.SetAsFocus();
+
+            if (castAlert?.Coordinates == null)
+                continue;
+
+            var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == castAlert.NetEntity);
+
+            if (device == null)
+                continue;
+
+            var alarmState = GetAlarmState(device.Value.NetEntity, device.Value.Group);
+
+            if (currTrackedEntity != device.Value.NetEntity &&
+                !ShowInactiveAlarms.Pressed &&
+                alarmState <= AtmosAlarmType.Normal)
+                continue;
+
+            AddTrackedEntityToNavMap(device.Value, alarmState);
+        }
+    }
+
+    private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
+    {
+        if (netEntity == null)
+            return;
+
+        if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner, out var console))
+            return;
+
+        _trackedEntity = netEntity;
+
+        if (netEntity != null)
+        {
+            // Tab switching
+            if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false)
+            {
+                var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity);
+
+                switch (device?.Group)
+                {
+                    case AtmosAlertsComputerGroup.AirAlarm:
+                        MasterTabContainer.CurrentTab = 1; break;
+                    case AtmosAlertsComputerGroup.FireAlarm:
+                        MasterTabContainer.CurrentTab = 2; break;
+                }
+            }
+
+            // Get the scroll position of the selected entity on the selected button the UI
+            ActivateAutoScrollToFocus();
+        }
+
+        // Send message to console that the focus has changed
+        SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        AutoScrollToFocus();
+
+        // Device silencing update
+        foreach ((var device, var remainingTime) in _deviceSilencingProgress)
+        {
+            var t = remainingTime - args.DeltaSeconds;
+
+            if (t <= 0)
+                _deviceSilencingProgress.Remove(device);
+
+            else
+                _deviceSilencingProgress[device] = t;
+        }
+    }
+
+    private void ActivateAutoScrollToFocus()
+    {
+        _autoScrollActive = false;
+        _autoScrollAwaitsUpdate = true;
+    }
+
+    private void AutoScrollToFocus()
+    {
+        if (!_autoScrollActive)
+            return;
+
+        var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) 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 child in scroll.Children)
+        {
+            if (child is not VScrollBar)
+                continue;
+
+            var castChild = child as VScrollBar;
+
+            if (castChild != null)
+            {
+                vScrollBar = castChild;
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+    {
+        nextScrollPosition = null;
+
+        var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) 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 == null || control is not AtmosAlarmEntryContainer)
+                continue;
+
+            if (((AtmosAlarmEntryContainer) control).NetEntity == _trackedEntity)
+                return true;
+
+            nextScrollPosition += control.Height;
+        }
+
+        // Failed to find control
+        nextScrollPosition = null;
+
+        return false;
+    }
+
+    private AtmosAlarmType GetAlarmState(NetEntity netEntity, AtmosAlertsComputerGroup group)
+    {
+        var alarms = (group == AtmosAlertsComputerGroup.AirAlarm) ? _airAlarms : _fireAlarms;
+        var alarmState = alarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState;
+
+        if (alarmState == null)
+            return AtmosAlarmType.Invalid;
+
+        return alarmState.Value;
+    }
+
+    private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState)
+    {
+        (SpriteSpecifier.Texture, Color)? output = null;
+
+        switch (alarmState)
+        {
+            case AtmosAlarmType.Invalid:
+                output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
+            case AtmosAlarmType.Normal:
+                output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
+            case AtmosAlarmType.Warning:
+                output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
+            case AtmosAlarmType.Danger:
+                output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
+        }
+
+        return output;
+    }
+}
diff --git a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs
new file mode 100644 (file)
index 0000000..d9a475d
--- /dev/null
@@ -0,0 +1,348 @@
+using Content.Server.Atmos.Monitor.Components;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.Power.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Consoles;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.Atmos.Monitor.Components;
+using Content.Shared.Pinpointer;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Server.Atmos.Monitor.Systems;
+
+public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
+{
+    [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+    [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
+    [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+    private const float UpdateTime = 1.0f;
+
+    // Note: this data does not need to be saved
+    private float _updateTimer = 1.0f;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // Console events
+        SubscribeLocalEvent<AtmosAlertsComputerComponent, ComponentInit>(OnConsoleInit);
+        SubscribeLocalEvent<AtmosAlertsComputerComponent, EntParentChangedMessage>(OnConsoleParentChanged);
+        SubscribeLocalEvent<AtmosAlertsComputerComponent, AtmosAlertsComputerFocusChangeMessage>(OnFocusChangedMessage);
+
+        // Grid events
+        SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
+        SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
+    }
+
+    #region Event handling 
+
+    private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args)
+    {
+        InitalizeConsole(uid, component);
+    }
+
+    private void OnConsoleParentChanged(EntityUid uid, AtmosAlertsComputerComponent component, EntParentChangedMessage args)
+    {
+        InitalizeConsole(uid, component);
+    }
+
+    private void OnFocusChangedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerFocusChangeMessage args)
+    {
+        component.FocusDevice = args.FocusDevice;
+    }
+
+    private void OnGridSplit(ref GridSplitEvent args)
+    {
+        // Collect grids
+        var allGrids = args.NewGrids.ToList();
+
+        if (!allGrids.Contains(args.Grid))
+            allGrids.Add(args.Grid);
+
+        // Update atmos monitoring consoles that stand upon an updated grid
+        var query = AllEntityQuery<AtmosAlertsComputerComponent, 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;
+
+            InitalizeConsole(ent, entConsole);
+        }
+    }
+
+    private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
+    {
+        var xform = Transform(uid);
+        var gridUid = xform.GridUid;
+
+        if (gridUid == null)
+            return;
+
+        if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
+            return;
+
+        var netEntity = EntityManager.GetNetEntity(uid);
+
+        var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
+        while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+        {
+            if (gridUid != entXform.GridUid)
+                continue;
+
+            if (args.Anchored)
+                entConsole.AtmosDevices.Add(data.Value);
+
+            else if (!args.Anchored)
+                entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
+        }
+    }
+
+    #endregion
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        _updateTimer += frameTime;
+
+        if (_updateTimer >= UpdateTime)
+        {
+            _updateTimer -= UpdateTime;
+
+            // Keep a list of UI entries for each gridUid, in case multiple consoles stand on the same grid
+            var airAlarmEntriesForEachGrid = new Dictionary<EntityUid, AtmosAlertsComputerEntry[]>();
+            var fireAlarmEntriesForEachGrid = new Dictionary<EntityUid, AtmosAlertsComputerEntry[]>();
+
+            var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
+            while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+            {
+                if (entXform?.GridUid == null)
+                    continue;
+
+                // Make a list of alarm state data for all the air and fire alarms on the grid
+                if (!airAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var airAlarmEntries))
+                {
+                    airAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.AirAlarm).ToArray();
+                    airAlarmEntriesForEachGrid[entXform.GridUid.Value] = airAlarmEntries;
+                }
+
+                if (!fireAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var fireAlarmEntries))
+                {
+                    fireAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.FireAlarm).ToArray();
+                    fireAlarmEntriesForEachGrid[entXform.GridUid.Value] = fireAlarmEntries;
+                }
+
+                // Determine the highest level of alert for the console (based on non-silenced alarms)
+                var highestAlert = AtmosAlarmType.Invalid;
+
+                foreach (var entry in airAlarmEntries)
+                {
+                    if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity))
+                        highestAlert = entry.AlarmState;
+                }
+
+                foreach (var entry in fireAlarmEntries)
+                {
+                    if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity))
+                        highestAlert = entry.AlarmState;
+                }
+
+                // Update the appearance of the console based on the highest recorded level of alert
+                if (TryComp<AppearanceComponent>(ent, out var entAppearance))
+                    _appearance.SetData(ent, AtmosAlertsComputerVisuals.ComputerLayerScreen, (int) highestAlert, entAppearance);
+
+                // If the console UI is open, send UI data to each subscribed session
+                UpdateUIState(ent, airAlarmEntries, fireAlarmEntries, entConsole, entXform);
+            }
+        }
+    }
+
+    public void UpdateUIState
+        (EntityUid uid,
+        AtmosAlertsComputerEntry[] airAlarmStateData,
+        AtmosAlertsComputerEntry[] fireAlarmStateData,
+        AtmosAlertsComputerComponent component,
+        TransformComponent xform)
+    {
+        if (!_userInterfaceSystem.IsUiOpen(uid, AtmosAlertsComputerUiKey.Key))
+            return;
+
+        var gridUid = xform.GridUid!.Value;
+
+        if (!HasComp<MapGridComponent>(gridUid))
+            return;
+
+        // The grid must have a NavMapComponent to visualize the map in the UI
+        EnsureComp<NavMapComponent>(gridUid);
+
+        // Gathering remaining data to be send to the client
+        var focusAlarmData = GetFocusAlarmData(uid, GetEntity(component.FocusDevice), gridUid);
+
+        // Set the UI state
+        _userInterfaceSystem.SetUiState(uid, AtmosAlertsComputerUiKey.Key,
+            new AtmosAlertsComputerBoundInterfaceState(airAlarmStateData, fireAlarmStateData, focusAlarmData));
+    }
+
+    private List<AtmosAlertsComputerEntry> GetAlarmStateData(EntityUid gridUid, AtmosAlertsComputerGroup group)
+    {
+        var alarmStateData = new List<AtmosAlertsComputerEntry>();
+
+        var queryAlarms = AllEntityQuery<AtmosAlertsDeviceComponent, AtmosAlarmableComponent, DeviceNetworkComponent, TransformComponent>();
+        while (queryAlarms.MoveNext(out var ent, out var entDevice, out var entAtmosAlarmable, out var entDeviceNetwork, out var entXform))
+        {
+            if (entXform.GridUid != gridUid)
+                continue;
+
+            if (!entXform.Anchored)
+                continue;
+
+            if (entDevice.Group != group)
+                continue;
+
+            // If emagged, change the alarm type to normal
+            var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
+
+            // Unpowered alarms can't sound
+            if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
+                alarmState = AtmosAlarmType.Invalid;
+
+            var entry = new AtmosAlertsComputerEntry
+                (GetNetEntity(ent),
+                GetNetCoordinates(entXform.Coordinates),
+                entDevice.Group,
+                alarmState,
+                MetaData(ent).EntityName,
+                entDeviceNetwork.Address);
+
+            alarmStateData.Add(entry);
+        }
+
+        return alarmStateData;
+    }
+
+    private AtmosAlertsFocusDeviceData? GetFocusAlarmData(EntityUid uid, EntityUid? focusDevice, EntityUid gridUid)
+    {
+        if (focusDevice == null)
+            return null;
+
+        var focusDeviceXform = Transform(focusDevice.Value);
+
+        if (!focusDeviceXform.Anchored ||
+            focusDeviceXform.GridUid != gridUid ||
+            !TryComp<AirAlarmComponent>(focusDevice.Value, out var focusDeviceAirAlarm))
+        {
+            return null;
+        }
+
+        // Force update the sensors attached to the alarm
+        if (!_userInterfaceSystem.IsUiOpen(focusDevice.Value, SharedAirAlarmInterfaceKey.Key))
+        {
+            _atmosDevNet.Register(focusDevice.Value, null);
+            _atmosDevNet.Sync(focusDevice.Value, null);
+
+            foreach ((var address, var _) in focusDeviceAirAlarm.SensorData)
+                _atmosDevNet.Register(uid, null);
+        }
+
+        // Get the sensor data
+        var temperatureData = (_airAlarmSystem.CalculateTemperatureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal);
+        var pressureData = (_airAlarmSystem.CalculatePressureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal);
+        var gasData = new Dictionary<Gas, (float, float, AtmosAlarmType)>();
+
+        foreach ((var address, var sensorData) in focusDeviceAirAlarm.SensorData)
+        {
+            if (sensorData.TemperatureThreshold.CheckThreshold(sensorData.Temperature, out var temperatureState) &&
+                (int) temperatureState > (int) temperatureData.Item2)
+            {
+                temperatureData = (temperatureData.Item1, temperatureState);
+            }
+
+            if (sensorData.PressureThreshold.CheckThreshold(sensorData.Pressure, out var pressureState) &&
+                (int) pressureState > (int) pressureData.Item2)
+            {
+                pressureData = (pressureData.Item1, pressureState);
+            }
+
+            if (focusDeviceAirAlarm.SensorData.Sum(g => g.Value.TotalMoles) > 1e-8)
+            {
+                foreach ((var gas, var threshold) in sensorData.GasThresholds)
+                {
+                    if (!gasData.ContainsKey(gas))
+                    {
+                        float mol = _airAlarmSystem.CalculateGasMolarConcentrationAverage(focusDeviceAirAlarm, gas, out var percentage);
+
+                        if (mol < 1e-8)
+                            continue;
+
+                        gasData[gas] = (mol, percentage, AtmosAlarmType.Normal);
+                    }
+
+                    if (threshold.CheckThreshold(gasData[gas].Item2, out var gasState) &&
+                        (int) gasState > (int) gasData[gas].Item3)
+                    {
+                        gasData[gas] = (gasData[gas].Item1, gasData[gas].Item2, gasState);
+                    }
+                }
+            }
+        }
+
+        return new AtmosAlertsFocusDeviceData(GetNetEntity(focusDevice.Value), temperatureData, pressureData, gasData);
+    }
+
+    private HashSet<AtmosAlertsDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
+    {
+        var atmosDeviceNavMapData = new HashSet<AtmosAlertsDeviceNavMapData>();
+
+        var query = AllEntityQuery<AtmosAlertsDeviceComponent, 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);
+        }
+
+        return atmosDeviceNavMapData;
+    }
+
+    private bool TryGetAtmosDeviceNavMapData
+        (EntityUid uid,
+        AtmosAlertsDeviceComponent component,
+        TransformComponent xform,
+        EntityUid gridUid,
+        [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
+    {
+        output = null;
+
+        if (xform.GridUid != gridUid)
+            return false;
+
+        if (!xform.Anchored)
+            return false;
+
+        output = new AtmosAlertsDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), component.Group);
+
+        return true;
+    }
+
+    private void InitalizeConsole(EntityUid uid, AtmosAlertsComputerComponent component)
+    {
+        var xform = Transform(uid);
+
+        if (xform.GridUid == null)
+            return;
+
+        var grid = xform.GridUid.Value;
+        component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
+
+        Dirty(uid, component);
+    }
+}
index eebac18501a86ed321fe8371ae7dc80e69e95f3c..ca01ef13072fa59afc36d7b36a8a3465e2495535 100644 (file)
@@ -1,4 +1,3 @@
-using System.Linq;
 using Content.Server.Atmos.Monitor.Components;
 using Content.Server.Atmos.Piping.Components;
 using Content.Server.DeviceLinking.Systems;
@@ -22,6 +21,7 @@ using Content.Shared.Power;
 using Content.Shared.Wires;
 using Robust.Server.GameObjects;
 using Robust.Shared.Player;
+using System.Linq;
 
 namespace Content.Server.Atmos.Monitor.Systems;
 
@@ -582,6 +582,20 @@ public sealed class AirAlarmSystem : EntitySystem
             ? alarm.SensorData.Values.Select(v => v.Temperature).Average()
             : 0f;
     }
+    public float CalculateGasMolarConcentrationAverage(AirAlarmComponent alarm, Gas gas, out float percentage)
+    {
+        percentage = 0f;
+
+        var data = alarm.SensorData.Values.SelectMany(v => v.Gases.Where(g => g.Key == gas));
+
+        if (data.Count() == 0)
+            return 0f;
+
+        var averageMol = data.Select(kvp => kvp.Value).Average();
+        percentage = data.Select(kvp => kvp.Value).Sum() / alarm.SensorData.Values.Select(v => v.TotalMoles).Sum();
+
+        return averageMol;
+    }
 
     public void UpdateUI(EntityUid uid, AirAlarmComponent? alarm = null, DeviceNetworkComponent? devNet = null, AtmosAlarmableComponent? alarmable = null)
     {
diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs
new file mode 100644 (file)
index 0000000..d64c890
--- /dev/null
@@ -0,0 +1,235 @@
+using Content.Shared.Atmos.Consoles;
+using Content.Shared.Atmos.Monitor;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Atmos.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedAtmosAlertsComputerSystem))]
+public sealed partial class AtmosAlertsComputerComponent : Component
+{
+    /// <summary>
+    /// The current entity of interest (selected via the console UI)
+    /// </summary>
+    [ViewVariables]
+    public NetEntity? FocusDevice;
+
+    /// <summary>
+    /// A list of all the atmos devices that will be used to populate the nav map
+    /// </summary>
+    [ViewVariables, AutoNetworkedField]
+    public HashSet<AtmosAlertsDeviceNavMapData> AtmosDevices = new();
+
+    /// <summary>
+    /// A list of all the air alarms that have had their alerts silenced on this particular console
+    /// </summary>
+    [ViewVariables, AutoNetworkedField]
+    public HashSet<NetEntity> SilencedDevices = new();
+}
+
+[Serializable, NetSerializable]
+public struct AtmosAlertsDeviceNavMapData
+{
+    /// <summary>
+    /// The entity in question
+    /// </summary>
+    public NetEntity NetEntity;
+
+    /// <summary>
+    /// Location of the entity
+    /// </summary>
+    public NetCoordinates NetCoordinates;
+
+    /// <summary>
+    /// Used to determine what map icons to use
+    /// </summary>
+    public AtmosAlertsComputerGroup Group;
+
+    /// <summary>
+    /// Populate the atmos monitoring console nav map with a single entity
+    /// </summary>
+    public AtmosAlertsDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, AtmosAlertsComputerGroup group)
+    {
+        NetEntity = netEntity;
+        NetCoordinates = netCoordinates;
+        Group = group;
+    }
+}
+
+[Serializable, NetSerializable]
+public struct AtmosAlertsFocusDeviceData
+{
+    /// <summary>
+    /// Focus entity
+    /// </summary>
+    public NetEntity NetEntity;
+
+    /// <summary>
+    /// Temperature (K) and related alert state
+    /// </summary>
+    public (float, AtmosAlarmType) TemperatureData;
+
+    /// <summary>
+    /// Pressure (kPA) and related alert state
+    /// </summary>
+    public (float, AtmosAlarmType) PressureData;
+
+    /// <summary>
+    /// Moles, percentage, and related alert state, for all detected gases 
+    /// </summary>
+    public Dictionary<Gas, (float, float, AtmosAlarmType)> GasData;
+
+    /// <summary>
+    /// Populates the atmos monitoring console focus entry with atmospheric data
+    /// </summary>
+    public AtmosAlertsFocusDeviceData
+        (NetEntity netEntity,
+        (float, AtmosAlarmType) temperatureData,
+        (float, AtmosAlarmType) pressureData,
+        Dictionary<Gas, (float, float, AtmosAlarmType)> gasData)
+    {
+        NetEntity = netEntity;
+        TemperatureData = temperatureData;
+        PressureData = pressureData;
+        GasData = gasData;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class AtmosAlertsComputerBoundInterfaceState : BoundUserInterfaceState
+{
+    /// <summary>
+    /// A list of all air alarms
+    /// </summary>
+    public AtmosAlertsComputerEntry[] AirAlarms;
+
+    /// <summary>
+    /// A list of all fire alarms
+    /// </summary>
+    public AtmosAlertsComputerEntry[] FireAlarms;
+
+    /// <summary>
+    /// Data for the UI focus (if applicable)
+    /// </summary>
+    public AtmosAlertsFocusDeviceData? FocusData;
+
+    /// <summary>
+    /// Sends data from the server to the client to populate the atmos monitoring console UI
+    /// </summary>
+    public AtmosAlertsComputerBoundInterfaceState(AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
+    {
+        AirAlarms = airAlarms;
+        FireAlarms = fireAlarms;
+        FocusData = focusData;
+    }
+}
+
+[Serializable, NetSerializable]
+public struct AtmosAlertsComputerEntry
+{
+    /// <summary>
+    /// The entity in question
+    /// </summary>
+    public NetEntity NetEntity;
+
+    /// <summary>
+    /// Location of the entity
+    /// </summary>
+    public NetCoordinates Coordinates;
+
+    /// <summary>
+    /// The type of entity
+    /// </summary>
+    public AtmosAlertsComputerGroup Group;
+
+    /// <summary>
+    /// Current alarm state
+    /// </summary>
+    public AtmosAlarmType AlarmState;
+
+    /// <summary>
+    /// Localised device name
+    /// </summary>
+    public string EntityName;
+
+    /// <summary>
+    /// Device network address
+    /// </summary>
+    public string Address;
+
+    /// <summary>
+    /// Used to populate the atmos monitoring console UI with data from a single air alarm
+    /// </summary>
+    public AtmosAlertsComputerEntry
+        (NetEntity entity,
+        NetCoordinates coordinates,
+        AtmosAlertsComputerGroup group,
+        AtmosAlarmType alarmState,
+        string entityName,
+        string address)
+    {
+        NetEntity = entity;
+        Coordinates = coordinates;
+        Group = group;
+        AlarmState = alarmState;
+        EntityName = entityName;
+        Address = address;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class AtmosAlertsComputerFocusChangeMessage : BoundUserInterfaceMessage
+{
+    public NetEntity? FocusDevice;
+
+    /// <summary>
+    /// Used to inform the server that the specified focus for the atmos monitoring console has been changed by the client
+    /// </summary>
+    public AtmosAlertsComputerFocusChangeMessage(NetEntity? focusDevice)
+    {
+        FocusDevice = focusDevice;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class AtmosAlertsComputerDeviceSilencedMessage : BoundUserInterfaceMessage
+{
+    public NetEntity AtmosDevice;
+    public bool SilenceDevice = true;
+
+    /// <summary>
+    /// Used to inform the server that the client has silenced alerts from the specified device to this atmos monitoring console 
+    /// </summary>
+    public AtmosAlertsComputerDeviceSilencedMessage(NetEntity atmosDevice, bool silenceDevice = true)
+    {
+        AtmosDevice = atmosDevice;
+        SilenceDevice = silenceDevice;
+    }
+}
+
+/// <summary>
+/// List of all the different atmos device groups
+/// </summary>
+public enum AtmosAlertsComputerGroup
+{
+    Invalid,
+    AirAlarm,
+    FireAlarm,
+}
+
+[NetSerializable, Serializable]
+public enum AtmosAlertsComputerVisuals
+{
+    ComputerLayerScreen,
+}
+
+/// <summary>
+/// UI key associated with the atmos monitoring console
+/// </summary>
+[Serializable, NetSerializable]
+public enum AtmosAlertsComputerUiKey
+{
+    Key
+}
diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs
new file mode 100644 (file)
index 0000000..881d60b
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access([])]
+public sealed partial class AtmosAlertsDeviceComponent : Component
+{
+    /// <summary>
+    /// The group that the entity belongs to
+    /// </summary>
+    [DataField, ViewVariables]
+    public AtmosAlertsComputerGroup Group;
+}
diff --git a/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs b/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs
new file mode 100644 (file)
index 0000000..7e2b2b0
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.Atmos.Components;
+
+namespace Content.Shared.Atmos.Consoles;
+
+public abstract partial class SharedAtmosAlertsComputerSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<AtmosAlertsComputerComponent, AtmosAlertsComputerDeviceSilencedMessage>(OnDeviceSilencedMessage);
+    }
+
+    private void OnDeviceSilencedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerDeviceSilencedMessage args)
+    {
+        if (args.SilenceDevice)
+            component.SilencedDevices.Add(args.AtmosDevice);
+
+        else
+            component.SilencedDevices.Remove(args.AtmosDevice);
+
+        Dirty(uid, component);
+    }
+}
diff --git a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl
new file mode 100644 (file)
index 0000000..a1640c5
--- /dev/null
@@ -0,0 +1,35 @@
+atmos-alerts-window-title = Atmospheric Alerts Computer
+atmos-alerts-window-station-name = [color=white][font size=14]{$stationName}[/font][/color]
+atmos-alerts-window-unknown-location = Unknown location
+
+atmos-alerts-window-tab-no-alerts = Alerts
+atmos-alerts-window-tab-alerts = Alerts ({$value})
+atmos-alerts-window-tab-air-alarms = Air alarms
+atmos-alerts-window-tab-fire-alarms = Fire alarms
+
+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-pressure-label = Pressure
+atmos-alerts-window-pressure-value = {$value} kPa
+atmos-alerts-window-oxygenation-label = Oxygenation
+atmos-alerts-window-oxygenation-value = {$value}% 
+atmos-alerts-window-other-gases-label = Other present gases
+atmos-alerts-window-other-gases-value = {$shorthand} ({$value}%) 
+atmos-alerts-window-other-gases-value-nil = None
+atmos-alerts-window-silence-alerts = Silence alerts from this alarm
+
+atmos-alerts-window-label-alert-types = Alert levels:
+atmos-alerts-window-normal-state = Normal
+atmos-alerts-window-warning-state = Warning
+atmos-alerts-window-danger-state = Danger!
+atmos-alerts-window-invalid-state = Inactive
+
+atmos-alerts-window-no-active-alerts = [font size=16][color=white]No active alerts -[/color] [color={$color}]situation normal[/color][/font]
+atmos-alerts-window-no-data-available = No data available
+atmos-alerts-window-alerts-being-silenced = Silencing alerts...
+
+atmos-alerts-window-toggle-overlays = Toggle alarm display
+
+atmos-alerts-window-flavor-left = Contact an atmospheric technician for assistance
+atmos-alerts-window-flavor-right = v1.8
\ No newline at end of file
index cadb99f5fe82dc321f1792f7d0325ee965acd142..fb2925a4627522000fc40d2490bea6c2f6591565 100644 (file)
@@ -21,8 +21,8 @@
 - type: entity
   parent: BaseComputerCircuitboard
   id: AlertsComputerCircuitboard
-  name: alerts computer board
-  description: A computer printed circuit board for an alerts computer.
+  name: atmospheric alerts computer board
+  description: A computer printed circuit board for an atmospheric alerts computer.
   components:
     - type: ComputerBoard
       prototype: ComputerAlert
index 5327d69cff2c7521095da8af592dc3b23e1ee09f..742865049868677fd6b7b745b968bffccb9840e1 100644 (file)
@@ -1,8 +1,8 @@
 - type: entity
   parent: BaseComputer
   id: ComputerAlert
-  name: alerts computer
-  description: Used to access the station's automated alert system.
+  name: atmospheric alerts computer
+  description: Used to access the station's atmospheric automated alert system.
   components:
   - type: Computer
     board: AlertsComputerCircuitboard
     - map: ["computerLayerKeyboard"]
       state: generic_keyboard
     - map: ["computerLayerScreen"]
-      state: alert-2
+      state: alert-0
     - map: ["computerLayerKeys"]
       state: atmos_key
+  - type: GenericVisualizer
+    visuals:
+      enum.ComputerVisuals.Powered:
+        computerLayerScreen:
+          True: { visible: true, shader: unshaded }
+          False: { visible: false }
+        computerLayerKeys:
+          True: { visible: true, shader: unshaded }
+          False: { visible: true, shader: shaded }   
+      enum.AtmosAlertsComputerVisuals.ComputerLayerScreen:
+        computerLayerScreen:
+          0: { state: alert-0 }
+          1: { state: alert-0 }
+          2: { state: alert-1 }
+          3: { state: alert-2 }
+          4: { state: alert-2 }
+  - type: AtmosAlertsComputer
+  - type: ActivatableUI
+    singleUser: true
+    key: enum.AtmosAlertsComputerUiKey.Key
+  - type: UserInterface
+    interfaces:
+        enum.AtmosAlertsComputerUiKey.Key:
+            type: AtmosAlertsComputerBoundUserInterface
 
 - type: entity
   parent: BaseComputer
index 285b7a4770a26cfe4a3098dad21fc479ce758bb8..c65c39d11fb32308a1e18395a3a8fb9e784d6dc0 100644 (file)
@@ -53,6 +53,8 @@
       - AirAlarm
   - type: AtmosDevice
   - type: AirAlarm
+  - type: AtmosAlertsDevice
+    group: AirAlarm
   - type: Clickable
   - type: InteractionOutline
   - type: UserInterface
index 6cf7ba1614269054afe54a71fd17f608f8ee5ff1..917a94ddc99511419fdfed2ad787e0b18a590bee 100644 (file)
@@ -42,6 +42,8 @@
   - type: Clickable
   - type: InteractionOutline
   - type: FireAlarm
+  - type: AtmosAlertsDevice
+    group: FireAlarm
   - type: ContainerFill
     containers:
       board: [ FireAlarmElectronics ]
diff --git a/Resources/Textures/Interface/AtmosMonitoring/status_bg.png b/Resources/Textures/Interface/AtmosMonitoring/status_bg.png
new file mode 100644 (file)
index 0000000..165a9b9
Binary files /dev/null and b/Resources/Textures/Interface/AtmosMonitoring/status_bg.png differ