diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml new file mode 100644 index 00000000000..96f136abf0e --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs new file mode 100644 index 00000000000..b0d0365ef6b --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -0,0 +1,215 @@ +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 readonly IEntityManager _entManager; + private readonly IResourceCache _cache; + + private Dictionary _alarmStrings = new Dictionary() + { + [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 _gasShorthands = new Dictionary() + { + [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); + + _entManager = IoCManager.Resolve(); + _cache = IoCManager.Resolve(); + + NetEntity = uid; + Coordinates = coordinates; + + // Load fonts + var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11); + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + var smallFont = new VectorFont(_cache.GetResource("/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) + { + NetEntity = entry.NetEntity; + Coordinates = _entManager.GetCoordinates(entry.Coordinates); + + // Load fonts + var normalFont = new VectorFont(_cache.GetResource("/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; + } +} \ No newline at end of file diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs new file mode 100644 index 00000000000..08cae979b9b --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs @@ -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(Owner, out var xform); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (AtmosAlertsComputerBoundInterfaceState) state; + + if (castState == null) + return; + + EntMan.TryGetComponent(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 index 00000000000..8824a776ee6 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs new file mode 100644 index 00000000000..a55321833cd --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -0,0 +1,550 @@ +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 Content.Shared.Pinpointer; +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 Robust.Shared.ContentPack; +using Robust.Shared.Prototypes; +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? _allAlarms = null; + + private IEnumerable? _activeAlarms = null; + private Dictionary _deviceSilencingProgress = new(); + + public event Action? SendFocusChangeMessageAction; + public event Action? 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(); + _spriteSystem = _entManager.System(); + + // 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(owner, out var xform)) + { + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(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(_owner.Value, out var console)) + return; + + foreach (var device in console.AtmosDevices) + { + var alarmState = GetAlarmState(device.NetEntity); + + 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(_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(_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; + _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); + + 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 => + { + if (_trackedEntity == newEntryContainer.NetEntity) + { + _trackedEntity = null; + } + + else + { + _trackedEntity = newEntryContainer.NetEntity; + + if (newEntryContainer.Coordinates != null) + NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value); + } + + // Send message to console that the focus has changed + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + + // Update affected UI elements across all tables + UpdateConsoleTable(console, AlertsTable, _trackedEntity); + UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity); + UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity); + }; + + // On toggling the silence check box + newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.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 = (AtmosAlarmEntryContainer)tableChild; + + entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData); + + if (_trackedEntity != entry.NetEntity) + { + var silenced = console.SilencedDevices; + 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) + { + foreach (var tableChild in table.Children) + { + if (tableChild is not AtmosAlarmEntryContainer) + continue; + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + if (entryContainer.NetEntity != currTrackedEntity) + entryContainer.RemoveAsFocus(); + + else if (entryContainer.NetEntity == currTrackedEntity) + entryContainer.SetAsFocus(); + } + } + + private void SetTrackedEntityFromNavMap(NetEntity? netEntity) + { + if (netEntity == null) + return; + + if (!_entManager.TryGetComponent(_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); + + if (device == _trackedEntity) + _trackedEntity = null; + } + + 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) + { + var alarmState = _allAlarms?.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; + } +} \ No newline at end of file diff --git a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs new file mode 100644 index 00000000000..758fde88f13 --- /dev/null +++ b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs @@ -0,0 +1,356 @@ +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 Robust.Shared.ContentPack; +using Robust.Shared.Prototypes; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Access.Components; +using Content.Shared.Database; +using Content.Shared.NameIdentifier; +using Content.Shared.Stacks; +using JetBrains.Annotations; +using Robust.Shared.Utility; + +namespace Content.Server.Atmos.Monitor.Systems; + +public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = 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(OnConsoleInit); + SubscribeLocalEvent(OnConsoleParentChanged); + SubscribeLocalEvent(OnFocusChangedMessage); + + // Grid events + SubscribeLocalEvent(OnGridSplit); + SubscribeLocalEvent(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(); + 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(); + 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(); + var fireAlarmEntriesForEachGrid = new Dictionary(); + + var query = AllEntityQuery(); + 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(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 (!_uiSystem.IsUiOpen(uid, AtmosAlertsComputerUiKey.Key)) + return; + + var gridUid = xform.GridUid!.Value; + + if (!HasComp(gridUid)) + return; + + // The grid must have a NavMapComponent to visualize the map in the UI + EnsureComp(gridUid); + + // Gathering remaining data to be send to the client + var focusAlarmData = GetFocusAlarmData(uid, GetEntity(component.FocusDevice), gridUid); + + // Set the UI state + _uiSystem.TrySetUiState(uid, AtmosAlertsComputerUiKey.Key, + new AtmosAlertsComputerBoundInterfaceState(airAlarmStateData, fireAlarmStateData, focusAlarmData)); + } + + private List GetAlarmStateData(EntityUid gridUid, AtmosAlertsComputerGroup group) + { + var alarmStateData = new List(); + + var queryAlarms = AllEntityQuery(); + 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(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(focusDevice.Value, out var focusDeviceAirAlarm)) + { + return null; + } + + // Force update the sensors attached to the alarm + if (!_uiSystem.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(); + + 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 GetAllAtmosDeviceNavMapData(EntityUid gridUid) + { + var atmosDeviceNavMapData = new HashSet(); + + var query = AllEntityQuery(); + 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); + } +} diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs index 2922d0796a9..240f21ad42e 100644 --- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs @@ -593,6 +593,21 @@ public float CalculateTemperatureAverage(AirAlarmComponent alarm) : 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) { if (!Resolve(uid, ref alarm, ref devNet, ref alarmable)) diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs new file mode 100644 index 00000000000..d64c8907afb --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs @@ -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 +{ + /// + /// The current entity of interest (selected via the console UI) + /// + [ViewVariables] + public NetEntity? FocusDevice; + + /// + /// A list of all the atmos devices that will be used to populate the nav map + /// + [ViewVariables, AutoNetworkedField] + public HashSet AtmosDevices = new(); + + /// + /// A list of all the air alarms that have had their alerts silenced on this particular console + /// + [ViewVariables, AutoNetworkedField] + public HashSet SilencedDevices = new(); +} + +[Serializable, NetSerializable] +public struct AtmosAlertsDeviceNavMapData +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates NetCoordinates; + + /// + /// Used to determine what map icons to use + /// + public AtmosAlertsComputerGroup Group; + + /// + /// Populate the atmos monitoring console nav map with a single entity + /// + public AtmosAlertsDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, AtmosAlertsComputerGroup group) + { + NetEntity = netEntity; + NetCoordinates = netCoordinates; + Group = group; + } +} + +[Serializable, NetSerializable] +public struct AtmosAlertsFocusDeviceData +{ + /// + /// Focus entity + /// + public NetEntity NetEntity; + + /// + /// Temperature (K) and related alert state + /// + public (float, AtmosAlarmType) TemperatureData; + + /// + /// Pressure (kPA) and related alert state + /// + public (float, AtmosAlarmType) PressureData; + + /// + /// Moles, percentage, and related alert state, for all detected gases + /// + public Dictionary GasData; + + /// + /// Populates the atmos monitoring console focus entry with atmospheric data + /// + public AtmosAlertsFocusDeviceData + (NetEntity netEntity, + (float, AtmosAlarmType) temperatureData, + (float, AtmosAlarmType) pressureData, + Dictionary gasData) + { + NetEntity = netEntity; + TemperatureData = temperatureData; + PressureData = pressureData; + GasData = gasData; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosAlertsComputerBoundInterfaceState : BoundUserInterfaceState +{ + /// + /// A list of all air alarms + /// + public AtmosAlertsComputerEntry[] AirAlarms; + + /// + /// A list of all fire alarms + /// + public AtmosAlertsComputerEntry[] FireAlarms; + + /// + /// Data for the UI focus (if applicable) + /// + public AtmosAlertsFocusDeviceData? FocusData; + + /// + /// Sends data from the server to the client to populate the atmos monitoring console UI + /// + public AtmosAlertsComputerBoundInterfaceState(AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData) + { + AirAlarms = airAlarms; + FireAlarms = fireAlarms; + FocusData = focusData; + } +} + +[Serializable, NetSerializable] +public struct AtmosAlertsComputerEntry +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates Coordinates; + + /// + /// The type of entity + /// + public AtmosAlertsComputerGroup Group; + + /// + /// Current alarm state + /// + public AtmosAlarmType AlarmState; + + /// + /// Localised device name + /// + public string EntityName; + + /// + /// Device network address + /// + public string Address; + + /// + /// Used to populate the atmos monitoring console UI with data from a single air alarm + /// + 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; + + /// + /// Used to inform the server that the specified focus for the atmos monitoring console has been changed by the client + /// + public AtmosAlertsComputerFocusChangeMessage(NetEntity? focusDevice) + { + FocusDevice = focusDevice; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosAlertsComputerDeviceSilencedMessage : BoundUserInterfaceMessage +{ + public NetEntity AtmosDevice; + public bool SilenceDevice = true; + + /// + /// Used to inform the server that the client has silenced alerts from the specified device to this atmos monitoring console + /// + public AtmosAlertsComputerDeviceSilencedMessage(NetEntity atmosDevice, bool silenceDevice = true) + { + AtmosDevice = atmosDevice; + SilenceDevice = silenceDevice; + } +} + +/// +/// List of all the different atmos device groups +/// +public enum AtmosAlertsComputerGroup +{ + Invalid, + AirAlarm, + FireAlarm, +} + +[NetSerializable, Serializable] +public enum AtmosAlertsComputerVisuals +{ + ComputerLayerScreen, +} + +/// +/// UI key associated with the atmos monitoring console +/// +[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 index 00000000000..881d60b084c --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +[RegisterComponent, NetworkedComponent] +[Access([])] +public sealed partial class AtmosAlertsDeviceComponent : Component +{ + /// + /// The group that the entity belongs to + /// + [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 index 00000000000..7e2b2b04670 --- /dev/null +++ b/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs @@ -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(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 index 00000000000..a1640c5e9d5 --- /dev/null +++ b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 1210f302fcc..35a941e759c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 4fd2129f7a5..39fd34016b9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -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 automated atmospheric alert system. components: - type: Computer board: AlertsComputerCircuitboard @@ -13,9 +13,33 @@ - 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: + - key: enum.AtmosAlertsComputerUiKey.Key + type: AtmosAlertsComputerBoundUserInterface - type: entity parent: BaseComputer diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml index 62f2000593b..70e204f694e 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml @@ -53,6 +53,8 @@ - AirAlarm - type: AtmosDevice - type: AirAlarm + - type: AtmosAlertsDevice + group: AirAlarm - type: Clickable - type: InteractionOutline - type: UserInterface diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml index 05988fbc217..c39adcde3b6 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml @@ -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 index 00000000000..165a9b9d9f1 Binary files /dev/null and b/Resources/Textures/Interface/AtmosMonitoring/status_bg.png differ