Merge pull request #10721 from vulppine/air-alarm-fixup

Air sensors & air alarm fixup
This commit is contained in:
Flipp Syder
2022-09-02 13:00:33 -07:00
committed by GitHub
51 changed files with 3168 additions and 2582 deletions

View File

@@ -0,0 +1,20 @@
using Content.Shared.Atmos.Monitor;
namespace Content.Client.Atmos.Monitor;
[RegisterComponent]
public sealed class AtmosAlarmableVisualsComponent : Component
{
[DataField("layerMap")]
public string LayerMap { get; } = string.Empty;
[DataField("alarmStates")]
public readonly Dictionary<AtmosAlarmType, string> AlarmStates = new();
[DataField("hideOnDepowered")]
public readonly List<string>? HideOnDepowered;
// eh...
[DataField("setOnDepowered")]
public readonly Dictionary<string, string>? SetOnDepowered;
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Power;
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 sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmableVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || !args.Sprite.LayerMapTryGet(component.LayerMap, out var layer))
return;
if (!args.AppearanceData.TryGetValue(PowerDeviceVisuals.Powered, out var poweredObject) ||
poweredObject is not bool powered)
{
return;
}
if (component.HideOnDepowered != null)
{
foreach (var visLayer in component.HideOnDepowered)
{
if (args.Sprite.LayerMapTryGet(visLayer, out int powerVisibilityLayer))
args.Sprite.LayerSetVisible(powerVisibilityLayer, powered);
}
}
if (component.SetOnDepowered != null && !powered)
{
foreach (var (setLayer, powerState) in component.SetOnDepowered)
{
if (args.Sprite.LayerMapTryGet(setLayer, out int setStateLayer))
args.Sprite.LayerSetState(setStateLayer, new RSI.StateId(powerState));
}
}
if (args.AppearanceData.TryGetValue(AtmosMonitorVisuals.AlarmType, out var alarmTypeObject)
&& alarmTypeObject is AtmosAlarmType alarmType
&& powered
&& component.AlarmStates.TryGetValue(alarmType, out var state))
{
args.Sprite.LayerSetState(layer, new RSI.StateId(state));
}
}
}

View File

@@ -1,70 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Power;
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 sealed 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;
[Obsolete("Subscribe to your component being initialised instead.")]
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
IoCManager.InjectDependencies(this);
}
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
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>(PowerDeviceVisuals.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>(AtmosMonitorVisuals.Offset, out Vector2 offset))
{
sprite.Offset = offset;
}
if (component.TryGetData<AtmosMonitorAlarmType>(AtmosMonitorVisuals.AlarmType, out var alarmType)
&& powered)
if (_alarmStates.TryGetValue(alarmType, out var state))
sprite.LayerSetState(layer, new RSI.StateId(state));
}
}
}

View File

@@ -6,82 +6,78 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Content.Client.Atmos.Monitor.UI
namespace Content.Client.Atmos.Monitor.UI;
public sealed class AirAlarmBoundUserInterface : BoundUserInterface
{
public sealed class AirAlarmBoundUserInterface : BoundUserInterface
private AirAlarmWindow? _window;
public AirAlarmBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
private AirAlarmWindow? _window;
}
public AirAlarmBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{}
protected override void Open()
{
base.Open();
protected override void Open()
_window = new AirAlarmWindow();
if (State != null)
{
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;
UpdateState(State);
}
private void ResyncAllDevices()
_window.OpenCentered();
_window.OnClose += Close;
_window.AtmosDeviceDataChanged += OnDeviceDataChanged;
_window.AtmosAlarmThresholdChanged += OnThresholdChanged;
_window.AirAlarmModeChanged += OnAirAlarmModeChanged;
_window.ResyncAllRequested += ResyncAllDevices;
_window.AirAlarmTabChange += OnTabChanged;
}
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(string address, AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null)
{
SendMessage(new AirAlarmUpdateAlarmThresholdMessage(address, type, threshold, gas));
}
private void OnTabChanged(AirAlarmTab tab)
{
SendMessage(new AirAlarmTabSetMessage(tab));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not AirAlarmUIState cast || _window == null)
{
SendMessage(new AirAlarmResyncAllDevicesMessage());
return;
}
private void OnDeviceDataChanged(string address, IAtmosDeviceData data)
{
SendMessage(new AirAlarmUpdateDeviceDataMessage(address, data));
}
_window.UpdateState(cast);
}
private void OnAirAlarmModeChanged(AirAlarmMode mode)
{
SendMessage(new AirAlarmUpdateAlarmModeMessage(mode));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
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();
}
if (disposing) _window?.Dispose();
}
}

View File

@@ -18,20 +18,22 @@
</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 Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 2 0" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'air-alarm-ui-window-address-label'}" />
<Label Text="{Loc 'air-alarm-ui-window-device-count-label'}" />
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Name="CDeviceAddress" HorizontalAlignment="Right" />
<Label Name="CDeviceTotal" HorizontalAlignment="Right" />
</BoxContainer>
</BoxContainer>
<Button Name="CResyncButton" Text="{Loc 'air-alarm-ui-window-resync-devices-label'}" HorizontalExpand="True" />
</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"/>
@@ -40,32 +42,11 @@
<ScrollContainer VerticalExpand="True">
<BoxContainer Name="CScrubberContainer" Orientation="Vertical"/>
</ScrollContainer>
<!-- Alarm thresholds -->
<!-- Sensors -->
<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>
<BoxContainer Name="CSensorContainer" Orientation="Vertical"/>
</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" />

View File

@@ -13,180 +13,141 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI
namespace Content.Client.Atmos.Monitor.UI;
[GenerateTypedNameReferences]
public sealed partial class AirAlarmWindow : DefaultWindow
{
[GenerateTypedNameReferences]
public sealed partial class AirAlarmWindow : DefaultWindow
public event Action<string, IAtmosDeviceData>? AtmosDeviceDataChanged;
public event Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? AtmosAlarmThresholdChanged;
public event Action<AirAlarmMode>? AirAlarmModeChanged;
public event Action<string>? ResyncDeviceRequested;
public event Action? ResyncAllRequested;
public event Action<AirAlarmTab>? AirAlarmTabChange;
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 _ventDevices => CVentContainer;
private BoxContainer _scrubberDevices => CScrubberContainer;
private Dictionary<string, PumpControl> _pumps = new();
private Dictionary<string, ScrubberControl> _scrubbers = new();
private Dictionary<string, SensorInfo> _sensors = new();
private Button _resyncDevices => CResyncButton;
private Dictionary<Gas, Label> _gasLabels = new();
private OptionButton _modes => CModeButton;
public AirAlarmWindow()
{
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;
RobustXamlLoader.Load(this);
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()
foreach (var mode in Enum.GetValues<AirAlarmMode>())
{
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();
};
_modes.AddItem($"{mode}", (int) mode);
}
public void SetAddress(string address)
_modes.OnItemSelected += args =>
{
_address.Text = address;
_modes.SelectId(args.Id);
AirAlarmModeChanged!.Invoke((AirAlarmMode) args.Id);
};
_tabContainer.SetTabTitle(0, Loc.GetString("air-alarm-ui-window-tab-vents"));
_tabContainer.SetTabTitle(1, Loc.GetString("air-alarm-ui-window-tab-scrubbers"));
_tabContainer.SetTabTitle(2, Loc.GetString("air-alarm-ui-window-tab-sensors"));
_tabContainer.OnTabChanged += idx =>
{
AirAlarmTabChange!((AirAlarmTab) idx);
};
_resyncDevices.OnPressed += _ =>
{
_ventDevices.RemoveAllChildren();
_pumps.Clear();
_scrubberDevices.RemoveAllChildren();
_scrubbers.Clear();
CSensorContainer.RemoveAllChildren();
_sensors.Clear();
ResyncAllRequested!.Invoke();
};
}
public void UpdateState(AirAlarmUIState state)
{
_address.Text = state.Address;
_deviceTotal.Text = $"{state.DeviceCount}";
_pressure.SetMarkup(Loc.GetString("air-alarm-ui-window-pressure", ("pressure", $"{state.PressureAverage:0.##}")));
_temperature.SetMarkup(Loc.GetString("air-alarm-ui-window-temperature", ("tempC", $"{TemperatureHelpers.KelvinToCelsius(state.TemperatureAverage):0.#}"), ("temperature", $"{state.TemperatureAverage:0.##}")));
_alarmState.SetMarkup(Loc.GetString("air-alarm-ui-window-alarm-state", ("state", $"{state.AlarmType}")));
UpdateModeSelector(state.Mode);
foreach (var (addr, dev) in state.DeviceData)
{
UpdateDeviceData(addr, dev);
}
public void UpdateGasData(ref AirAlarmAirData state)
_tabContainer.CurrentTab = (int) state.Tab;
}
public void UpdateModeSelector(AirAlarmMode mode)
{
_modes.SelectId((int) mode);
}
public void UpdateDeviceData(string addr, IAtmosDeviceData device)
{
switch (device)
{
_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}")));
case GasVentPumpData pump:
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);
}
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.##}"));
}
break;
case GasVentScrubberData scrubber:
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);
}
public void UpdateModeSelector(AirAlarmMode mode)
{
_modes.SelectId((int) mode);
}
break;
case AtmosSensorData sensor:
if (!_sensors.TryGetValue(addr, out var sensorControl))
{
var control = new SensorInfo(sensor, addr);
control.OnThresholdUpdate += AtmosAlarmThresholdChanged;
_sensors.Add(addr, control);
CSensorContainer.AddChild(control);
}
else
{
sensorControl.ChangeData(sensor);
}
public void UpdateDeviceData(string addr, IAtmosDeviceData device)
{
switch (device)
{
case GasVentPumpData pump:
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 (!_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;
}
break;
}
}
}

View File

@@ -8,96 +8,99 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets
namespace Content.Client.Atmos.Monitor.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class PumpControl : BoxContainer
{
[GenerateTypedNameReferences]
public sealed 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)
{
private GasVentPumpData _data;
private string _address;
RobustXamlLoader.Load(this);
public event Action<string, IAtmosDeviceData>? PumpDataChanged;
Name = address;
private CheckBox _enabled => CEnableDevice;
private CollapsibleHeading _addressLabel => CAddress;
private OptionButton _pumpDirection => CPumpDirection;
private OptionButton _pressureCheck => CPressureCheck;
private FloatSpinBox _externalBound => CExternalBound;
private FloatSpinBox _internalBound => CInternalBound;
_data = data;
_address = address;
public PumpControl(GasVentPumpData data, string address)
_addressLabel.Title = Loc.GetString("air-alarm-ui-atmos-net-device-label", ("address", $"{address}"));
_enabled.Pressed = data.Enabled;
_enabled.OnToggled += _ =>
{
RobustXamlLoader.Load(this);
_data.Enabled = _enabled.Pressed;
PumpDataChanged?.Invoke(_address, _data);
};
this.Name = address;
_internalBound.Value = (float) _data.InternalPressureBound;
_internalBound.OnValueChanged += _ =>
{
_data.InternalPressureBound = _internalBound.Value;
PumpDataChanged?.Invoke(_address, _data);
};
_internalBound.IsValid += value => value >= 0;
_data = data;
_address = address;
_externalBound.Value = (float) _data.ExternalPressureBound;
_externalBound.OnValueChanged += _ =>
{
_data.ExternalPressureBound = _externalBound.Value;
PumpDataChanged?.Invoke(_address, _data);
};
_externalBound.IsValid += value => value >= 0;
_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);
};
foreach (var value in Enum.GetValues<VentPumpDirection>())
{
_pumpDirection.AddItem(Loc.GetString($"{value}"), (int) value);
}
public void ChangeData(GasVentPumpData data)
_pumpDirection.SelectId((int) _data.PumpDirection);
_pumpDirection.OnItemSelected += args =>
{
_data.Enabled = data.Enabled;
_enabled.Pressed = _data.Enabled;
_pumpDirection.SelectId(args.Id);
_data.PumpDirection = (VentPumpDirection) args.Id;
PumpDataChanged?.Invoke(_address, _data);
};
_data.PumpDirection = data.PumpDirection;
_pumpDirection.SelectId((int) _data.PumpDirection);
_data.PressureChecks = data.PressureChecks;
_pressureCheck.SelectId((int) _data.PressureChecks);
_data.ExternalPressureBound = data.ExternalPressureBound;
_externalBound.Value = _data.ExternalPressureBound;
_data.InternalPressureBound = data.InternalPressureBound;
_internalBound.Value = _data.InternalPressureBound;
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 = _data.ExternalPressureBound;
_data.InternalPressureBound = data.InternalPressureBound;
_internalBound.Value = _data.InternalPressureBound;
}
}

