Atmospheric alerts computer (#25938)

* 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
This commit is contained in:
chromiumboy
2024-09-04 20:13:17 -05:00
committed by GitHub
parent 04bb4b53a5
commit 63ba0f61ea
16 changed files with 1711 additions and 6 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

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

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Server.Atmos.Monitor.Components; using Content.Server.Atmos.Monitor.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.DeviceLinking.Systems; using Content.Server.DeviceLinking.Systems;
@@ -22,6 +21,7 @@ using Content.Shared.Power;
using Content.Shared.Wires; using Content.Shared.Wires;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Player; using Robust.Shared.Player;
using System.Linq;
namespace Content.Server.Atmos.Monitor.Systems; namespace Content.Server.Atmos.Monitor.Systems;
@@ -582,6 +582,20 @@ public sealed class AirAlarmSystem : EntitySystem
? alarm.SensorData.Values.Select(v => v.Temperature).Average() ? alarm.SensorData.Values.Select(v => v.Temperature).Average()
: 0f; : 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) public void UpdateUI(EntityUid uid, AirAlarmComponent? alarm = null, DeviceNetworkComponent? devNet = null, AtmosAlarmableComponent? alarmable = null)
{ {

View File

@@ -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
}

View File

@@ -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;
}

View File

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

View File

@@ -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

View File

@@ -21,8 +21,8 @@
- type: entity - type: entity
parent: BaseComputerCircuitboard parent: BaseComputerCircuitboard
id: AlertsComputerCircuitboard id: AlertsComputerCircuitboard
name: alerts computer board name: atmospheric alerts computer board
description: A computer printed circuit board for an alerts computer. description: A computer printed circuit board for an atmospheric alerts computer.
components: components:
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerAlert prototype: ComputerAlert

View File

@@ -1,8 +1,8 @@
- type: entity - type: entity
parent: BaseComputer parent: BaseComputer
id: ComputerAlert id: ComputerAlert
name: alerts computer name: atmospheric alerts computer
description: Used to access the station's automated alert system. description: Used to access the station's atmospheric automated alert system.
components: components:
- type: Computer - type: Computer
board: AlertsComputerCircuitboard board: AlertsComputerCircuitboard
@@ -13,9 +13,33 @@
- map: ["computerLayerKeyboard"] - map: ["computerLayerKeyboard"]
state: generic_keyboard state: generic_keyboard
- map: ["computerLayerScreen"] - map: ["computerLayerScreen"]
state: alert-2 state: alert-0
- map: ["computerLayerKeys"] - map: ["computerLayerKeys"]
state: atmos_key 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 - type: entity
parent: BaseComputer parent: BaseComputer

View File

@@ -53,6 +53,8 @@
- AirAlarm - AirAlarm
- type: AtmosDevice - type: AtmosDevice
- type: AirAlarm - type: AirAlarm
- type: AtmosAlertsDevice
group: AirAlarm
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: UserInterface - type: UserInterface

View File

@@ -42,6 +42,8 @@
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: FireAlarm - type: FireAlarm
- type: AtmosAlertsDevice
group: FireAlarm
- type: ContainerFill - type: ContainerFill
containers: containers:
board: [ FireAlarmElectronics ] board: [ FireAlarmElectronics ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B