Adds fire/air alarms (#5018)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> Co-authored-by: E F R <602406+Efruit@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
67
Content.Client/Atmos/Monitor/AtmosMonitorVisualizer.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor
|
||||||
|
{
|
||||||
|
public class AtmosMonitorVisualizer : AppearanceVisualizer
|
||||||
|
{
|
||||||
|
[Dependency] IEntityManager _entityManager = default!;
|
||||||
|
[DataField("layerMap")]
|
||||||
|
private string _layerMap { get; } = string.Empty;
|
||||||
|
|
||||||
|
[DataField("alarmStates")]
|
||||||
|
private readonly Dictionary<AtmosMonitorAlarmType, string> _alarmStates = new();
|
||||||
|
|
||||||
|
[DataField("hideOnDepowered")]
|
||||||
|
private readonly List<string>? _hideOnDepowered;
|
||||||
|
|
||||||
|
// eh...
|
||||||
|
[DataField("setOnDepowered")]
|
||||||
|
private readonly Dictionary<string, string>? _setOnDepowered;
|
||||||
|
|
||||||
|
public override void InitializeEntity(EntityUid entity)
|
||||||
|
{
|
||||||
|
base.InitializeEntity(entity);
|
||||||
|
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnChangeData(AppearanceComponent component)
|
||||||
|
{
|
||||||
|
if (!_entityManager.TryGetComponent<SpriteComponent>(component.Owner, out var sprite))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!sprite.LayerMapTryGet(_layerMap, out int layer))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.TryGetData<bool>("powered", out var powered))
|
||||||
|
{
|
||||||
|
if (_hideOnDepowered != null)
|
||||||
|
foreach (var visLayer in _hideOnDepowered)
|
||||||
|
if (sprite.LayerMapTryGet(visLayer, out int powerVisibilityLayer))
|
||||||
|
sprite.LayerSetVisible(powerVisibilityLayer, powered);
|
||||||
|
|
||||||
|
if (_setOnDepowered != null && !powered)
|
||||||
|
foreach (var (setLayer, state) in _setOnDepowered)
|
||||||
|
if (sprite.LayerMapTryGet(setLayer, out int setStateLayer))
|
||||||
|
sprite.LayerSetState(setStateLayer, new RSI.StateId(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.TryGetData<Vector2>("offset", out Vector2 offset))
|
||||||
|
{
|
||||||
|
sprite.Offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.TryGetData<AtmosMonitorAlarmType>("alarmType", out var alarmType)
|
||||||
|
&& powered)
|
||||||
|
if (_alarmStates.TryGetValue(alarmType, out var state))
|
||||||
|
sprite.LayerSetState(layer, new RSI.StateId(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor.UI
|
||||||
|
{
|
||||||
|
public class AirAlarmBoundUserInterface : BoundUserInterface
|
||||||
|
{
|
||||||
|
private AirAlarmWindow? _window;
|
||||||
|
|
||||||
|
public AirAlarmBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
|
||||||
|
{}
|
||||||
|
|
||||||
|
protected override void Open()
|
||||||
|
{
|
||||||
|
base.Open();
|
||||||
|
|
||||||
|
_window = new AirAlarmWindow();
|
||||||
|
|
||||||
|
if (State != null) UpdateState(State);
|
||||||
|
|
||||||
|
_window.OpenCentered();
|
||||||
|
|
||||||
|
_window.OnClose += Close;
|
||||||
|
_window.AtmosDeviceDataChanged += OnDeviceDataChanged;
|
||||||
|
_window.AtmosAlarmThresholdChanged += OnThresholdChanged;
|
||||||
|
_window.AirAlarmModeChanged += OnAirAlarmModeChanged;
|
||||||
|
_window.ResyncAllRequested += ResyncAllDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResyncAllDevices()
|
||||||
|
{
|
||||||
|
SendMessage(new AirAlarmResyncAllDevicesMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDeviceDataChanged(string address, IAtmosDeviceData data)
|
||||||
|
{
|
||||||
|
SendMessage(new AirAlarmUpdateDeviceDataMessage(address, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAirAlarmModeChanged(AirAlarmMode mode)
|
||||||
|
{
|
||||||
|
SendMessage(new AirAlarmUpdateAlarmModeMessage(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThresholdChanged(AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null)
|
||||||
|
{
|
||||||
|
SendMessage(new AirAlarmUpdateAlarmThresholdMessage(type, threshold, gas));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||||
|
{
|
||||||
|
if (_window == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case AirAlarmSetAddressMessage addrMsg:
|
||||||
|
_window.SetAddress(addrMsg.Address);
|
||||||
|
break;
|
||||||
|
case AirAlarmUpdateDeviceDataMessage deviceMsg:
|
||||||
|
_window.UpdateDeviceData(deviceMsg.Address, deviceMsg.Data);
|
||||||
|
break;
|
||||||
|
case AirAlarmUpdateAlarmModeMessage alarmMsg:
|
||||||
|
_window.UpdateModeSelector(alarmMsg.Mode);
|
||||||
|
break;
|
||||||
|
case AirAlarmUpdateAlarmThresholdMessage thresholdMsg:
|
||||||
|
_window.UpdateThreshold(ref thresholdMsg);
|
||||||
|
break;
|
||||||
|
case AirAlarmUpdateAirDataMessage airDataMsg:
|
||||||
|
_window.UpdateGasData(ref airDataMsg.AirData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
if (disposing) _window?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<SS14Window xmlns="https://spacestation14.io"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
MinSize="500 500" Title="Air Alarm">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
|
||||||
|
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
|
||||||
|
<!-- Left column (pressure, temperature, alarm state) -->
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="0 0 2 0" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-pressure-label'}" />
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-temperature-label'}" />
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-alarm-state-label'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<RichTextLabel Name="CPressureLabel" />
|
||||||
|
<RichTextLabel Name="CTemperatureLabel" />
|
||||||
|
<RichTextLabel Name="CStatusLabel" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Right column (address, device total) -->
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="0 0 2 0" HorizontalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Gas/Device Data -->
|
||||||
|
<TabContainer Name="CTabContainer" VerticalExpand="True" Margin="0 0 0 2">
|
||||||
|
<!-- Gas readout -->
|
||||||
|
<ScrollContainer VerticalExpand="True">
|
||||||
|
<BoxContainer Name="CGasContainer" Orientation="Vertical" VerticalExpand="True" Margin="2 2 2 2" />
|
||||||
|
</ScrollContainer>
|
||||||
|
<!-- Vent devices -->
|
||||||
|
<ScrollContainer VerticalExpand="True">
|
||||||
|
<BoxContainer Name="CVentContainer" Orientation="Vertical"/>
|
||||||
|
</ScrollContainer>
|
||||||
|
<!-- Scrubber devices -->
|
||||||
|
<ScrollContainer VerticalExpand="True">
|
||||||
|
<BoxContainer Name="CScrubberContainer" Orientation="Vertical"/>
|
||||||
|
</ScrollContainer>
|
||||||
|
<!-- Alarm thresholds -->
|
||||||
|
<ScrollContainer VerticalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||||
|
<BoxContainer Name="CPressureThreshold" Orientation="Vertical" Margin="2 0 2 0" />
|
||||||
|
<BoxContainer Name="CTemperatureThreshold" Orientation="Vertical" Margin="2 0 2 0" />
|
||||||
|
<Collapsible Orientation="Vertical">
|
||||||
|
<CollapsibleHeading Title="Gases" />
|
||||||
|
<CollapsibleBody Margin="4 2 4 2">
|
||||||
|
<BoxContainer Name="CGasThresholdContainer" Orientation="Vertical" Margin="2 0 2 0" />
|
||||||
|
</CollapsibleBody>
|
||||||
|
</Collapsible>
|
||||||
|
</BoxContainer>
|
||||||
|
</ScrollContainer>
|
||||||
|
</TabContainer>
|
||||||
|
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 2">
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-address-label'}" HorizontalExpand="True"/>
|
||||||
|
<Label Name="CDeviceAddress" HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 4 0" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-device-count-label'}" HorizontalExpand="True"/>
|
||||||
|
<Label Name="CDeviceTotal" HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 4 0" />
|
||||||
|
</BoxContainer>
|
||||||
|
<Button Name="CResyncButton" Text="{Loc 'air-alarm-ui-window-resync-devices-label'}" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Mode buttons -->
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-window-mode-label'}" Margin="0 0 2 0" />
|
||||||
|
<OptionButton Name="CModeButton" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</SS14Window>
|
||||||
196
Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Client.Atmos.Monitor.UI.Widgets;
|
||||||
|
using Content.Client.Message;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
|
using Content.Shared.Temperature;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor.UI
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class AirAlarmWindow : SS14Window
|
||||||
|
{
|
||||||
|
public event Action<string, IAtmosDeviceData>? AtmosDeviceDataChanged;
|
||||||
|
public event Action<AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? AtmosAlarmThresholdChanged;
|
||||||
|
public event Action<AirAlarmMode>? AirAlarmModeChanged;
|
||||||
|
public event Action<string>? ResyncDeviceRequested;
|
||||||
|
public event Action? ResyncAllRequested;
|
||||||
|
|
||||||
|
private Label _address => CDeviceAddress;
|
||||||
|
private Label _deviceTotal => CDeviceTotal;
|
||||||
|
private RichTextLabel _pressure => CPressureLabel;
|
||||||
|
private RichTextLabel _temperature => CTemperatureLabel;
|
||||||
|
private RichTextLabel _alarmState => CStatusLabel;
|
||||||
|
|
||||||
|
private TabContainer _tabContainer => CTabContainer;
|
||||||
|
private BoxContainer _gasReadout => CGasContainer;
|
||||||
|
private BoxContainer _ventDevices => CVentContainer;
|
||||||
|
private BoxContainer _scrubberDevices => CScrubberContainer;
|
||||||
|
private BoxContainer _pressureThreshold => CPressureThreshold;
|
||||||
|
private BoxContainer _temperatureThreshold => CTemperatureThreshold;
|
||||||
|
private BoxContainer _gasThreshold => CGasThresholdContainer;
|
||||||
|
|
||||||
|
private Dictionary<string, PumpControl> _pumps = new();
|
||||||
|
private Dictionary<string, ScrubberControl> _scrubbers = new();
|
||||||
|
private Button _resyncDevices => CResyncButton;
|
||||||
|
|
||||||
|
private ThresholdControl? _pressureThresholdControl;
|
||||||
|
private ThresholdControl? _temperatureThresholdControl;
|
||||||
|
private Dictionary<Gas, ThresholdControl> _gasThresholdControls = new();
|
||||||
|
|
||||||
|
private Dictionary<Gas, Label> _gasLabels = new();
|
||||||
|
|
||||||
|
private OptionButton _modes => CModeButton;
|
||||||
|
|
||||||
|
public AirAlarmWindow()
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
foreach (var mode in Enum.GetValues<AirAlarmMode>())
|
||||||
|
_modes.AddItem($"{mode}", (int) mode);
|
||||||
|
|
||||||
|
_modes.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
_modes.SelectId(args.Id);
|
||||||
|
AirAlarmModeChanged!.Invoke((AirAlarmMode) args.Id);
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var gas in Enum.GetValues<Gas>())
|
||||||
|
{
|
||||||
|
var gasLabel = new Label();
|
||||||
|
_gasReadout.AddChild(gasLabel);
|
||||||
|
_gasLabels.Add(gas, gasLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tabContainer.SetTabTitle(0, Loc.GetString("air-alarm-ui-window-tab-gas"));
|
||||||
|
_tabContainer.SetTabTitle(1, Loc.GetString("air-alarm-ui-window-tab-vents"));
|
||||||
|
_tabContainer.SetTabTitle(2, Loc.GetString("air-alarm-ui-window-tab-scrubbers"));
|
||||||
|
_tabContainer.SetTabTitle(3, Loc.GetString("air-alarm-ui-window-tab-thresholds"));
|
||||||
|
|
||||||
|
_resyncDevices.OnPressed += _ =>
|
||||||
|
{
|
||||||
|
_ventDevices.RemoveAllChildren();
|
||||||
|
_pumps.Clear();
|
||||||
|
_scrubberDevices.RemoveAllChildren();
|
||||||
|
_scrubbers.Clear();
|
||||||
|
ResyncAllRequested!.Invoke();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAddress(string address)
|
||||||
|
{
|
||||||
|
_address.Text = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateGasData(ref AirAlarmAirData state)
|
||||||
|
{
|
||||||
|
_pressure.SetMarkup(Loc.GetString("air-alarm-ui-window-pressure", ("pressure", $"{state.Pressure:0.##}")));
|
||||||
|
_temperature.SetMarkup(Loc.GetString("air-alarm-ui-window-temperature", ("tempC", $"{TemperatureHelpers.KelvinToCelsius(state.Temperature ?? 0):0.#}"), ("temperature", $"{state.Temperature:0.##}")));
|
||||||
|
_alarmState.SetMarkup(Loc.GetString("air-alarm-ui-window-alarm-state", ("state", $"{state.AlarmState}")));
|
||||||
|
|
||||||
|
if (state.Gases != null)
|
||||||
|
foreach (var (gas, amount) in state.Gases)
|
||||||
|
_gasLabels[gas].Text = Loc.GetString("air-alarm-ui-gases", ("gas", $"{gas}"), ("amount", $"{amount:0.####}"), ("percentage", $"{(amount / state.TotalMoles):0.##}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateModeSelector(AirAlarmMode mode)
|
||||||
|
{
|
||||||
|
_modes.SelectId((int) mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDeviceData(string addr, IAtmosDeviceData device)
|
||||||
|
{
|
||||||
|
switch (device)
|
||||||
|
{
|
||||||
|
case GasVentPumpData pump:
|
||||||
|
if (!pump.Dirty) pump = GasVentPumpData.Default();
|
||||||
|
|
||||||
|
if (!_pumps.TryGetValue(addr, out var pumpControl))
|
||||||
|
{
|
||||||
|
var control= new PumpControl(pump, addr);
|
||||||
|
control.PumpDataChanged += AtmosDeviceDataChanged!.Invoke;
|
||||||
|
_pumps.Add(addr, control);
|
||||||
|
CVentContainer.AddChild(control);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pumpControl.ChangeData(pump);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case GasVentScrubberData scrubber:
|
||||||
|
if (!scrubber.Dirty) scrubber = GasVentScrubberData.Default();
|
||||||
|
|
||||||
|
if (!_scrubbers.TryGetValue(addr, out var scrubberControl))
|
||||||
|
{
|
||||||
|
var control = new ScrubberControl(scrubber, addr);
|
||||||
|
control.ScrubberDataChanged += AtmosDeviceDataChanged!.Invoke;
|
||||||
|
_scrubbers.Add(addr, control);
|
||||||
|
CScrubberContainer.AddChild(control);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scrubberControl.ChangeData(scrubber);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deviceTotal.Text = $"{_pumps.Count + _scrubbers.Count}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateThreshold(ref AirAlarmUpdateAlarmThresholdMessage message)
|
||||||
|
{
|
||||||
|
switch (message.Type)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdType.Pressure:
|
||||||
|
if (_pressureThresholdControl == null)
|
||||||
|
{
|
||||||
|
_pressureThresholdControl = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-pressure-title"), message.Threshold, message.Type);
|
||||||
|
_pressureThresholdControl.ThresholdDataChanged += AtmosAlarmThresholdChanged!.Invoke;
|
||||||
|
_pressureThreshold.AddChild(_pressureThresholdControl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pressureThresholdControl.UpdateThresholdData(message.Threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdType.Temperature:
|
||||||
|
if (_temperatureThresholdControl == null)
|
||||||
|
{
|
||||||
|
_temperatureThresholdControl = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"), message.Threshold, message.Type);
|
||||||
|
_temperatureThresholdControl.ThresholdDataChanged += AtmosAlarmThresholdChanged!.Invoke;
|
||||||
|
_temperatureThreshold.AddChild(_temperatureThresholdControl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_temperatureThresholdControl.UpdateThresholdData(message.Threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdType.Gas:
|
||||||
|
if (_gasThresholdControls.TryGetValue((Gas) message.Gas!, out var control))
|
||||||
|
{
|
||||||
|
control.UpdateThresholdData(message.Threshold);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gasThreshold = new ThresholdControl(Loc.GetString($"air-alarm-ui-thresholds-gas-title", ("gas", $"{(Gas) message.Gas!}")), message.Threshold, AtmosMonitorThresholdType.Gas, (Gas) message.Gas!, 100);
|
||||||
|
gasThreshold.ThresholdDataChanged += AtmosAlarmThresholdChanged!.Invoke;
|
||||||
|
_gasThresholdControls.Add((Gas) message.Gas!, gasThreshold);
|
||||||
|
_gasThreshold.AddChild(gasThreshold);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<BoxContainer xmlns="https://spacestation14.io"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Orientation="Vertical" Margin="2 0 2 4">
|
||||||
|
<Collapsible Orientation="Vertical">
|
||||||
|
<CollapsibleHeading Name="CAddress" />
|
||||||
|
<!-- Upper row: toggle, direction, checks -->
|
||||||
|
<CollapsibleBody>
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin ="0 0 0 2">
|
||||||
|
<CheckBox Name="CEnableDevice" Text="{Loc 'air-alarm-ui-widget-enable'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2" HorizontalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-vent-pump-label'}" Margin="0 0 0 1" />
|
||||||
|
<OptionButton Name="CPumpDirection" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-vent-pressure-label'}" Margin="0 0 0 1" />
|
||||||
|
<OptionButton Name="CPressureCheck" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Lower row: pressure bounds -->
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-vent-external-bound-label'}" Margin="0 0 0 1" />
|
||||||
|
<FloatSpinBox Name="CExternalBound" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-vent-internal-bound-label'}" Margin="0 0 0 1" />
|
||||||
|
<FloatSpinBox Name="CInternalBound" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</CollapsibleBody>
|
||||||
|
</Collapsible>
|
||||||
|
</BoxContainer>
|
||||||
103
Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor.UI.Widgets
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class PumpControl : BoxContainer
|
||||||
|
{
|
||||||
|
private GasVentPumpData _data;
|
||||||
|
private string _address;
|
||||||
|
|
||||||
|
public event Action<string, IAtmosDeviceData>? PumpDataChanged;
|
||||||
|
|
||||||
|
private CheckBox _enabled => CEnableDevice;
|
||||||
|
private CollapsibleHeading _addressLabel => CAddress;
|
||||||
|
private OptionButton _pumpDirection => CPumpDirection;
|
||||||
|
private OptionButton _pressureCheck => CPressureCheck;
|
||||||
|
private FloatSpinBox _externalBound => CExternalBound;
|
||||||
|
private FloatSpinBox _internalBound => CInternalBound;
|
||||||
|
|
||||||
|
public PumpControl(GasVentPumpData data, string address)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
this.Name = address;
|
||||||
|
|
||||||
|
_data = data;
|
||||||
|
_address = address;
|
||||||
|
|
||||||
|
_addressLabel.Title = Loc.GetString("air-alarm-ui-atmos-net-device-label", ("address", $"{address}"));
|
||||||
|
|
||||||
|
_enabled.Pressed = data.Enabled;
|
||||||
|
_enabled.OnToggled += _ =>
|
||||||
|
{
|
||||||
|
_data.Enabled = _enabled.Pressed;
|
||||||
|
PumpDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
|
||||||
|
_internalBound.Value = (float) _data.InternalPressureBound!;
|
||||||
|
_internalBound.OnValueChanged += _ =>
|
||||||
|
{
|
||||||
|
_data.InternalPressureBound = _internalBound.Value;
|
||||||
|
PumpDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
_internalBound.IsValid += value => value >= 0;
|
||||||
|
|
||||||
|
_externalBound.Value = (float) _data.ExternalPressureBound!;
|
||||||
|
_externalBound.OnValueChanged += _ =>
|
||||||
|
{
|
||||||
|
_data.ExternalPressureBound = _externalBound.Value;
|
||||||
|
PumpDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
_externalBound.IsValid += value => value >= 0;
|
||||||
|
|
||||||
|
foreach (var value in Enum.GetValues<VentPumpDirection>())
|
||||||
|
_pumpDirection.AddItem(Loc.GetString($"{value}"), (int) value);
|
||||||
|
|
||||||
|
_pumpDirection.SelectId((int) _data.PumpDirection!);
|
||||||
|
_pumpDirection.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
_pumpDirection.SelectId(args.Id);
|
||||||
|
_data.PumpDirection = (VentPumpDirection) args.Id;
|
||||||
|
PumpDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var value in Enum.GetValues<VentPressureBound>())
|
||||||
|
_pressureCheck.AddItem(Loc.GetString($"{value}"), (int) value);
|
||||||
|
|
||||||
|
_pressureCheck.SelectId((int) _data.PressureChecks!);
|
||||||
|
_pressureCheck.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
_pressureCheck.SelectId(args.Id);
|
||||||
|
_data.PressureChecks = (VentPressureBound) args.Id;
|
||||||
|
PumpDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeData(GasVentPumpData data)
|
||||||
|
{
|
||||||
|
_data.Enabled = data.Enabled;
|
||||||
|
_enabled.Pressed = _data.Enabled!;
|
||||||
|
|
||||||
|
_data.PumpDirection = data.PumpDirection;
|
||||||
|
_pumpDirection.SelectId((int) _data.PumpDirection!);
|
||||||
|
|
||||||
|
_data.PressureChecks = data.PressureChecks;
|
||||||
|
_pressureCheck.SelectId((int) _data.PressureChecks!);
|
||||||
|
|
||||||
|
_data.ExternalPressureBound = data.ExternalPressureBound;
|
||||||
|
_externalBound.Value = (float) _data.ExternalPressureBound!;
|
||||||
|
|
||||||
|
_data.InternalPressureBound = data.InternalPressureBound;
|
||||||
|
_internalBound.Value = (float) _data.InternalPressureBound!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<BoxContainer xmlns="https://spacestation14.io"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Orientation="Vertical" Margin="2 0 2 4">
|
||||||
|
<Collapsible Orientation="Vertical">
|
||||||
|
<CollapsibleHeading Name="CAddress" />
|
||||||
|
<CollapsibleBody>
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
|
||||||
|
<CheckBox Name="CEnableDevice" Text="{Loc 'air-alarm-ui-widget-enable'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Upper row: toggle, direction, volume rate, widenet -->
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2" HorizontalExpand="True">
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-scrubber-pump-direction-label'}" Margin="0 0 0 1"/>
|
||||||
|
<OptionButton Name="CPumpDirection" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'air-alarm-ui-scrubber-volume-rate-label'}" Margin="0 0 0 1" />
|
||||||
|
<FloatSpinBox Name="CVolumeRate" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer>
|
||||||
|
<CheckBox Name="CWideNet" Text="{Loc 'air-alarm-ui-scrubber-wide-net-label'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Lower row: every single gas -->
|
||||||
|
<Collapsible Orientation="Vertical" Margin="2 2 2 2">
|
||||||
|
<CollapsibleHeading Title="Gas filters" />
|
||||||
|
<CollapsibleBody>
|
||||||
|
<GridContainer HorizontalExpand="True" Name="CGasContainer" Columns="3" />
|
||||||
|
</CollapsibleBody>
|
||||||
|
</Collapsible>
|
||||||
|
</BoxContainer>
|
||||||
|
</CollapsibleBody>
|
||||||
|
</Collapsible>
|
||||||
|
</BoxContainer>
|
||||||
123
Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor.UI.Widgets
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class ScrubberControl : BoxContainer
|
||||||
|
{
|
||||||
|
private GasVentScrubberData _data;
|
||||||
|
private string _address;
|
||||||
|
|
||||||
|
public event Action<string, IAtmosDeviceData>? ScrubberDataChanged;
|
||||||
|
|
||||||
|
private CheckBox _enabled => CEnableDevice;
|
||||||
|
private CollapsibleHeading _addressLabel => CAddress;
|
||||||
|
private OptionButton _pumpDirection => CPumpDirection;
|
||||||
|
private FloatSpinBox _volumeRate => CVolumeRate;
|
||||||
|
private CheckBox _wideNet => CWideNet;
|
||||||
|
|
||||||
|
private GridContainer _gases => CGasContainer;
|
||||||
|
private Dictionary<Gas, Button> _gasControls = new();
|
||||||
|
|
||||||
|
public ScrubberControl(GasVentScrubberData data, string address)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
this.Name = address;
|
||||||
|
|
||||||
|
_data = data;
|
||||||
|
_address = address;
|
||||||
|
|
||||||
|
_addressLabel.Title = Loc.GetString("air-alarm-ui-atmos-net-device-label", ("address", $"{address}"));
|
||||||
|
|
||||||
|
_enabled.Pressed = data.Enabled;
|
||||||
|
_enabled.OnToggled += _ =>
|
||||||
|
{
|
||||||
|
_data.Enabled = _enabled.Pressed;
|
||||||
|
ScrubberDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
|
||||||
|
_wideNet.Pressed = data.WideNet;
|
||||||
|
_wideNet.OnToggled += _ =>
|
||||||
|
{
|
||||||
|
_data.WideNet = _wideNet.Pressed;
|
||||||
|
ScrubberDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
|
||||||
|
_volumeRate.Value = (float) _data.VolumeRate!;
|
||||||
|
_volumeRate.OnValueChanged += _ =>
|
||||||
|
{
|
||||||
|
_data.VolumeRate = _volumeRate.Value;
|
||||||
|
ScrubberDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
_volumeRate.IsValid += value => value >= 0;
|
||||||
|
|
||||||
|
foreach (var value in Enum.GetValues<ScrubberPumpDirection>())
|
||||||
|
_pumpDirection.AddItem(Loc.GetString($"{value}"), (int) value);
|
||||||
|
|
||||||
|
_pumpDirection.SelectId((int) _data.PumpDirection!);
|
||||||
|
_pumpDirection.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
_pumpDirection.SelectId(args.Id);
|
||||||
|
_data.PumpDirection = (ScrubberPumpDirection) args.Id;
|
||||||
|
ScrubberDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var value in Enum.GetValues<Gas>())
|
||||||
|
{
|
||||||
|
var gasButton = new Button
|
||||||
|
{
|
||||||
|
Name = value.ToString(),
|
||||||
|
Text = Loc.GetString($"{value}"),
|
||||||
|
ToggleMode = true,
|
||||||
|
HorizontalExpand = true,
|
||||||
|
Pressed = _data.FilterGases!.Contains(value)
|
||||||
|
};
|
||||||
|
gasButton.OnToggled += args =>
|
||||||
|
{
|
||||||
|
if (args.Pressed)
|
||||||
|
_data.FilterGases.Add(value);
|
||||||
|
else
|
||||||
|
_data.FilterGases.Remove(value);
|
||||||
|
|
||||||
|
ScrubberDataChanged?.Invoke(_address, _data);
|
||||||
|
};
|
||||||
|
_gasControls.Add(value, gasButton);
|
||||||
|
_gases.AddChild(gasButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeData(GasVentScrubberData data)
|
||||||
|
{
|
||||||
|
_data.Enabled = data.Enabled;
|
||||||
|
_enabled.Pressed = _data.Enabled;
|
||||||
|
|
||||||
|
_data.PumpDirection = data.PumpDirection;
|
||||||
|
_pumpDirection.SelectId((int) _data.PumpDirection!);
|
||||||
|
|
||||||
|
_data.VolumeRate = data.VolumeRate;
|
||||||
|
_volumeRate.Value = (float) _data.VolumeRate!;
|
||||||
|
|
||||||
|
_data.WideNet = data.WideNet;
|
||||||
|
_wideNet.Pressed = _data.WideNet;
|
||||||
|
|
||||||
|
var intersect = _data.FilterGases!.Intersect(data.FilterGases!);
|
||||||
|
|
||||||
|
foreach (var value in Enum.GetValues<Gas>())
|
||||||
|
if (!intersect.Contains(value))
|
||||||
|
_gasControls[value].Pressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<BoxContainer xmlns="https://spacestation14.io"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Orientation="Vertical" Margin="0 0 0 4">
|
||||||
|
<Collapsible Orientation="Vertical">
|
||||||
|
<CollapsibleHeading Name="CName" />
|
||||||
|
<CollapsibleBody>
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<CheckBox Name="CIgnore" Text="{Loc 'Ignore'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
<!-- Upper row: Danger bounds -->
|
||||||
|
<BoxContainer Name="CDangerBounds" Orientation="Horizontal" Margin="0 0 0 2"/>
|
||||||
|
<!-- Lower row: Warning bounds -->
|
||||||
|
<BoxContainer Name="CWarningBounds" Orientation="Horizontal" Margin="0 0 0 2"/>
|
||||||
|
</BoxContainer>
|
||||||
|
</CollapsibleBody>
|
||||||
|
</Collapsible>
|
||||||
|
</BoxContainer>
|
||||||
294
Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml.cs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Content.Client.Atmos.Monitor.UI.Widgets
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class ThresholdControl : BoxContainer
|
||||||
|
{
|
||||||
|
private AtmosAlarmThreshold _threshold;
|
||||||
|
private AtmosMonitorThresholdType _type;
|
||||||
|
private Gas? _gas;
|
||||||
|
|
||||||
|
public event Action<AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? ThresholdDataChanged;
|
||||||
|
|
||||||
|
private CollapsibleHeading _name => CName;
|
||||||
|
private CheckBox _ignore => CIgnore;
|
||||||
|
private BoxContainer _dangerBounds => CDangerBounds;
|
||||||
|
private BoxContainer _warningBounds => CWarningBounds;
|
||||||
|
private ThresholdBoundControl _upperBoundControl;
|
||||||
|
private ThresholdBoundControl _lowerBoundControl;
|
||||||
|
private ThresholdBoundControl _upperWarningBoundControl;
|
||||||
|
private ThresholdBoundControl _lowerWarningBoundControl;
|
||||||
|
|
||||||
|
// i have played myself by making threshold values nullable to
|
||||||
|
// indicate validity/disabled status, with several layers of side effect
|
||||||
|
// dependent on the other three values when you change one :HECK:
|
||||||
|
public ThresholdControl(string name, AtmosAlarmThreshold threshold, AtmosMonitorThresholdType type, Gas? gas = null, float modifier = 1)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
_threshold = threshold;
|
||||||
|
_type = type;
|
||||||
|
_gas = gas;
|
||||||
|
|
||||||
|
_name.Title = name;
|
||||||
|
|
||||||
|
// i miss rust macros
|
||||||
|
|
||||||
|
_upperBoundControl = new ThresholdBoundControl("upper-bound", _threshold.UpperBound, modifier);
|
||||||
|
_upperBoundControl.OnBoundChanged += value =>
|
||||||
|
{
|
||||||
|
// a lot of threshold logic is baked into the properties,
|
||||||
|
// so setting this just returns if a change occurred or not
|
||||||
|
_threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Upper, value);
|
||||||
|
return _threshold.UpperBound;
|
||||||
|
};
|
||||||
|
_upperBoundControl.OnBoundEnabled += () =>
|
||||||
|
{
|
||||||
|
var value = 0f;
|
||||||
|
|
||||||
|
if (_threshold.LowerWarningBound != null)
|
||||||
|
value = (float) _threshold.LowerWarningBound + 0.1f;
|
||||||
|
else if (_threshold.LowerBound != null)
|
||||||
|
value = (float) _threshold.LowerBound + 0.1f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
_upperBoundControl.OnValidBoundChanged += () =>
|
||||||
|
{
|
||||||
|
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
|
||||||
|
};
|
||||||
|
_dangerBounds.AddChild(_upperBoundControl);
|
||||||
|
|
||||||
|
_lowerBoundControl = new ThresholdBoundControl("lower-bound", _threshold.LowerBound, modifier);
|
||||||
|
_lowerBoundControl.OnBoundChanged += value =>
|
||||||
|
{
|
||||||
|
_threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, value);
|
||||||
|
return _threshold.LowerBound;
|
||||||
|
};
|
||||||
|
_lowerBoundControl.OnBoundEnabled += () =>
|
||||||
|
{
|
||||||
|
var value = 0f;
|
||||||
|
|
||||||
|
if (_threshold.UpperWarningBound != null)
|
||||||
|
value = (float) _threshold.UpperWarningBound - 0.1f;
|
||||||
|
else if (_threshold.UpperBound != null)
|
||||||
|
value = (float) _threshold.UpperBound - 0.1f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
_lowerBoundControl.OnValidBoundChanged += () =>
|
||||||
|
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
|
||||||
|
_dangerBounds.AddChild(_lowerBoundControl);
|
||||||
|
|
||||||
|
_upperWarningBoundControl = new ThresholdBoundControl("upper-warning-bound", _threshold.UpperWarningBound, modifier);
|
||||||
|
_upperWarningBoundControl.OnBoundChanged += value =>
|
||||||
|
{
|
||||||
|
_threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Upper, value);
|
||||||
|
return _threshold.UpperWarningBound;
|
||||||
|
};
|
||||||
|
_upperWarningBoundControl.OnBoundEnabled += () =>
|
||||||
|
{
|
||||||
|
var value = 0f;
|
||||||
|
|
||||||
|
if (_threshold.LowerWarningBound != null)
|
||||||
|
value = (float) _threshold.LowerWarningBound + 0.1f;
|
||||||
|
else if (_threshold.LowerBound != null)
|
||||||
|
value = (float) _threshold.LowerBound + 0.1f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
_upperWarningBoundControl.OnValidBoundChanged += () =>
|
||||||
|
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
|
||||||
|
_warningBounds.AddChild(_upperWarningBoundControl);
|
||||||
|
|
||||||
|
_lowerWarningBoundControl = new ThresholdBoundControl("lower-warning-bound", _threshold.LowerWarningBound, modifier);
|
||||||
|
_lowerWarningBoundControl.OnBoundChanged += value =>
|
||||||
|
{
|
||||||
|
_threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Lower, value);
|
||||||
|
return _threshold.LowerWarningBound;
|
||||||
|
};
|
||||||
|
_lowerWarningBoundControl.OnBoundEnabled += () =>
|
||||||
|
{
|
||||||
|
var value = 0f;
|
||||||
|
|
||||||
|
if (_threshold.UpperWarningBound != null)
|
||||||
|
value = (float) _threshold.UpperWarningBound - 0.1f;
|
||||||
|
else if (_threshold.UpperBound != null)
|
||||||
|
value = (float) _threshold.UpperBound - 0.1f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
_lowerWarningBoundControl.OnValidBoundChanged += () =>
|
||||||
|
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
|
||||||
|
|
||||||
|
_warningBounds.AddChild(_lowerWarningBoundControl);
|
||||||
|
|
||||||
|
_ignore.OnToggled += args =>
|
||||||
|
{
|
||||||
|
_threshold.Ignore = args.Pressed;
|
||||||
|
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
|
||||||
|
};
|
||||||
|
_ignore.Pressed = _threshold.Ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateThresholdData(AtmosAlarmThreshold threshold)
|
||||||
|
{
|
||||||
|
_upperBoundControl.SetValue(threshold.UpperBound);
|
||||||
|
_lowerBoundControl.SetValue(threshold.LowerBound);
|
||||||
|
_upperWarningBoundControl.SetValue(threshold.UpperWarningBound);
|
||||||
|
_lowerWarningBoundControl.SetValue(threshold.LowerWarningBound);
|
||||||
|
_ignore.Pressed = threshold.Ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class ThresholdBoundControl : BoxContainer
|
||||||
|
{
|
||||||
|
// raw values to use in thresholds, prefer these
|
||||||
|
// over directly setting Modified(Value/LastValue)
|
||||||
|
// when working with the FloatSpinBox
|
||||||
|
private float? _value;
|
||||||
|
private float _lastValue;
|
||||||
|
|
||||||
|
// convenience thing for getting multiplied values
|
||||||
|
// and also setting value to a usable value
|
||||||
|
private float? ModifiedValue
|
||||||
|
{
|
||||||
|
get => _value * _modifier;
|
||||||
|
set => _value = value / _modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ModifiedLastValue
|
||||||
|
{
|
||||||
|
get => _lastValue * _modifier;
|
||||||
|
set => _lastValue = value / _modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _modifier;
|
||||||
|
|
||||||
|
private FloatSpinBox _bound;
|
||||||
|
private CheckBox _boundEnabled;
|
||||||
|
|
||||||
|
public event Action? OnValidBoundChanged;
|
||||||
|
public Func<float?, float?>? OnBoundChanged;
|
||||||
|
public Func<float>? OnBoundEnabled;
|
||||||
|
|
||||||
|
public void SetValue(float? value)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
|
||||||
|
if (_value == null)
|
||||||
|
{
|
||||||
|
_boundEnabled.Pressed = false;
|
||||||
|
_bound.Value = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_boundEnabled.Pressed = true;
|
||||||
|
_bound.Value = (float) ModifiedValue!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifier indicates what factor the value should be multiplied by.
|
||||||
|
// Mostly useful to convert tiny decimals to human-readable 'percentages'
|
||||||
|
// (yes it's still a float, but floatspinbox unfucks that)
|
||||||
|
public ThresholdBoundControl(string name, float? value, float modifier = 1)
|
||||||
|
{
|
||||||
|
_modifier = modifier > 0 ? modifier : 1;
|
||||||
|
_value = value;
|
||||||
|
|
||||||
|
this.HorizontalExpand = true;
|
||||||
|
this.Orientation = LayoutOrientation.Vertical;
|
||||||
|
|
||||||
|
this.AddChild(new Label { Text = Loc.GetString($"air-alarm-ui-thresholds-{name}") });
|
||||||
|
_bound = new FloatSpinBox(.01f, 2);
|
||||||
|
this.AddChild(_bound);
|
||||||
|
|
||||||
|
_boundEnabled = new CheckBox
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("Enabled")
|
||||||
|
};
|
||||||
|
this.AddChild(_boundEnabled);
|
||||||
|
|
||||||
|
_bound.Value = ModifiedValue ?? 0;
|
||||||
|
_lastValue = _value ?? 0;
|
||||||
|
_boundEnabled.Pressed = _value != null;
|
||||||
|
|
||||||
|
_bound.OnValueChanged += ChangeValue;
|
||||||
|
_bound.IsValid += ValidateThreshold;
|
||||||
|
_boundEnabled.OnToggled += ToggleBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangeValue(FloatSpinBox.FloatSpinBoxEventArgs args)
|
||||||
|
{
|
||||||
|
// ensure that the value in the spinbox is transformed
|
||||||
|
ModifiedValue = args.Value;
|
||||||
|
// set the value in the scope above
|
||||||
|
var value = OnBoundChanged!(_value);
|
||||||
|
// is the value not null, or has it changed?
|
||||||
|
if (value != null || value != _lastValue)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
_lastValue = (float) value!;
|
||||||
|
OnValidBoundChanged!.Invoke();
|
||||||
|
}
|
||||||
|
// otherwise, just set it to the last known value
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_value = _lastValue;
|
||||||
|
_bound.Value = ModifiedLastValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleBound(BaseButton.ButtonToggledEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Pressed)
|
||||||
|
{
|
||||||
|
var value = OnBoundChanged!(_lastValue);
|
||||||
|
|
||||||
|
if (value != _lastValue)
|
||||||
|
{
|
||||||
|
value = OnBoundChanged!(OnBoundEnabled!());
|
||||||
|
|
||||||
|
if (value == null || value < 0)
|
||||||
|
{
|
||||||
|
// TODO: Improve UX here, this is ass
|
||||||
|
// basically this implies that the bound
|
||||||
|
// you currently have is too aggressive
|
||||||
|
// for the other set of values, so a
|
||||||
|
// default value (which is +/-0.1) can't
|
||||||
|
// be used
|
||||||
|
_boundEnabled.Pressed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_value = value;
|
||||||
|
|
||||||
|
_bound.Value = (float) ModifiedValue!;
|
||||||
|
_lastValue = (float) _value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_value = null;
|
||||||
|
_bound.Value = 0f;
|
||||||
|
OnBoundChanged!(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnValidBoundChanged!.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateThreshold(float value) => (_value != null) && (value >= 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,6 +317,10 @@ namespace Content.Client.Entry
|
|||||||
"NukeCodePaper",
|
"NukeCodePaper",
|
||||||
"GhostRadio",
|
"GhostRadio",
|
||||||
"Armor",
|
"Armor",
|
||||||
|
"AtmosMonitor",
|
||||||
|
"AtmosAlarmable",
|
||||||
|
"FireAlarm",
|
||||||
|
"AirAlarm",
|
||||||
"Guardian",
|
"Guardian",
|
||||||
"GuardianCreator",
|
"GuardianCreator",
|
||||||
"GuardianHost",
|
"GuardianHost",
|
||||||
|
|||||||
97
Content.IntegrationTests/Tests/Atmos/AlarmThresholdTest.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.Atmos
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[TestOf(typeof(AtmosAlarmThreshold))]
|
||||||
|
public class AlarmThresholdTest : ContentIntegrationTest
|
||||||
|
{
|
||||||
|
private const string Prototypes = @"
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: testThreshold
|
||||||
|
upperBound: 5
|
||||||
|
lowerBound: 1
|
||||||
|
upperWarnAround: 0.5
|
||||||
|
lowerWarnAround: 1.5
|
||||||
|
";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestAlarmThreshold()
|
||||||
|
{
|
||||||
|
var server = StartServerDummyTicker(new ServerContentIntegrationOption
|
||||||
|
{
|
||||||
|
ExtraPrototypes = Prototypes
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.WaitIdleAsync();
|
||||||
|
|
||||||
|
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||||
|
AtmosAlarmThreshold threshold = default!;
|
||||||
|
|
||||||
|
await server.WaitPost(() =>
|
||||||
|
{
|
||||||
|
threshold = prototypeManager.Index<AtmosAlarmThreshold>("testThreshold");
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
// ensure upper/lower bounds are calculated
|
||||||
|
Assert.That(threshold.UpperWarningBound, Is.EqualTo(5f * 0.5f));
|
||||||
|
Assert.That(threshold.LowerWarningBound, Is.EqualTo(1f * 1.5f));
|
||||||
|
|
||||||
|
// ensure that setting bounds to zero/
|
||||||
|
// negative numbers is an invalid
|
||||||
|
// set
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Upper, 0);
|
||||||
|
Assert.That(threshold.UpperBound, Is.EqualTo(5f));
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Upper, -1);
|
||||||
|
Assert.That(threshold.UpperBound, Is.EqualTo(5f));
|
||||||
|
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, 0);
|
||||||
|
Assert.That(threshold.LowerBound, Is.EqualTo(1f));
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, -1);
|
||||||
|
Assert.That(threshold.LowerBound, Is.EqualTo(1f));
|
||||||
|
|
||||||
|
|
||||||
|
// test if making the lower bound higher
|
||||||
|
// than upper is invalid
|
||||||
|
// aka just returns the previous value
|
||||||
|
// instead of setting it to null
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, 6f);
|
||||||
|
Assert.That(threshold.LowerBound, Is.EqualTo(1f));
|
||||||
|
|
||||||
|
// same as above, sets it lower
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Upper, 0.5f);
|
||||||
|
Assert.That(threshold.UpperBound, Is.EqualTo(5f));
|
||||||
|
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Upper, threshold.UpperBound + 1);
|
||||||
|
Assert.That(threshold.UpperWarningPercentage, Is.EqualTo(0.5f));
|
||||||
|
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Lower, threshold.LowerBound - 1);
|
||||||
|
Assert.That(threshold.LowerWarningPercentage, Is.EqualTo(1.5f));
|
||||||
|
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Upper, threshold.LowerBound - 1);
|
||||||
|
Assert.That(threshold.UpperWarningPercentage, Is.EqualTo(0.5f));
|
||||||
|
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Lower, threshold.UpperBound + 1);
|
||||||
|
Assert.That(threshold.LowerWarningPercentage, Is.EqualTo(1.5f));
|
||||||
|
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Upper, null);
|
||||||
|
threshold.TrySetWarningBound(AtmosMonitorThresholdBound.Lower, null);
|
||||||
|
|
||||||
|
Assert.That(threshold.UpperWarningBound, Is.EqualTo(null));
|
||||||
|
Assert.That(threshold.LowerWarningBound, Is.EqualTo(null));
|
||||||
|
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Upper, null);
|
||||||
|
threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, null);
|
||||||
|
|
||||||
|
Assert.That(threshold.UpperBound, Is.EqualTo(null));
|
||||||
|
Assert.That(threshold.LowerBound, Is.EqualTo(null));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Server.VendingMachines; // TODO: Move this out of vending machines???
|
||||||
|
using Content.Server.WireHacking;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
using static Content.Shared.Wires.SharedWiresComponent;
|
||||||
|
using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public class AirAlarmComponent : Component, IWires
|
||||||
|
{
|
||||||
|
[ComponentDependency] public readonly ApcPowerReceiverComponent? DeviceRecvComponent = default!;
|
||||||
|
[ComponentDependency] public readonly AtmosMonitorComponent? AtmosMonitorComponent = default!;
|
||||||
|
[ComponentDependency] public readonly DeviceNetworkComponent? DeviceNetComponent = default!;
|
||||||
|
[ComponentDependency] public readonly WiresComponent? WiresComponent = null;
|
||||||
|
|
||||||
|
private AirAlarmSystem? _airAlarmSystem;
|
||||||
|
|
||||||
|
[ViewVariables] public AirAlarmMode CurrentMode { get; set; }
|
||||||
|
|
||||||
|
// Remember to null this afterwards.
|
||||||
|
[ViewVariables] public IAirAlarmModeUpdate? CurrentModeUpdater { get; set; }
|
||||||
|
|
||||||
|
public override string Name => "AirAlarm";
|
||||||
|
|
||||||
|
public Dictionary<string, IAtmosDeviceData> DeviceData = new();
|
||||||
|
|
||||||
|
public HashSet<NetUserId> ActivePlayers = new();
|
||||||
|
|
||||||
|
public bool FullAccess = false;
|
||||||
|
public bool CanSync = true;
|
||||||
|
|
||||||
|
// <-- Wires -->
|
||||||
|
|
||||||
|
private CancellationTokenSource _powerPulsedCancel = new();
|
||||||
|
private int PowerPulsedTimeout = 30;
|
||||||
|
|
||||||
|
private enum Wires
|
||||||
|
{
|
||||||
|
// Cutting this kills power.
|
||||||
|
// Pulsing it disrupts power.
|
||||||
|
Power,
|
||||||
|
// Cutting this allows full access.
|
||||||
|
// Pulsing this does nothing.
|
||||||
|
Access,
|
||||||
|
// Cutting/Remending this resets ONLY from panic mode.
|
||||||
|
// Pulsing this sets panic mode.
|
||||||
|
Panic,
|
||||||
|
// Cutting this clears sync'd devices, and makes
|
||||||
|
// the alarm unable to resync.
|
||||||
|
// Pulsing this resyncs all devices (ofc current
|
||||||
|
// implementation just auto-does this anyways)
|
||||||
|
DeviceSync,
|
||||||
|
// This does nothing. (placeholder for AI wire,
|
||||||
|
// if that ever gets implemented)
|
||||||
|
Dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterWires(WiresComponent.WiresBuilder builder)
|
||||||
|
{
|
||||||
|
foreach (var wire in Enum.GetValues<Wires>())
|
||||||
|
builder.CreateWire(wire);
|
||||||
|
|
||||||
|
UpdateWires();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateWires()
|
||||||
|
{
|
||||||
|
if (_airAlarmSystem == null)
|
||||||
|
_airAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||||
|
|
||||||
|
if (WiresComponent == null) return;
|
||||||
|
|
||||||
|
var pwrLightState = (PowerPulsed, PowerCut) switch {
|
||||||
|
(true, false) => StatusLightState.BlinkingFast,
|
||||||
|
(_, true) => StatusLightState.Off,
|
||||||
|
(_, _) => StatusLightState.On
|
||||||
|
};
|
||||||
|
|
||||||
|
var powerLight = new StatusLightData(Color.Yellow, pwrLightState, "POWR");
|
||||||
|
|
||||||
|
var accessLight = new StatusLightData(
|
||||||
|
Color.Green,
|
||||||
|
WiresComponent.IsWireCut(Wires.Access) ? StatusLightState.Off : StatusLightState.On,
|
||||||
|
"ACC"
|
||||||
|
);
|
||||||
|
|
||||||
|
var panicLight = new StatusLightData(
|
||||||
|
Color.Red,
|
||||||
|
CurrentMode == AirAlarmMode.Panic ? StatusLightState.On : StatusLightState.Off,
|
||||||
|
"PAN"
|
||||||
|
);
|
||||||
|
|
||||||
|
var syncLightState = StatusLightState.BlinkingSlow;
|
||||||
|
|
||||||
|
if (AtmosMonitorComponent != null && !AtmosMonitorComponent.NetEnabled)
|
||||||
|
syncLightState = StatusLightState.Off;
|
||||||
|
else if (DeviceData.Count != 0)
|
||||||
|
syncLightState = StatusLightState.On;
|
||||||
|
|
||||||
|
var syncLight = new StatusLightData(Color.Orange, syncLightState, "NET");
|
||||||
|
|
||||||
|
WiresComponent.SetStatus(AirAlarmWireStatus.Power, powerLight);
|
||||||
|
WiresComponent.SetStatus(AirAlarmWireStatus.Access, accessLight);
|
||||||
|
WiresComponent.SetStatus(AirAlarmWireStatus.Panic, panicLight);
|
||||||
|
WiresComponent.SetStatus(AirAlarmWireStatus.DeviceSync, syncLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _powerCut;
|
||||||
|
private bool PowerCut
|
||||||
|
{
|
||||||
|
get => _powerCut;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_powerCut = value;
|
||||||
|
SetPower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _powerPulsed;
|
||||||
|
private bool PowerPulsed
|
||||||
|
{
|
||||||
|
get => _powerPulsed && !_powerCut;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_powerPulsed = value;
|
||||||
|
SetPower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPower()
|
||||||
|
{
|
||||||
|
if (DeviceRecvComponent != null
|
||||||
|
&& WiresComponent != null)
|
||||||
|
DeviceRecvComponent.PowerDisabled = PowerPulsed || PowerCut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||||
|
{
|
||||||
|
if (DeviceNetComponent == null) return;
|
||||||
|
|
||||||
|
if (_airAlarmSystem == null)
|
||||||
|
_airAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||||
|
|
||||||
|
switch (args.Action)
|
||||||
|
{
|
||||||
|
case Pulse:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.Power:
|
||||||
|
PowerPulsed = true;
|
||||||
|
_powerPulsedCancel.Cancel();
|
||||||
|
_powerPulsedCancel = new CancellationTokenSource();
|
||||||
|
Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout),
|
||||||
|
() => PowerPulsed = false,
|
||||||
|
_powerPulsedCancel.Token);
|
||||||
|
break;
|
||||||
|
case Wires.Panic:
|
||||||
|
if (CurrentMode != AirAlarmMode.Panic)
|
||||||
|
_airAlarmSystem.SetMode(Owner, DeviceNetComponent.Address, AirAlarmMode.Panic, true, false);
|
||||||
|
break;
|
||||||
|
case Wires.DeviceSync:
|
||||||
|
_airAlarmSystem.SyncAllDevices(Owner);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Mend:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.Power:
|
||||||
|
_powerPulsedCancel.Cancel();
|
||||||
|
PowerPulsed = false;
|
||||||
|
PowerCut = false;
|
||||||
|
break;
|
||||||
|
case Wires.Panic:
|
||||||
|
if (CurrentMode == AirAlarmMode.Panic)
|
||||||
|
_airAlarmSystem.SetMode(Owner, DeviceNetComponent.Address, AirAlarmMode.Filtering, true, false);
|
||||||
|
break;
|
||||||
|
case Wires.Access:
|
||||||
|
FullAccess = false;
|
||||||
|
break;
|
||||||
|
case Wires.DeviceSync:
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
AtmosMonitorComponent.NetEnabled = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Cut:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.DeviceSync:
|
||||||
|
DeviceData.Clear();
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
{
|
||||||
|
AtmosMonitorComponent.NetworkAlarmStates.Clear();
|
||||||
|
AtmosMonitorComponent.NetEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Wires.Power:
|
||||||
|
PowerCut = true;
|
||||||
|
break;
|
||||||
|
case Wires.Access:
|
||||||
|
FullAccess = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWires();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
// AtmosAlarmables are entities that can be alarmed
|
||||||
|
// by a linked AtmosMonitor (alarmer?) if a threshold
|
||||||
|
// is passed in some way. The intended use is to
|
||||||
|
// do something in case something dangerous happens,
|
||||||
|
// e.g., activate firelocks in case a temperature
|
||||||
|
// threshold is reached
|
||||||
|
//
|
||||||
|
// It goes:
|
||||||
|
//
|
||||||
|
// AtmosMonitor -> AtmosDeviceUpdateEvent
|
||||||
|
// -> Threshold calculation
|
||||||
|
// -> AtmosMonitorAlarmEvent
|
||||||
|
// -> Everything linked to that monitor (targetted)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A component to add to device network devices if you want them to be alarmed
|
||||||
|
/// by an atmospheric monitor.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public class AtmosAlarmableComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "AtmosAlarmable";
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public List<EntityUid> LinkedMonitors { get; set; } = new();
|
||||||
|
|
||||||
|
[ViewVariables] public AtmosMonitorAlarmType LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||||
|
[ViewVariables] public AtmosMonitorAlarmType HighestNetworkState = AtmosMonitorAlarmType.Normal;
|
||||||
|
[ViewVariables] public bool IgnoreAlarms { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of prototypes that this alarmable can be
|
||||||
|
/// alarmed by - must be a prototype with AtmosMonitor
|
||||||
|
/// attached to it
|
||||||
|
/// </summary>
|
||||||
|
[DataField("alarmedBy")]
|
||||||
|
public List<string> AlarmedByPrototypes { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
114
Content.Server/Atmos/Monitor/Components/AtmosMonitorComponent.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Atmos.Piping.Components;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Sound;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public class AtmosMonitorComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "AtmosMonitor";
|
||||||
|
|
||||||
|
// Requires power. No logic related to this will be performed here, however,
|
||||||
|
// save for ensuring that the data is valid upon set.
|
||||||
|
// This is how the system discovers entities that are alarmable.
|
||||||
|
[ComponentDependency] public readonly ApcPowerReceiverComponent? PowerRecvComponent = default!;
|
||||||
|
[ComponentDependency] public readonly AtmosDeviceComponent? AtmosDeviceComponent = default!;
|
||||||
|
|
||||||
|
// Whether this monitor can send alarms,
|
||||||
|
// or recieve atmos command events.
|
||||||
|
//
|
||||||
|
// Useful for wires; i.e., pulsing a monitor wire
|
||||||
|
// will make it send an alert, and cutting
|
||||||
|
// it will make it so that alerts are no longer
|
||||||
|
// sent/receieved.
|
||||||
|
//
|
||||||
|
// Note that this cancels every single network
|
||||||
|
// event, including ones that may not be
|
||||||
|
// related to atmos monitor events.
|
||||||
|
[ViewVariables]
|
||||||
|
public bool NetEnabled = true;
|
||||||
|
|
||||||
|
// Entities that the monitor will alarm. Stores only EntityUids, is populated
|
||||||
|
// when this component starts up.
|
||||||
|
[ViewVariables]
|
||||||
|
public List<EntityUid> LinkedEntities = new();
|
||||||
|
|
||||||
|
[DataField("temperatureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
|
||||||
|
public readonly string? TemperatureThresholdId;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public AtmosAlarmThreshold? TemperatureThreshold;
|
||||||
|
|
||||||
|
[DataField("pressureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
|
||||||
|
public readonly string? PressureThresholdId;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public AtmosAlarmThreshold? PressureThreshold;
|
||||||
|
|
||||||
|
// monitor fire - much different from temperature
|
||||||
|
// since there's events for fire, setting this to true
|
||||||
|
// will make the atmos monitor act like a smoke detector,
|
||||||
|
// immediately signalling danger if there's a fire
|
||||||
|
[DataField("monitorFire")]
|
||||||
|
public bool MonitorFire = false;
|
||||||
|
|
||||||
|
[DataField("displayMaxAlarmInNet")]
|
||||||
|
public bool DisplayMaxAlarmInNet = false;
|
||||||
|
|
||||||
|
[DataField("alarmSound")]
|
||||||
|
public SoundSpecifier AlarmSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/alarm.ogg");
|
||||||
|
|
||||||
|
[DataField("alarmVolume")]
|
||||||
|
public float AlarmVolume { get; set; } = -10;
|
||||||
|
|
||||||
|
// really messy but this is parsed at runtime after
|
||||||
|
// prototypes are initialized, there's no
|
||||||
|
// way without implementing a new
|
||||||
|
// type serializer
|
||||||
|
[DataField("gasThresholds")]
|
||||||
|
public Dictionary<Gas, string>? GasThresholdIds;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
|
||||||
|
|
||||||
|
// Stores a reference to the gas on the tile this is on.
|
||||||
|
[ViewVariables]
|
||||||
|
public GasMixture? TileGas;
|
||||||
|
|
||||||
|
// Stores the last alarm state of this alarm.
|
||||||
|
[ViewVariables]
|
||||||
|
public AtmosMonitorAlarmType LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||||
|
|
||||||
|
// feeling real dirty about this one
|
||||||
|
// Caches the alarm states it recieves from the rest of the network.
|
||||||
|
// This is so that the highest alarm in the network can be calculated
|
||||||
|
// from any monitor without having to reping every alarm.
|
||||||
|
[ViewVariables]
|
||||||
|
public Dictionary<string, AtmosMonitorAlarmType> NetworkAlarmStates = new();
|
||||||
|
|
||||||
|
// Calculates the highest alarm in the network, including itself.
|
||||||
|
[ViewVariables]
|
||||||
|
public AtmosMonitorAlarmType HighestAlarmInNetwork
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var state = AtmosMonitorAlarmType.Normal;
|
||||||
|
foreach (var (_, netState) in NetworkAlarmStates)
|
||||||
|
if (state < netState)
|
||||||
|
state = netState;
|
||||||
|
|
||||||
|
if (LastAlarmState > state) state = LastAlarmState;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Server.VendingMachines; // TODO: Move this out of vending machines???
|
||||||
|
using Content.Server.WireHacking;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using static Content.Shared.Wires.SharedWiresComponent;
|
||||||
|
using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public class FireAlarmComponent : Component, IWires
|
||||||
|
{
|
||||||
|
[ComponentDependency] public readonly ApcPowerReceiverComponent? DeviceRecvComponent = default!;
|
||||||
|
[ComponentDependency] public readonly AtmosMonitorComponent? AtmosMonitorComponent = default!;
|
||||||
|
[ComponentDependency] public readonly WiresComponent? WiresComponent = null;
|
||||||
|
|
||||||
|
private AtmosMonitorSystem? _atmosMonitorSystem;
|
||||||
|
|
||||||
|
public override string Name => "FireAlarm";
|
||||||
|
|
||||||
|
private CancellationTokenSource _powerPulsedCancel = new();
|
||||||
|
private int PowerPulsedTimeout = 30;
|
||||||
|
|
||||||
|
// Much more simpler than the air alarm wire set.
|
||||||
|
private enum Wires
|
||||||
|
{
|
||||||
|
// Cutting this kills power,
|
||||||
|
// pulsing it disrupts.
|
||||||
|
Power,
|
||||||
|
// Cutting this disables network
|
||||||
|
// connectivity,
|
||||||
|
// pulsing it sets off an alarm.
|
||||||
|
Alarm,
|
||||||
|
Dummy1,
|
||||||
|
Dummy2,
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _powerCut;
|
||||||
|
private bool PowerCut
|
||||||
|
{
|
||||||
|
get => _powerCut;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_powerCut = value;
|
||||||
|
SetPower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _powerPulsed;
|
||||||
|
private bool PowerPulsed
|
||||||
|
{
|
||||||
|
get => _powerPulsed && !_powerCut;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_powerPulsed = value;
|
||||||
|
SetPower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPower()
|
||||||
|
{
|
||||||
|
if (DeviceRecvComponent != null
|
||||||
|
&& WiresComponent != null)
|
||||||
|
DeviceRecvComponent.PowerDisabled = PowerPulsed || PowerCut;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void RegisterWires(WiresComponent.WiresBuilder builder)
|
||||||
|
{
|
||||||
|
builder.CreateWire(Wires.Power);
|
||||||
|
builder.CreateWire(Wires.Alarm);
|
||||||
|
builder.CreateWire(Wires.Dummy1);
|
||||||
|
builder.CreateWire(Wires.Dummy2);
|
||||||
|
|
||||||
|
UpdateWires();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateWires()
|
||||||
|
{
|
||||||
|
if (WiresComponent == null) return;
|
||||||
|
|
||||||
|
var powerLight = new StatusLightData(Color.Yellow, StatusLightState.On, "POWR");
|
||||||
|
|
||||||
|
if (PowerPulsed)
|
||||||
|
powerLight = new StatusLightData(Color.Yellow, StatusLightState.BlinkingFast, "POWR");
|
||||||
|
else if (PowerCut)
|
||||||
|
powerLight = new StatusLightData(Color.Yellow, StatusLightState.Off, "POWR");
|
||||||
|
|
||||||
|
var syncLight = new StatusLightData(Color.Orange, StatusLightState.On, "NET");
|
||||||
|
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
if (!AtmosMonitorComponent.NetEnabled)
|
||||||
|
syncLight = new StatusLightData(Color.Orange, StatusLightState.Off, "NET");
|
||||||
|
else if (AtmosMonitorComponent.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger)
|
||||||
|
syncLight = new StatusLightData(Color.Orange, StatusLightState.BlinkingFast, "NET");
|
||||||
|
|
||||||
|
WiresComponent.SetStatus(FireAlarmWireStatus.Power, powerLight);
|
||||||
|
WiresComponent.SetStatus(FireAlarmWireStatus.Alarm, syncLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||||
|
{
|
||||||
|
if (_atmosMonitorSystem == null)
|
||||||
|
_atmosMonitorSystem = EntitySystem.Get<AtmosMonitorSystem>();
|
||||||
|
|
||||||
|
switch (args.Action)
|
||||||
|
{
|
||||||
|
case Pulse:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.Power:
|
||||||
|
PowerPulsed = true;
|
||||||
|
_powerPulsedCancel.Cancel();
|
||||||
|
_powerPulsedCancel = new CancellationTokenSource();
|
||||||
|
Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout),
|
||||||
|
() => PowerPulsed = false,
|
||||||
|
_powerPulsedCancel.Token);
|
||||||
|
break;
|
||||||
|
case Wires.Alarm:
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
_atmosMonitorSystem.Alert(Owner, AtmosMonitorAlarmType.Danger);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Mend:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.Power:
|
||||||
|
_powerPulsedCancel.Cancel();
|
||||||
|
PowerPulsed = false;
|
||||||
|
PowerCut = false;
|
||||||
|
break;
|
||||||
|
case Wires.Alarm:
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
AtmosMonitorComponent.NetEnabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Cut:
|
||||||
|
switch (args.Identifier)
|
||||||
|
{
|
||||||
|
case Wires.Power:
|
||||||
|
PowerCut = true;
|
||||||
|
break;
|
||||||
|
case Wires.Alarm:
|
||||||
|
if (AtmosMonitorComponent != null)
|
||||||
|
AtmosMonitorComponent.NetEnabled = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWires();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
242
Content.Server/Atmos/Monitor/Systems/AirAlarmModes.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
|
using Content.Server.Atmos.Piping.Unary.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is an interface that air alarm modes use
|
||||||
|
/// in order to execute the defined modes.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAlarmMode
|
||||||
|
{
|
||||||
|
// This is executed the moment the mode
|
||||||
|
// is set. This is to ensure that 'dumb'
|
||||||
|
// modes such as Filter/Panic are immediately
|
||||||
|
// set.
|
||||||
|
/// <summary>
|
||||||
|
/// Executed the mode is set on an air alarm.
|
||||||
|
/// This is to ensure that modes like Filter/Panic
|
||||||
|
/// are immediately set.
|
||||||
|
/// </summary>
|
||||||
|
public void Execute(EntityUid uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAirAlarmModeUpdate
|
||||||
|
//
|
||||||
|
// This is an interface that AirAlarmSystem uses
|
||||||
|
// in order to 'update' air alarm modes so that
|
||||||
|
// modes like Replace can be implemented.
|
||||||
|
/// <summary>
|
||||||
|
/// An interface that AirAlarmSystem uses
|
||||||
|
/// in order to update air alarm modes that
|
||||||
|
/// need updating (e.g., Replace)
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAlarmModeUpdate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is checked by AirAlarmSystem when
|
||||||
|
/// a mode is updated. This should be set
|
||||||
|
/// to a DeviceNetwork address, or some
|
||||||
|
/// unique identifier that ID's the
|
||||||
|
/// owner of the mode's executor.
|
||||||
|
/// </summary>
|
||||||
|
public string NetOwner { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This is executed every time the air alarm
|
||||||
|
/// update loop is fully executed. This should
|
||||||
|
/// be where all the logic goes.
|
||||||
|
/// </summary>
|
||||||
|
public void Update(EntityUid uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmModeFactory
|
||||||
|
{
|
||||||
|
private static IAirAlarmMode _filterMode = new AirAlarmFilterMode();
|
||||||
|
private static IAirAlarmMode _fillMode = new AirAlarmFillMode();
|
||||||
|
private static IAirAlarmMode _panicMode = new AirAlarmPanicMode();
|
||||||
|
private static IAirAlarmMode _noneMode = new AirAlarmNoneMode();
|
||||||
|
|
||||||
|
// still not a fan since ReplaceMode must have an allocation
|
||||||
|
// but it's whatever
|
||||||
|
public static IAirAlarmMode? ModeToExecutor(AirAlarmMode mode) => mode switch
|
||||||
|
{
|
||||||
|
AirAlarmMode.Filtering => _filterMode,
|
||||||
|
AirAlarmMode.Fill => _fillMode,
|
||||||
|
AirAlarmMode.Panic => _panicMode,
|
||||||
|
AirAlarmMode.None => _noneMode,
|
||||||
|
AirAlarmMode.Replace => new AirAlarmReplaceMode(),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// like a tiny little EntitySystem
|
||||||
|
public abstract class AirAlarmModeExecutor : IAirAlarmMode
|
||||||
|
{
|
||||||
|
[Dependency] public readonly IEntityManager EntityManager = default!;
|
||||||
|
public readonly DeviceNetworkSystem DeviceNetworkSystem;
|
||||||
|
public readonly AirAlarmSystem AirAlarmSystem;
|
||||||
|
|
||||||
|
public abstract void Execute(EntityUid uid);
|
||||||
|
|
||||||
|
public AirAlarmModeExecutor()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
|
||||||
|
DeviceNetworkSystem = EntitySystem.Get<DeviceNetworkSystem>();
|
||||||
|
AirAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmNoneMode : AirAlarmModeExecutor
|
||||||
|
{
|
||||||
|
public override void Execute(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (addr, device) in alarm.DeviceData)
|
||||||
|
{
|
||||||
|
device.Enabled = false;
|
||||||
|
AirAlarmSystem.SetData(uid, addr, device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmFilterMode : AirAlarmModeExecutor
|
||||||
|
{
|
||||||
|
public override void Execute(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (addr, device) in alarm.DeviceData)
|
||||||
|
{
|
||||||
|
switch (device)
|
||||||
|
{
|
||||||
|
case GasVentPumpData pumpData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FilterModePreset);
|
||||||
|
break;
|
||||||
|
case GasVentScrubberData scrubberData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FilterModePreset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmPanicMode : AirAlarmModeExecutor
|
||||||
|
{
|
||||||
|
public override void Execute(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (addr, device) in alarm.DeviceData)
|
||||||
|
{
|
||||||
|
switch (device)
|
||||||
|
{
|
||||||
|
case GasVentPumpData pumpData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.PanicModePreset);
|
||||||
|
break;
|
||||||
|
case GasVentScrubberData scrubberData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.PanicModePreset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmFillMode : AirAlarmModeExecutor
|
||||||
|
{
|
||||||
|
public override void Execute(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (addr, device) in alarm.DeviceData)
|
||||||
|
{
|
||||||
|
switch (device)
|
||||||
|
{
|
||||||
|
case GasVentPumpData pumpData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FillModePreset);
|
||||||
|
break;
|
||||||
|
case GasVentScrubberData scrubberData:
|
||||||
|
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FillModePreset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AirAlarmReplaceMode : AirAlarmModeExecutor, IAirAlarmModeUpdate
|
||||||
|
{
|
||||||
|
private Dictionary<string, IAtmosDeviceData> _devices = new();
|
||||||
|
private float _lastPressure = Atmospherics.OneAtmosphere;
|
||||||
|
private AtmosMonitorComponent? _monitor;
|
||||||
|
private AtmosAlarmableComponent? _alarmable;
|
||||||
|
|
||||||
|
public string NetOwner { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public override void Execute(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent alarmable))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_devices = alarm.DeviceData;
|
||||||
|
_monitor = monitor;
|
||||||
|
_alarmable = alarmable;
|
||||||
|
_alarmable.IgnoreAlarms = true;
|
||||||
|
SetSiphon(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (_monitor == null
|
||||||
|
|| _alarmable == null
|
||||||
|
|| _monitor.TileGas == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// just a little pointer
|
||||||
|
var mixture = _monitor.TileGas;
|
||||||
|
|
||||||
|
_lastPressure = mixture.Pressure;
|
||||||
|
if (_lastPressure <= 0.2f) // anything below and it might get stuck
|
||||||
|
{
|
||||||
|
_alarmable.IgnoreAlarms = false;
|
||||||
|
AirAlarmSystem.SetMode(uid, NetOwner!, AirAlarmMode.Filtering, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetSiphon(EntityUid uid)
|
||||||
|
{
|
||||||
|
foreach (var (addr, device) in _devices)
|
||||||
|
{
|
||||||
|
switch (device)
|
||||||
|
{
|
||||||
|
case GasVentPumpData pumpData:
|
||||||
|
pumpData = GasVentPumpData.PanicModePreset;
|
||||||
|
pumpData.IgnoreAlarms = true;
|
||||||
|
AirAlarmSystem.SetData(uid, addr, pumpData);
|
||||||
|
break;
|
||||||
|
case GasVentScrubberData scrubberData:
|
||||||
|
scrubberData = GasVentScrubberData.PanicModePreset;
|
||||||
|
scrubberData.IgnoreAlarms = true;
|
||||||
|
AirAlarmSystem.SetData(uid, addr, scrubberData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
541
Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.Piping.Components;
|
||||||
|
using Content.Server.DeviceNetwork;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Server.WireHacking;
|
||||||
|
using Content.Shared.Access.Components;
|
||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Popups;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Systems
|
||||||
|
{
|
||||||
|
// AirAlarm system - specific for atmos devices, rather than
|
||||||
|
// atmos monitors.
|
||||||
|
//
|
||||||
|
// oh boy, message passing!
|
||||||
|
//
|
||||||
|
// Commands should always be sent into packet's Command
|
||||||
|
// data key. In response, a packet will be transmitted
|
||||||
|
// with the response type as its command, and the
|
||||||
|
// response data in its data key.
|
||||||
|
public class AirAlarmSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
|
||||||
|
[Dependency] private readonly AtmosMonitorSystem _atmosMonitorSystem = default!;
|
||||||
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
|
[Dependency] private readonly AccessReaderSystem _accessSystem = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
|
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||||
|
|
||||||
|
#region Device Network API
|
||||||
|
|
||||||
|
public const int Freq = AtmosMonitorSystem.AtmosMonitorApcFreq;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to set device data within the air alarm's network.
|
||||||
|
/// </summary>
|
||||||
|
public const string AirAlarmSetData = "air_alarm_set_device_data";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to request a sync from devices in an air alarm's network.
|
||||||
|
/// </summary>
|
||||||
|
public const string AirAlarmSyncCmd = "air_alarm_sync_devices";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to set an air alarm's mode.
|
||||||
|
/// </summary>
|
||||||
|
public const string AirAlarmSetMode = "air_alarm_set_mode";
|
||||||
|
|
||||||
|
// -- Packet Data --
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data response to an AirAlarmSetData command.
|
||||||
|
/// </summary>
|
||||||
|
public const string AirAlarmSetDataStatus = "air_alarm_set_device_data_status";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data response to an AirAlarmSync command. Contains
|
||||||
|
/// IAtmosDeviceData in this system's implementation.
|
||||||
|
/// </summary>
|
||||||
|
public const string AirAlarmSyncData = "air_alarm_device_sync_data";
|
||||||
|
|
||||||
|
// -- API --
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the data for an air alarm managed device.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address of the device.</param>
|
||||||
|
/// <param name="data">The data to send to the device.</param>
|
||||||
|
public void SetData(EntityUid uid, string address, IAtmosDeviceData data)
|
||||||
|
{
|
||||||
|
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||||
|
&& !monitor.NetEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AirAlarmSetData,
|
||||||
|
// [AirAlarmTypeData] = type,
|
||||||
|
[AirAlarmSetData] = data
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNet.QueuePacket(uid, address, Freq, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast a sync packet to an air alarm's local network.
|
||||||
|
/// </summary>
|
||||||
|
public void SyncAllDevices(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||||
|
&& !monitor.NetEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AirAlarmSyncCmd
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNet.QueuePacket(uid, string.Empty, Freq, payload, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a sync packet to a specific device from an air alarm.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address of the device.</param>
|
||||||
|
public void SyncDevice(EntityUid uid, string address)
|
||||||
|
{
|
||||||
|
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||||
|
&& !monitor.NetEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AirAlarmSyncCmd
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNet.QueuePacket(uid, address, Freq, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sync this air alarm's mode with the rest of the network.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The mode to sync with the rest of the network.</param>
|
||||||
|
public void SyncMode(EntityUid uid, AirAlarmMode mode)
|
||||||
|
{
|
||||||
|
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||||
|
&& !monitor.NetEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AirAlarmSetMode,
|
||||||
|
[AirAlarmSetMode] = mode
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNet.QueuePacket(uid, string.Empty, Freq, payload, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, PacketSentEvent>(OnPacketRecv);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, PowerChangedEvent>(OnPowerChanged);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AirAlarmResyncAllDevicesMessage>(OnResyncAll);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateAlarmModeMessage>(OnUpdateAlarmMode);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateAlarmThresholdMessage>(OnUpdateThreshold);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateDeviceDataMessage>(OnUpdateDeviceData);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, BoundUIClosedEvent>(OnClose);
|
||||||
|
SubscribeLocalEvent<AirAlarmComponent, InteractHandEvent>(OnInteract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPowerChanged(EntityUid uid, AirAlarmComponent component, PowerChangedEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Powered)
|
||||||
|
{
|
||||||
|
ForceCloseAllInterfaces(uid);
|
||||||
|
component.CurrentModeUpdater = null;
|
||||||
|
component.DeviceData.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClose(EntityUid uid, AirAlarmComponent component, BoundUIClosedEvent args)
|
||||||
|
{
|
||||||
|
component.ActivePlayers.Remove(args.Session.UserId);
|
||||||
|
if (component.ActivePlayers.Count == 0)
|
||||||
|
RemoveActiveInterface(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInteract(EntityUid uid, AirAlarmComponent component, InteractHandEvent args)
|
||||||
|
{
|
||||||
|
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(uid, out WiresComponent wire) && wire.IsPanelOpen)
|
||||||
|
{
|
||||||
|
args.Handled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(uid, out ApcPowerReceiverComponent recv) && !recv.Powered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_uiSystem.GetUiOrNull(component.Owner, SharedAirAlarmInterfaceKey.Key)?.Open(actor.PlayerSession);
|
||||||
|
component.ActivePlayers.Add(actor.PlayerSession.UserId);
|
||||||
|
AddActiveInterface(uid);
|
||||||
|
SendAddress(uid);
|
||||||
|
SendAlarmMode(uid);
|
||||||
|
SendThresholds(uid);
|
||||||
|
SyncAllDevices(uid);
|
||||||
|
SendAirData(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResyncAll(EntityUid uid, AirAlarmComponent component, AirAlarmResyncAllDevicesMessage args)
|
||||||
|
{
|
||||||
|
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||||
|
{
|
||||||
|
component.DeviceData.Clear();
|
||||||
|
SyncAllDevices(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateAlarmMode(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmModeMessage args)
|
||||||
|
{
|
||||||
|
string addr = string.Empty;
|
||||||
|
if (EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)) addr = netConn.Address;
|
||||||
|
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||||
|
SetMode(uid, addr, args.Mode, true, false);
|
||||||
|
else
|
||||||
|
SendAlarmMode(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateThreshold(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmThresholdMessage args)
|
||||||
|
{
|
||||||
|
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||||
|
SetThreshold(uid, args.Threshold, args.Type, args.Gas);
|
||||||
|
else
|
||||||
|
SendThresholds(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateDeviceData(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateDeviceDataMessage args)
|
||||||
|
{
|
||||||
|
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||||
|
SetDeviceData(uid, args.Address, args.Data);
|
||||||
|
else
|
||||||
|
SyncDevice(uid, args.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AccessCheck(EntityUid uid, EntityUid? user, AirAlarmComponent? component = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AccessReaderComponent reader) || user == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_accessSystem.IsAllowed(reader, user.Value) && !component.FullAccess)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("air-alarm-ui-access-denied"), user.Value, Filter.Entities(user.Value));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosAlarm(EntityUid uid, AirAlarmComponent component, AtmosMonitorAlarmEvent args)
|
||||||
|
{
|
||||||
|
if (component.ActivePlayers.Count != 0)
|
||||||
|
{
|
||||||
|
SyncAllDevices(uid);
|
||||||
|
SendAirData(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
string addr = string.Empty;
|
||||||
|
if (EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)) addr = netConn.Address;
|
||||||
|
|
||||||
|
|
||||||
|
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||||
|
{
|
||||||
|
SetMode(uid, addr, AirAlarmMode.None, true);
|
||||||
|
// set mode to off to mimic the vents/scrubbers being turned off
|
||||||
|
// update UI
|
||||||
|
//
|
||||||
|
// no, the mode isn't processed here - it's literally just
|
||||||
|
// set to what mimics 'off'
|
||||||
|
}
|
||||||
|
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||||
|
{
|
||||||
|
// if the mode is still set to off, set it to filtering instead
|
||||||
|
// alternatively, set it to the last saved mode
|
||||||
|
//
|
||||||
|
// no, this still doesn't execute the mode
|
||||||
|
SetMode(uid, addr, AirAlarmMode.Filtering, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Air Alarm Settings
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a threshold on an air alarm.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="threshold">New threshold data.</param>
|
||||||
|
public void SetThreshold(EntityUid uid, AtmosAlarmThreshold threshold, AtmosMonitorThresholdType type, Gas? gas = null, AirAlarmComponent? controller = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref controller)) return;
|
||||||
|
|
||||||
|
_atmosMonitorSystem.SetThreshold(uid, type, threshold, gas);
|
||||||
|
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(type, threshold, gas));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set an air alarm's mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">The origin address of this mode set. Used for network sync.</param>
|
||||||
|
/// <param name="mode">The mode to set the alarm to.</param>
|
||||||
|
/// <param name="sync">Whether to sync this mode change to the network or not. Defaults to false.</param>
|
||||||
|
/// <param name="uiOnly">Whether this change is for the UI only, or if it changes the air alarm's operating mode. Defaults to true.</param>
|
||||||
|
public void SetMode(EntityUid uid, string origin, AirAlarmMode mode, bool sync = false, bool uiOnly = true, AirAlarmComponent? controller = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref controller)) return;
|
||||||
|
controller.CurrentMode = mode;
|
||||||
|
|
||||||
|
// setting it to UI only maans we don't have
|
||||||
|
// to deal with the issue of not-single-owner
|
||||||
|
// alarm mode executors
|
||||||
|
if (!uiOnly)
|
||||||
|
{
|
||||||
|
var newMode = AirAlarmModeFactory.ModeToExecutor(mode);
|
||||||
|
if (newMode != null)
|
||||||
|
{
|
||||||
|
newMode.Execute(uid);
|
||||||
|
if (newMode is IAirAlarmModeUpdate updatedMode)
|
||||||
|
{
|
||||||
|
controller.CurrentModeUpdater = updatedMode;
|
||||||
|
controller.CurrentModeUpdater.NetOwner = origin;
|
||||||
|
}
|
||||||
|
else if (controller.CurrentModeUpdater != null)
|
||||||
|
controller.CurrentModeUpdater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only one air alarm in a network can use an air alarm mode
|
||||||
|
// that updates, so even if it's a ui-only change,
|
||||||
|
// we have to invalidte the last mode's updater and
|
||||||
|
// remove it because otherwise it'll execute a now
|
||||||
|
// invalid mode
|
||||||
|
else if (controller.CurrentModeUpdater != null
|
||||||
|
&& controller.CurrentModeUpdater.NetOwner != origin)
|
||||||
|
controller.CurrentModeUpdater = null;
|
||||||
|
|
||||||
|
// controller.SendMessage(new AirAlarmUpdateAlarmModeMessage(mode));
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmModeMessage(mode));
|
||||||
|
|
||||||
|
|
||||||
|
// setting sync deals with the issue of air alarms
|
||||||
|
// in the same network needing to have the same mode
|
||||||
|
// as other alarms
|
||||||
|
if (sync) SyncMode(uid, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets device data. Practically a wrapper around the packet sending function, SetData.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address to send the new data to.</param>
|
||||||
|
/// <param name="devData">The device data to be sent.</param>
|
||||||
|
public void SetDeviceData(EntityUid uid, string address, IAtmosDeviceData devData, AirAlarmComponent? controller = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref controller)) return;
|
||||||
|
|
||||||
|
devData.Dirty = true;
|
||||||
|
SetData(uid, address, devData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPacketRecv(EntityUid uid, AirAlarmComponent controller, PacketSentEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case AirAlarmSyncData:
|
||||||
|
if (!args.Data.TryGetValue(AirAlarmSyncData, out IAtmosDeviceData? data)
|
||||||
|
|| data == null
|
||||||
|
|| !controller.CanSync) break;
|
||||||
|
|
||||||
|
// Save into component.
|
||||||
|
// Sync data to interface.
|
||||||
|
// _airAlarmDataSystem.UpdateDeviceData(uid, args.SenderAddress, data);
|
||||||
|
//
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateDeviceDataMessage(args.SenderAddress, data));
|
||||||
|
if (controller.WiresComponent != null) controller.UpdateWires();
|
||||||
|
if (!controller.DeviceData.TryAdd(args.SenderAddress, data))
|
||||||
|
controller.DeviceData[args.SenderAddress] = data;
|
||||||
|
|
||||||
|
return;
|
||||||
|
case AirAlarmSetDataStatus:
|
||||||
|
if (!args.Data.TryGetValue(AirAlarmSetDataStatus, out bool dataStatus)) break;
|
||||||
|
|
||||||
|
// Sync data to interface.
|
||||||
|
// This should say if the result
|
||||||
|
// failed, or succeeded. Don't save it.l
|
||||||
|
SyncDevice(uid, args.SenderAddress);
|
||||||
|
|
||||||
|
return;
|
||||||
|
case AirAlarmSetMode:
|
||||||
|
if (!args.Data.TryGetValue(AirAlarmSetMode, out AirAlarmMode alarmMode)) break;
|
||||||
|
|
||||||
|
SetMode(uid, args.SenderAddress, alarmMode);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UI
|
||||||
|
|
||||||
|
// List of active user interfaces.
|
||||||
|
private HashSet<EntityUid> _activeUserInterfaces = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an active interface to be updated.
|
||||||
|
/// </summary>
|
||||||
|
public void AddActiveInterface(EntityUid uid) =>
|
||||||
|
_activeUserInterfaces.Add(uid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an active interface from the system update loop.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveActiveInterface(EntityUid uid) =>
|
||||||
|
_activeUserInterfaces.Remove(uid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force closes all interfaces currently open related to this air alarm.
|
||||||
|
/// </summary>
|
||||||
|
public void ForceCloseAllInterfaces(EntityUid uid) =>
|
||||||
|
_uiSystem.TryCloseAll(uid, SharedAirAlarmInterfaceKey.Key);
|
||||||
|
|
||||||
|
private void SendAddress(EntityUid uid, DeviceNetworkComponent? netConn = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref netConn)) return;
|
||||||
|
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmSetAddressMessage(netConn.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update an interface's air data. This is all the 'hot' data
|
||||||
|
/// that an air alarm contains server-side. Updated with a whopping 8
|
||||||
|
/// delay automatically once a UI is in the loop.
|
||||||
|
/// </summary>
|
||||||
|
public void SendAirData(EntityUid uid, AirAlarmComponent? alarm = null, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref alarm, ref monitor, ref power)) return;
|
||||||
|
|
||||||
|
if (!power.Powered) return;
|
||||||
|
|
||||||
|
|
||||||
|
if (monitor.TileGas != null)
|
||||||
|
{
|
||||||
|
var gases = new Dictionary<Gas, float>();
|
||||||
|
|
||||||
|
foreach (var gas in Enum.GetValues<Gas>())
|
||||||
|
gases.Add(gas, monitor.TileGas.GetMoles(gas));
|
||||||
|
|
||||||
|
var airData = new AirAlarmAirData(monitor.TileGas.Pressure, monitor.TileGas.Temperature, monitor.TileGas.TotalMoles, monitor.LastAlarmState, gases);
|
||||||
|
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAirDataMessage(airData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send an air alarm mode to any open interface related to an air alarm.
|
||||||
|
/// </summary>
|
||||||
|
public void SendAlarmMode(EntityUid uid, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null, AirAlarmComponent? controller = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor, ref power, ref controller)
|
||||||
|
|| !power.Powered) return;
|
||||||
|
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmModeMessage(controller.CurrentMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send all thresholds to any open interface related to a given air alarm.
|
||||||
|
/// </summary>
|
||||||
|
public void SendThresholds(EntityUid uid, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null, AirAlarmComponent? controller = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor, ref power, ref controller)
|
||||||
|
|| !power.Powered) return;
|
||||||
|
|
||||||
|
if (monitor.PressureThreshold == null
|
||||||
|
&& monitor.TemperatureThreshold == null
|
||||||
|
&& monitor.GasThresholds == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (monitor.PressureThreshold != null)
|
||||||
|
{
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Pressure, monitor.PressureThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.TemperatureThreshold != null)
|
||||||
|
{
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Temperature, monitor.TemperatureThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.GasThresholds != null)
|
||||||
|
{
|
||||||
|
foreach (var (gas, threshold) in monitor.GasThresholds)
|
||||||
|
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Gas, threshold, gas));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAtmosUpdate(EntityUid uid, AirAlarmComponent alarm, AtmosDeviceUpdateEvent args)
|
||||||
|
{
|
||||||
|
if (alarm.CurrentModeUpdater != null)
|
||||||
|
alarm.CurrentModeUpdater.Update(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float _delay = 8f;
|
||||||
|
private float _timer = 0f;
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
_timer += frameTime;
|
||||||
|
if (_timer >= _delay)
|
||||||
|
{
|
||||||
|
_timer = 0f;
|
||||||
|
foreach (var uid in _activeUserInterfaces)
|
||||||
|
{
|
||||||
|
SendAirData(uid);
|
||||||
|
_uiSystem.TrySetUiState(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUIState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.DeviceNetwork;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Systems
|
||||||
|
{
|
||||||
|
public class AtmosAlarmableSystem : EntitySystem
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<AtmosAlarmableComponent, PacketSentEvent>(OnPacketRecv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, PacketSentEvent args)
|
||||||
|
{
|
||||||
|
if (component.IgnoreAlarms) return;
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|
||||||
|
&& cmd == AtmosMonitorSystem.AtmosMonitorAlarmCmd)
|
||||||
|
{
|
||||||
|
// does it have a state & network max state?
|
||||||
|
// does it have a source?
|
||||||
|
// and can this be alarmed by the source?
|
||||||
|
// if so, raise an alarm
|
||||||
|
if (args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out AtmosMonitorAlarmType state)
|
||||||
|
&& args.Data.TryGetValue(AtmosMonitorSystem.AtmosMonitorAlarmNetMax, out AtmosMonitorAlarmType netMax)
|
||||||
|
&& args.Data.TryGetValue(AtmosMonitorSystem.AtmosMonitorAlarmSrc, out string? source)
|
||||||
|
&& component.AlarmedByPrototypes.Contains(source))
|
||||||
|
{
|
||||||
|
component.LastAlarmState = state;
|
||||||
|
component.HighestNetworkState = netMax;
|
||||||
|
RaiseLocalEvent(component.Owner, new AtmosMonitorAlarmEvent(state, netMax));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
488
Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Atmos.Piping.EntitySystems;
|
||||||
|
using Content.Server.Atmos.Piping.Components;
|
||||||
|
using Content.Server.DeviceNetwork;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Systems
|
||||||
|
{
|
||||||
|
// AtmosMonitorSystem. Grabs all the AtmosAlarmables connected
|
||||||
|
// to it via local APC net, and starts sending updates of the
|
||||||
|
// current atmosphere. Monitors fire (which always triggers as
|
||||||
|
// a danger), and atmos (which triggers based on set thresholds).
|
||||||
|
public class AtmosMonitorSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
|
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
|
||||||
|
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
/// <summary>
|
||||||
|
/// Command to alarm the network that something has happened.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmCmd = "atmos_monitor_alarm_update";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to sync this monitor's alarm state with the rest of the network.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmSyncCmd = "atmos_monitor_alarm_sync";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to reset all alarms on a network.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmResetAllCmd = "atmos_monitor_alarm_reset_all";
|
||||||
|
|
||||||
|
// Packet data
|
||||||
|
/// <summary>
|
||||||
|
/// Data response that contains the threshold types in an atmos monitor alarm.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmThresholdTypes = "atmos_monitor_alarm_threshold_types";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data response that contains the source of an atmos alarm.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmSrc = "atmos_monitor_alarm_source";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data response that contains the maximum alarm in an atmos alarm network.
|
||||||
|
/// </summary>
|
||||||
|
public const string AtmosMonitorAlarmNetMax = "atmos_monitor_alarm_net_max";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Frequency (all prototypes that use AtmosMonitor should use this)
|
||||||
|
/// </summary>
|
||||||
|
public const int AtmosMonitorApcFreq = 1621;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, ComponentInit>(OnAtmosMonitorInit);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, ComponentStartup>(OnAtmosMonitorStartup);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, ComponentShutdown>(OnAtmosMonitorShutdown);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, TileFireEvent>(OnFireEvent);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, PowerChangedEvent>(OnPowerChangedEvent);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, BeforePacketSentEvent>(BeforePacketRecv);
|
||||||
|
SubscribeLocalEvent<AtmosMonitorComponent, PacketSentEvent>(OnPacketRecv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosMonitorInit(EntityUid uid, AtmosMonitorComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
if (component.TemperatureThresholdId != null)
|
||||||
|
component.TemperatureThreshold = _prototypeManager.Index<AtmosAlarmThreshold>(component.TemperatureThresholdId);
|
||||||
|
|
||||||
|
if (component.PressureThresholdId != null)
|
||||||
|
component.PressureThreshold = _prototypeManager.Index<AtmosAlarmThreshold>(component.PressureThresholdId);
|
||||||
|
|
||||||
|
if (component.GasThresholdIds != null)
|
||||||
|
{
|
||||||
|
component.GasThresholds = new();
|
||||||
|
foreach (var (gas, id) in component.GasThresholdIds)
|
||||||
|
if (_prototypeManager.TryIndex<AtmosAlarmThreshold>(id, out var gasThreshold))
|
||||||
|
component.GasThresholds.Add(gas, gasThreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosMonitorStartup(EntityUid uid, AtmosMonitorComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
if (component.PowerRecvComponent == null
|
||||||
|
&& component.AtmosDeviceComponent != null)
|
||||||
|
{
|
||||||
|
_atmosDeviceSystem.LeaveAtmosphere(component.AtmosDeviceComponent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkPos.Add(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosMonitorShutdown(EntityUid uid, AtmosMonitorComponent component, ComponentShutdown args)
|
||||||
|
{
|
||||||
|
if (_checkPos.Contains(uid)) _checkPos.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hackiest shit ever but there's no PostStartup event
|
||||||
|
private HashSet<EntityUid> _checkPos = new();
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
foreach (var uid in _checkPos)
|
||||||
|
OpenAirOrReposition(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenAirOrReposition(EntityUid uid, AtmosMonitorComponent? component = null, AppearanceComponent? appearance = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component, ref appearance)) return;
|
||||||
|
|
||||||
|
var transform = Transform(component.Owner);
|
||||||
|
// atmos alarms will first attempt to get the air
|
||||||
|
// directly underneath it - if not, then it will
|
||||||
|
// instead place itself directly in front of the tile
|
||||||
|
// it is facing, and then visually shift itself back
|
||||||
|
// via sprite offsets (SS13 style but fuck it)
|
||||||
|
var coords = transform.Coordinates;
|
||||||
|
|
||||||
|
if (_atmosphereSystem.IsTileAirBlocked(coords))
|
||||||
|
{
|
||||||
|
|
||||||
|
var rotPos = transform.LocalRotation.RotateVec(new Vector2(0, -1));
|
||||||
|
transform.Anchored = false;
|
||||||
|
coords = coords.Offset(rotPos);
|
||||||
|
transform.Coordinates = coords;
|
||||||
|
|
||||||
|
appearance.SetData("offset", -rotPos);
|
||||||
|
|
||||||
|
transform.Anchored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GasMixture? air = _atmosphereSystem.GetTileMixture(coords);
|
||||||
|
component.TileGas = air;
|
||||||
|
|
||||||
|
_checkPos.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeforePacketRecv(EntityUid uid, AtmosMonitorComponent component, BeforePacketSentEvent args)
|
||||||
|
{
|
||||||
|
if (!component.NetEnabled) args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPacketRecv(EntityUid uid, AtmosMonitorComponent component, PacketSentEvent args)
|
||||||
|
{
|
||||||
|
// sync the internal 'last alarm state' from
|
||||||
|
// the other alarms, so that we can calculate
|
||||||
|
// the highest network alarm state at any time
|
||||||
|
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent? alarmable)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// ignore packets from self, ignore from different frequency
|
||||||
|
if (netConn.Address == args.SenderAddress) return;
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
// sync on alarm or explicit sync
|
||||||
|
case AtmosMonitorAlarmCmd:
|
||||||
|
case AtmosMonitorAlarmSyncCmd:
|
||||||
|
if (args.Data.TryGetValue(AtmosMonitorAlarmSrc, out string? src)
|
||||||
|
&& alarmable.AlarmedByPrototypes.Contains(src)
|
||||||
|
&& args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out AtmosMonitorAlarmType state)
|
||||||
|
&& !component.NetworkAlarmStates.TryAdd(args.SenderAddress, state))
|
||||||
|
component.NetworkAlarmStates[args.SenderAddress] = state;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorAlarmResetAllCmd:
|
||||||
|
if (args.Data.TryGetValue(AtmosMonitorAlarmSrc, out string? resetSrc)
|
||||||
|
&& alarmable.AlarmedByPrototypes.Contains(resetSrc))
|
||||||
|
{
|
||||||
|
component.LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||||
|
component.NetworkAlarmStates.Clear();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.DisplayMaxAlarmInNet)
|
||||||
|
{
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
|
||||||
|
appearanceComponent.SetData("alarmType", component.HighestAlarmInNetwork);
|
||||||
|
|
||||||
|
if (component.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPowerChangedEvent(EntityUid uid, AtmosMonitorComponent component, PowerChangedEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Powered)
|
||||||
|
{
|
||||||
|
if (component.AtmosDeviceComponent != null
|
||||||
|
&& component.AtmosDeviceComponent.JoinedGrid != null)
|
||||||
|
{
|
||||||
|
_atmosDeviceSystem.LeaveAtmosphere(component.AtmosDeviceComponent);
|
||||||
|
component.TileGas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear memory when power cycled
|
||||||
|
component.LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||||
|
component.NetworkAlarmStates.Clear();
|
||||||
|
}
|
||||||
|
else if (args.Powered)
|
||||||
|
{
|
||||||
|
if (component.AtmosDeviceComponent != null
|
||||||
|
&& component.AtmosDeviceComponent.JoinedGrid == null)
|
||||||
|
{
|
||||||
|
_atmosDeviceSystem.JoinAtmosphere(component.AtmosDeviceComponent);
|
||||||
|
var coords = Transform(component.Owner).Coordinates;
|
||||||
|
var air = _atmosphereSystem.GetTileMixture(coords);
|
||||||
|
component.TileGas = air;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData("powered", args.Powered);
|
||||||
|
appearanceComponent.SetData("alarmType", component.LastAlarmState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, TileFireEvent args)
|
||||||
|
{
|
||||||
|
if (component.PowerRecvComponent == null
|
||||||
|
|| !component.PowerRecvComponent.Powered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// if we're monitoring for atmos fire, then we make it similar to a smoke detector
|
||||||
|
// and just outright trigger a danger event
|
||||||
|
//
|
||||||
|
// somebody else can reset it :sunglasses:
|
||||||
|
if (component.MonitorFire
|
||||||
|
&& component.LastAlarmState != AtmosMonitorAlarmType.Danger)
|
||||||
|
Alert(uid, AtmosMonitorAlarmType.Danger, new []{ AtmosMonitorThresholdType.Temperature }, component); // technically???
|
||||||
|
|
||||||
|
// only monitor state elevation so that stuff gets alarmed quicker during a fire,
|
||||||
|
// let the atmos update loop handle when temperature starts to reach different
|
||||||
|
// thresholds and different states than normal -> warning -> danger
|
||||||
|
if (component.TemperatureThreshold != null
|
||||||
|
&& component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState)
|
||||||
|
&& temperatureState > component.LastAlarmState)
|
||||||
|
Alert(uid, AtmosMonitorAlarmType.Danger, new []{ AtmosMonitorThresholdType.Temperature }, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosUpdate(EntityUid uid, AtmosMonitorComponent component, AtmosDeviceUpdateEvent args)
|
||||||
|
{
|
||||||
|
if (component.PowerRecvComponent == null
|
||||||
|
|| !component.PowerRecvComponent.Powered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// can't hurt
|
||||||
|
// (in case something is making AtmosDeviceUpdateEvents
|
||||||
|
// outside the typical device loop)
|
||||||
|
if (component.AtmosDeviceComponent == null
|
||||||
|
|| component.AtmosDeviceComponent.JoinedGrid == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// if we're not monitoring atmos, don't bother
|
||||||
|
if (component.TemperatureThreshold == null
|
||||||
|
&& component.PressureThreshold == null
|
||||||
|
&& component.GasThresholds == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdateState(uid, component.TileGas, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checks the current air if it exceeds thresholds of
|
||||||
|
// any kind.
|
||||||
|
//
|
||||||
|
// If any threshold exceeds the other, that threshold
|
||||||
|
// immediately replaces the current recorded state.
|
||||||
|
//
|
||||||
|
// If the threshold does not match the current state
|
||||||
|
// of the monitor, it is set in the Alert call.
|
||||||
|
private void UpdateState(EntityUid uid, GasMixture? air, AtmosMonitorComponent? monitor = null)
|
||||||
|
{
|
||||||
|
if (air == null) return;
|
||||||
|
|
||||||
|
if (!Resolve(uid, ref monitor)) return;
|
||||||
|
|
||||||
|
AtmosMonitorAlarmType state = AtmosMonitorAlarmType.Normal;
|
||||||
|
List<AtmosMonitorThresholdType> alarmTypes = new();
|
||||||
|
|
||||||
|
if (monitor.TemperatureThreshold != null
|
||||||
|
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState)
|
||||||
|
&& temperatureState > state)
|
||||||
|
{
|
||||||
|
state = temperatureState;
|
||||||
|
alarmTypes.Add(AtmosMonitorThresholdType.Temperature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.PressureThreshold != null
|
||||||
|
&& monitor.PressureThreshold.CheckThreshold(air.Pressure, out var pressureState)
|
||||||
|
&& pressureState > state)
|
||||||
|
{
|
||||||
|
state = pressureState;
|
||||||
|
alarmTypes.Add(AtmosMonitorThresholdType.Pressure);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.GasThresholds != null)
|
||||||
|
{
|
||||||
|
foreach (var (gas, threshold) in monitor.GasThresholds)
|
||||||
|
{
|
||||||
|
var gasRatio = air.GetMoles(gas) / air.TotalMoles;
|
||||||
|
if (threshold.CheckThreshold(gasRatio, out var gasState)
|
||||||
|
&& gasState > state)
|
||||||
|
{
|
||||||
|
state = gasState;
|
||||||
|
alarmTypes.Add(AtmosMonitorThresholdType.Gas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the state of the current air doesn't match the last alarm state,
|
||||||
|
// we update the state
|
||||||
|
if (state != monitor.LastAlarmState)
|
||||||
|
{
|
||||||
|
Alert(uid, state, alarmTypes, monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alerts the network that the state of a monitor has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The alarm state to set this monitor to.</param>
|
||||||
|
/// <param name="alarms">The alarms that caused this alarm state.</param>
|
||||||
|
public void Alert(EntityUid uid, AtmosMonitorAlarmType state, IEnumerable<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor)) return;
|
||||||
|
monitor.LastAlarmState = state;
|
||||||
|
if (EntityManager.TryGetComponent(monitor.Owner, out AppearanceComponent? appearanceComponent))
|
||||||
|
appearanceComponent.SetData("alarmType", monitor.LastAlarmState);
|
||||||
|
|
||||||
|
BroadcastAlertPacket(monitor, alarms);
|
||||||
|
|
||||||
|
if (state == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, monitor);
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(monitor.Owner, out AtmosAlarmableComponent alarmable)
|
||||||
|
&& !alarmable.IgnoreAlarms)
|
||||||
|
RaiseLocalEvent(monitor.Owner, new AtmosMonitorAlarmEvent(monitor.LastAlarmState, monitor.HighestAlarmInNetwork));
|
||||||
|
// TODO: Central system that grabs *all* alarms from wired network
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayAlertSound(EntityUid uid, AtmosMonitorComponent? monitor = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor)) return;
|
||||||
|
|
||||||
|
SoundSystem.Play(Filter.Pvs(uid), monitor.AlarmSound.GetSound(), uid, AudioParams.Default.WithVolume(monitor.AlarmVolume));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets a single monitor's alarm.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset(EntityUid uid) =>
|
||||||
|
Alert(uid, AtmosMonitorAlarmType.Normal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets a network's alarms, using this monitor as a source.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The resulting packet will have this monitor set as the source, using its prototype ID if it has one - otherwise just sending an empty string.
|
||||||
|
/// </remarks>
|
||||||
|
public void ResetAll(EntityUid uid, AtmosMonitorComponent? monitor = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor)) return;
|
||||||
|
|
||||||
|
var prototype = Prototype(monitor.Owner);
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmResetAllCmd,
|
||||||
|
[AtmosMonitorAlarmSrc] = prototype != null ? prototype.ID : string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||||
|
monitor.NetworkAlarmStates.Clear();
|
||||||
|
|
||||||
|
Alert(uid, AtmosMonitorAlarmType.Normal, null, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (TODO: maybe just cache monitors in other monitors?)
|
||||||
|
/// <summary>
|
||||||
|
/// Syncs the current state of this monitor to the network (to avoid alerting other monitors).
|
||||||
|
/// </summary>
|
||||||
|
private void Sync(AtmosMonitorComponent monitor)
|
||||||
|
{
|
||||||
|
if (!monitor.NetEnabled) return;
|
||||||
|
|
||||||
|
var prototype = Prototype(monitor.Owner);
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmSyncCmd,
|
||||||
|
[DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
|
||||||
|
[AtmosMonitorAlarmSrc] = prototype != null ? prototype.ID : string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcasts an alert packet to all devices on the network,
|
||||||
|
/// which consists of the current alarm types,
|
||||||
|
/// the highest alarm currently cached by this monitor,
|
||||||
|
/// and the current alarm state of the monitor (so other
|
||||||
|
/// alarms can sync to it).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Alarmables use the highest alarm to ensure that a monitor's
|
||||||
|
/// state doesn't override if the alarm is lower. The state
|
||||||
|
/// is synced between monitors the moment a monitor sends out an alarm,
|
||||||
|
/// or if it is explicitly synced (see ResetAll/Sync).
|
||||||
|
/// </remarks>
|
||||||
|
private void BroadcastAlertPacket(AtmosMonitorComponent monitor, IEnumerable<AtmosMonitorThresholdType>? alarms = null)
|
||||||
|
{
|
||||||
|
if (!monitor.NetEnabled) return;
|
||||||
|
|
||||||
|
string source = string.Empty;
|
||||||
|
if (alarms == null) alarms = new List<AtmosMonitorThresholdType>();
|
||||||
|
var prototype = Prototype(monitor.Owner);
|
||||||
|
if (prototype != null) source = prototype.ID;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload
|
||||||
|
{
|
||||||
|
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmCmd,
|
||||||
|
[DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
|
||||||
|
[AtmosMonitorAlarmNetMax] = monitor.HighestAlarmInNetwork,
|
||||||
|
[AtmosMonitorAlarmThresholdTypes] = alarms,
|
||||||
|
[AtmosMonitorAlarmSrc] = source
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a monitor's threshold.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type of threshold to change.</param>
|
||||||
|
/// <param name="threshold">Threshold data.</param>
|
||||||
|
/// <param name="gas">Gas, if applicable.</param>
|
||||||
|
public void SetThreshold(EntityUid uid, AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null, AtmosMonitorComponent? monitor = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref monitor)) return;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdType.Pressure:
|
||||||
|
monitor.PressureThreshold = threshold;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdType.Temperature:
|
||||||
|
monitor.TemperatureThreshold = threshold;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdType.Gas:
|
||||||
|
if (gas == null || monitor.GasThresholds == null) return;
|
||||||
|
monitor.GasThresholds[(Gas) gas] = threshold;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AtmosMonitorAlarmEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public AtmosMonitorAlarmType Type { get; }
|
||||||
|
public AtmosMonitorAlarmType HighestNetworkType { get; }
|
||||||
|
|
||||||
|
public AtmosMonitorAlarmEvent(AtmosMonitorAlarmType type, AtmosMonitorAlarmType netMax)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
HighestNetworkType = netMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.Atmos.Monitor.Systems
|
||||||
|
{
|
||||||
|
public class FireAlarmSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly AtmosMonitorSystem _monitorSystem = default!;
|
||||||
|
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<FireAlarmComponent, InteractHandEvent>(OnInteractHand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInteractHand(EntityUid uid, FireAlarmComponent component, InteractHandEvent args)
|
||||||
|
{
|
||||||
|
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(args.User, out ActorComponent? actor)
|
||||||
|
&& EntityManager.TryGetComponent(uid, out AtmosMonitorComponent? monitor)
|
||||||
|
&& EntityManager.TryGetComponent(uid, out ApcPowerReceiverComponent? power)
|
||||||
|
&& power.Powered)
|
||||||
|
{
|
||||||
|
if (monitor.HighestAlarmInNetwork == AtmosMonitorAlarmType.Normal)
|
||||||
|
{
|
||||||
|
_monitorSystem.Alert(uid, AtmosMonitorAlarmType.Danger);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_monitorSystem.ResetAll(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Server.Atmos.Piping.Unary.Components;
|
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ using System;
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Atmos.Piping.Binary.Components;
|
using Content.Server.Atmos.Piping.Binary.Components;
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
using Content.Server.Atmos.Piping.Unary.Components;
|
|
||||||
using Content.Server.NodeContainer;
|
using Content.Server.NodeContainer;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Content.Shared.Atmos.Visuals;
|
using Content.Shared.Atmos.Visuals;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
@@ -14,6 +15,9 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public bool IsDirty { get; set; } = false;
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool Welded { get; set; } = false;
|
public bool Welded { get; set; } = false;
|
||||||
|
|
||||||
@@ -33,19 +37,30 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
|||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public float InternalPressureBound { get; set; } = 0f;
|
public float InternalPressureBound { get; set; } = 0f;
|
||||||
}
|
|
||||||
|
|
||||||
public enum VentPumpDirection : sbyte
|
public GasVentPumpData ToAirAlarmData()
|
||||||
{
|
{
|
||||||
Siphoning = 0,
|
if (!IsDirty) return new GasVentPumpData { Dirty = IsDirty };
|
||||||
Releasing = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
return new GasVentPumpData
|
||||||
public enum VentPressureBound : sbyte
|
{
|
||||||
{
|
Enabled = Enabled,
|
||||||
NoBound = 0,
|
Dirty = IsDirty,
|
||||||
InternalBound = 1,
|
PumpDirection = PumpDirection,
|
||||||
ExternalBound = 2,
|
PressureChecks = PressureChecks,
|
||||||
|
ExternalPressureBound = ExternalPressureBound,
|
||||||
|
InternalPressureBound = InternalPressureBound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FromAirAlarmData(GasVentPumpData data)
|
||||||
|
{
|
||||||
|
Enabled = data.Enabled;
|
||||||
|
IsDirty = data.Dirty;
|
||||||
|
PumpDirection = (VentPumpDirection) data.PumpDirection!;
|
||||||
|
PressureChecks = (VentPressureBound) data.PressureChecks!;
|
||||||
|
ExternalPressureBound = (float) data.ExternalPressureBound!;
|
||||||
|
InternalPressureBound = (float) data.InternalPressureBound!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
@@ -14,6 +16,9 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public bool IsDirty { get; set; } = false;
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool Welded { get; set; } = false;
|
public bool Welded { get; set; } = false;
|
||||||
|
|
||||||
@@ -22,13 +27,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
|||||||
public string OutletName { get; set; } = "pipe";
|
public string OutletName { get; set; } = "pipe";
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public readonly HashSet<Gas> FilterGases = new()
|
public readonly HashSet<Gas> FilterGases = GasVentScrubberData.DefaultFilterGases;
|
||||||
{
|
|
||||||
Gas.CarbonDioxide,
|
|
||||||
Gas.Plasma,
|
|
||||||
Gas.Tritium,
|
|
||||||
Gas.WaterVapor
|
|
||||||
};
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public ScrubberPumpDirection PumpDirection { get; set; } = ScrubberPumpDirection.Scrubbing;
|
public ScrubberPumpDirection PumpDirection { get; set; } = ScrubberPumpDirection.Scrubbing;
|
||||||
@@ -38,11 +37,36 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
|||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool WideNet { get; set; } = false;
|
public bool WideNet { get; set; } = false;
|
||||||
}
|
|
||||||
|
|
||||||
public enum ScrubberPumpDirection : sbyte
|
public GasVentScrubberData ToAirAlarmData()
|
||||||
{
|
{
|
||||||
Siphoning = 0,
|
if (!IsDirty) return new GasVentScrubberData { Dirty = IsDirty };
|
||||||
Scrubbing = 1,
|
|
||||||
|
return new GasVentScrubberData
|
||||||
|
{
|
||||||
|
Enabled = Enabled,
|
||||||
|
Dirty = IsDirty,
|
||||||
|
FilterGases = FilterGases,
|
||||||
|
PumpDirection = PumpDirection,
|
||||||
|
VolumeRate = VolumeRate,
|
||||||
|
WideNet = WideNet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FromAirAlarmData(GasVentScrubberData data)
|
||||||
|
{
|
||||||
|
Enabled = data.Enabled;
|
||||||
|
IsDirty = data.Dirty;
|
||||||
|
PumpDirection = (ScrubberPumpDirection) data.PumpDirection!;
|
||||||
|
VolumeRate = (float) data.VolumeRate!;
|
||||||
|
WideNet = data.WideNet;
|
||||||
|
|
||||||
|
if (!data.FilterGases!.SequenceEqual(FilterGases))
|
||||||
|
{
|
||||||
|
FilterGases.Clear();
|
||||||
|
foreach (var gas in data.FilterGases!)
|
||||||
|
FilterGases.Add(gas);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
using Content.Server.Atmos.Piping.Unary.Components;
|
using Content.Server.Atmos.Piping.Unary.Components;
|
||||||
|
using Content.Server.DeviceNetwork;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
using Content.Server.NodeContainer;
|
using Content.Server.NodeContainer;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Content.Shared.Atmos.Visuals;
|
using Content.Shared.Atmos.Visuals;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -16,6 +25,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
public class GasVentPumpSystem : EntitySystem
|
public class GasVentPumpSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
|
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -23,6 +33,9 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
|
|
||||||
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceUpdateEvent>(OnGasVentPumpUpdated);
|
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceUpdateEvent>(OnGasVentPumpUpdated);
|
||||||
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceDisabledEvent>(OnGasVentPumpLeaveAtmosphere);
|
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceDisabledEvent>(OnGasVentPumpLeaveAtmosphere);
|
||||||
|
SubscribeLocalEvent<GasVentPumpComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||||
|
SubscribeLocalEvent<GasVentPumpComponent, PowerChangedEvent>(OnPowerChanged);
|
||||||
|
SubscribeLocalEvent<GasVentPumpComponent, PacketSentEvent>(OnPacketRecv);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGasVentPumpUpdated(EntityUid uid, GasVentPumpComponent vent, AtmosDeviceUpdateEvent args)
|
private void OnGasVentPumpUpdated(EntityUid uid, GasVentPumpComponent vent, AtmosDeviceUpdateEvent args)
|
||||||
@@ -99,5 +112,56 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
appearance.SetData(VentPumpVisuals.State, VentPumpState.Off);
|
appearance.SetData(VentPumpVisuals.State, VentPumpState.Off);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAtmosAlarm(EntityUid uid, GasVentPumpComponent component, AtmosMonitorAlarmEvent args)
|
||||||
|
{
|
||||||
|
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||||
|
{
|
||||||
|
component.Enabled = false;
|
||||||
|
}
|
||||||
|
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||||
|
{
|
||||||
|
component.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPowerChanged(EntityUid uid, GasVentPumpComponent component, PowerChangedEvent args)
|
||||||
|
{
|
||||||
|
component.Enabled = args.Powered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPacketRecv(EntityUid uid, GasVentPumpComponent component, PacketSentEvent args)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent alarmable)
|
||||||
|
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload();
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case AirAlarmSystem.AirAlarmSyncCmd:
|
||||||
|
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSyncData);
|
||||||
|
payload.Add(AirAlarmSystem.AirAlarmSyncData, component.ToAirAlarmData());
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(uid, args.SenderAddress, AirAlarmSystem.Freq, payload);
|
||||||
|
|
||||||
|
return;
|
||||||
|
case AirAlarmSystem.AirAlarmSetData:
|
||||||
|
if (!args.Data.TryGetValue(AirAlarmSystem.AirAlarmSetData, out GasVentPumpData? setData))
|
||||||
|
break;
|
||||||
|
|
||||||
|
component.FromAirAlarmData(setData);
|
||||||
|
alarmable.IgnoreAlarms = setData.IgnoreAlarms;
|
||||||
|
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSetDataStatus);
|
||||||
|
payload.Add(AirAlarmSystem.AirAlarmSetDataStatus, true);
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(uid, string.Empty, AirAlarmSystem.Freq, payload, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
using Content.Server.Atmos.Piping.Unary.Components;
|
using Content.Server.Atmos.Piping.Unary.Components;
|
||||||
|
using Content.Server.DeviceNetwork;
|
||||||
|
using Content.Server.DeviceNetwork.Components;
|
||||||
|
using Content.Server.DeviceNetwork.Systems;
|
||||||
using Content.Server.NodeContainer;
|
using Content.Server.NodeContainer;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
|
using Content.Server.Power.Components;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping.Unary.Visuals;
|
using Content.Shared.Atmos.Piping.Unary.Visuals;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
@@ -17,6 +25,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
public class GasVentScrubberSystem : EntitySystem
|
public class GasVentScrubberSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
|
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -24,6 +33,10 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
|
|
||||||
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceUpdateEvent>(OnVentScrubberUpdated);
|
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceUpdateEvent>(OnVentScrubberUpdated);
|
||||||
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceDisabledEvent>(OnVentScrubberLeaveAtmosphere);
|
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceDisabledEvent>(OnVentScrubberLeaveAtmosphere);
|
||||||
|
SubscribeLocalEvent<GasVentScrubberComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||||
|
SubscribeLocalEvent<GasVentScrubberComponent, PowerChangedEvent>(OnPowerChanged);
|
||||||
|
SubscribeLocalEvent<GasVentScrubberComponent, PacketSentEvent>(OnPacketRecv);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnVentScrubberUpdated(EntityUid uid, GasVentScrubberComponent scrubber, AtmosDeviceUpdateEvent args)
|
private void OnVentScrubberUpdated(EntityUid uid, GasVentScrubberComponent scrubber, AtmosDeviceUpdateEvent args)
|
||||||
@@ -102,5 +115,53 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
outlet.AssumeAir(removed);
|
outlet.AssumeAir(removed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAtmosAlarm(EntityUid uid, GasVentScrubberComponent component, AtmosMonitorAlarmEvent args)
|
||||||
|
{
|
||||||
|
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||||
|
{
|
||||||
|
component.Enabled = false;
|
||||||
|
}
|
||||||
|
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||||
|
{
|
||||||
|
component.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPowerChanged(EntityUid uid, GasVentScrubberComponent component, PowerChangedEvent args) =>
|
||||||
|
component.Enabled = args.Powered;
|
||||||
|
|
||||||
|
private void OnPacketRecv(EntityUid uid, GasVentScrubberComponent component, PacketSentEvent args)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)
|
||||||
|
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent alarmable)
|
||||||
|
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = new NetworkPayload();
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case AirAlarmSystem.AirAlarmSyncCmd:
|
||||||
|
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSyncData);
|
||||||
|
payload.Add(AirAlarmSystem.AirAlarmSyncData, component.ToAirAlarmData());
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(uid, args.SenderAddress, AirAlarmSystem.Freq, payload);
|
||||||
|
|
||||||
|
return;
|
||||||
|
case AirAlarmSystem.AirAlarmSetData:
|
||||||
|
if (!args.Data.TryGetValue(AirAlarmSystem.AirAlarmSetData, out GasVentScrubberData? setData))
|
||||||
|
break;
|
||||||
|
|
||||||
|
component.FromAirAlarmData(setData);
|
||||||
|
alarmable.IgnoreAlarms = setData.IgnoreAlarms;
|
||||||
|
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSetDataStatus);
|
||||||
|
payload.Add(AirAlarmSystem.AirAlarmSetDataStatus, true);
|
||||||
|
|
||||||
|
_deviceNetSystem.QueuePacket(uid, string.Empty, AirAlarmSystem.Freq, payload, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using Content.Server.Doors.Components;
|
using Content.Server.Atmos.Monitor.Components;
|
||||||
|
using Content.Server.Atmos.Monitor.Systems;
|
||||||
|
using Content.Server.Doors.Components;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
using Content.Shared.Doors;
|
using Content.Shared.Doors;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
|
||||||
namespace Content.Server.Doors.Systems
|
namespace Content.Server.Doors.Systems
|
||||||
{
|
{
|
||||||
@@ -18,6 +22,7 @@ namespace Content.Server.Doors.Systems
|
|||||||
SubscribeLocalEvent<FirelockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
|
SubscribeLocalEvent<FirelockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
|
||||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry);
|
SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry);
|
||||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
|
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
|
||||||
|
SubscribeLocalEvent<FirelockComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args)
|
private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args)
|
||||||
@@ -62,8 +67,31 @@ namespace Content.Server.Doors.Systems
|
|||||||
|
|
||||||
private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args)
|
private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args)
|
||||||
{
|
{
|
||||||
// Firelocks can't autoclose, they must be manually closed
|
// Make firelocks autoclose, but only if the last alarm type it
|
||||||
args.Cancel();
|
// remembers was a danger. This is to prevent people from
|
||||||
|
// flooding hallways with endless bad air/fire.
|
||||||
|
if (!EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent alarmable))
|
||||||
|
{
|
||||||
|
args.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (alarmable.HighestNetworkState != AtmosMonitorAlarmType.Danger)
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosMonitorAlarmEvent args)
|
||||||
|
{
|
||||||
|
if (component.DoorComponent == null) return;
|
||||||
|
|
||||||
|
if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||||
|
{
|
||||||
|
if (component.DoorComponent.State == SharedDoorComponent.DoorState.Closed)
|
||||||
|
component.DoorComponent.Open();
|
||||||
|
}
|
||||||
|
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||||
|
{
|
||||||
|
component.EmergencyPressureStop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
257
Content.Shared/Atmos/Monitor/AtmosAlarmThresholdPrototype.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Monitor
|
||||||
|
{
|
||||||
|
// mostly based around floats and percentages, no literals
|
||||||
|
// except for the range boundaries
|
||||||
|
[Prototype("alarmThreshold")]
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AtmosAlarmThreshold : IPrototype, ISerializationHooks
|
||||||
|
{
|
||||||
|
[DataField("id", required: true)]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("ignore")]
|
||||||
|
public bool Ignore = false;
|
||||||
|
|
||||||
|
// zero bounds are not allowed - just
|
||||||
|
// set the bound to null if you want
|
||||||
|
// to disable it
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("upperBound")]
|
||||||
|
public float? UpperBound { get; private set; }
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("lowerBound")]
|
||||||
|
public float? LowerBound { get; private set; }
|
||||||
|
|
||||||
|
// upper warning percentage
|
||||||
|
// must always cause UpperWarningBound
|
||||||
|
// to be smaller
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("upperWarnAround")]
|
||||||
|
public float? UpperWarningPercentage { get; private set; }
|
||||||
|
|
||||||
|
// lower warning percentage
|
||||||
|
// must always cause LowerWarningBound
|
||||||
|
// to be larger
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("lowerWarnAround")]
|
||||||
|
public float? LowerWarningPercentage { get; private set; }
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public float? UpperWarningBound
|
||||||
|
{
|
||||||
|
get => CalculateWarningBound(AtmosMonitorThresholdBound.Upper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public float? LowerWarningBound
|
||||||
|
{
|
||||||
|
get => CalculateWarningBound(AtmosMonitorThresholdBound.Lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ISerializationHooks.AfterDeserialization()
|
||||||
|
{
|
||||||
|
if (UpperBound <= LowerBound)
|
||||||
|
UpperBound = null;
|
||||||
|
|
||||||
|
if (LowerBound >= UpperBound)
|
||||||
|
LowerBound = null;
|
||||||
|
|
||||||
|
if (UpperWarningPercentage != null)
|
||||||
|
TrySetWarningBound(AtmosMonitorThresholdBound.Upper, UpperBound * UpperWarningPercentage);
|
||||||
|
|
||||||
|
if (LowerWarningPercentage != null)
|
||||||
|
TrySetWarningBound(AtmosMonitorThresholdBound.Lower, LowerBound * LowerWarningPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to check a threshold against some calculated value
|
||||||
|
public bool CheckThreshold(float value, out AtmosMonitorAlarmType state)
|
||||||
|
{
|
||||||
|
state = AtmosMonitorAlarmType.Normal;
|
||||||
|
if (Ignore) return false;
|
||||||
|
|
||||||
|
if (value >= UpperBound || value <= LowerBound)
|
||||||
|
{
|
||||||
|
state = AtmosMonitorAlarmType.Danger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value >= UpperWarningBound || value <= LowerWarningBound)
|
||||||
|
{
|
||||||
|
state = AtmosMonitorAlarmType.Warning;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the primary bound, takes a hard value
|
||||||
|
public bool TrySetPrimaryBound(AtmosMonitorThresholdBound bound, float? input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
UpperBound = null;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
LowerBound = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float value = (float) input;
|
||||||
|
|
||||||
|
if (value <= 0f || float.IsNaN(value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
(float target, int compare)? targetValue = null;
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
if (float.IsPositiveInfinity(value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (LowerBound != null)
|
||||||
|
targetValue = ((float) LowerBound, -1);
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
if (float.IsNegativeInfinity(value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (UpperBound != null)
|
||||||
|
targetValue = ((float) UpperBound, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid = true;
|
||||||
|
if (targetValue != null)
|
||||||
|
{
|
||||||
|
var result = targetValue.Value.target.CompareTo(value);
|
||||||
|
isValid = targetValue.Value.compare == result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
UpperBound = value;
|
||||||
|
return true;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
LowerBound = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the warning bound, takes a hard value
|
||||||
|
//
|
||||||
|
// this will always set the percentage and
|
||||||
|
// the raw value at the same time
|
||||||
|
public bool TrySetWarningBound(AtmosMonitorThresholdBound bound, float? input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
UpperWarningPercentage = null;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
LowerWarningPercentage = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
if (UpperBound == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float upperWarning = (float) (input / UpperBound);
|
||||||
|
float upperTestValue = (upperWarning * (float) UpperBound);
|
||||||
|
|
||||||
|
if (upperWarning > 1f
|
||||||
|
|| upperTestValue < LowerWarningBound
|
||||||
|
|| upperTestValue < LowerBound)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
UpperWarningPercentage = upperWarning;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
if (LowerBound == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float lowerWarning = (float) (input / LowerBound);
|
||||||
|
float testValue = (lowerWarning * (float) LowerBound);
|
||||||
|
|
||||||
|
if (lowerWarning < 1f
|
||||||
|
|| testValue > UpperWarningBound
|
||||||
|
|| testValue > UpperBound)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
LowerWarningPercentage = lowerWarning;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float? CalculateWarningBound(AtmosMonitorThresholdBound bound)
|
||||||
|
{
|
||||||
|
float? value = null;
|
||||||
|
|
||||||
|
switch (bound)
|
||||||
|
{
|
||||||
|
case AtmosMonitorThresholdBound.Upper:
|
||||||
|
if (UpperBound == null || UpperWarningPercentage == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
value = UpperBound * UpperWarningPercentage;
|
||||||
|
break;
|
||||||
|
case AtmosMonitorThresholdBound.Lower:
|
||||||
|
if (LowerBound == null || LowerWarningPercentage == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
value = LowerBound * LowerWarningPercentage;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AtmosMonitorThresholdBound
|
||||||
|
{
|
||||||
|
Upper,
|
||||||
|
Lower
|
||||||
|
}
|
||||||
|
|
||||||
|
// not really used in the prototype but in code,
|
||||||
|
// to differentiate between the different
|
||||||
|
// fields you can find this prototype in
|
||||||
|
public enum AtmosMonitorThresholdType
|
||||||
|
{
|
||||||
|
Temperature,
|
||||||
|
Pressure,
|
||||||
|
Gas
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Content.Shared/Atmos/Monitor/AtmosMonitorAlarmType.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Monitor
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum AtmosMonitorAlarmType : sbyte
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Danger = 2 // 1 << 1 is the exact same thing and we're not really doing **bitmasking** are we?
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum SharedAirAlarmInterfaceKey
|
||||||
|
{
|
||||||
|
Key
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum AirAlarmMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Filtering,
|
||||||
|
Fill,
|
||||||
|
Panic,
|
||||||
|
Replace
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum AirAlarmWireStatus
|
||||||
|
{
|
||||||
|
Power,
|
||||||
|
Access,
|
||||||
|
Panic,
|
||||||
|
DeviceSync
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public readonly struct AirAlarmAirData
|
||||||
|
{
|
||||||
|
public readonly float? Pressure { get; }
|
||||||
|
public readonly float? Temperature { get; }
|
||||||
|
public readonly float? TotalMoles { get; }
|
||||||
|
public readonly AtmosMonitorAlarmType AlarmState { get; }
|
||||||
|
|
||||||
|
private readonly Dictionary<Gas, float>? _gases;
|
||||||
|
public readonly IReadOnlyDictionary<Gas, float>? Gases { get => _gases; }
|
||||||
|
|
||||||
|
public AirAlarmAirData(float? pressure, float? temperature, float? moles, AtmosMonitorAlarmType state, Dictionary<Gas, float>? gases)
|
||||||
|
{
|
||||||
|
Pressure = pressure;
|
||||||
|
Temperature = temperature;
|
||||||
|
TotalMoles = moles;
|
||||||
|
AlarmState = state;
|
||||||
|
_gases = gases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAtmosDeviceData
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public bool Dirty { get; set; }
|
||||||
|
public bool IgnoreAlarms { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// would be nice to include the entire state here
|
||||||
|
// but it's already handled by messages
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmUIState : BoundUserInterfaceState
|
||||||
|
{}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmResyncAllDevicesMessage : BoundUserInterfaceMessage
|
||||||
|
{}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmSetAddressMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public string Address { get; }
|
||||||
|
|
||||||
|
public AirAlarmSetAddressMessage(string address)
|
||||||
|
{
|
||||||
|
Address = address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmUpdateAirDataMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public AirAlarmAirData AirData;
|
||||||
|
|
||||||
|
public AirAlarmUpdateAirDataMessage(AirAlarmAirData airData)
|
||||||
|
{
|
||||||
|
AirData = airData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmUpdateAlarmModeMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public AirAlarmMode Mode { get; }
|
||||||
|
|
||||||
|
public AirAlarmUpdateAlarmModeMessage(AirAlarmMode mode)
|
||||||
|
{
|
||||||
|
Mode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmUpdateDeviceDataMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public string Address { get; }
|
||||||
|
public IAtmosDeviceData Data { get; }
|
||||||
|
|
||||||
|
public AirAlarmUpdateDeviceDataMessage(string addr, IAtmosDeviceData data)
|
||||||
|
{
|
||||||
|
Address = addr;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class AirAlarmUpdateAlarmThresholdMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public AtmosAlarmThreshold Threshold { get; }
|
||||||
|
public AtmosMonitorThresholdType Type { get; }
|
||||||
|
public Gas? Gas { get; }
|
||||||
|
|
||||||
|
public AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null)
|
||||||
|
{
|
||||||
|
Threshold = threshold;
|
||||||
|
Type = type;
|
||||||
|
Gas = gas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Monitor.Components
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum FireAlarmWireStatus
|
||||||
|
{
|
||||||
|
Power,
|
||||||
|
Alarm
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Piping.Unary.Components
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class GasVentPumpData : IAtmosDeviceData
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public bool Dirty { get; set; }
|
||||||
|
public bool IgnoreAlarms { get; set; } = false;
|
||||||
|
public VentPumpDirection? PumpDirection { get; set; }
|
||||||
|
public VentPressureBound? PressureChecks { get; set; }
|
||||||
|
public float? ExternalPressureBound { get; set; }
|
||||||
|
public float? InternalPressureBound { get; set; }
|
||||||
|
|
||||||
|
// Presets for 'dumb' air alarm modes
|
||||||
|
|
||||||
|
public static GasVentPumpData FilterModePreset = new GasVentPumpData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
PumpDirection = VentPumpDirection.Releasing,
|
||||||
|
PressureChecks = VentPressureBound.ExternalBound,
|
||||||
|
ExternalPressureBound = Atmospherics.OneAtmosphere,
|
||||||
|
InternalPressureBound = 0f
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentPumpData FillModePreset = new GasVentPumpData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Dirty = true,
|
||||||
|
PumpDirection = VentPumpDirection.Releasing,
|
||||||
|
PressureChecks = VentPressureBound.ExternalBound,
|
||||||
|
ExternalPressureBound = Atmospherics.OneAtmosphere * 50,
|
||||||
|
InternalPressureBound = 0f
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentPumpData PanicModePreset = new GasVentPumpData
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Dirty = true,
|
||||||
|
PumpDirection = VentPumpDirection.Releasing,
|
||||||
|
PressureChecks = VentPressureBound.ExternalBound,
|
||||||
|
ExternalPressureBound = Atmospherics.OneAtmosphere,
|
||||||
|
InternalPressureBound = 0f
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentPumpData Default()
|
||||||
|
{
|
||||||
|
return new GasVentPumpData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
PumpDirection = VentPumpDirection.Releasing,
|
||||||
|
PressureChecks = VentPressureBound.ExternalBound,
|
||||||
|
ExternalPressureBound = Atmospherics.OneAtmosphere,
|
||||||
|
InternalPressureBound = 0f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum VentPumpDirection : sbyte
|
||||||
|
{
|
||||||
|
Siphoning = 0,
|
||||||
|
Releasing = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum VentPressureBound : sbyte
|
||||||
|
{
|
||||||
|
NoBound = 0,
|
||||||
|
InternalBound = 1,
|
||||||
|
ExternalBound = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Shared.Atmos.Monitor.Components;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Atmos.Piping.Unary.Components
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class GasVentScrubberData : IAtmosDeviceData
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public bool Dirty { get; set; }
|
||||||
|
public bool IgnoreAlarms { get; set; } = false;
|
||||||
|
public HashSet<Gas>? FilterGases { get; set; }
|
||||||
|
public ScrubberPumpDirection? PumpDirection { get; set; }
|
||||||
|
public float? VolumeRate { get; set; }
|
||||||
|
public bool WideNet { get; set; }
|
||||||
|
|
||||||
|
public static HashSet<Gas> DefaultFilterGases = new()
|
||||||
|
{
|
||||||
|
Gas.CarbonDioxide,
|
||||||
|
Gas.Plasma,
|
||||||
|
Gas.Tritium,
|
||||||
|
Gas.WaterVapor,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Presets for 'dumb' air alarm modes
|
||||||
|
|
||||||
|
public static GasVentScrubberData FilterModePreset = new GasVentScrubberData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
FilterGases = GasVentScrubberData.DefaultFilterGases,
|
||||||
|
PumpDirection = ScrubberPumpDirection.Scrubbing,
|
||||||
|
VolumeRate = 200f,
|
||||||
|
WideNet = false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentScrubberData FillModePreset = new GasVentScrubberData
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Dirty = true,
|
||||||
|
FilterGases = GasVentScrubberData.DefaultFilterGases,
|
||||||
|
PumpDirection = ScrubberPumpDirection.Scrubbing,
|
||||||
|
VolumeRate = 200f,
|
||||||
|
WideNet = false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentScrubberData PanicModePreset = new GasVentScrubberData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Dirty = true,
|
||||||
|
FilterGases = GasVentScrubberData.DefaultFilterGases,
|
||||||
|
PumpDirection = ScrubberPumpDirection.Siphoning,
|
||||||
|
VolumeRate = 200f,
|
||||||
|
WideNet = false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static GasVentScrubberData Default()
|
||||||
|
{
|
||||||
|
return new GasVentScrubberData
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
FilterGases = GasVentScrubberData.DefaultFilterGases,
|
||||||
|
PumpDirection = ScrubberPumpDirection.Scrubbing,
|
||||||
|
VolumeRate = 200f,
|
||||||
|
WideNet = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum ScrubberPumpDirection : sbyte
|
||||||
|
{
|
||||||
|
Siphoning = 0,
|
||||||
|
Scrubbing = 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Resources/Locale/en-US/atmos/air-alarm-ui.ftl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# UI
|
||||||
|
|
||||||
|
## Window
|
||||||
|
|
||||||
|
air-alarm-ui-access-denied = Insufficient access!
|
||||||
|
|
||||||
|
air-alarm-ui-window-pressure-label = Pressure
|
||||||
|
air-alarm-ui-window-temperature-label = Temperature
|
||||||
|
air-alarm-ui-window-alarm-state-label = Status
|
||||||
|
|
||||||
|
air-alarm-ui-window-address-label = Address
|
||||||
|
air-alarm-ui-window-device-count-label = Total Devices
|
||||||
|
air-alarm-ui-window-resync-devices-label = Resync
|
||||||
|
|
||||||
|
air-alarm-ui-window-mode-label = Mode
|
||||||
|
|
||||||
|
air-alarm-ui-window-pressure = {$pressure} kPa
|
||||||
|
air-alarm-ui-window-temperature = {$tempC} C ({$temperature} K)
|
||||||
|
air-alarm-ui-window-alarm-state = {$state}
|
||||||
|
|
||||||
|
air-alarm-ui-window-tab-gas = Gases
|
||||||
|
air-alarm-ui-window-tab-vents = Vents
|
||||||
|
air-alarm-ui-window-tab-scrubbers = Scrubbers
|
||||||
|
air-alarm-ui-window-tab-thresholds = Thresholds
|
||||||
|
|
||||||
|
air-alarm-ui-gases = {$gas}: {$amount} mol ({$percentage}%)
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
air-alarm-ui-widget-enable = Enabled
|
||||||
|
air-alarm-ui-widget-ignore = Ignore
|
||||||
|
air-alarm-ui-atmos-net-device-label = Address: {$address}
|
||||||
|
|
||||||
|
### Vent pumps
|
||||||
|
|
||||||
|
air-alarm-ui-vent-pump-label = Vent direction
|
||||||
|
air-alarm-ui-vent-pressure-label = Pressure bound
|
||||||
|
air-alarm-ui-vent-external-bound-label = External bound
|
||||||
|
air-alarm-ui-vent-internal-bound-label = Internal bound
|
||||||
|
|
||||||
|
### Scrubbers
|
||||||
|
|
||||||
|
air-alarm-ui-scrubber-pump-direction-label = Direction
|
||||||
|
air-alarm-ui-scrubber-volume-rate-label = Rate (L)
|
||||||
|
air-alarm-ui-scrubber-wide-net-label = WideNet
|
||||||
|
|
||||||
|
### Thresholds
|
||||||
|
|
||||||
|
air-alarm-ui-thresholds-pressure-title = Pressure (kPa)
|
||||||
|
air-alarm-ui-thresholds-temperature-title = Temperature (K)
|
||||||
|
air-alarm-ui-thresholds-gas-title = {$gas} (%)
|
||||||
|
air-alarm-ui-thresholds-upper-bound = Upper bound
|
||||||
|
air-alarm-ui-thresholds-lower-bound = Lower bound
|
||||||
|
air-alarm-ui-thresholds-upper-warning-bound = Upper warning bound
|
||||||
|
air-alarm-ui-thresholds-lower-warning-bound = Lower warning bound
|
||||||
31
Resources/Prototypes/Atmospherics/thresholds.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
- type: alarmThreshold
|
||||||
|
id: stationTemperature
|
||||||
|
upperBound: 1465.75 # T20C * 5
|
||||||
|
lowerBound: 193.15 # T20C - 100
|
||||||
|
upperWarnAround: 0.25
|
||||||
|
lowerWarnAround: 1.1
|
||||||
|
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: stationPressure
|
||||||
|
upperBound: 550 # as defined in Atmospherics.cs
|
||||||
|
lowerBound: 20 # as defined in Atmospherics.cs
|
||||||
|
upperWarnAround: 0.7
|
||||||
|
lowerWarnAround: 2.5
|
||||||
|
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: stationOxygen
|
||||||
|
lowerBound: 0.0010
|
||||||
|
lowerWarnAround: 1.5
|
||||||
|
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: stationCO2
|
||||||
|
upperBound: 0.0025
|
||||||
|
upperWarnAround: 0.5
|
||||||
|
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: ignore # just ignore nitrogen??? ??? ???
|
||||||
|
ignore: true
|
||||||
|
|
||||||
|
- type: alarmThreshold
|
||||||
|
id: danger # just any gas you don't want at all
|
||||||
|
upperBound: 0.0001
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
- type: entity
|
||||||
|
id: AirAlarmElectronics
|
||||||
|
parent: BaseItem
|
||||||
|
name: air alarm electronics
|
||||||
|
components:
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- AirAlarmElectronics
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Misc/module.rsi
|
||||||
|
state: door_electronics
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: FireAlarmElectronics
|
||||||
|
parent: BaseItem
|
||||||
|
name: fire alarm electronics
|
||||||
|
components:
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- FireAlarmElectronics
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Misc/module.rsi
|
||||||
|
state: door_electronics
|
||||||
@@ -4,6 +4,16 @@
|
|||||||
name: firelock
|
name: firelock
|
||||||
description: Apply crowbar.
|
description: Apply crowbar.
|
||||||
components:
|
components:
|
||||||
|
- type: AtmosAlarmable
|
||||||
|
alarmedBy:
|
||||||
|
- FireAlarm
|
||||||
|
- AirAlarm
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
- type: ExtensionCableReceiver
|
||||||
|
- type: DeviceNetworkComponent
|
||||||
|
deviceNetId: Apc
|
||||||
|
frequency: 1621
|
||||||
|
- type: ApcNetworkConnection
|
||||||
- type: InteractionOutline
|
- type: InteractionOutline
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageContainer: Inorganic
|
damageContainer: Inorganic
|
||||||
|
|||||||
@@ -21,6 +21,15 @@
|
|||||||
placement:
|
placement:
|
||||||
mode: SnapgridCenter
|
mode: SnapgridCenter
|
||||||
components:
|
components:
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
- type: ExtensionCableReceiver
|
||||||
|
- type: DeviceNetworkComponent
|
||||||
|
deviceNetId: Apc
|
||||||
|
frequency: 1621
|
||||||
|
- type: ApcNetworkConnection
|
||||||
|
- type: AtmosAlarmable
|
||||||
|
alarmedBy:
|
||||||
|
- AirAlarm
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
netsync: false
|
netsync: false
|
||||||
drawdepth: FloorObjects
|
drawdepth: FloorObjects
|
||||||
@@ -73,6 +82,15 @@
|
|||||||
placement:
|
placement:
|
||||||
mode: SnapgridCenter
|
mode: SnapgridCenter
|
||||||
components:
|
components:
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
- type: ExtensionCableReceiver
|
||||||
|
- type: DeviceNetworkComponent
|
||||||
|
deviceNetId: Apc
|
||||||
|
frequency: 1621
|
||||||
|
- type: ApcNetworkConnection
|
||||||
|
- type: AtmosAlarmable
|
||||||
|
alarmedBy:
|
||||||
|
- AirAlarm
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
netsync: false
|
netsync: false
|
||||||
drawdepth: FloorObjects
|
drawdepth: FloorObjects
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
- type: entity
|
||||||
|
id: AirAlarm
|
||||||
|
name: air alarm
|
||||||
|
description: An air alarm. Alarms... air?
|
||||||
|
components:
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
- type: ExtensionCableReceiver
|
||||||
|
- type: DeviceNetworkComponent
|
||||||
|
deviceNetId: Apc
|
||||||
|
frequency: 1621
|
||||||
|
- type: ApcNetworkConnection
|
||||||
|
- type: AtmosMonitor
|
||||||
|
temperatureThreshold: stationTemperature
|
||||||
|
pressureThreshold: stationPressure
|
||||||
|
gasThresholds:
|
||||||
|
Oxygen: stationOxygen
|
||||||
|
Nitrogen: ignore
|
||||||
|
CarbonDioxide: stationCO2
|
||||||
|
Plasma: danger # everything below is usually bad
|
||||||
|
Tritium: danger
|
||||||
|
WaterVapor: danger
|
||||||
|
- type: AtmosAlarmable
|
||||||
|
alarmedBy: ["AirAlarm"]
|
||||||
|
- type: AtmosDevice
|
||||||
|
- type: AirAlarm
|
||||||
|
- type: Clickable
|
||||||
|
- type: InteractionOutline
|
||||||
|
- type: UserInterface
|
||||||
|
interfaces:
|
||||||
|
- key: enum.SharedAirAlarmInterfaceKey.Key
|
||||||
|
type: AirAlarmBoundUserInterface
|
||||||
|
- key: enum.WiresUiKey.Key
|
||||||
|
type: WiresBoundUserInterface
|
||||||
|
- type: Wires
|
||||||
|
BoardName: "Air Alarm"
|
||||||
|
LayoutId: AirAlarm
|
||||||
|
- type: Physics
|
||||||
|
- type: Fixtures
|
||||||
|
fixtures:
|
||||||
|
- shape:
|
||||||
|
!type:PhysShapeAabb
|
||||||
|
bounds: "-0.25,-0.25,0.3,0.25"
|
||||||
|
layer: [ Passable ]
|
||||||
|
- type: AccessReader
|
||||||
|
access: [["Engineering"]]
|
||||||
|
- type: Appearance
|
||||||
|
visuals:
|
||||||
|
- type: AtmosMonitorVisualizer
|
||||||
|
layerMap: "airAlarmBase"
|
||||||
|
alarmStates:
|
||||||
|
Normal: alarm0
|
||||||
|
Warning: alarm2
|
||||||
|
Danger: alarm1
|
||||||
|
setOnDepowered:
|
||||||
|
airAlarmBase: alarmp
|
||||||
|
- type: WiresVisualizer
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
layers:
|
||||||
|
- state: alarm0
|
||||||
|
map: ["airAlarmBase"] # TODO: fire alarm enum
|
||||||
|
- state: alarmx
|
||||||
|
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||||
|
- type: Transform
|
||||||
|
anchored: true
|
||||||
|
- type: Construction
|
||||||
|
graph: air_alarm
|
||||||
|
node: air_alarm
|
||||||
|
placement:
|
||||||
|
mode: SnapgridCenter
|
||||||
|
snap:
|
||||||
|
- Wallmount
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: AirAlarmAssembly
|
||||||
|
name: air alarm assembly
|
||||||
|
description: An air alarm. Doesn't look like it'll be alarming air any time soon.
|
||||||
|
components:
|
||||||
|
- type: Clickable
|
||||||
|
- type: InteractionOutline
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
state: alarm_b1
|
||||||
|
- type: Construction
|
||||||
|
graph: air_alarm
|
||||||
|
node: assembly
|
||||||
|
placement:
|
||||||
|
mode: SnapgridCenter
|
||||||
|
snap:
|
||||||
|
- Wallmount
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
- type: entity
|
||||||
|
id: FireAlarm
|
||||||
|
name: fire alarm
|
||||||
|
description: A fire alarm. Spicy!
|
||||||
|
components:
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
- type: ExtensionCableReceiver
|
||||||
|
- type: DeviceNetworkComponent
|
||||||
|
deviceNetId: Apc
|
||||||
|
frequency: 1621
|
||||||
|
- type: ApcNetworkConnection
|
||||||
|
- type: AtmosMonitor
|
||||||
|
monitorFire: true
|
||||||
|
displayMaxAlarmInNet: true
|
||||||
|
- type: AtmosDevice
|
||||||
|
- type: AtmosAlarmable
|
||||||
|
alarmedBy: ["FireAlarm"] # alarm itself, network effect
|
||||||
|
- type: Clickable
|
||||||
|
- type: InteractionOutline
|
||||||
|
- type: FireAlarm
|
||||||
|
- type: Appearance
|
||||||
|
visuals:
|
||||||
|
- type: AtmosMonitorVisualizer
|
||||||
|
layerMap: "fireAlarmState"
|
||||||
|
alarmStates:
|
||||||
|
Normal: fire_off
|
||||||
|
Warning: fire_off # shouldn't be alarming at a warning
|
||||||
|
Danger: fire_on
|
||||||
|
hideOnDepowered: ["fireAlarmState"]
|
||||||
|
- type: WiresVisualizer
|
||||||
|
- type: UserInterface
|
||||||
|
interfaces:
|
||||||
|
- key: enum.WiresUiKey.Key
|
||||||
|
type: WiresBoundUserInterface
|
||||||
|
- type: Wires
|
||||||
|
BoardName: "Fire Alarm"
|
||||||
|
LayoutId: FireAlarm
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
layers:
|
||||||
|
- state: fire0
|
||||||
|
map: ["fireAlarmBase"]
|
||||||
|
- state: fire_off
|
||||||
|
map: ["fireAlarmState"]
|
||||||
|
- state: fire_b2
|
||||||
|
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||||
|
- type: Transform
|
||||||
|
anchored: true
|
||||||
|
- type: Construction
|
||||||
|
graph: fire_alarm
|
||||||
|
node: fire_alarm
|
||||||
|
placement:
|
||||||
|
mode: SnapgridCenter
|
||||||
|
snap:
|
||||||
|
- Wallmount
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: FireAlarmAssembly
|
||||||
|
name: fire alarm assembly
|
||||||
|
description: A fire alarm assembly. Very mild.
|
||||||
|
components:
|
||||||
|
- type: Clickable
|
||||||
|
- type: InteractionOutline
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
state: fire_b1
|
||||||
|
- type: Construction
|
||||||
|
graph: fire_alarm
|
||||||
|
node: assembly
|
||||||
|
placement:
|
||||||
|
mode: SnapgridCenter
|
||||||
|
snap:
|
||||||
|
- Wallmount
|
||||||
157
Resources/Prototypes/Recipes/Construction/Graphs/air_alarms.yml
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
- type: constructionGraph
|
||||||
|
id: air_alarm
|
||||||
|
start: start
|
||||||
|
graph:
|
||||||
|
- node: start
|
||||||
|
edges:
|
||||||
|
- to: assembly
|
||||||
|
steps:
|
||||||
|
- material: Steel
|
||||||
|
amount: 2
|
||||||
|
doAfter: 2.0
|
||||||
|
|
||||||
|
- node: assembly
|
||||||
|
entity: AirAlarmAssembly
|
||||||
|
actions:
|
||||||
|
- !type:SpriteStateChange
|
||||||
|
state: alarm_b1
|
||||||
|
edges:
|
||||||
|
- to: wired
|
||||||
|
steps:
|
||||||
|
- material: Cable
|
||||||
|
amount: 2
|
||||||
|
doAfter: 1
|
||||||
|
- to: start
|
||||||
|
completed:
|
||||||
|
- !type:SpawnPrototype
|
||||||
|
prototype: SheetSteel1
|
||||||
|
amount: 2
|
||||||
|
- !type:DeleteEntity {}
|
||||||
|
steps:
|
||||||
|
- tool: Welding
|
||||||
|
doAfter: 2
|
||||||
|
|
||||||
|
- node: wired
|
||||||
|
entity: AirAlarmAssembly
|
||||||
|
edges:
|
||||||
|
- to: electronics
|
||||||
|
steps:
|
||||||
|
- tag: AirAlarmElectronics
|
||||||
|
store: board
|
||||||
|
name: "air alarm electronics"
|
||||||
|
icon:
|
||||||
|
sprite: "Objects/Misc/module.rsi"
|
||||||
|
state: "door_electronics" # /tg/ uses the same sprite, right?
|
||||||
|
doAfter: 1
|
||||||
|
- to: assembly
|
||||||
|
completed:
|
||||||
|
- !type:SpawnPrototype
|
||||||
|
prototype: CableApcStack1
|
||||||
|
amount: 2
|
||||||
|
steps:
|
||||||
|
- tool: Cutting
|
||||||
|
doAfter: 1
|
||||||
|
|
||||||
|
- node: electronics
|
||||||
|
actions:
|
||||||
|
- !type:SpriteStateChange
|
||||||
|
state: alarm_b2
|
||||||
|
edges:
|
||||||
|
- to: air_alarm
|
||||||
|
steps:
|
||||||
|
- tool: Screwing
|
||||||
|
doAfter: 2
|
||||||
|
|
||||||
|
- node: air_alarm
|
||||||
|
entity: AirAlarm
|
||||||
|
edges:
|
||||||
|
- to: wired
|
||||||
|
conditions:
|
||||||
|
- !type:AllWiresCut {}
|
||||||
|
- !type:WirePanel {}
|
||||||
|
- !type:ContainerNotEmpty
|
||||||
|
container: board
|
||||||
|
completed:
|
||||||
|
- !type:EmptyAllContainers {}
|
||||||
|
steps:
|
||||||
|
- tool: Prying
|
||||||
|
doAfter: 1
|
||||||
|
|
||||||
|
- type: constructionGraph
|
||||||
|
id: fire_alarm
|
||||||
|
start: start
|
||||||
|
graph:
|
||||||
|
- node: start
|
||||||
|
edges:
|
||||||
|
- to: assembly
|
||||||
|
steps:
|
||||||
|
- material: Steel
|
||||||
|
amount: 2
|
||||||
|
doAfter: 2.0
|
||||||
|
|
||||||
|
- node: assembly
|
||||||
|
entity: FireAlarmAssembly
|
||||||
|
actions:
|
||||||
|
- !type:SpriteStateChange
|
||||||
|
state: fire_b1
|
||||||
|
edges:
|
||||||
|
- to: wired
|
||||||
|
steps:
|
||||||
|
- material: Cable
|
||||||
|
amount: 2
|
||||||
|
doAfter: 1
|
||||||
|
- to: start
|
||||||
|
completed:
|
||||||
|
- !type:SpawnPrototype
|
||||||
|
prototype: SheetSteel1
|
||||||
|
amount: 2
|
||||||
|
- !type:DeleteEntity {}
|
||||||
|
steps:
|
||||||
|
- tool: Welding
|
||||||
|
doAfter: 2
|
||||||
|
|
||||||
|
- node: wired
|
||||||
|
entity: FireAlarmAssembly
|
||||||
|
edges:
|
||||||
|
- to: electronics
|
||||||
|
steps:
|
||||||
|
- tag: FireAlarmElectronics
|
||||||
|
store: board
|
||||||
|
name: "fire alarm electronics"
|
||||||
|
icon:
|
||||||
|
sprite: "Objects/Misc/module.rsi"
|
||||||
|
state: "door_electronics" # /tg/ uses the same sprite, right?
|
||||||
|
doAfter: 1
|
||||||
|
- to: assembly
|
||||||
|
completed:
|
||||||
|
- !type:SpawnPrototype
|
||||||
|
prototype: CableApcStack1
|
||||||
|
amount: 2
|
||||||
|
steps:
|
||||||
|
- tool: Cutting
|
||||||
|
doAfter: 1
|
||||||
|
|
||||||
|
- node: electronics
|
||||||
|
actions:
|
||||||
|
- !type:SpriteStateChange
|
||||||
|
state: fire_b2
|
||||||
|
edges:
|
||||||
|
- to: fire_alarm
|
||||||
|
steps:
|
||||||
|
- tool: Screwing
|
||||||
|
doAfter: 2
|
||||||
|
|
||||||
|
- node: fire_alarm
|
||||||
|
entity: FireAlarm
|
||||||
|
edges:
|
||||||
|
- to: wired
|
||||||
|
conditions:
|
||||||
|
- !type:AllWiresCut {}
|
||||||
|
- !type:WirePanel {}
|
||||||
|
- !type:ContainerNotEmpty
|
||||||
|
container: board
|
||||||
|
completed:
|
||||||
|
- !type:EmptyAllContainers {}
|
||||||
|
steps:
|
||||||
|
- tool: Prying
|
||||||
|
doAfter: 1
|
||||||
35
Resources/Prototypes/Recipes/Construction/atmos_alarms.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
- type: construction
|
||||||
|
name: air alarm
|
||||||
|
id: AirAlarmFixture
|
||||||
|
graph: air_alarm
|
||||||
|
startNode: start
|
||||||
|
targetNode: air_alarm
|
||||||
|
category: Structures
|
||||||
|
description: An air alarm. Alarms... air?
|
||||||
|
icon:
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
state: alarm0
|
||||||
|
placementMode: SnapgridCenter
|
||||||
|
objectType: Structure
|
||||||
|
canRotate: true
|
||||||
|
canBuildInImpassable: true
|
||||||
|
conditions:
|
||||||
|
- !type:WallmountCondition {}
|
||||||
|
|
||||||
|
- type: construction
|
||||||
|
name: fire alarm
|
||||||
|
id: FireAlarm
|
||||||
|
graph: fire_alarm
|
||||||
|
startNode: start
|
||||||
|
targetNode: fire_alarm
|
||||||
|
category: Structures
|
||||||
|
description: A fire alarm. Spicy!
|
||||||
|
icon:
|
||||||
|
sprite: Structures/Wallmounts/air_monitors.rsi
|
||||||
|
state: fire0
|
||||||
|
placementMode: SnapgridCenter
|
||||||
|
objectType: Structure
|
||||||
|
canRotate: true
|
||||||
|
canBuildInImpassable: true
|
||||||
|
conditions:
|
||||||
|
- !type:WallmountCondition {}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
# Alphabetical order is now apparently required.
|
# Alphabetical order is now apparently required.
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: AirAlarmElectronics
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: Baguette
|
id: Baguette
|
||||||
|
|
||||||
@@ -108,6 +111,9 @@
|
|||||||
- type: Tag
|
- type: Tag
|
||||||
id: ExplosivePassable
|
id: ExplosivePassable
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: FireAlarmElectronics
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: FireAxe
|
id: FireAxe
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 222 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 851 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 317 B |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 203 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 247 B |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"version": 1, "size": {"x": 32, "y": 32}, "states": [{"name": "alarm0", "directions": 4, "delays": [[0.5, 0.1, 0.1, 1.0, 0.2, 0.1], [0.5, 0.1, 0.1, 1.0, 0.2, 0.1], [0.5, 0.1, 0.1, 1.0, 0.2, 0.1], [0.5, 0.1, 0.1, 1.0, 0.2, 0.1]]}, {"name": "alarm1", "directions": 4, "delays": [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]}, {"name": "alarm2", "directions": 4, "delays": [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]}, {"name": "alarm_b1", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "alarm_b2", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "alarm_bitem", "directions": 1, "delays": [[1.0]]}, {"name": "alarmp", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "alarmx", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "auth_off", "directions": 1, "delays": [[1.0]]}, {"name": "auth_on", "directions": 1, "delays": [[0.1, 0.1]]}, {"name": "fire0", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_0", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_1", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_2", "directions": 4, "delays": [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]}, {"name": "fire_3", "directions": 4, "delays": [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]}, {"name": "fire_b0", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_b1", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_b2", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_bitem", "directions": 1, "delays": [[1.0]]}, {"name": "fire_detected", "directions": 4, "delays": [[0.4, 0.4], [0.4, 0.4], [0.4, 0.4], [0.4, 0.4]]}, {"name": "fire_emagged", "directions": 4, "delays": [[0.5, 0.5, 0.3, 0.5, 0.2, 0.5], [0.5, 0.5, 0.3, 0.5, 0.2, 0.5], [0.5, 0.5, 0.3, 0.5, 0.2, 0.5], [0.5, 0.5, 0.3, 0.5, 0.2, 0.5]]}, {"name": "fire_off", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "fire_on", "directions": 4, "delays": [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]}, {"name": "fire_overlay", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "firex", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
|
||||||