View File

@@ -11,113 +11,116 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets
namespace Content.Client.Atmos.Monitor.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class ScrubberControl : BoxContainer
{
[GenerateTypedNameReferences]
public sealed 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)
{
private GasVentScrubberData _data;
private string _address;
RobustXamlLoader.Load(this);
public event Action<string, IAtmosDeviceData>? ScrubberDataChanged;
Name = address;
private CheckBox _enabled => CEnableDevice;
private CollapsibleHeading _addressLabel => CAddress;
private OptionButton _pumpDirection => CPumpDirection;
private FloatSpinBox _volumeRate => CVolumeRate;
private CheckBox _wideNet => CWideNet;
_data = data;
_address = address;
private GridContainer _gases => CGasContainer;
private Dictionary<Gas, Button> _gasControls = new();
_addressLabel.Title = Loc.GetString("air-alarm-ui-atmos-net-device-label", ("address", $"{address}"));
public ScrubberControl(GasVentScrubberData data, string address)
_enabled.Pressed = data.Enabled;
_enabled.OnToggled += _ =>
{
RobustXamlLoader.Load(this);
_data.Enabled = _enabled.Pressed;
ScrubberDataChanged?.Invoke(_address, _data);
};
this.Name = address;
_wideNet.Pressed = data.WideNet;
_wideNet.OnToggled += _ =>
{
_data.WideNet = _wideNet.Pressed;
ScrubberDataChanged?.Invoke(_address, _data);
};
_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 = _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);
}
_volumeRate.Value = _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);
}
public void ChangeData(GasVentScrubberData data)
_pumpDirection.SelectId((int) _data.PumpDirection);
_pumpDirection.OnItemSelected += args =>
{
_data.Enabled = data.Enabled;
_enabled.Pressed = _data.Enabled;
_pumpDirection.SelectId(args.Id);
_data.PumpDirection = (ScrubberPumpDirection) args.Id;
ScrubberDataChanged?.Invoke(_address, _data);
};
_data.PumpDirection = data.PumpDirection;
_pumpDirection.Select((int) _data.PumpDirection);
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);
_data.VolumeRate = data.VolumeRate;
_volumeRate.Value = _data.VolumeRate;
ScrubberDataChanged?.Invoke(_address, _data);
};
_gasControls.Add(value, gasButton);
_gases.AddChild(gasButton);
}
_data.WideNet = data.WideNet;
_wideNet.Pressed = _data.WideNet;
}
var intersect = _data.FilterGases.Intersect(data.FilterGases);
public void ChangeData(GasVentScrubberData data)
{
_data.Enabled = data.Enabled;
_enabled.Pressed = _data.Enabled;
foreach (var value in Enum.GetValues<Gas>())
if (!intersect.Contains(value))
_gasControls[value].Pressed = false;
_data.PumpDirection = data.PumpDirection;
_pumpDirection.Select((int) _data.PumpDirection);
_data.VolumeRate = data.VolumeRate;
_volumeRate.Value = _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;
}
}
}
}

View File

@@ -0,0 +1,42 @@
<BoxContainer xmlns="https://spacestation14.io" HorizontalExpand="True">
<Collapsible Orientation="Vertical">
<CollapsibleHeading Name="SensorAddress" />
<CollapsibleBody Margin="2">
<BoxContainer Orientation="Vertical">
<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="PressureLabel" />
<RichTextLabel Name="TemperatureLabel" />
<RichTextLabel Name="AlarmStateLabel" />
</BoxContainer>
</BoxContainer>
<Collapsible Orientation="Vertical" Margin="2">
<CollapsibleHeading Title="{Loc 'air-alarm-ui-sensor-gases'}" />
<CollapsibleBody>
<BoxContainer Name="GasContainer" Orientation="Vertical" Margin="2" />
</CollapsibleBody>
</Collapsible>
<Collapsible Orientation="Vertical" Margin="2">
<CollapsibleHeading Title="{Loc 'air-alarm-ui-sensor-thresholds'}" />
<CollapsibleBody>
<BoxContainer Orientation="Vertical">
<Control Name="PressureThresholdContainer" Margin="2 0 2 0" />
<Control Name="TemperatureThresholdContainer" Margin="2 0 2 0" />
<Collapsible Orientation="Vertical" Margin="2 0 2 0">
<CollapsibleHeading Title="{Loc 'air-alarm-ui-sensor-gases'}" />
<CollapsibleBody Margin="4 2 4 2">
<BoxContainer Name="GasThresholds" Orientation="Vertical" Margin="2 0 2 0" />
</CollapsibleBody>
</Collapsible>
</BoxContainer>
</CollapsibleBody>
</Collapsible>
</BoxContainer>
</CollapsibleBody>
</Collapsible>
</BoxContainer>

View File

@@ -0,0 +1,105 @@
using Content.Client.Message;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Atmos.Monitor.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class SensorInfo : BoxContainer
{
public Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? OnThresholdUpdate;
private string _address;
private ThresholdControl _pressureThreshold;
private ThresholdControl _temperatureThreshold;
private Dictionary<Gas, ThresholdControl> _gasThresholds = new();
private Dictionary<Gas, Label> _gasLabels = new();
public SensorInfo(AtmosSensorData data, string address)
{
RobustXamlLoader.Load(this);
_address = address;
SensorAddress.Title = $"{address} : {data.AlarmState}";
PressureLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-pressure", ("pressure", $"{data.Pressure:0.##}")));
TemperatureLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-temperature", ("tempC", $"{TemperatureHelpers.KelvinToCelsius(data.Temperature):0.#}"), ("temperature", $"{data.Temperature:0.##}")));
AlarmStateLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-alarm-state", ("state", $"{data.AlarmState}")));
foreach (var (gas, amount) in data.Gases)
{
var label = new Label();
label.Text = Loc.GetString("air-alarm-ui-gases", ("gas", $"{gas}"),
("amount", $"{amount:0.####}"),
("percentage", $"{(amount / data.TotalMoles):0.##}"));
GasContainer.AddChild(label);
_gasLabels.Add(gas, label);
}
_pressureThreshold =
new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-pressure-title"), data.PressureThreshold, AtmosMonitorThresholdType.Pressure);
PressureThresholdContainer.AddChild(_pressureThreshold);
_temperatureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"), data.TemperatureThreshold,
AtmosMonitorThresholdType.Temperature);
TemperatureThresholdContainer.AddChild(_temperatureThreshold);
_pressureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
{
OnThresholdUpdate!(_address, type, threshold, arg3);
};
_temperatureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
{
OnThresholdUpdate!(_address, type, threshold, arg3);
};
foreach (var (gas, threshold) in data.GasThresholds)
{
var gasThresholdControl = new ThresholdControl(Loc.GetString($"air-alarm-ui-thresholds-gas-title", ("gas", $"{gas}")), threshold, AtmosMonitorThresholdType.Gas, gas, 100);
gasThresholdControl.ThresholdDataChanged += (type, threshold, arg3) =>
{
OnThresholdUpdate!(_address, type, threshold, arg3);
};
_gasThresholds.Add(gas, gasThresholdControl);
GasThresholds.AddChild(gasThresholdControl);
}
}
public void ChangeData(AtmosSensorData data)
{
SensorAddress.Title = $"{_address} : {data.AlarmState}";
PressureLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-pressure", ("pressure", $"{data.Pressure:0.##}")));
TemperatureLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-temperature", ("tempC", $"{TemperatureHelpers.KelvinToCelsius(data.Temperature):0.#}"), ("temperature", $"{data.Temperature:0.##}")));
AlarmStateLabel.SetMarkup(Loc.GetString("air-alarm-ui-window-alarm-state", ("state", $"{data.AlarmState}")));
foreach (var (gas, amount) in data.Gases)
{
if (!_gasLabels.TryGetValue(gas, out var label))
{
continue;
}
label.Text = Loc.GetString("air-alarm-ui-gases", ("gas", $"{gas}"),
("amount", $"{amount:0.####}"),
("percentage", $"{(amount / data.TotalMoles):0.##}"));
}
_pressureThreshold.UpdateThresholdData(data.PressureThreshold);
_temperatureThreshold.UpdateThresholdData(data.TemperatureThreshold);
foreach (var (gas, control) in _gasThresholds)
{
if (!data.GasThresholds.TryGetValue(gas, out var threshold))
{
continue;
}
control.UpdateThresholdData(threshold);
}
}
}

View File

@@ -8,287 +8,290 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
// holy FUCK
// this technically works because some of this you can *not* do in XAML but holy FUCK
namespace Content.Client.Atmos.Monitor.UI.Widgets
namespace Content.Client.Atmos.Monitor.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class ThresholdControl : BoxContainer
{
[GenerateTypedNameReferences]
public sealed 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)
{
private AtmosAlarmThreshold _threshold;
private AtmosMonitorThresholdType _type;
private Gas? _gas;
RobustXamlLoader.Load(this);
public event Action<AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? ThresholdDataChanged;
_threshold = threshold;
_type = type;
_gas = gas;
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;
_name.Title = name;
// 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)
// i miss rust macros
_upperBoundControl = new ThresholdBoundControl("upper-bound", _threshold.UpperBound, modifier);
_upperBoundControl.OnBoundChanged += value =>
{
RobustXamlLoader.Load(this);
// 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;
_threshold = threshold;
_type = type;
_gas = gas;
if (_threshold.LowerWarningBound != null)
value = (float) _threshold.LowerWarningBound + 0.1f;
else if (_threshold.LowerBound != null)
value = (float) _threshold.LowerBound + 0.1f;
_name.Title = name;
return value;
};
_upperBoundControl.OnValidBoundChanged += () =>
{
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
};
_dangerBounds.AddChild(_upperBoundControl);
// i miss rust macros
_lowerBoundControl = new ThresholdBoundControl("lower-bound", _threshold.LowerBound, modifier);
_lowerBoundControl.OnBoundChanged += value =>
{
_threshold.TrySetPrimaryBound(AtmosMonitorThresholdBound.Lower, value);
return _threshold.LowerBound;
};
_lowerBoundControl.OnBoundEnabled += () =>
{
var value = 0f;
_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.UpperWarningBound != null)
value = (float) _threshold.UpperWarningBound - 0.1f;
else if (_threshold.UpperBound != null)
value = (float) _threshold.UpperBound - 0.1f;
if (_threshold.LowerWarningBound != null)
value = (float) _threshold.LowerWarningBound + 0.1f;
else if (_threshold.LowerBound != null)
value = (float) _threshold.LowerBound + 0.1f;
return value;
};
_lowerBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
_dangerBounds.AddChild(_lowerBoundControl);
return value;
};
_upperBoundControl.OnValidBoundChanged += () =>
{
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
};
_dangerBounds.AddChild(_upperBoundControl);
_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;
_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.LowerWarningBound != null)
value = (float) _threshold.LowerWarningBound + 0.1f;
else if (_threshold.LowerBound != null)
value = (float) _threshold.LowerBound + 0.1f;
if (_threshold.UpperWarningBound != null)
value = (float) _threshold.UpperWarningBound - 0.1f;
else if (_threshold.UpperBound != null)
value = (float) _threshold.UpperBound - 0.1f;
return value;
};
_upperWarningBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
_warningBounds.AddChild(_upperWarningBoundControl);
return value;
};
_lowerBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
_dangerBounds.AddChild(_lowerBoundControl);
_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;
_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.UpperWarningBound != null)
value = (float) _threshold.UpperWarningBound - 0.1f;
else if (_threshold.UpperBound != null)
value = (float) _threshold.UpperBound - 0.1f;
if (_threshold.LowerWarningBound != null)
value = (float) _threshold.LowerWarningBound + 0.1f;
else if (_threshold.LowerBound != null)
value = (float) _threshold.LowerBound + 0.1f;
return value;
};
_lowerWarningBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
return value;
};
_upperWarningBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
_warningBounds.AddChild(_upperWarningBoundControl);
_warningBounds.AddChild(_lowerWarningBoundControl);
_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;
_ignore.OnToggled += args =>
{
_threshold.Ignore = args.Pressed;
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
};
_ignore.Pressed = _threshold.Ignore;
}
if (_threshold.UpperWarningBound != null)
value = (float) _threshold.UpperWarningBound - 0.1f;
else if (_threshold.UpperBound != null)
value = (float) _threshold.UpperBound - 0.1f;
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;
}
return value;
};
_lowerWarningBoundControl.OnValidBoundChanged += () =>
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
_warningBounds.AddChild(_lowerWarningBoundControl);
private sealed 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;
_ignore.OnToggled += args =>
{
_threshold.Ignore = args.Pressed;
ThresholdDataChanged!.Invoke(_type, _threshold, _gas);
};
_ignore.Pressed = _threshold.Ignore;
// convenience thing for getting multiplied values
// and also setting value to a usable value
private float? ModifiedValue
{
get => _value * _modifier;
set => _value = value / _modifier;
}
public void UpdateThresholdData(AtmosAlarmThreshold threshold)
private float ModifiedLastValue
{
_upperBoundControl.SetValue(threshold.UpperBound);
_lowerBoundControl.SetValue(threshold.LowerBound);
_upperWarningBoundControl.SetValue(threshold.UpperWarningBound);
_lowerWarningBoundControl.SetValue(threshold.LowerWarningBound);
_ignore.Pressed = threshold.Ignore;
get => _lastValue * _modifier;
set => _lastValue = value / _modifier;
}
private float _modifier;
private sealed class ThresholdBoundControl : BoxContainer
private FloatSpinBox _bound;
private CheckBox _boundEnabled;
public event Action? OnValidBoundChanged;
public Func<float?, float?>? OnBoundChanged;
public Func<float>? OnBoundEnabled;
public void SetValue(float? value)
{
// 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;
_value = value;
// convenience thing for getting multiplied values
// and also setting value to a usable value
private float? ModifiedValue
if (_value == null)
{
get => _value * _modifier;
set => _value = value / _modifier;
_boundEnabled.Pressed = false;
_bound.Value = 0;
}
private float ModifiedLastValue
else
{
get => _lastValue * _modifier;
set => _lastValue = value / _modifier;
_boundEnabled.Pressed = true;
_bound.Value = (float) ModifiedValue!;
}
}
private float _modifier;
// 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;
private FloatSpinBox _bound;
private CheckBox _boundEnabled;
HorizontalExpand = true;
Orientation = LayoutOrientation.Vertical;
public event Action? OnValidBoundChanged;
public Func<float?, float?>? OnBoundChanged;
public Func<float>? OnBoundEnabled;
AddChild(new Label { Text = Loc.GetString($"air-alarm-ui-thresholds-{name}") });
_bound = new FloatSpinBox(.01f, 2);
AddChild(_bound);
public void SetValue(float? value)
_boundEnabled = new CheckBox
{
Text = Loc.GetString("Enabled")
};
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;
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);
}
_lastValue = (float) value!;
OnValidBoundChanged!.Invoke();
}
// otherwise, just set it to the last known value
else
{
_value = _lastValue;
_bound.Value = ModifiedLastValue;
}
}
private bool ValidateThreshold(float value) => (_value != null) && (value >= 0);
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)
{
return _value != null && value >= 0;
}
}
}

View File

@@ -1,20 +1,26 @@
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Robust.Shared.Network;
namespace Content.Server.Atmos.Monitor.Components
namespace Content.Server.Atmos.Monitor.Components;
[RegisterComponent]
public sealed class AirAlarmComponent : Component
{
[RegisterComponent]
public sealed class AirAlarmComponent : Component
{
[ViewVariables] public AirAlarmMode CurrentMode { get; set; } = AirAlarmMode.Filtering;
[ViewVariables] public AirAlarmMode CurrentMode { get; set; } = AirAlarmMode.Filtering;
// Remember to null this afterwards.
[ViewVariables] public IAirAlarmModeUpdate? CurrentModeUpdater { get; set; }
// Remember to null this afterwards.
[ViewVariables] public IAirAlarmModeUpdate? CurrentModeUpdater { get; set; }
public Dictionary<string, IAtmosDeviceData> DeviceData = new();
[ViewVariables] public AirAlarmTab CurrentTab { get; set; }
public HashSet<NetUserId> ActivePlayers = new();
public readonly HashSet<string> KnownDevices = new();
public readonly Dictionary<string, GasVentPumpData> VentData = new();
public readonly Dictionary<string, GasVentScrubberData> ScrubberData = new();
public readonly Dictionary<string, AtmosSensorData> SensorData = new();
public bool CanSync = true;
}
public HashSet<NetUserId> ActivePlayers = new();
public bool CanSync = true;
}

View File

@@ -1,41 +1,60 @@
using Content.Shared.Atmos.Monitor;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Atmos.Monitor.Components
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
// -> AtmosAlarmEvent
// -> 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 alarmer. This will store every single alert received, and
/// calculate the highest alert based on the alerts received. Equally, if you
/// link other alarmables to this, it will store the alerts from them to
/// calculate the highest network alert.
/// </summary>
[RegisterComponent]
public sealed class AtmosAlarmableComponent : Component
{
// 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)
[ViewVariables]
public readonly Dictionary<string, AtmosAlarmType> NetworkAlarmStates = new();
[ViewVariables] public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal;
[ViewVariables] public bool IgnoreAlarms { get; set; } = false;
[DataField("alarmSound")]
public SoundSpecifier AlarmSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/alarm.ogg");
[DataField("alarmVolume")]
public float AlarmVolume { get; set; } = -10;
/// <summary>
/// A component to add to device network devices if you want them to be alarmed
/// by an atmospheric monitor.
/// List of tags to check for when synchronizing alarms.
/// </summary>
[RegisterComponent]
public sealed class AtmosAlarmableComponent : Component
{
[ViewVariables]
public List<EntityUid> LinkedMonitors { get; set; } = new();
[DataField("syncWith", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<TagPrototype>))]
public HashSet<string> SyncWithTags { get; } = new();
[ViewVariables] public AtmosMonitorAlarmType LastAlarmState = AtmosMonitorAlarmType.Normal;
[ViewVariables] public AtmosMonitorAlarmType HighestNetworkState = AtmosMonitorAlarmType.Normal;
[ViewVariables] public bool IgnoreAlarms { get; set; } = false;
[DataField("monitorAlertTypes")]
public HashSet<AtmosMonitorThresholdType>? MonitorAlertTypes { get; }
/// <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();
}
/// <summary>
/// If this device should receive only. If it can only
/// receive, that means that attempting to sync outwards
/// will result in nothing happening.
/// </summary>
[DataField("receiveOnly")]
public bool ReceiveOnly { get; }
}

View File

@@ -3,98 +3,67 @@ using Content.Shared.Atmos.Monitor;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Monitor.Components
namespace Content.Server.Atmos.Monitor.Components;
[RegisterComponent]
public sealed class AtmosMonitorComponent : Component
{
[RegisterComponent]
public sealed class AtmosMonitorComponent : Component
{
// 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;
// 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;
[DataField("temperatureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
public readonly string? TemperatureThresholdId;
[ViewVariables]
public AtmosAlarmThreshold? TemperatureThreshold;
[ViewVariables]
public AtmosAlarmThreshold? TemperatureThreshold;
[DataField("pressureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
public readonly string? PressureThresholdId;
[DataField("pressureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
public readonly string? PressureThresholdId;
[ViewVariables]
public AtmosAlarmThreshold? PressureThreshold;
[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;
// 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;
// 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;
[DataField("displayMaxAlarmInNet")]
public bool DisplayMaxAlarmInNet = false;
[ViewVariables]
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
[DataField("alarmSound")]
public SoundSpecifier AlarmSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/alarm.ogg");
// Stores a reference to the gas on the tile this is on.
[ViewVariables]
public GasMixture? TileGas;
[DataField("alarmVolume")]
public float AlarmVolume { get; set; } = -10;
// Stores the last alarm state of this alarm.
[ViewVariables]
public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal;
// 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 HashSet<AtmosMonitorThresholdType> TrippedThresholds = new();
[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;
}
}
}
/// <summary>
/// Registered devices in this atmos monitor. Alerts will be sent directly
/// to these devices.
/// </summary>
[ViewVariables] public HashSet<string> RegisteredDevices = new();
}

View File

@@ -1,7 +1,6 @@
namespace Content.Server.Atmos.Monitor.Components
namespace Content.Server.Atmos.Monitor.Components;
[RegisterComponent]
public sealed class FireAlarmComponent : Component
{
[RegisterComponent]
public sealed class FireAlarmComponent : Component
{
}
}
}

View File

@@ -5,233 +5,168 @@ using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
namespace Content.Server.Atmos.Monitor
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 an interface that air alarm modes use
/// in order to execute the defined modes.
/// 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 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.
public string NetOwner { get; set; }
/// <summary>
/// An interface that AirAlarmSystem uses
/// in order to update air alarm modes that
/// need updating (e.g., Replace)
/// This is executed every time the air alarm
/// update loop is fully executed. This should
/// be where all the logic goes.
/// </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 void Update(EntityUid uid);
}
public sealed 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();
public sealed 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
// still not a fan since ReplaceMode must have an allocation
// but it's whatever
public static IAirAlarmMode? ModeToExecutor(AirAlarmMode mode)
{
return 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
// 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()
{
[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 sealed 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 sealed 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 sealed 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 sealed 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 sealed 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;
}
}
}
IoCManager.InjectDependencies(this);
DeviceNetworkSystem = EntitySystem.Get<DeviceNetworkSystem>();
AirAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
}
}
public sealed class AirAlarmNoneMode : AirAlarmModeExecutor
{
public override void Execute(EntityUid uid)
{
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent? alarm))
return;
foreach (var (addr, device) in alarm.VentData)
{
device.Enabled = false;
AirAlarmSystem.SetData(uid, addr, device);
}
foreach (var (addr, device) in alarm.ScrubberData)
{
device.Enabled = false;
AirAlarmSystem.SetData(uid, addr, device);
}
}
}
public sealed class AirAlarmFilterMode : AirAlarmModeExecutor
{
public override void Execute(EntityUid uid)
{
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent? alarm))
return;
foreach (var (addr, device) in alarm.VentData)
{
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FilterModePreset);
}
foreach (var (addr, device) in alarm.ScrubberData)
{
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FilterModePreset);
}
}
}
public sealed class AirAlarmPanicMode : AirAlarmModeExecutor
{
public override void Execute(EntityUid uid)
{
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent? alarm))
return;
foreach (var (addr, device) in alarm.VentData)
{
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.PanicModePreset);
}
foreach (var (addr, device) in alarm.ScrubberData)
{
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.PanicModePreset);
}
}
}
public sealed class AirAlarmFillMode : AirAlarmModeExecutor
{
public override void Execute(EntityUid uid)
{
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent? alarm))
return;
foreach (var (addr, device) in alarm.VentData)
{
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FillModePreset);
}
foreach (var (addr, device) in alarm.ScrubberData)
{
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FillModePreset);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,316 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos.Monitor.Components;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.Monitor.Systems
namespace Content.Server.Atmos.Monitor.Systems;
public sealed class AtmosAlarmableSystem : EntitySystem
{
public sealed class AtmosAlarmableSystem : EntitySystem
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
/// <summary>
/// An alarm. Has three valid states: Normal, Warning, Danger.
/// Will attempt to fetch the tags from the alarming entity
/// to send over.
/// </summary>
public const string AlertCmd = "atmos_alarm";
public const string AlertSource = "atmos_alarm_source";
public const string AlertTypes = "atmos_alarm_types";
/// <summary>
/// Syncs alerts from this alarm receiver to other alarm receivers.
/// Creates a network effect as a result. Note: if the alert receiver
/// is not aware of the device beforehand, it will not sync.
/// </summary>
public const string SyncAlerts = "atmos_alarmable_sync_alerts";
public const string ResetAll = "atmos_alarmable_reset_all";
public override void Initialize()
{
public override void Initialize()
SubscribeLocalEvent<AtmosAlarmableComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<AtmosAlarmableComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
SubscribeLocalEvent<AtmosAlarmableComponent, PowerChangedEvent>(OnPowerChange);
}
private void OnInit(EntityUid uid, AtmosAlarmableComponent component, ComponentInit args)
{
TryUpdateAlert(
uid,
TryGetHighestAlert(uid, out var alarm) ? alarm.Value : AtmosAlarmType.Normal,
component,
false);
}
private void OnPowerChange(EntityUid uid, AtmosAlarmableComponent component, PowerChangedEvent args)
{
if (!args.Powered)
{
SubscribeLocalEvent<AtmosAlarmableComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
Reset(uid, component);
}
private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, DeviceNetworkPacketEvent args)
else
{
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), true);
}
}
TryUpdateAlert(
uid,
TryGetHighestAlert(uid, out var alarm) ? alarm.Value : AtmosAlarmType.Normal,
component,
false);
}
}
private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, DeviceNetworkPacketEvent args)
{
if (component.IgnoreAlarms) return;
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent? netConn))
return;
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|| !args.Data.TryGetValue(AlertSource, out HashSet<string>? sourceTags))
{
return;
}
var isValid = sourceTags.Any(source => component.SyncWithTags.Contains(source));
if (!isValid)
{
return;
}
switch (cmd)
{
case AlertCmd:
// Set the alert state, and then cache it so we can calculate
// the maximum alarm state at all times.
if (!args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out AtmosAlarmType state))
{
break;
}
if (args.Data.TryGetValue(AlertTypes, out HashSet<AtmosMonitorThresholdType>? types) && component.MonitorAlertTypes != null)
{
isValid = types.Any(type => component.MonitorAlertTypes.Contains(type));
}
if (!component.NetworkAlarmStates.ContainsKey(args.SenderAddress))
{
if (!isValid)
{
break;
}
component.NetworkAlarmStates.Add(args.SenderAddress, state);
}
else
{
// This is because if the alert is no longer valid,
// it may mean that the threshold we need to look at has
// been removed from the threshold types passed:
// basically, we need to reset this state to normal here.
component.NetworkAlarmStates[args.SenderAddress] = isValid ? state : AtmosAlarmType.Normal;
}
if (!TryGetHighestAlert(uid, out var netMax, component))
{
netMax = AtmosAlarmType.Normal;
}
TryUpdateAlert(uid, netMax.Value, component);
break;
case ResetAll:
Reset(uid, component);
break;
case SyncAlerts:
if (!args.Data.TryGetValue(SyncAlerts,
out IReadOnlyDictionary<string, AtmosAlarmType>? alarms))
{
break;
}
foreach (var (key, alarm) in alarms)
{
if (!component.NetworkAlarmStates.TryAdd(key, alarm))
{
component.NetworkAlarmStates[key] = alarm;
}
}
if (TryGetHighestAlert(uid, out var maxAlert, component))
{
TryUpdateAlert(uid, maxAlert.Value, component);
}
break;
}
}
private void TryUpdateAlert(EntityUid uid, AtmosAlarmType type, AtmosAlarmableComponent alarmable, bool sync = true)
{
if (alarmable.LastAlarmState == type)
{
return;
}
if (sync)
{
SyncAlertsToNetwork(uid, null, alarmable);
}
alarmable.LastAlarmState = type;
UpdateAppearance(uid, type);
PlayAlertSound(uid, type, alarmable);
RaiseLocalEvent(uid, new AtmosAlarmEvent(type), true);
}
public void SyncAlertsToNetwork(EntityUid uid, string? address = null, AtmosAlarmableComponent? alarmable = null, TagComponent? tags = null)
{
if (!Resolve(uid, ref alarmable, ref tags) || alarmable.ReceiveOnly)
{
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = SyncAlerts,
[SyncAlerts] = alarmable.NetworkAlarmStates,
[AlertSource] = tags.Tags
};
_deviceNet.QueuePacket(uid, address, payload);
}
/// <summary>
/// Forces this alarmable to have a specific alert. This will not be reset until the alarmable
/// is manually reset. This will store the alarmable as a device in its network states.
/// </summary>
/// <param name="uid"></param>
/// <param name="alarmType"></param>
/// <param name="alarmable"></param>
public void ForceAlert(EntityUid uid, AtmosAlarmType alarmType,
AtmosAlarmableComponent? alarmable = null, DeviceNetworkComponent? devNet = null, TagComponent? tags = null)
{
if (!Resolve(uid, ref alarmable, ref devNet, ref tags))
{
return;
}
TryUpdateAlert(uid, alarmType, alarmable, false);
if (alarmable.ReceiveOnly)
{
return;
}
if (!alarmable.NetworkAlarmStates.TryAdd(devNet.Address, alarmType))
{
alarmable.NetworkAlarmStates[devNet.Address] = alarmType;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = AlertCmd,
[DeviceNetworkConstants.CmdSetState] = alarmType,
[AlertSource] = tags.Tags
};
_deviceNet.QueuePacket(uid, null, payload);
}
/// <summary>
/// Resets the state of this alarmable to normal.
/// </summary>
/// <param name="uid"></param>
/// <param name="alarmable"></param>
public void Reset(EntityUid uid, AtmosAlarmableComponent? alarmable = null)
{
if (!Resolve(uid, ref alarmable))
{
return;
}
TryUpdateAlert(uid, AtmosAlarmType.Normal, alarmable, false);
alarmable.NetworkAlarmStates.Clear();
}
public void ResetAllOnNetwork(EntityUid uid, AtmosAlarmableComponent? alarmable = null, TagComponent? tags = null)
{
if (!Resolve(uid, ref alarmable, ref tags) || alarmable.ReceiveOnly)
{
return;
}
Reset(uid, alarmable);
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = ResetAll,
[AlertSource] = tags.Tags
};
_deviceNet.QueuePacket(uid, null, payload);
}
/// <summary>
/// Tries to get the highest possible alert stored in this alarm.
/// </summary>
/// <param name="uid"></param>
/// <param name="alarm"></param>
/// <param name="alarmable"></param>
/// <returns></returns>
public bool TryGetHighestAlert(EntityUid uid, [NotNullWhen(true)] out AtmosAlarmType? alarm,
AtmosAlarmableComponent? alarmable = null)
{
alarm = null;
if (!Resolve(uid, ref alarmable))
{
return false;
}
foreach (var alarmState in alarmable.NetworkAlarmStates.Values)
{
alarm = alarm == null || alarm < alarmState ? alarmState : alarm;
}
return alarm != null;
}
private void PlayAlertSound(EntityUid uid, AtmosAlarmType alarm, AtmosAlarmableComponent alarmable)
{
if (alarm == AtmosAlarmType.Danger)
{
_audioSystem.PlayPvs(alarmable.AlarmSound, uid, AudioParams.Default.WithVolume(alarmable.AlarmVolume));
}
}
private void UpdateAppearance(EntityUid uid, AtmosAlarmType alarm)
{
_appearance.SetData(uid, AtmosMonitorVisuals.AlarmType, alarm);
}
}
public sealed class AtmosAlarmEvent : EntityEventArgs
{
public AtmosAlarmType AlarmType { get; }
public AtmosAlarmEvent(AtmosAlarmType netMax)
{
AlarmType = netMax;
}
}

View File

@@ -0,0 +1,55 @@
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.Atmos.Monitor.Components;
namespace Content.Server.Atmos.Monitor.Systems;
/// <summary>
/// Generic device network commands useful for atmos devices,
/// as well as some helper commands.
/// </summary>
public sealed class AtmosDeviceNetworkSystem : EntitySystem
{
/// <summary>
/// Register a device's address on this device.
/// </summary>
public const string RegisterDevice = "atmos_register_device";
/// <summary>
/// Synchronize the data this device has with the sender.
/// </summary>
public const string SyncData = "atmos_sync_data";
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
public void Register(EntityUid uid, string? address)
{
var registerPayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = RegisterDevice
};
_deviceNet.QueuePacket(uid, address, registerPayload);
}
public void Sync(EntityUid uid, string? address)
{
var syncPayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = SyncData
};
_deviceNet.QueuePacket(uid, address, syncPayload);
}
public void SetDeviceState(EntityUid uid, string address, IAtmosDeviceData data)
{
var payload = new NetworkPayload()
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdSetState,
[DeviceNetworkConstants.CmdSetState] = data
};
_deviceNet.QueuePacket(uid, address, payload);
}
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Atmos.Monitor.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.EntitySystems;
@@ -9,472 +10,372 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Atmos.Monitor.Systems
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 sealed class AtmosMonitorSystem : EntitySystem
{
// 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 sealed 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
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
// Packet data
public const string AtmosMonitorThresholdData = "atmos_monitor_threshold_data";
public const string AtmosMonitorThresholdDataType = "atmos_monitor_threshold_type";
public const string AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas";
public override void Initialize()
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
SubscribeLocalEvent<AtmosMonitorComponent, ComponentInit>(OnAtmosMonitorInit);
SubscribeLocalEvent<AtmosMonitorComponent, ComponentStartup>(OnAtmosMonitorStartup);
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
SubscribeLocalEvent<AtmosMonitorComponent, TileFireEvent>(OnFireEvent);
SubscribeLocalEvent<AtmosMonitorComponent, PowerChangedEvent>(OnPowerChangedEvent);
SubscribeLocalEvent<AtmosMonitorComponent, BeforePacketSentEvent>(BeforePacketRecv);
SubscribeLocalEvent<AtmosMonitorComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
}
// Commands
/// <summary>
/// Command to alarm the network that something has happened.
/// </summary>
public const string AtmosMonitorAlarmCmd = "atmos_monitor_alarm_update";
private void OnAtmosMonitorInit(EntityUid uid, AtmosMonitorComponent component, ComponentInit args)
{
if (component.TemperatureThresholdId != null)
component.TemperatureThreshold = new(_prototypeManager.Index<AtmosAlarmThreshold>(component.TemperatureThresholdId));
/// <summary>
/// Command to sync this monitor's alarm state with the rest of the network.
/// </summary>
public const string AtmosMonitorAlarmSyncCmd = "atmos_monitor_alarm_sync";
if (component.PressureThresholdId != null)
component.PressureThreshold = new(_prototypeManager.Index<AtmosAlarmThreshold>(component.PressureThresholdId));
/// <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";
public override void Initialize()
if (component.GasThresholdIds != null)
{
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, DeviceNetworkPacketEvent>(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)
{
component.GasThresholds = new();
foreach (var (gas, id) in component.GasThresholdIds)
if (_prototypeManager.TryIndex<AtmosAlarmThreshold>(id, out var gasThreshold))
component.GasThresholds.Add(gas, gasThreshold);
if (_prototypeManager.TryIndex<AtmosAlarmThreshold>(id, out var gasThreshold))
component.GasThresholds.Add(gas, new(gasThreshold));
}
}
}
private void OnAtmosMonitorStartup(EntityUid uid, AtmosMonitorComponent component, ComponentStartup args)
private void OnAtmosMonitorStartup(EntityUid uid, AtmosMonitorComponent component, ComponentStartup args)
{
if (!HasComp<ApcPowerReceiverComponent>(uid)
&& TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent))
{
if (!HasComp<ApcPowerReceiverComponent>(uid)
&& TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent))
{
_atmosDeviceSystem.LeaveAtmosphere(atmosDeviceComponent);
return;
}
_atmosDeviceSystem.LeaveAtmosphere(atmosDeviceComponent);
}
}
_checkPos.Add(uid);
private void BeforePacketRecv(EntityUid uid, AtmosMonitorComponent component, BeforePacketSentEvent args)
{
if (!component.NetEnabled) args.Cancel();
}
private void OnPacketRecv(EntityUid uid, AtmosMonitorComponent component, DeviceNetworkPacketEvent 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))
{
return;
}
private void OnAtmosMonitorShutdown(EntityUid uid, AtmosMonitorComponent component, ComponentShutdown args)
switch (cmd)
{
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);
if (transform.GridUid == null)
return;
// 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;
var pos = _transformSystem.GetGridOrMapTilePosition(uid, transform);
if (_atmosphereSystem.IsTileAirBlocked(transform.GridUid.Value, pos))
{
var rotPos = transform.LocalRotation.RotateVec(new Vector2(0, -1));
transform.Anchored = false;
coords = coords.Offset(rotPos);
transform.Coordinates = coords;
appearance.SetData(AtmosMonitorVisuals.Offset, - new Vector2i(0, -1));
transform.Anchored = true;
}
GasMixture? air = _atmosphereSystem.GetContainingMixture(uid, true);
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, DeviceNetworkPacketEvent 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(AtmosMonitorVisuals.AlarmType, component.HighestAlarmInNetwork);
if (component.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, component);
}
}
private void OnPowerChangedEvent(EntityUid uid, AtmosMonitorComponent component, PowerChangedEvent args)
{
if (TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent))
{
if (!args.Powered)
case AtmosDeviceNetworkSystem.RegisterDevice:
component.RegisteredDevices.Add(args.SenderAddress);
break;
case AtmosAlarmableSystem.ResetAll:
Reset(uid);
// Don't clear alarm states here.
break;
case AtmosMonitorSetThresholdCmd:
if (args.Data.TryGetValue(AtmosMonitorThresholdData, out AtmosAlarmThreshold? thresholdData)
&& args.Data.TryGetValue(AtmosMonitorThresholdDataType, out AtmosMonitorThresholdType? thresholdType))
{
if (atmosDeviceComponent.JoinedGrid != null)
{
_atmosDeviceSystem.LeaveAtmosphere(atmosDeviceComponent);
component.TileGas = null;
}
// clear memory when power cycled
component.LastAlarmState = AtmosMonitorAlarmType.Normal;
component.NetworkAlarmStates.Clear();
args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas);
SetThreshold(uid, thresholdType.Value, thresholdData, gas);
}
else if (args.Powered)
break;
case AtmosDeviceNetworkSystem.SyncData:
var payload = new NetworkPayload();
payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
if (component.TileGas != null)
{
if (atmosDeviceComponent.JoinedGrid == null)
var gases = new Dictionary<Gas, float>();
foreach (var gas in Enum.GetValues<Gas>())
{
_atmosDeviceSystem.JoinAtmosphere(atmosDeviceComponent);
var air = _atmosphereSystem.GetContainingMixture(uid, true);
component.TileGas = air;
gases.Add(gas, component.TileGas.GetMoles(gas));
}
payload.Add(AtmosDeviceNetworkSystem.SyncData, new AtmosSensorData(
component.TileGas.Pressure,
component.TileGas.Temperature,
component.TileGas.TotalMoles,
component.LastAlarmState,
gases,
component.PressureThreshold ?? new(),
component.TemperatureThreshold ?? new(),
component.GasThresholds ?? new()
));
}
_deviceNetSystem.QueuePacket(uid, args.SenderAddress, payload);
break;
}
}
private void OnPowerChangedEvent(EntityUid uid, AtmosMonitorComponent component, PowerChangedEvent args)
{
if (TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent))
{
if (!args.Powered)
{
if (atmosDeviceComponent.JoinedGrid != null)
{
_atmosDeviceSystem.LeaveAtmosphere(atmosDeviceComponent);
component.TileGas = null;
}
}
else if (args.Powered)
{
if (atmosDeviceComponent.JoinedGrid == null)
{
_atmosDeviceSystem.JoinAtmosphere(atmosDeviceComponent);
var air = _atmosphereSystem.GetContainingMixture(uid, true);
component.TileGas = air;
}
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
appearanceComponent.SetData(AtmosMonitorVisuals.AlarmType, component.LastAlarmState);
Alert(uid, component.LastAlarmState);
}
}
}
private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, ref TileFireEvent args)
{
if (!this.IsPowered(uid, EntityManager))
return;
private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, ref TileFireEvent args)
{
if (!this.IsPowered(uid, EntityManager))
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 (!this.IsPowered(uid, EntityManager))
return;
// can't hurt
// (in case something is making AtmosDeviceUpdateEvents
// outside the typical device loop)
if (!TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent)
|| 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 we're monitoring for atmos fire, then we make it similar to a smoke detector
// and just outright trigger a danger event
//
// 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)
// somebody else can reset it :sunglasses:
if (component.MonitorFire
&& component.LastAlarmState != AtmosAlarmType.Danger)
{
if (air == null) return;
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
Alert(uid, AtmosAlarmType.Danger, null, component); // technically???
}
if (!Resolve(uid, ref monitor)) return;
// 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)
{
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
Alert(uid, AtmosAlarmType.Danger, null, component);
}
}
AtmosMonitorAlarmType state = AtmosMonitorAlarmType.Normal;
List<AtmosMonitorThresholdType> alarmTypes = new();
private void OnAtmosUpdate(EntityUid uid, AtmosMonitorComponent component, AtmosDeviceUpdateEvent args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (monitor.TemperatureThreshold != null
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState)
&& temperatureState > state)
// can't hurt
// (in case something is making AtmosDeviceUpdateEvents
// outside the typical device loop)
if (!TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent)
|| 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;
var state = AtmosAlarmType.Normal;
HashSet<AtmosMonitorThresholdType> alarmTypes = new(monitor.TrippedThresholds);
if (monitor.TemperatureThreshold != null
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState))
{
if (temperatureState > state)
{
state = temperatureState;
alarmTypes.Add(AtmosMonitorThresholdType.Temperature);
}
else if (temperatureState == AtmosAlarmType.Normal)
{
alarmTypes.Remove(AtmosMonitorThresholdType.Temperature);
}
}
if (monitor.PressureThreshold != null
&& monitor.PressureThreshold.CheckThreshold(air.Pressure, out var pressureState)
&& pressureState > state)
if (monitor.PressureThreshold != null
&& monitor.PressureThreshold.CheckThreshold(air.Pressure, out var pressureState)
)
{
if (pressureState > state)
{
state = pressureState;
alarmTypes.Add(AtmosMonitorThresholdType.Pressure);
}
if (monitor.GasThresholds != null)
else if (pressureState == AtmosAlarmType.Normal)
{
foreach (var (gas, threshold) in monitor.GasThresholds)
alarmTypes.Remove(AtmosMonitorThresholdType.Pressure);
}
}
if (monitor.GasThresholds != null)
{
var tripped = false;
foreach (var (gas, threshold) in monitor.GasThresholds)
{
var gasRatio = air.GetMoles(gas) / air.TotalMoles;
if (threshold.CheckThreshold(gasRatio, out var gasState)
&& gasState > state)
{
var gasRatio = air.GetMoles(gas) / air.TotalMoles;
if (threshold.CheckThreshold(gasRatio, out var gasState)
&& gasState > state)
{
state = gasState;
alarmTypes.Add(AtmosMonitorThresholdType.Gas);
}
state = gasState;
tripped = true;
}
}
// if the state of the current air doesn't match the last alarm state,
// we update the state
if (state != monitor.LastAlarmState)
if (tripped)
{
Alert(uid, state, alarmTypes, monitor);
alarmTypes.Add(AtmosMonitorThresholdType.Gas);
}
else
{
alarmTypes.Remove(AtmosMonitorThresholdType.Gas);
}
}
/// <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 the state of the current air doesn't match the last alarm state,
// we update the state
if (state != monitor.LastAlarmState || !alarmTypes.SetEquals(monitor.TrippedThresholds))
{
if (!Resolve(uid, ref monitor)) return;
monitor.LastAlarmState = state;
if (EntityManager.TryGetComponent(monitor.Owner, out AppearanceComponent? appearanceComponent))
appearanceComponent.SetData(AtmosMonitorVisuals.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), true);
// 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(monitor.AlarmSound.GetSound(), Filter.Pvs(uid), 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, null, payload);
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, null, payload);
}
/// <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, null, payload);
}
/// <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;
}
Alert(uid, state, alarmTypes, monitor);
}
}
public sealed class AtmosMonitorAlarmEvent : EntityEventArgs
/// <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, AtmosAlarmType state, HashSet<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null)
{
public AtmosMonitorAlarmType Type { get; }
public AtmosMonitorAlarmType HighestNetworkType { get; }
if (!Resolve(uid, ref monitor)) return;
public AtmosMonitorAlarmEvent(AtmosMonitorAlarmType type, AtmosMonitorAlarmType netMax)
monitor.LastAlarmState = state;
monitor.TrippedThresholds = alarms ?? monitor.TrippedThresholds;
BroadcastAlertPacket(monitor);
// TODO: Central system that grabs *all* alarms from wired network
}
/// <summary>
/// Resets a single monitor's alarm.
/// </summary>
private void Reset(EntityUid uid)
{
Alert(uid, AtmosAlarmType.Normal);
}
/// <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, TagComponent? tags = null)
{
if (!monitor.NetEnabled) return;
if (!Resolve(monitor.Owner, ref tags))
{
Type = type;
HighestNetworkType = netMax;
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = AtmosAlarmableSystem.AlertCmd,
[DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
[AtmosAlarmableSystem.AlertSource] = tags.Tags,
[AtmosAlarmableSystem.AlertTypes] = monitor.TrippedThresholds
};
foreach (var addr in monitor.RegisteredDevices)
{
_deviceNetSystem.QueuePacket(monitor.Owner, addr, payload);
}
}
/// <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;
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.AlertLevel;
using Content.Server.Atmos.Monitor.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.AlertLevel;
@@ -8,50 +9,57 @@ using Content.Shared.Interaction;
using Content.Shared.Emag.Systems;
using Robust.Server.GameObjects;
namespace Content.Server.Atmos.Monitor.Systems
namespace Content.Server.Atmos.Monitor.Systems;
public sealed class FireAlarmSystem : EntitySystem
{
public sealed class FireAlarmSystem : EntitySystem
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public override void Initialize()
{
[Dependency] private readonly AtmosMonitorSystem _monitorSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
SubscribeLocalEvent<FireAlarmComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<FireAlarmComponent, DeviceListUpdateEvent>(OnDeviceListSync);
SubscribeLocalEvent<FireAlarmComponent, GotEmaggedEvent>(OnEmagged);
}
public override void Initialize()
private void OnDeviceListSync(EntityUid uid, FireAlarmComponent component, DeviceListUpdateEvent args)
{
_atmosDevNet.Register(uid, null);
_atmosDevNet.Sync(uid, null);
}
private void OnInteractHand(EntityUid uid, FireAlarmComponent component, InteractHandEvent args)
{
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
return;
if (this.IsPowered(uid, EntityManager))
{
SubscribeLocalEvent<FireAlarmComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<FireAlarmComponent, GotEmaggedEvent>(OnEmagged);
}
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)
&& this.IsPowered(uid, EntityManager))
if (!_atmosAlarmable.TryGetHighestAlert(uid, out var alarm))
{
if (monitor.HighestAlarmInNetwork == AtmosMonitorAlarmType.Normal)
{
_monitorSystem.Alert(uid, AtmosMonitorAlarmType.Danger);
}
else
{
_monitorSystem.ResetAll(uid);
}
alarm = AtmosAlarmType.Normal;
}
}
private void OnEmagged(EntityUid uid, FireAlarmComponent component, GotEmaggedEvent args)
{
if (TryComp<AtmosMonitorComponent>(uid, out var atmosMonitor))
if (alarm == AtmosAlarmType.Normal)
{
if (atmosMonitor?.MonitorFire == true)
{
atmosMonitor.MonitorFire = false;
_monitorSystem.Alert(uid, AtmosMonitorAlarmType.Emagged);
args.Handled = true;
}
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Danger);
}
else
{
_atmosAlarmable.ResetAllOnNetwork(uid);
}
}
}
private void OnEmagged(EntityUid uid, FireAlarmComponent component, GotEmaggedEvent args)
{
if (TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
{
// Remove the atmos alarmable component permanently from this device.
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
RemCompDeferred<AtmosAlarmableComponent>(uid);
}
}
}

View File

@@ -37,7 +37,7 @@ public sealed class AirAlarmPanicWire : BaseWireAction
{
base.Initialize();
_airAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
_airAlarmSystem = EntityManager.System<AirAlarmSystem>();
}
public override bool Cut(EntityUid user, Wire wire)

View File

@@ -17,7 +17,7 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
private string _text = "NETW";
private Color _color = Color.Orange;
private AtmosMonitorSystem _atmosMonitorSystem = default!;
private AtmosAlarmableSystem _atmosAlarmableSystem = default!;
public override object StatusKey { get; } = AtmosMonitorAlarmWireActionKeys.Network;
@@ -27,7 +27,12 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
if (IsPowered(wire.Owner) && EntityManager.TryGetComponent<AtmosMonitorComponent>(wire.Owner, out var monitor))
{
lightState = monitor.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger
if (!_atmosAlarmableSystem.TryGetHighestAlert(wire.Owner, out var alarm))
{
alarm = AtmosAlarmType.Normal;
}
lightState = alarm == AtmosAlarmType.Danger
? StatusLightState.BlinkingFast
: StatusLightState.On;
}
@@ -42,14 +47,14 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
{
base.Initialize();
_atmosMonitorSystem = EntitySystem.Get<AtmosMonitorSystem>();
_atmosAlarmableSystem = EntityManager.System<AtmosAlarmableSystem>();
}
public override bool Cut(EntityUid user, Wire wire)
{
if (EntityManager.TryGetComponent<AtmosMonitorComponent>(wire.Owner, out var monitor))
if (EntityManager.TryGetComponent<AtmosAlarmableComponent>(wire.Owner, out var monitor))
{
monitor.NetEnabled = false;
monitor.IgnoreAlarms = true;
}
return true;
@@ -57,9 +62,9 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
public override bool Mend(EntityUid user, Wire wire)
{
if (EntityManager.TryGetComponent<AtmosMonitorComponent>(wire.Owner, out var monitor))
if (EntityManager.TryGetComponent<AtmosAlarmableComponent>(wire.Owner, out var monitor))
{
monitor.NetEnabled = true;
monitor.IgnoreAlarms = false;
}
return true;
@@ -69,7 +74,7 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
{
if (_alarmOnPulse)
{
_atmosMonitorSystem.Alert(wire.Owner, AtmosMonitorAlarmType.Danger);
_atmosAlarmableSystem.ForceAlert(wire.Owner, AtmosAlarmType.Danger);
}
return true;

View File

@@ -38,7 +38,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceUpdateEvent>(OnGasVentPumpUpdated);
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceDisabledEvent>(OnGasVentPumpLeaveAtmosphere);
SubscribeLocalEvent<GasVentPumpComponent, AtmosDeviceEnabledEvent>(OnGasVentPumpEnterAtmosphere);
SubscribeLocalEvent<GasVentPumpComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<GasVentPumpComponent, AtmosAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<GasVentPumpComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<GasVentPumpComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
SubscribeLocalEvent<GasVentPumpComponent, ComponentInit>(OnInit);
@@ -158,13 +158,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
UpdateState(uid, component);
}
private void OnAtmosAlarm(EntityUid uid, GasVentPumpComponent component, AtmosMonitorAlarmEvent args)
private void OnAtmosAlarm(EntityUid uid, GasVentPumpComponent component, AtmosAlarmEvent args)
{
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
if (args.AlarmType == AtmosAlarmType.Danger)
{
component.Enabled = false;
}
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
else if (args.AlarmType == AtmosAlarmType.Normal)
{
component.Enabled = true;
}
@@ -181,7 +181,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnPacketRecv(EntityUid uid, GasVentPumpComponent component, DeviceNetworkPacketEvent args)
{
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent? netConn)
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent? alarmable)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
return;
@@ -189,24 +188,19 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
switch (cmd)
{
case AirAlarmSystem.AirAlarmSyncCmd:
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSyncData);
payload.Add(AirAlarmSystem.AirAlarmSyncData, component.ToAirAlarmData());
case AtmosDeviceNetworkSystem.SyncData:
payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
payload.Add(AtmosDeviceNetworkSystem.SyncData, component.ToAirAlarmData());
_deviceNetSystem.QueuePacket(uid, args.SenderAddress, payload, device: netConn);
return;
case AirAlarmSystem.AirAlarmSetData:
if (!args.Data.TryGetValue(AirAlarmSystem.AirAlarmSetData, out GasVentPumpData? setData))
case DeviceNetworkConstants.CmdSetState:
if (!args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out GasVentPumpData? setData))
break;
component.FromAirAlarmData(setData);
UpdateState(uid, component);
alarmable.IgnoreAlarms = setData.IgnoreAlarms;
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSetDataStatus);
payload.Add(AirAlarmSystem.AirAlarmSetDataStatus, true);
_deviceNetSystem.QueuePacket(uid, null, payload, device: netConn);
return;
}

View File

@@ -36,7 +36,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceUpdateEvent>(OnVentScrubberUpdated);
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceEnabledEvent>(OnVentScrubberEnterAtmosphere);
SubscribeLocalEvent<GasVentScrubberComponent, AtmosDeviceDisabledEvent>(OnVentScrubberLeaveAtmosphere);
SubscribeLocalEvent<GasVentScrubberComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<GasVentScrubberComponent, AtmosAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<GasVentScrubberComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<GasVentScrubberComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
}
@@ -124,13 +124,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return true;
}
private void OnAtmosAlarm(EntityUid uid, GasVentScrubberComponent component, AtmosMonitorAlarmEvent args)
private void OnAtmosAlarm(EntityUid uid, GasVentScrubberComponent component, AtmosAlarmEvent args)
{
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
if (args.AlarmType == AtmosAlarmType.Danger)
{
component.Enabled = false;
}
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
else if (args.AlarmType == AtmosAlarmType.Normal)
{
component.Enabled = true;
}
@@ -147,7 +147,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnPacketRecv(EntityUid uid, GasVentScrubberComponent component, DeviceNetworkPacketEvent args)
{
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent? netConn)
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent? alarmable)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
return;
@@ -155,24 +154,19 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
switch (cmd)
{
case AirAlarmSystem.AirAlarmSyncCmd:
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSyncData);
payload.Add(AirAlarmSystem.AirAlarmSyncData, component.ToAirAlarmData());
case AtmosDeviceNetworkSystem.SyncData:
payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
payload.Add(AtmosDeviceNetworkSystem.SyncData, component.ToAirAlarmData());
_deviceNetSystem.QueuePacket(uid, args.SenderAddress, payload, device: netConn);
return;
case AirAlarmSystem.AirAlarmSetData:
if (!args.Data.TryGetValue(AirAlarmSystem.AirAlarmSetData, out GasVentScrubberData? setData))
case DeviceNetworkConstants.CmdSetState:
if (!args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out GasVentScrubberData? setData))
break;
component.FromAirAlarmData(setData);
UpdateState(uid, component);
alarmable.IgnoreAlarms = setData.IgnoreAlarms;
payload.Add(DeviceNetworkConstants.Command, AirAlarmSystem.AirAlarmSetDataStatus);
payload.Add(AirAlarmSystem.AirAlarmSetDataStatus, true);
_deviceNetSystem.QueuePacket(uid, null, payload, device: netConn);
return;
}

View File

@@ -1,4 +1,5 @@
using Content.Server.DeviceNetwork.Components;
using System.Linq;
using Content.Server.DeviceNetwork.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
@@ -25,7 +26,10 @@ public sealed class DeviceListSystem : EntitySystem
if (!merge)
deviceList.Devices.Clear();
deviceList.Devices.UnionWith(devices);
var devicesList = devices.ToList();
deviceList.Devices.UnionWith(devicesList);
RaiseLocalEvent(uid, new DeviceListUpdateEvent(devicesList));
}
/// <summary>
@@ -91,3 +95,13 @@ public sealed class DeviceListSystem : EntitySystem
args.Cancel();
}
}
public sealed class DeviceListUpdateEvent : EntityEventArgs
{
public DeviceListUpdateEvent(List<EntityUid> devices)
{
Devices = devices;
}
public List<EntityUid> Devices { get; }
}

View File

@@ -30,6 +30,8 @@ public sealed class NetworkConfiguratorSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<NetworkConfiguratorComponent, MapInitEvent>(OnMapInit);
//Interaction
SubscribeLocalEvent<NetworkConfiguratorComponent, AfterInteractEvent>((uid, component, args) => OnUsed(uid, component, args.Target, args.User, args.CanReach)); //TODO: Replace with utility verb?
@@ -63,6 +65,12 @@ public sealed class NetworkConfiguratorSystem : EntitySystem
}
}
private void OnMapInit(EntityUid uid, NetworkConfiguratorComponent component, MapInitEvent args)
{
component.Devices.Clear();
UpdateUiState(uid, component);
}
private void TryAddNetworkDevice(EntityUid? targetUid, EntityUid configuratorUid, EntityUid userUid,
NetworkConfiguratorComponent? configurator = null)
{
@@ -77,10 +85,26 @@ public sealed class NetworkConfiguratorSystem : EntitySystem
if (!targetUid.HasValue || !Resolve(targetUid.Value, ref device, false))
return;
if (string.IsNullOrEmpty(device.Address))
var address = device.Address;
if (string.IsNullOrEmpty(address))
{
_popupSystem.PopupCursor(Loc.GetString("network-configurator-device-failed", ("device", targetUid)), Filter.Entities(userUid));
return;
// This primarily checks if the entity in question is pre-map init or not.
// This is because otherwise, anything that uses DeviceNetwork will not
// have an address populated, as all devices that use DeviceNetwork
// obtain their address on map init. If the entity is post-map init,
// and it still doesn't have an address, it will fail. Otherwise,
// it stores the entity's UID as a string for visual effect, that way
// a mapper can reference the devices they've gathered by UID, instead of
// by device network address. These entries, if the multitool is still in
// the map after it being saved, are cleared upon mapinit.
if (MetaData(targetUid.Value).EntityLifeStage == EntityLifeStage.MapInitialized)
{
_popupSystem.PopupCursor(Loc.GetString("network-configurator-device-failed", ("device", targetUid)),
Filter.Entities(userUid));
return;
}
address = $"UID: {targetUid.Value.ToString()}";
}
if (configurator.Devices.ContainsValue(targetUid.Value))
@@ -89,7 +113,7 @@ public sealed class NetworkConfiguratorSystem : EntitySystem
return;
}
configurator.Devices.Add(device.Address, targetUid.Value);
configurator.Devices.Add(address, targetUid.Value);
_popupSystem.PopupCursor(Loc.GetString("network-configurator-device-saved", ("address", device.Address), ("device", targetUid)),
Filter.Entities(userUid), PopupType.Medium);

View File

@@ -25,6 +25,8 @@ namespace Content.Server.Doors.Components
[DataField("lockedPryTimeModifier")]
public float LockedPryTimeModifier = 1.5f;
[DataField("autocloseDelay")] public TimeSpan AutocloseDelay = TimeSpan.FromSeconds(3f);
public bool EmergencyPressureStop()
{
var doorSys = EntitySystem.Get<DoorSystem>();

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.Monitor.Components;
using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Doors.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
@@ -12,6 +13,7 @@ namespace Content.Server.Doors.Systems
public sealed class FirelockSystem : EntitySystem
{
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
public override void Initialize()
{
@@ -21,9 +23,10 @@ namespace Content.Server.Doors.Systems
SubscribeLocalEvent<FirelockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<FirelockComponent, DoorGetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry);
SubscribeLocalEvent<FirelockComponent, DoorStateChangedEvent>(OnUpdateState);
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
SubscribeLocalEvent<FirelockComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<FirelockComponent, AtmosAlarmEvent>(OnAtmosAlarm);
}
private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args)
@@ -60,30 +63,40 @@ namespace Content.Server.Doors.Systems
}
}
private void OnUpdateState(EntityUid uid, FirelockComponent component, DoorStateChangedEvent args)
{
var ev = new BeforeDoorAutoCloseEvent();
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
{
return;
}
_doorSystem.SetNextStateChange(uid, component.AutocloseDelay);
}
private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args)
{
if (!this.IsPowered(uid, EntityManager))
args.Cancel();
// Make firelocks autoclose, but only if the last alarm type it
// 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)
if (_atmosAlarmable.TryGetHighestAlert(uid, out var alarm) && alarm != AtmosAlarmType.Danger || alarm == null)
args.Cancel();
}
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosMonitorAlarmEvent args)
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosAlarmEvent args)
{
if (!TryComp<DoorComponent>(uid, out var doorComponent)) return;
if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
if (args.AlarmType == AtmosAlarmType.Normal)
{
if (doorComponent.State == DoorState.Closed)
_doorSystem.TryOpen(uid);
}
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
else if (args.AlarmType == AtmosAlarmType.Danger)
{
component.EmergencyPressureStop();
}

View File

@@ -0,0 +1,267 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor;
// mostly based around floats and percentages, no literals
// except for the range boundaries
[Prototype("alarmThreshold")]
[Serializable, NetSerializable]
public sealed class AtmosAlarmThreshold : IPrototype, ISerializationHooks
{
[IdDataField]
public string ID { get; } = default!;
[ViewVariables]
[DataField("ignore")]
public bool Ignore;
// 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 => CalculateWarningBound(AtmosMonitorThresholdBound.Upper);
[ViewVariables]
public float? LowerWarningBound => CalculateWarningBound(AtmosMonitorThresholdBound.Lower);
public AtmosAlarmThreshold()
{
}
public AtmosAlarmThreshold(AtmosAlarmThreshold other)
{
Ignore = other.Ignore;
UpperBound = other.UpperBound;
LowerBound = other.LowerBound;
UpperWarningPercentage = other.UpperWarningPercentage;
LowerWarningPercentage = other.LowerWarningPercentage;
}
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 AtmosAlarmType state)
{
state = AtmosAlarmType.Normal;
if (Ignore)
{
return false;
}
if (value >= UpperBound || value <= LowerBound)
{
state = AtmosAlarmType.Danger;
return true;
}
if (value >= UpperWarningBound || value <= LowerWarningBound)
{
state = AtmosAlarmType.Warning;
return true;
}
return true;
}
// 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;
}
var 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;
}
var 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;
var upperWarning = (float) (input / UpperBound);
var 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;
var lowerWarning = (float) (input / LowerBound);
var 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
}
[Serializable, NetSerializable]
public enum AtmosMonitorVisuals : byte
{
AlarmType,
}

View File

@@ -1,259 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor
{
// mostly based around floats and percentages, no literals
// except for the range boundaries
[Prototype("alarmThreshold")]
[Serializable, NetSerializable]
public sealed class AtmosAlarmThreshold : IPrototype, ISerializationHooks
{
[IdDataFieldAttribute]
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
}
[Serializable, NetSerializable]
public enum AtmosMonitorVisuals : byte
{
Offset,
AlarmType,
}
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor;
[Serializable, NetSerializable]
public enum AtmosAlarmType : sbyte
{
Normal = 0,
Warning = 1,
Danger = 2, // 1 << 1 is the exact same thing and we're not really doing **bitmasking** are we?
Emagged = 3,
}

View File

@@ -1,13 +0,0 @@
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?
Emagged = 3,
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.Atmos.Monitor.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor;
[Serializable, NetSerializable]
public sealed class AtmosSensorData : IAtmosDeviceData
{
public AtmosSensorData(float pressure, float temperature, float totalMoles, AtmosAlarmType alarmState, Dictionary<Gas, float> gases, AtmosAlarmThreshold pressureThreshold, AtmosAlarmThreshold temperatureThreshold, Dictionary<Gas, AtmosAlarmThreshold> gasThresholds)
{
Pressure = pressure;
Temperature = temperature;
TotalMoles = totalMoles;
AlarmState = alarmState;
Gases = gases;
PressureThreshold = pressureThreshold;
TemperatureThreshold = temperatureThreshold;
GasThresholds = gasThresholds;
}
public bool Enabled { get; set; }
public bool Dirty { get; set; }
public bool IgnoreAlarms { get; set; }
/// Most fields are readonly, because it's data that's meant to be transmitted.
/// <summary>
/// Current pressure detected by this sensor.
/// </summary>
public float Pressure { get; }
/// <summary>
/// Current temperature detected by this sensor.
/// </summary>
public float Temperature { get; }
/// <summary>
/// Current amount of moles detected by this sensor.
/// </summary>
public float TotalMoles { get; }
/// <summary>
/// Current alarm state of this sensor. Does not reflect the highest alarm state on the network.
/// </summary>
public AtmosAlarmType AlarmState { get; }
/// <summary>
/// Current number of gases on this sensor.
/// </summary>
public Dictionary<Gas, float> Gases { get; }
public AtmosAlarmThreshold PressureThreshold { get; }
public AtmosAlarmThreshold TemperatureThreshold { get; }
public Dictionary<Gas, AtmosAlarmThreshold> GasThresholds { get; }
}

View File

@@ -1,130 +1,129 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor.Components
namespace Content.Shared.Atmos.Monitor.Components;
[Serializable, NetSerializable]
public enum SharedAirAlarmInterfaceKey
{
[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 sealed class AirAlarmUIState : BoundUserInterfaceState
{}
[Serializable, NetSerializable]
public sealed class AirAlarmResyncAllDevicesMessage : BoundUserInterfaceMessage
{}
[Serializable, NetSerializable]
public sealed class AirAlarmSetAddressMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public AirAlarmSetAddressMessage(string address)
{
Address = address;
}
}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateAirDataMessage : BoundUserInterfaceMessage
{
public AirAlarmAirData AirData;
public AirAlarmUpdateAirDataMessage(AirAlarmAirData airData)
{
AirData = airData;
}
}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateAlarmModeMessage : BoundUserInterfaceMessage
{
public AirAlarmMode Mode { get; }
public AirAlarmUpdateAlarmModeMessage(AirAlarmMode mode)
{
Mode = mode;
}
}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateDeviceDataMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public IAtmosDeviceData Data { get; }
public AirAlarmUpdateDeviceDataMessage(string addr, IAtmosDeviceData data)
{
Address = addr;
Data = data;
}
}
[Serializable, NetSerializable]
public sealed 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;
}
}
Key
}
[Serializable, NetSerializable]
public enum AirAlarmMode
{
None,
Filtering,
Fill,
Panic,
}
[Serializable, NetSerializable]
public enum AirAlarmWireStatus
{
Power,
Access,
Panic,
DeviceSync
}
public interface IAtmosDeviceData
{
public bool Enabled { get; set; }
public bool Dirty { get; set; }
public bool IgnoreAlarms { get; set; }
}
[Serializable, NetSerializable]
public sealed class AirAlarmUIState : BoundUserInterfaceState
{
public AirAlarmUIState(string address, int deviceCount, float pressureAverage, float temperatureAverage, Dictionary<string, IAtmosDeviceData> deviceData, AirAlarmMode mode, AirAlarmTab tab, AtmosAlarmType alarmType)
{
Address = address;
DeviceCount = deviceCount;
PressureAverage = pressureAverage;
TemperatureAverage = temperatureAverage;
DeviceData = deviceData;
Mode = mode;
Tab = tab;
AlarmType = alarmType;
}
public string Address { get; }
public int DeviceCount { get; }
public float PressureAverage { get; }
public float TemperatureAverage { get; }
/// <summary>
/// Every single device data that can be seen from this
/// air alarm. This includes vents, scrubbers, and sensors.
/// The device data you get, however, depends on the current
/// selected tab.
/// </summary>
public Dictionary<string, IAtmosDeviceData> DeviceData { get; }
public AirAlarmMode Mode { get; }
public AirAlarmTab Tab { get; }
public AtmosAlarmType AlarmType { get; }
}
[Serializable, NetSerializable]
public sealed class AirAlarmTabSetMessage : BoundUserInterfaceMessage
{
public AirAlarmTabSetMessage(AirAlarmTab tab)
{
Tab = tab;
}
public AirAlarmTab Tab { get; }
}
[Serializable, NetSerializable]
public sealed class AirAlarmResyncAllDevicesMessage : BoundUserInterfaceMessage
{}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateAlarmModeMessage : BoundUserInterfaceMessage
{
public AirAlarmMode Mode { get; }
public AirAlarmUpdateAlarmModeMessage(AirAlarmMode mode)
{
Mode = mode;
}
}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateDeviceDataMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public IAtmosDeviceData Data { get; }
public AirAlarmUpdateDeviceDataMessage(string addr, IAtmosDeviceData data)
{
Address = addr;
Data = data;
}
}
[Serializable, NetSerializable]
public sealed class AirAlarmUpdateAlarmThresholdMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public AtmosAlarmThreshold Threshold { get; }
public AtmosMonitorThresholdType Type { get; }
public Gas? Gas { get; }
public AirAlarmUpdateAlarmThresholdMessage(string address, AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null)
{
Address = address;
Threshold = threshold;
Type = type;
Gas = gas;
}
}
public enum AirAlarmTab
{
Vent,
Scrubber,
Sensors,
Settings
}

View File

@@ -1,11 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Monitor.Components
{
[Serializable, NetSerializable]
public enum FireAlarmWireStatus
{
Power,
Alarm
}
}

View File

@@ -44,6 +44,17 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
ExternalPressureBound = Atmospherics.OneAtmosphere,
InternalPressureBound = 0f
};
public static GasVentPumpData ReplaceModePreset = new GasVentPumpData
{
Enabled = false,
IgnoreAlarms = true,
Dirty = true,
PumpDirection = VentPumpDirection.Releasing,
PressureChecks = VentPressureBound.ExternalBound,
ExternalPressureBound = Atmospherics.OneAtmosphere,
InternalPressureBound = 0f
};
}
[Serializable, NetSerializable]

View File

@@ -30,7 +30,7 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
public static GasVentScrubberData FilterModePreset = new GasVentScrubberData
{
Enabled = true,
FilterGases = GasVentScrubberData.DefaultFilterGases,
FilterGases = new(GasVentScrubberData.DefaultFilterGases),
PumpDirection = ScrubberPumpDirection.Scrubbing,
VolumeRate = 200f,
WideNet = false
@@ -40,7 +40,7 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
{
Enabled = false,
Dirty = true,
FilterGases = GasVentScrubberData.DefaultFilterGases,
FilterGases = new(GasVentScrubberData.DefaultFilterGases),
PumpDirection = ScrubberPumpDirection.Scrubbing,
VolumeRate = 200f,
WideNet = false
@@ -50,7 +50,18 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
{
Enabled = true,
Dirty = true,
FilterGases = GasVentScrubberData.DefaultFilterGases,
FilterGases = new(GasVentScrubberData.DefaultFilterGases),
PumpDirection = ScrubberPumpDirection.Siphoning,
VolumeRate = 200f,
WideNet = false
};
public static GasVentScrubberData ReplaceModePreset = new GasVentScrubberData
{
Enabled = true,
IgnoreAlarms = true,
Dirty = true,
FilterGases = new(GasVentScrubberData.DefaultFilterGases),
PumpDirection = ScrubberPumpDirection.Siphoning,
VolumeRate = 200f,
WideNet = false

View File

@@ -18,10 +18,9 @@ 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-window-tab-sensors = Sensors
air-alarm-ui-gases = {$gas}: {$amount} mol ({$percentage}%)
@@ -48,6 +47,8 @@ air-alarm-ui-scrubber-wide-net-label = WideNet
### Thresholds
air-alarm-ui-sensor-gases = Gases
air-alarm-ui-sensor-thresholds = Thresholds
air-alarm-ui-thresholds-pressure-title = Pressure (kPa)
air-alarm-ui-thresholds-temperature-title = Temperature (K)
air-alarm-ui-thresholds-gas-title = {$gas} (%)

View File

@@ -19,3 +19,4 @@ device-frequency-prototype-name-surveillance-camera-entertainment = Entertainmen
# prefixes for randomly generated device addresses
device-address-prefix-vent = Vnt-
device-address-prefix-scrubber = Scr-
device-address-prefix-sensor = Sns-

View File

@@ -5,9 +5,9 @@
description: Apply crowbar.
components:
- type: AtmosAlarmable
alarmedBy:
- FireAlarm
- AirAlarm
syncWith:
- FireAlarm
- AirAlarm
- type: ApcPowerReceiver
- type: ExtensionCableReceiver
- type: DeviceNetwork

View File

@@ -32,9 +32,25 @@
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-vent
- type: AtmosAlarmable
alarmedBy:
- AirAlarm
sendBroadcastAttemptEvent: true
- type: WiredNetworkConnection
- type: AtmosDevice
- type: AtmosMonitor
temperatureThreshold: stationTemperature
pressureThreshold: stationPressure
gasThresholds:
Oxygen: stationOxygen
Nitrogen: ignore
CarbonDioxide: stationCO2
Plasma: danger # everything below is usually bad
Tritium: danger
WaterVapor: danger
Miasma: danger
NitrousOxide: danger
Frezon: danger
- type: Tag
tags:
- GasVent
- type: Sprite
netsync: false
drawdepth: FloorObjects
@@ -108,9 +124,22 @@
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-scrubber
- type: AtmosAlarmable
alarmedBy:
- AirAlarm
- type: AtmosMonitor
temperatureThreshold: stationTemperature
pressureThreshold: stationPressure
gasThresholds:
Oxygen: stationOxygen
Nitrogen: ignore
CarbonDioxide: stationCO2
Plasma: danger # everything below is usually bad
Tritium: danger
WaterVapor: danger
Miasma: danger
NitrousOxide: danger
Frezon: danger
- type: Tag
tags:
- GasScrubber
- type: Sprite
netsync: false
drawdepth: FloorObjects

View File

@@ -0,0 +1,97 @@
- type: entity
id: AirSensor
name: air sensor
description: Air sensor. It senses air.
placement:
mode: SnapgridCenter
components:
- type: Transform
anchored: true
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:DoActsBehavior
acts: ["Destruction"]
- type: Physics
canCollide: false
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
mass: 5
mask:
- ItemMask
restitution: 0.3
friction: 0.2
- type: Clickable
- type: InteractionOutline
- type: ApcPowerReceiver
- type: ExtensionCableReceiver
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-sensor
sendBroadcastAttemptEvent: true
- type: WiredNetworkConnection
- type: AtmosDevice
- type: AtmosMonitor
temperatureThreshold: stationTemperature
pressureThreshold: stationPressure
gasThresholds:
Oxygen: stationOxygen
Nitrogen: ignore
CarbonDioxide: stationCO2
Plasma: danger # everything below is usually bad
Tritium: danger
WaterVapor: danger
Miasma: danger
NitrousOxide: danger
Frezon: danger
- type: Tag
tags:
- AirSensor
- type: AccessReader
access: [ [ "Atmospherics" ] ]
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
sensor:
True: { state: gsensor1 }
False: { state: gsensor0 }
- type: Construction
graph: AirSensor
node: sensor
- type: Sprite
netsync: false
drawdepth: FloorObjects
sprite: Structures/Specific/Atmospherics/sensor.rsi
layers:
- state: gsensor1
map: [ "sensor" ]
- type: entity
parent: BaseItem
id: AirSensorAssembly
name: air sensor assembly
description: Air sensor assembly. An assembly of air sensors?
components:
- type: Item
size: 10
- type: Anchorable
- type: Construction
graph: AirSensor
node: assembly
- type: Sprite
netsync: false
drawdepth: FloorObjects
sprite: Structures/Specific/Atmospherics/sensor.rsi
layers:
- state: gsensor0

View File

@@ -16,21 +16,23 @@
sendBroadcastAttemptEvent: true
- type: WiredNetworkConnection
- type: DeviceList
- type: AtmosMonitor
temperatureThreshold: stationTemperature
pressureThreshold: stationPressure
gasThresholds:
Oxygen: stationOxygen
Nitrogen: ignore
CarbonDioxide: stationCO2
Plasma: danger # everything below is usually bad
Tritium: danger
WaterVapor: danger
Miasma: danger
NitrousOxide: danger
Frezon: danger
- type: AtmosAlarmable
alarmedBy: ["AirAlarm"]
syncWith:
- AirAlarm
- AirSensor
- GasVent
- GasScrubber
- type: AtmosAlarmableVisuals
layerMap: "airAlarmBase"
alarmStates:
Normal: alarm0
Warning: alarm2
Danger: alarm1
setOnDepowered:
airAlarmBase: alarmp
- type: Tag
tags:
- AirAlarm
- type: AtmosDevice
- type: AirAlarm
- type: Clickable
@@ -47,15 +49,6 @@
- type: AccessReader
access: [["Atmospherics"]]
- type: Appearance
visuals:
- type: AtmosMonitorVisualizer
layerMap: "airAlarmBase"
alarmStates:
Normal: alarm0
Warning: alarm2
Danger: alarm1
setOnDepowered:
airAlarmBase: alarmp
- type: WiresVisuals
- type: Sprite
sprite: Structures/Wallmounts/air_monitors.rsi

View File

@@ -13,26 +13,35 @@
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
- type: ApcNetworkConnection
- type: AtmosMonitor
monitorFire: true
displayMaxAlarmInNet: true
sendBroadcastAttemptEvent: true
- type: DeviceList
- type: WiredNetworkConnection
- type: AtmosDevice
- type: AtmosAlarmable
alarmedBy: ["FireAlarm"] # alarm itself, network effect
syncWith:
- FireAlarm
- AirSensor
- GasVent
- GasScrubber
monitorAlertTypes:
- Temperature
- type: AtmosAlarmableVisuals
layerMap: "fireAlarmState"
alarmStates:
Normal: fire_off
Warning: fire_off # shouldn't be alarming at a warning
Danger: fire_on
Emagged: fire_emagged
hideOnDepowered: [ "fireAlarmState" ]
- type: Tag
tags:
- FireAlarm
- type: Clickable
- type: InteractionOutline
- type: FireAlarm
- type: AccessReader
access: [ [ "Atmospherics" ] ]
- type: Appearance
visuals:
- type: AtmosMonitorVisualizer
layerMap: "fireAlarmState"
alarmStates:
Normal: fire_off
Warning: fire_off # shouldn't be alarming at a warning
Danger: fire_on
Emagged: fire_emagged
hideOnDepowered: ["fireAlarmState"]
- type: WiresVisuals
- type: AlertLevelDisplay
alertVisuals:

View File

@@ -0,0 +1,42 @@
- type: constructionGraph
id: AirSensor
start: start
graph:
- node: start
edges:
- to: assembly
steps:
- material: Steel
amount: 2
doAfter: 1
- node: assembly
entity: AirSensorAssembly
actions:
- !type:SpriteStateChange
state: gsensor0
edges:
- to: start
conditions:
- !type:EntityAnchored
anchored: false
completed:
- !type:SpawnPrototype
prototype: SheetSteel1
amount: 2
- !type:DeleteEntity {}
steps:
- tool: Screwing
doAfter: 2
- to: sensor
conditions:
- !type:EntityAnchored {}
steps:
- tool: Welding
doAfter: 5
- node: sensor
entity: AirSensor
edges:
- to: assembly
steps:
- tool: Welding
doAfter: 5

View File

@@ -284,6 +284,21 @@
conditions:
- !type:WallmountCondition {}
- type: construction
name: air sensor
id: AirSensor
graph: AirSensor
startNode: start
targetNode: sensor
category: construction-category-structures
description: An air sensor. Senses air.
icon:
sprite: Structures/Specific/Atmospherics/sensor.rsi
state: gsensor1
placementMode: SnapgridCenter
objectType: Structure
canRotate: true
# ATMOS PIPES
- type: construction
name: gas pipe half

View File

@@ -1,8 +1,14 @@
# Alphabetical order is now apparently required.
- type: Tag
id: AirAlarm
- type: Tag
id: AirAlarmElectronics
- type: Tag
id: AirSensor
- type: Tag
id: ATVKeys
@@ -180,6 +186,9 @@
- type: Tag
id: ExplosivePassable
- type: Tag
id: FireAlarm
- type: Tag
id: FireAlarmElectronics
@@ -210,6 +219,12 @@
- type: Tag
id: ForceNoFixRotations # fixrotations command WON'T target this
- type: Tag
id: GasScrubber
- type: Tag
id: GasVent
- type: Tag
id: Gauze

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View File

@@ -0,0 +1,13 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC 3.0 BY-SA",
"copyright": "Taken from /tg/station on commit https://github.com/tgstation/tgstation/commit/1dd5021b2c5b64e954935bdf77bc973c34016a04",
"states": [
{ "name": "gsensor0" },
{ "name": "gsensor1" }
]
}