Adds fire/air alarms (#5018)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> Co-authored-by: E F R <602406+Efruit@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
224
Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs
Normal file
224
Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Atmos.Monitor.Systems;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.VendingMachines; // TODO: Move this out of vending machines???
|
||||
using Content.Server.WireHacking;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using static Content.Shared.Wires.SharedWiresComponent;
|
||||
using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class AirAlarmComponent : Component, IWires
|
||||
{
|
||||
[ComponentDependency] public readonly ApcPowerReceiverComponent? DeviceRecvComponent = default!;
|
||||
[ComponentDependency] public readonly AtmosMonitorComponent? AtmosMonitorComponent = default!;
|
||||
[ComponentDependency] public readonly DeviceNetworkComponent? DeviceNetComponent = default!;
|
||||
[ComponentDependency] public readonly WiresComponent? WiresComponent = null;
|
||||
|
||||
private AirAlarmSystem? _airAlarmSystem;
|
||||
|
||||
[ViewVariables] public AirAlarmMode CurrentMode { get; set; }
|
||||
|
||||
// Remember to null this afterwards.
|
||||
[ViewVariables] public IAirAlarmModeUpdate? CurrentModeUpdater { get; set; }
|
||||
|
||||
public override string Name => "AirAlarm";
|
||||
|
||||
public Dictionary<string, IAtmosDeviceData> DeviceData = new();
|
||||
|
||||
public HashSet<NetUserId> ActivePlayers = new();
|
||||
|
||||
public bool FullAccess = false;
|
||||
public bool CanSync = true;
|
||||
|
||||
// <-- Wires -->
|
||||
|
||||
private CancellationTokenSource _powerPulsedCancel = new();
|
||||
private int PowerPulsedTimeout = 30;
|
||||
|
||||
private enum Wires
|
||||
{
|
||||
// Cutting this kills power.
|
||||
// Pulsing it disrupts power.
|
||||
Power,
|
||||
// Cutting this allows full access.
|
||||
// Pulsing this does nothing.
|
||||
Access,
|
||||
// Cutting/Remending this resets ONLY from panic mode.
|
||||
// Pulsing this sets panic mode.
|
||||
Panic,
|
||||
// Cutting this clears sync'd devices, and makes
|
||||
// the alarm unable to resync.
|
||||
// Pulsing this resyncs all devices (ofc current
|
||||
// implementation just auto-does this anyways)
|
||||
DeviceSync,
|
||||
// This does nothing. (placeholder for AI wire,
|
||||
// if that ever gets implemented)
|
||||
Dummy
|
||||
}
|
||||
|
||||
public void RegisterWires(WiresComponent.WiresBuilder builder)
|
||||
{
|
||||
foreach (var wire in Enum.GetValues<Wires>())
|
||||
builder.CreateWire(wire);
|
||||
|
||||
UpdateWires();
|
||||
}
|
||||
|
||||
public void UpdateWires()
|
||||
{
|
||||
if (_airAlarmSystem == null)
|
||||
_airAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||
|
||||
if (WiresComponent == null) return;
|
||||
|
||||
var pwrLightState = (PowerPulsed, PowerCut) switch {
|
||||
(true, false) => StatusLightState.BlinkingFast,
|
||||
(_, true) => StatusLightState.Off,
|
||||
(_, _) => StatusLightState.On
|
||||
};
|
||||
|
||||
var powerLight = new StatusLightData(Color.Yellow, pwrLightState, "POWR");
|
||||
|
||||
var accessLight = new StatusLightData(
|
||||
Color.Green,
|
||||
WiresComponent.IsWireCut(Wires.Access) ? StatusLightState.Off : StatusLightState.On,
|
||||
"ACC"
|
||||
);
|
||||
|
||||
var panicLight = new StatusLightData(
|
||||
Color.Red,
|
||||
CurrentMode == AirAlarmMode.Panic ? StatusLightState.On : StatusLightState.Off,
|
||||
"PAN"
|
||||
);
|
||||
|
||||
var syncLightState = StatusLightState.BlinkingSlow;
|
||||
|
||||
if (AtmosMonitorComponent != null && !AtmosMonitorComponent.NetEnabled)
|
||||
syncLightState = StatusLightState.Off;
|
||||
else if (DeviceData.Count != 0)
|
||||
syncLightState = StatusLightState.On;
|
||||
|
||||
var syncLight = new StatusLightData(Color.Orange, syncLightState, "NET");
|
||||
|
||||
WiresComponent.SetStatus(AirAlarmWireStatus.Power, powerLight);
|
||||
WiresComponent.SetStatus(AirAlarmWireStatus.Access, accessLight);
|
||||
WiresComponent.SetStatus(AirAlarmWireStatus.Panic, panicLight);
|
||||
WiresComponent.SetStatus(AirAlarmWireStatus.DeviceSync, syncLight);
|
||||
}
|
||||
|
||||
private bool _powerCut;
|
||||
private bool PowerCut
|
||||
{
|
||||
get => _powerCut;
|
||||
set
|
||||
{
|
||||
_powerCut = value;
|
||||
SetPower();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _powerPulsed;
|
||||
private bool PowerPulsed
|
||||
{
|
||||
get => _powerPulsed && !_powerCut;
|
||||
set
|
||||
{
|
||||
_powerPulsed = value;
|
||||
SetPower();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPower()
|
||||
{
|
||||
if (DeviceRecvComponent != null
|
||||
&& WiresComponent != null)
|
||||
DeviceRecvComponent.PowerDisabled = PowerPulsed || PowerCut;
|
||||
}
|
||||
|
||||
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||
{
|
||||
if (DeviceNetComponent == null) return;
|
||||
|
||||
if (_airAlarmSystem == null)
|
||||
_airAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case Pulse:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.Power:
|
||||
PowerPulsed = true;
|
||||
_powerPulsedCancel.Cancel();
|
||||
_powerPulsedCancel = new CancellationTokenSource();
|
||||
Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout),
|
||||
() => PowerPulsed = false,
|
||||
_powerPulsedCancel.Token);
|
||||
break;
|
||||
case Wires.Panic:
|
||||
if (CurrentMode != AirAlarmMode.Panic)
|
||||
_airAlarmSystem.SetMode(Owner, DeviceNetComponent.Address, AirAlarmMode.Panic, true, false);
|
||||
break;
|
||||
case Wires.DeviceSync:
|
||||
_airAlarmSystem.SyncAllDevices(Owner);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Mend:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.Power:
|
||||
_powerPulsedCancel.Cancel();
|
||||
PowerPulsed = false;
|
||||
PowerCut = false;
|
||||
break;
|
||||
case Wires.Panic:
|
||||
if (CurrentMode == AirAlarmMode.Panic)
|
||||
_airAlarmSystem.SetMode(Owner, DeviceNetComponent.Address, AirAlarmMode.Filtering, true, false);
|
||||
break;
|
||||
case Wires.Access:
|
||||
FullAccess = false;
|
||||
break;
|
||||
case Wires.DeviceSync:
|
||||
if (AtmosMonitorComponent != null)
|
||||
AtmosMonitorComponent.NetEnabled = true;
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Cut:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.DeviceSync:
|
||||
DeviceData.Clear();
|
||||
if (AtmosMonitorComponent != null)
|
||||
{
|
||||
AtmosMonitorComponent.NetworkAlarmStates.Clear();
|
||||
AtmosMonitorComponent.NetEnabled = false;
|
||||
}
|
||||
|
||||
break;
|
||||
case Wires.Power:
|
||||
PowerCut = true;
|
||||
break;
|
||||
case Wires.Access:
|
||||
FullAccess = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateWires();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Components
|
||||
{
|
||||
// AtmosAlarmables are entities that can be alarmed
|
||||
// by a linked AtmosMonitor (alarmer?) if a threshold
|
||||
// is passed in some way. The intended use is to
|
||||
// do something in case something dangerous happens,
|
||||
// e.g., activate firelocks in case a temperature
|
||||
// threshold is reached
|
||||
//
|
||||
// It goes:
|
||||
//
|
||||
// AtmosMonitor -> AtmosDeviceUpdateEvent
|
||||
// -> Threshold calculation
|
||||
// -> AtmosMonitorAlarmEvent
|
||||
// -> Everything linked to that monitor (targetted)
|
||||
|
||||
/// <summary>
|
||||
/// A component to add to device network devices if you want them to be alarmed
|
||||
/// by an atmospheric monitor.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class AtmosAlarmableComponent : Component
|
||||
{
|
||||
public override string Name => "AtmosAlarmable";
|
||||
|
||||
[ViewVariables]
|
||||
public List<EntityUid> LinkedMonitors { get; set; } = new();
|
||||
|
||||
[ViewVariables] public AtmosMonitorAlarmType LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||
[ViewVariables] public AtmosMonitorAlarmType HighestNetworkState = AtmosMonitorAlarmType.Normal;
|
||||
[ViewVariables] public bool IgnoreAlarms { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// List of prototypes that this alarmable can be
|
||||
/// alarmed by - must be a prototype with AtmosMonitor
|
||||
/// attached to it
|
||||
/// </summary>
|
||||
[DataField("alarmedBy")]
|
||||
public List<string> AlarmedByPrototypes { get; } = new();
|
||||
}
|
||||
}
|
||||
114
Content.Server/Atmos/Monitor/Components/AtmosMonitorComponent.cs
Normal file
114
Content.Server/Atmos/Monitor/Components/AtmosMonitorComponent.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class AtmosMonitorComponent : Component
|
||||
{
|
||||
public override string Name => "AtmosMonitor";
|
||||
|
||||
// Requires power. No logic related to this will be performed here, however,
|
||||
// save for ensuring that the data is valid upon set.
|
||||
// This is how the system discovers entities that are alarmable.
|
||||
[ComponentDependency] public readonly ApcPowerReceiverComponent? PowerRecvComponent = default!;
|
||||
[ComponentDependency] public readonly AtmosDeviceComponent? AtmosDeviceComponent = default!;
|
||||
|
||||
// Whether this monitor can send alarms,
|
||||
// or recieve atmos command events.
|
||||
//
|
||||
// Useful for wires; i.e., pulsing a monitor wire
|
||||
// will make it send an alert, and cutting
|
||||
// it will make it so that alerts are no longer
|
||||
// sent/receieved.
|
||||
//
|
||||
// Note that this cancels every single network
|
||||
// event, including ones that may not be
|
||||
// related to atmos monitor events.
|
||||
[ViewVariables]
|
||||
public bool NetEnabled = true;
|
||||
|
||||
// Entities that the monitor will alarm. Stores only EntityUids, is populated
|
||||
// when this component starts up.
|
||||
[ViewVariables]
|
||||
public List<EntityUid> LinkedEntities = new();
|
||||
|
||||
[DataField("temperatureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
|
||||
public readonly string? TemperatureThresholdId;
|
||||
|
||||
[ViewVariables]
|
||||
public AtmosAlarmThreshold? TemperatureThreshold;
|
||||
|
||||
[DataField("pressureThreshold", customTypeSerializer: (typeof(PrototypeIdSerializer<AtmosAlarmThreshold>)))]
|
||||
public readonly string? PressureThresholdId;
|
||||
|
||||
[ViewVariables]
|
||||
public AtmosAlarmThreshold? PressureThreshold;
|
||||
|
||||
// monitor fire - much different from temperature
|
||||
// since there's events for fire, setting this to true
|
||||
// will make the atmos monitor act like a smoke detector,
|
||||
// immediately signalling danger if there's a fire
|
||||
[DataField("monitorFire")]
|
||||
public bool MonitorFire = false;
|
||||
|
||||
[DataField("displayMaxAlarmInNet")]
|
||||
public bool DisplayMaxAlarmInNet = false;
|
||||
|
||||
[DataField("alarmSound")]
|
||||
public SoundSpecifier AlarmSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/alarm.ogg");
|
||||
|
||||
[DataField("alarmVolume")]
|
||||
public float AlarmVolume { get; set; } = -10;
|
||||
|
||||
// really messy but this is parsed at runtime after
|
||||
// prototypes are initialized, there's no
|
||||
// way without implementing a new
|
||||
// type serializer
|
||||
[DataField("gasThresholds")]
|
||||
public Dictionary<Gas, string>? GasThresholdIds;
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
|
||||
|
||||
// Stores a reference to the gas on the tile this is on.
|
||||
[ViewVariables]
|
||||
public GasMixture? TileGas;
|
||||
|
||||
// Stores the last alarm state of this alarm.
|
||||
[ViewVariables]
|
||||
public AtmosMonitorAlarmType LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||
|
||||
// feeling real dirty about this one
|
||||
// Caches the alarm states it recieves from the rest of the network.
|
||||
// This is so that the highest alarm in the network can be calculated
|
||||
// from any monitor without having to reping every alarm.
|
||||
[ViewVariables]
|
||||
public Dictionary<string, AtmosMonitorAlarmType> NetworkAlarmStates = new();
|
||||
|
||||
// Calculates the highest alarm in the network, including itself.
|
||||
[ViewVariables]
|
||||
public AtmosMonitorAlarmType HighestAlarmInNetwork
|
||||
{
|
||||
get
|
||||
{
|
||||
var state = AtmosMonitorAlarmType.Normal;
|
||||
foreach (var (_, netState) in NetworkAlarmStates)
|
||||
if (state < netState)
|
||||
state = netState;
|
||||
|
||||
if (LastAlarmState > state) state = LastAlarmState;
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs
Normal file
170
Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Content.Server.Atmos.Monitor.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.VendingMachines; // TODO: Move this out of vending machines???
|
||||
using Content.Server.WireHacking;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using static Content.Shared.Wires.SharedWiresComponent;
|
||||
using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
|
||||
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class FireAlarmComponent : Component, IWires
|
||||
{
|
||||
[ComponentDependency] public readonly ApcPowerReceiverComponent? DeviceRecvComponent = default!;
|
||||
[ComponentDependency] public readonly AtmosMonitorComponent? AtmosMonitorComponent = default!;
|
||||
[ComponentDependency] public readonly WiresComponent? WiresComponent = null;
|
||||
|
||||
private AtmosMonitorSystem? _atmosMonitorSystem;
|
||||
|
||||
public override string Name => "FireAlarm";
|
||||
|
||||
private CancellationTokenSource _powerPulsedCancel = new();
|
||||
private int PowerPulsedTimeout = 30;
|
||||
|
||||
// Much more simpler than the air alarm wire set.
|
||||
private enum Wires
|
||||
{
|
||||
// Cutting this kills power,
|
||||
// pulsing it disrupts.
|
||||
Power,
|
||||
// Cutting this disables network
|
||||
// connectivity,
|
||||
// pulsing it sets off an alarm.
|
||||
Alarm,
|
||||
Dummy1,
|
||||
Dummy2,
|
||||
}
|
||||
|
||||
private bool _powerCut;
|
||||
private bool PowerCut
|
||||
{
|
||||
get => _powerCut;
|
||||
set
|
||||
{
|
||||
_powerCut = value;
|
||||
SetPower();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _powerPulsed;
|
||||
private bool PowerPulsed
|
||||
{
|
||||
get => _powerPulsed && !_powerCut;
|
||||
set
|
||||
{
|
||||
_powerPulsed = value;
|
||||
SetPower();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPower()
|
||||
{
|
||||
if (DeviceRecvComponent != null
|
||||
&& WiresComponent != null)
|
||||
DeviceRecvComponent.PowerDisabled = PowerPulsed || PowerCut;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterWires(WiresComponent.WiresBuilder builder)
|
||||
{
|
||||
builder.CreateWire(Wires.Power);
|
||||
builder.CreateWire(Wires.Alarm);
|
||||
builder.CreateWire(Wires.Dummy1);
|
||||
builder.CreateWire(Wires.Dummy2);
|
||||
|
||||
UpdateWires();
|
||||
}
|
||||
|
||||
public void UpdateWires()
|
||||
{
|
||||
if (WiresComponent == null) return;
|
||||
|
||||
var powerLight = new StatusLightData(Color.Yellow, StatusLightState.On, "POWR");
|
||||
|
||||
if (PowerPulsed)
|
||||
powerLight = new StatusLightData(Color.Yellow, StatusLightState.BlinkingFast, "POWR");
|
||||
else if (PowerCut)
|
||||
powerLight = new StatusLightData(Color.Yellow, StatusLightState.Off, "POWR");
|
||||
|
||||
var syncLight = new StatusLightData(Color.Orange, StatusLightState.On, "NET");
|
||||
|
||||
if (AtmosMonitorComponent != null)
|
||||
if (!AtmosMonitorComponent.NetEnabled)
|
||||
syncLight = new StatusLightData(Color.Orange, StatusLightState.Off, "NET");
|
||||
else if (AtmosMonitorComponent.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger)
|
||||
syncLight = new StatusLightData(Color.Orange, StatusLightState.BlinkingFast, "NET");
|
||||
|
||||
WiresComponent.SetStatus(FireAlarmWireStatus.Power, powerLight);
|
||||
WiresComponent.SetStatus(FireAlarmWireStatus.Alarm, syncLight);
|
||||
}
|
||||
|
||||
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||
{
|
||||
if (_atmosMonitorSystem == null)
|
||||
_atmosMonitorSystem = EntitySystem.Get<AtmosMonitorSystem>();
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case Pulse:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.Power:
|
||||
PowerPulsed = true;
|
||||
_powerPulsedCancel.Cancel();
|
||||
_powerPulsedCancel = new CancellationTokenSource();
|
||||
Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout),
|
||||
() => PowerPulsed = false,
|
||||
_powerPulsedCancel.Token);
|
||||
break;
|
||||
case Wires.Alarm:
|
||||
if (AtmosMonitorComponent != null)
|
||||
_atmosMonitorSystem.Alert(Owner, AtmosMonitorAlarmType.Danger);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case Mend:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.Power:
|
||||
_powerPulsedCancel.Cancel();
|
||||
PowerPulsed = false;
|
||||
PowerCut = false;
|
||||
break;
|
||||
case Wires.Alarm:
|
||||
if (AtmosMonitorComponent != null)
|
||||
AtmosMonitorComponent.NetEnabled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case Cut:
|
||||
switch (args.Identifier)
|
||||
{
|
||||
case Wires.Power:
|
||||
PowerCut = true;
|
||||
break;
|
||||
case Wires.Alarm:
|
||||
if (AtmosMonitorComponent != null)
|
||||
AtmosMonitorComponent.NetEnabled = false;
|
||||
break;
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
UpdateWires();
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Content.Server/Atmos/Monitor/Systems/AirAlarmModes.cs
Normal file
242
Content.Server/Atmos/Monitor/Systems/AirAlarmModes.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Atmos.Monitor.Systems;
|
||||
using Content.Server.Atmos.Piping.Unary.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor
|
||||
{
|
||||
/// <summary>
|
||||
/// This is an interface that air alarm modes use
|
||||
/// in order to execute the defined modes.
|
||||
/// </summary>
|
||||
public interface IAirAlarmMode
|
||||
{
|
||||
// This is executed the moment the mode
|
||||
// is set. This is to ensure that 'dumb'
|
||||
// modes such as Filter/Panic are immediately
|
||||
// set.
|
||||
/// <summary>
|
||||
/// Executed the mode is set on an air alarm.
|
||||
/// This is to ensure that modes like Filter/Panic
|
||||
/// are immediately set.
|
||||
/// </summary>
|
||||
public void Execute(EntityUid uid);
|
||||
}
|
||||
|
||||
// IAirAlarmModeUpdate
|
||||
//
|
||||
// This is an interface that AirAlarmSystem uses
|
||||
// in order to 'update' air alarm modes so that
|
||||
// modes like Replace can be implemented.
|
||||
/// <summary>
|
||||
/// An interface that AirAlarmSystem uses
|
||||
/// in order to update air alarm modes that
|
||||
/// need updating (e.g., Replace)
|
||||
/// </summary>
|
||||
public interface IAirAlarmModeUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// This is checked by AirAlarmSystem when
|
||||
/// a mode is updated. This should be set
|
||||
/// to a DeviceNetwork address, or some
|
||||
/// unique identifier that ID's the
|
||||
/// owner of the mode's executor.
|
||||
/// </summary>
|
||||
public string NetOwner { get; set; }
|
||||
/// <summary>
|
||||
/// This is executed every time the air alarm
|
||||
/// update loop is fully executed. This should
|
||||
/// be where all the logic goes.
|
||||
/// </summary>
|
||||
public void Update(EntityUid uid);
|
||||
}
|
||||
|
||||
public class AirAlarmModeFactory
|
||||
{
|
||||
private static IAirAlarmMode _filterMode = new AirAlarmFilterMode();
|
||||
private static IAirAlarmMode _fillMode = new AirAlarmFillMode();
|
||||
private static IAirAlarmMode _panicMode = new AirAlarmPanicMode();
|
||||
private static IAirAlarmMode _noneMode = new AirAlarmNoneMode();
|
||||
|
||||
// still not a fan since ReplaceMode must have an allocation
|
||||
// but it's whatever
|
||||
public static IAirAlarmMode? ModeToExecutor(AirAlarmMode mode) => mode switch
|
||||
{
|
||||
AirAlarmMode.Filtering => _filterMode,
|
||||
AirAlarmMode.Fill => _fillMode,
|
||||
AirAlarmMode.Panic => _panicMode,
|
||||
AirAlarmMode.None => _noneMode,
|
||||
AirAlarmMode.Replace => new AirAlarmReplaceMode(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
// like a tiny little EntitySystem
|
||||
public abstract class AirAlarmModeExecutor : IAirAlarmMode
|
||||
{
|
||||
[Dependency] public readonly IEntityManager EntityManager = default!;
|
||||
public readonly DeviceNetworkSystem DeviceNetworkSystem;
|
||||
public readonly AirAlarmSystem AirAlarmSystem;
|
||||
|
||||
public abstract void Execute(EntityUid uid);
|
||||
|
||||
public AirAlarmModeExecutor()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
DeviceNetworkSystem = EntitySystem.Get<DeviceNetworkSystem>();
|
||||
AirAlarmSystem = EntitySystem.Get<AirAlarmSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class AirAlarmNoneMode : AirAlarmModeExecutor
|
||||
{
|
||||
public override void Execute(EntityUid uid)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||
return;
|
||||
|
||||
foreach (var (addr, device) in alarm.DeviceData)
|
||||
{
|
||||
device.Enabled = false;
|
||||
AirAlarmSystem.SetData(uid, addr, device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AirAlarmFilterMode : AirAlarmModeExecutor
|
||||
{
|
||||
public override void Execute(EntityUid uid)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||
return;
|
||||
|
||||
foreach (var (addr, device) in alarm.DeviceData)
|
||||
{
|
||||
switch (device)
|
||||
{
|
||||
case GasVentPumpData pumpData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FilterModePreset);
|
||||
break;
|
||||
case GasVentScrubberData scrubberData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FilterModePreset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AirAlarmPanicMode : AirAlarmModeExecutor
|
||||
{
|
||||
public override void Execute(EntityUid uid)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||
return;
|
||||
|
||||
foreach (var (addr, device) in alarm.DeviceData)
|
||||
{
|
||||
switch (device)
|
||||
{
|
||||
case GasVentPumpData pumpData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.PanicModePreset);
|
||||
break;
|
||||
case GasVentScrubberData scrubberData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.PanicModePreset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AirAlarmFillMode : AirAlarmModeExecutor
|
||||
{
|
||||
public override void Execute(EntityUid uid)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm))
|
||||
return;
|
||||
|
||||
foreach (var (addr, device) in alarm.DeviceData)
|
||||
{
|
||||
switch (device)
|
||||
{
|
||||
case GasVentPumpData pumpData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentPumpData.FillModePreset);
|
||||
break;
|
||||
case GasVentScrubberData scrubberData:
|
||||
AirAlarmSystem.SetData(uid, addr, GasVentScrubberData.FillModePreset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AirAlarmReplaceMode : AirAlarmModeExecutor, IAirAlarmModeUpdate
|
||||
{
|
||||
private Dictionary<string, IAtmosDeviceData> _devices = new();
|
||||
private float _lastPressure = Atmospherics.OneAtmosphere;
|
||||
private AtmosMonitorComponent? _monitor;
|
||||
private AtmosAlarmableComponent? _alarmable;
|
||||
|
||||
public string NetOwner { get; set; } = string.Empty;
|
||||
|
||||
public override void Execute(EntityUid uid)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirAlarmComponent alarm)
|
||||
|| !EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent alarmable))
|
||||
return;
|
||||
|
||||
_devices = alarm.DeviceData;
|
||||
_monitor = monitor;
|
||||
_alarmable = alarmable;
|
||||
_alarmable.IgnoreAlarms = true;
|
||||
SetSiphon(uid);
|
||||
}
|
||||
|
||||
public void Update(EntityUid uid)
|
||||
{
|
||||
if (_monitor == null
|
||||
|| _alarmable == null
|
||||
|| _monitor.TileGas == null)
|
||||
return;
|
||||
|
||||
// just a little pointer
|
||||
var mixture = _monitor.TileGas;
|
||||
|
||||
_lastPressure = mixture.Pressure;
|
||||
if (_lastPressure <= 0.2f) // anything below and it might get stuck
|
||||
{
|
||||
_alarmable.IgnoreAlarms = false;
|
||||
AirAlarmSystem.SetMode(uid, NetOwner!, AirAlarmMode.Filtering, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSiphon(EntityUid uid)
|
||||
{
|
||||
foreach (var (addr, device) in _devices)
|
||||
{
|
||||
switch (device)
|
||||
{
|
||||
case GasVentPumpData pumpData:
|
||||
pumpData = GasVentPumpData.PanicModePreset;
|
||||
pumpData.IgnoreAlarms = true;
|
||||
AirAlarmSystem.SetData(uid, addr, pumpData);
|
||||
break;
|
||||
case GasVentScrubberData scrubberData:
|
||||
scrubberData = GasVentScrubberData.PanicModePreset;
|
||||
scrubberData.IgnoreAlarms = true;
|
||||
AirAlarmSystem.SetData(uid, addr, scrubberData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
541
Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
Normal file
541
Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
Normal file
@@ -0,0 +1,541 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.WireHacking;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems
|
||||
{
|
||||
// AirAlarm system - specific for atmos devices, rather than
|
||||
// atmos monitors.
|
||||
//
|
||||
// oh boy, message passing!
|
||||
//
|
||||
// Commands should always be sent into packet's Command
|
||||
// data key. In response, a packet will be transmitted
|
||||
// with the response type as its command, and the
|
||||
// response data in its data key.
|
||||
public class AirAlarmSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
|
||||
[Dependency] private readonly AtmosMonitorSystem _atmosMonitorSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
#region Device Network API
|
||||
|
||||
public const int Freq = AtmosMonitorSystem.AtmosMonitorApcFreq;
|
||||
|
||||
/// <summary>
|
||||
/// Command to set device data within the air alarm's network.
|
||||
/// </summary>
|
||||
public const string AirAlarmSetData = "air_alarm_set_device_data";
|
||||
|
||||
/// <summary>
|
||||
/// Command to request a sync from devices in an air alarm's network.
|
||||
/// </summary>
|
||||
public const string AirAlarmSyncCmd = "air_alarm_sync_devices";
|
||||
|
||||
/// <summary>
|
||||
/// Command to set an air alarm's mode.
|
||||
/// </summary>
|
||||
public const string AirAlarmSetMode = "air_alarm_set_mode";
|
||||
|
||||
// -- Packet Data --
|
||||
|
||||
/// <summary>
|
||||
/// Data response to an AirAlarmSetData command.
|
||||
/// </summary>
|
||||
public const string AirAlarmSetDataStatus = "air_alarm_set_device_data_status";
|
||||
|
||||
/// <summary>
|
||||
/// Data response to an AirAlarmSync command. Contains
|
||||
/// IAtmosDeviceData in this system's implementation.
|
||||
/// </summary>
|
||||
public const string AirAlarmSyncData = "air_alarm_device_sync_data";
|
||||
|
||||
// -- API --
|
||||
|
||||
/// <summary>
|
||||
/// Set the data for an air alarm managed device.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the device.</param>
|
||||
/// <param name="data">The data to send to the device.</param>
|
||||
public void SetData(EntityUid uid, string address, IAtmosDeviceData data)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||
&& !monitor.NetEnabled)
|
||||
return;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AirAlarmSetData,
|
||||
// [AirAlarmTypeData] = type,
|
||||
[AirAlarmSetData] = data
|
||||
};
|
||||
|
||||
_deviceNet.QueuePacket(uid, address, Freq, payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast a sync packet to an air alarm's local network.
|
||||
/// </summary>
|
||||
public void SyncAllDevices(EntityUid uid)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||
&& !monitor.NetEnabled)
|
||||
return;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AirAlarmSyncCmd
|
||||
};
|
||||
|
||||
_deviceNet.QueuePacket(uid, string.Empty, Freq, payload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a sync packet to a specific device from an air alarm.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the device.</param>
|
||||
public void SyncDevice(EntityUid uid, string address)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||
&& !monitor.NetEnabled)
|
||||
return;
|
||||
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AirAlarmSyncCmd
|
||||
};
|
||||
|
||||
_deviceNet.QueuePacket(uid, address, Freq, payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sync this air alarm's mode with the rest of the network.
|
||||
/// </summary>
|
||||
/// <param name="mode">The mode to sync with the rest of the network.</param>
|
||||
public void SyncMode(EntityUid uid, AirAlarmMode mode)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out AtmosMonitorComponent monitor)
|
||||
&& !monitor.NetEnabled)
|
||||
return;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AirAlarmSetMode,
|
||||
[AirAlarmSetMode] = mode
|
||||
};
|
||||
|
||||
_deviceNet.QueuePacket(uid, string.Empty, Freq, payload, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AirAlarmComponent, PacketSentEvent>(OnPacketRecv);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||
SubscribeLocalEvent<AirAlarmComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AirAlarmResyncAllDevicesMessage>(OnResyncAll);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateAlarmModeMessage>(OnUpdateAlarmMode);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateAlarmThresholdMessage>(OnUpdateThreshold);
|
||||
SubscribeLocalEvent<AirAlarmComponent, AirAlarmUpdateDeviceDataMessage>(OnUpdateDeviceData);
|
||||
SubscribeLocalEvent<AirAlarmComponent, BoundUIClosedEvent>(OnClose);
|
||||
SubscribeLocalEvent<AirAlarmComponent, InteractHandEvent>(OnInteract);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, AirAlarmComponent component, PowerChangedEvent args)
|
||||
{
|
||||
if (!args.Powered)
|
||||
{
|
||||
ForceCloseAllInterfaces(uid);
|
||||
component.CurrentModeUpdater = null;
|
||||
component.DeviceData.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClose(EntityUid uid, AirAlarmComponent component, BoundUIClosedEvent args)
|
||||
{
|
||||
component.ActivePlayers.Remove(args.Session.UserId);
|
||||
if (component.ActivePlayers.Count == 0)
|
||||
RemoveActiveInterface(uid);
|
||||
}
|
||||
|
||||
private void OnInteract(EntityUid uid, AirAlarmComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out WiresComponent wire) && wire.IsPanelOpen)
|
||||
{
|
||||
args.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out ApcPowerReceiverComponent recv) && !recv.Powered)
|
||||
return;
|
||||
|
||||
_uiSystem.GetUiOrNull(component.Owner, SharedAirAlarmInterfaceKey.Key)?.Open(actor.PlayerSession);
|
||||
component.ActivePlayers.Add(actor.PlayerSession.UserId);
|
||||
AddActiveInterface(uid);
|
||||
SendAddress(uid);
|
||||
SendAlarmMode(uid);
|
||||
SendThresholds(uid);
|
||||
SyncAllDevices(uid);
|
||||
SendAirData(uid);
|
||||
}
|
||||
|
||||
private void OnResyncAll(EntityUid uid, AirAlarmComponent component, AirAlarmResyncAllDevicesMessage args)
|
||||
{
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||
{
|
||||
component.DeviceData.Clear();
|
||||
SyncAllDevices(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUpdateAlarmMode(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmModeMessage args)
|
||||
{
|
||||
string addr = string.Empty;
|
||||
if (EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)) addr = netConn.Address;
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||
SetMode(uid, addr, args.Mode, true, false);
|
||||
else
|
||||
SendAlarmMode(uid);
|
||||
}
|
||||
|
||||
private void OnUpdateThreshold(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmThresholdMessage args)
|
||||
{
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||
SetThreshold(uid, args.Threshold, args.Type, args.Gas);
|
||||
else
|
||||
SendThresholds(uid);
|
||||
}
|
||||
|
||||
private void OnUpdateDeviceData(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateDeviceDataMessage args)
|
||||
{
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||
SetDeviceData(uid, args.Address, args.Data);
|
||||
else
|
||||
SyncDevice(uid, args.Address);
|
||||
}
|
||||
|
||||
private bool AccessCheck(EntityUid uid, EntityUid? user, AirAlarmComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out AccessReaderComponent reader) || user == null)
|
||||
return false;
|
||||
|
||||
if (!_accessSystem.IsAllowed(reader, user.Value) && !component.FullAccess)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("air-alarm-ui-access-denied"), user.Value, Filter.Entities(user.Value));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnAtmosAlarm(EntityUid uid, AirAlarmComponent component, AtmosMonitorAlarmEvent args)
|
||||
{
|
||||
if (component.ActivePlayers.Count != 0)
|
||||
{
|
||||
SyncAllDevices(uid);
|
||||
SendAirData(uid);
|
||||
}
|
||||
|
||||
string addr = string.Empty;
|
||||
if (EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn)) addr = netConn.Address;
|
||||
|
||||
|
||||
if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||
{
|
||||
SetMode(uid, addr, AirAlarmMode.None, true);
|
||||
// set mode to off to mimic the vents/scrubbers being turned off
|
||||
// update UI
|
||||
//
|
||||
// no, the mode isn't processed here - it's literally just
|
||||
// set to what mimics 'off'
|
||||
}
|
||||
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||
{
|
||||
// if the mode is still set to off, set it to filtering instead
|
||||
// alternatively, set it to the last saved mode
|
||||
//
|
||||
// no, this still doesn't execute the mode
|
||||
SetMode(uid, addr, AirAlarmMode.Filtering, true);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Air Alarm Settings
|
||||
|
||||
/// <summary>
|
||||
/// Set a threshold on an air alarm.
|
||||
/// </summary>
|
||||
/// <param name="threshold">New threshold data.</param>
|
||||
public void SetThreshold(EntityUid uid, AtmosAlarmThreshold threshold, AtmosMonitorThresholdType type, Gas? gas = null, AirAlarmComponent? controller = null)
|
||||
{
|
||||
if (!Resolve(uid, ref controller)) return;
|
||||
|
||||
_atmosMonitorSystem.SetThreshold(uid, type, threshold, gas);
|
||||
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(type, threshold, gas));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set an air alarm's mode.
|
||||
/// </summary>
|
||||
/// <param name="origin">The origin address of this mode set. Used for network sync.</param>
|
||||
/// <param name="mode">The mode to set the alarm to.</param>
|
||||
/// <param name="sync">Whether to sync this mode change to the network or not. Defaults to false.</param>
|
||||
/// <param name="uiOnly">Whether this change is for the UI only, or if it changes the air alarm's operating mode. Defaults to true.</param>
|
||||
public void SetMode(EntityUid uid, string origin, AirAlarmMode mode, bool sync = false, bool uiOnly = true, AirAlarmComponent? controller = null)
|
||||
{
|
||||
if (!Resolve(uid, ref controller)) return;
|
||||
controller.CurrentMode = mode;
|
||||
|
||||
// setting it to UI only maans we don't have
|
||||
// to deal with the issue of not-single-owner
|
||||
// alarm mode executors
|
||||
if (!uiOnly)
|
||||
{
|
||||
var newMode = AirAlarmModeFactory.ModeToExecutor(mode);
|
||||
if (newMode != null)
|
||||
{
|
||||
newMode.Execute(uid);
|
||||
if (newMode is IAirAlarmModeUpdate updatedMode)
|
||||
{
|
||||
controller.CurrentModeUpdater = updatedMode;
|
||||
controller.CurrentModeUpdater.NetOwner = origin;
|
||||
}
|
||||
else if (controller.CurrentModeUpdater != null)
|
||||
controller.CurrentModeUpdater = null;
|
||||
}
|
||||
}
|
||||
// only one air alarm in a network can use an air alarm mode
|
||||
// that updates, so even if it's a ui-only change,
|
||||
// we have to invalidte the last mode's updater and
|
||||
// remove it because otherwise it'll execute a now
|
||||
// invalid mode
|
||||
else if (controller.CurrentModeUpdater != null
|
||||
&& controller.CurrentModeUpdater.NetOwner != origin)
|
||||
controller.CurrentModeUpdater = null;
|
||||
|
||||
// controller.SendMessage(new AirAlarmUpdateAlarmModeMessage(mode));
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmModeMessage(mode));
|
||||
|
||||
|
||||
// setting sync deals with the issue of air alarms
|
||||
// in the same network needing to have the same mode
|
||||
// as other alarms
|
||||
if (sync) SyncMode(uid, mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets device data. Practically a wrapper around the packet sending function, SetData.
|
||||
/// </summary>
|
||||
/// <param name="address">The address to send the new data to.</param>
|
||||
/// <param name="devData">The device data to be sent.</param>
|
||||
public void SetDeviceData(EntityUid uid, string address, IAtmosDeviceData devData, AirAlarmComponent? controller = null)
|
||||
{
|
||||
if (!Resolve(uid, ref controller)) return;
|
||||
|
||||
devData.Dirty = true;
|
||||
SetData(uid, address, devData);
|
||||
}
|
||||
|
||||
private void OnPacketRecv(EntityUid uid, AirAlarmComponent controller, PacketSentEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
|
||||
return;
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case AirAlarmSyncData:
|
||||
if (!args.Data.TryGetValue(AirAlarmSyncData, out IAtmosDeviceData? data)
|
||||
|| data == null
|
||||
|| !controller.CanSync) break;
|
||||
|
||||
// Save into component.
|
||||
// Sync data to interface.
|
||||
// _airAlarmDataSystem.UpdateDeviceData(uid, args.SenderAddress, data);
|
||||
//
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateDeviceDataMessage(args.SenderAddress, data));
|
||||
if (controller.WiresComponent != null) controller.UpdateWires();
|
||||
if (!controller.DeviceData.TryAdd(args.SenderAddress, data))
|
||||
controller.DeviceData[args.SenderAddress] = data;
|
||||
|
||||
return;
|
||||
case AirAlarmSetDataStatus:
|
||||
if (!args.Data.TryGetValue(AirAlarmSetDataStatus, out bool dataStatus)) break;
|
||||
|
||||
// Sync data to interface.
|
||||
// This should say if the result
|
||||
// failed, or succeeded. Don't save it.l
|
||||
SyncDevice(uid, args.SenderAddress);
|
||||
|
||||
return;
|
||||
case AirAlarmSetMode:
|
||||
if (!args.Data.TryGetValue(AirAlarmSetMode, out AirAlarmMode alarmMode)) break;
|
||||
|
||||
SetMode(uid, args.SenderAddress, alarmMode);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI
|
||||
|
||||
// List of active user interfaces.
|
||||
private HashSet<EntityUid> _activeUserInterfaces = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds an active interface to be updated.
|
||||
/// </summary>
|
||||
public void AddActiveInterface(EntityUid uid) =>
|
||||
_activeUserInterfaces.Add(uid);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an active interface from the system update loop.
|
||||
/// </summary>
|
||||
public void RemoveActiveInterface(EntityUid uid) =>
|
||||
_activeUserInterfaces.Remove(uid);
|
||||
|
||||
/// <summary>
|
||||
/// Force closes all interfaces currently open related to this air alarm.
|
||||
/// </summary>
|
||||
public void ForceCloseAllInterfaces(EntityUid uid) =>
|
||||
_uiSystem.TryCloseAll(uid, SharedAirAlarmInterfaceKey.Key);
|
||||
|
||||
private void SendAddress(EntityUid uid, DeviceNetworkComponent? netConn = null)
|
||||
{
|
||||
if (!Resolve(uid, ref netConn)) return;
|
||||
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmSetAddressMessage(netConn.Address));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an interface's air data. This is all the 'hot' data
|
||||
/// that an air alarm contains server-side. Updated with a whopping 8
|
||||
/// delay automatically once a UI is in the loop.
|
||||
/// </summary>
|
||||
public void SendAirData(EntityUid uid, AirAlarmComponent? alarm = null, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null)
|
||||
{
|
||||
if (!Resolve(uid, ref alarm, ref monitor, ref power)) return;
|
||||
|
||||
if (!power.Powered) return;
|
||||
|
||||
|
||||
if (monitor.TileGas != null)
|
||||
{
|
||||
var gases = new Dictionary<Gas, float>();
|
||||
|
||||
foreach (var gas in Enum.GetValues<Gas>())
|
||||
gases.Add(gas, monitor.TileGas.GetMoles(gas));
|
||||
|
||||
var airData = new AirAlarmAirData(monitor.TileGas.Pressure, monitor.TileGas.Temperature, monitor.TileGas.TotalMoles, monitor.LastAlarmState, gases);
|
||||
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAirDataMessage(airData));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an air alarm mode to any open interface related to an air alarm.
|
||||
/// </summary>
|
||||
public void SendAlarmMode(EntityUid uid, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null, AirAlarmComponent? controller = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor, ref power, ref controller)
|
||||
|| !power.Powered) return;
|
||||
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmModeMessage(controller.CurrentMode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send all thresholds to any open interface related to a given air alarm.
|
||||
/// </summary>
|
||||
public void SendThresholds(EntityUid uid, AtmosMonitorComponent? monitor = null, ApcPowerReceiverComponent? power = null, AirAlarmComponent? controller = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor, ref power, ref controller)
|
||||
|| !power.Powered) return;
|
||||
|
||||
if (monitor.PressureThreshold == null
|
||||
&& monitor.TemperatureThreshold == null
|
||||
&& monitor.GasThresholds == null)
|
||||
return;
|
||||
|
||||
if (monitor.PressureThreshold != null)
|
||||
{
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Pressure, monitor.PressureThreshold));
|
||||
}
|
||||
|
||||
if (monitor.TemperatureThreshold != null)
|
||||
{
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Temperature, monitor.TemperatureThreshold));
|
||||
}
|
||||
|
||||
if (monitor.GasThresholds != null)
|
||||
{
|
||||
foreach (var (gas, threshold) in monitor.GasThresholds)
|
||||
_uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateAlarmThresholdMessage(AtmosMonitorThresholdType.Gas, threshold, gas));
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAtmosUpdate(EntityUid uid, AirAlarmComponent alarm, AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (alarm.CurrentModeUpdater != null)
|
||||
alarm.CurrentModeUpdater.Update(uid);
|
||||
}
|
||||
|
||||
private const float _delay = 8f;
|
||||
private float _timer = 0f;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
_timer += frameTime;
|
||||
if (_timer >= _delay)
|
||||
{
|
||||
_timer = 0f;
|
||||
foreach (var uid in _activeUserInterfaces)
|
||||
{
|
||||
SendAirData(uid);
|
||||
_uiSystem.TrySetUiState(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUIState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
43
Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
Normal file
43
Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems
|
||||
{
|
||||
public class AtmosAlarmableSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AtmosAlarmableComponent, PacketSentEvent>(OnPacketRecv);
|
||||
}
|
||||
|
||||
private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, PacketSentEvent args)
|
||||
{
|
||||
if (component.IgnoreAlarms) return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn))
|
||||
return;
|
||||
|
||||
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|
||||
&& cmd == AtmosMonitorSystem.AtmosMonitorAlarmCmd)
|
||||
{
|
||||
// does it have a state & network max state?
|
||||
// does it have a source?
|
||||
// and can this be alarmed by the source?
|
||||
// if so, raise an alarm
|
||||
if (args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out AtmosMonitorAlarmType state)
|
||||
&& args.Data.TryGetValue(AtmosMonitorSystem.AtmosMonitorAlarmNetMax, out AtmosMonitorAlarmType netMax)
|
||||
&& args.Data.TryGetValue(AtmosMonitorSystem.AtmosMonitorAlarmSrc, out string? source)
|
||||
&& component.AlarmedByPrototypes.Contains(source))
|
||||
{
|
||||
component.LastAlarmState = state;
|
||||
component.HighestNetworkState = netMax;
|
||||
RaiseLocalEvent(component.Owner, new AtmosMonitorAlarmEvent(state, netMax));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
488
Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
Normal file
488
Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
Normal file
@@ -0,0 +1,488 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems
|
||||
{
|
||||
// AtmosMonitorSystem. Grabs all the AtmosAlarmables connected
|
||||
// to it via local APC net, and starts sending updates of the
|
||||
// current atmosphere. Monitors fire (which always triggers as
|
||||
// a danger), and atmos (which triggers based on set thresholds).
|
||||
public class AtmosMonitorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
// Commands
|
||||
/// <summary>
|
||||
/// Command to alarm the network that something has happened.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmCmd = "atmos_monitor_alarm_update";
|
||||
|
||||
/// <summary>
|
||||
/// Command to sync this monitor's alarm state with the rest of the network.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmSyncCmd = "atmos_monitor_alarm_sync";
|
||||
|
||||
/// <summary>
|
||||
/// Command to reset all alarms on a network.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmResetAllCmd = "atmos_monitor_alarm_reset_all";
|
||||
|
||||
// Packet data
|
||||
/// <summary>
|
||||
/// Data response that contains the threshold types in an atmos monitor alarm.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmThresholdTypes = "atmos_monitor_alarm_threshold_types";
|
||||
|
||||
/// <summary>
|
||||
/// Data response that contains the source of an atmos alarm.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmSrc = "atmos_monitor_alarm_source";
|
||||
|
||||
/// <summary>
|
||||
/// Data response that contains the maximum alarm in an atmos alarm network.
|
||||
/// </summary>
|
||||
public const string AtmosMonitorAlarmNetMax = "atmos_monitor_alarm_net_max";
|
||||
|
||||
/// <summary>
|
||||
/// Frequency (all prototypes that use AtmosMonitor should use this)
|
||||
/// </summary>
|
||||
public const int AtmosMonitorApcFreq = 1621;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, ComponentInit>(OnAtmosMonitorInit);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, ComponentStartup>(OnAtmosMonitorStartup);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, ComponentShutdown>(OnAtmosMonitorShutdown);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, TileFireEvent>(OnFireEvent);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, PowerChangedEvent>(OnPowerChangedEvent);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, BeforePacketSentEvent>(BeforePacketRecv);
|
||||
SubscribeLocalEvent<AtmosMonitorComponent, PacketSentEvent>(OnPacketRecv);
|
||||
}
|
||||
|
||||
private void OnAtmosMonitorInit(EntityUid uid, AtmosMonitorComponent component, ComponentInit args)
|
||||
{
|
||||
if (component.TemperatureThresholdId != null)
|
||||
component.TemperatureThreshold = _prototypeManager.Index<AtmosAlarmThreshold>(component.TemperatureThresholdId);
|
||||
|
||||
if (component.PressureThresholdId != null)
|
||||
component.PressureThreshold = _prototypeManager.Index<AtmosAlarmThreshold>(component.PressureThresholdId);
|
||||
|
||||
if (component.GasThresholdIds != null)
|
||||
{
|
||||
component.GasThresholds = new();
|
||||
foreach (var (gas, id) in component.GasThresholdIds)
|
||||
if (_prototypeManager.TryIndex<AtmosAlarmThreshold>(id, out var gasThreshold))
|
||||
component.GasThresholds.Add(gas, gasThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAtmosMonitorStartup(EntityUid uid, AtmosMonitorComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.PowerRecvComponent == null
|
||||
&& component.AtmosDeviceComponent != null)
|
||||
{
|
||||
_atmosDeviceSystem.LeaveAtmosphere(component.AtmosDeviceComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
_checkPos.Add(uid);
|
||||
}
|
||||
|
||||
private void OnAtmosMonitorShutdown(EntityUid uid, AtmosMonitorComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (_checkPos.Contains(uid)) _checkPos.Remove(uid);
|
||||
}
|
||||
|
||||
// hackiest shit ever but there's no PostStartup event
|
||||
private HashSet<EntityUid> _checkPos = new();
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var uid in _checkPos)
|
||||
OpenAirOrReposition(uid);
|
||||
}
|
||||
|
||||
private void OpenAirOrReposition(EntityUid uid, AtmosMonitorComponent? component = null, AppearanceComponent? appearance = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref appearance)) return;
|
||||
|
||||
var transform = Transform(component.Owner);
|
||||
// atmos alarms will first attempt to get the air
|
||||
// directly underneath it - if not, then it will
|
||||
// instead place itself directly in front of the tile
|
||||
// it is facing, and then visually shift itself back
|
||||
// via sprite offsets (SS13 style but fuck it)
|
||||
var coords = transform.Coordinates;
|
||||
|
||||
if (_atmosphereSystem.IsTileAirBlocked(coords))
|
||||
{
|
||||
|
||||
var rotPos = transform.LocalRotation.RotateVec(new Vector2(0, -1));
|
||||
transform.Anchored = false;
|
||||
coords = coords.Offset(rotPos);
|
||||
transform.Coordinates = coords;
|
||||
|
||||
appearance.SetData("offset", -rotPos);
|
||||
|
||||
transform.Anchored = true;
|
||||
}
|
||||
|
||||
GasMixture? air = _atmosphereSystem.GetTileMixture(coords);
|
||||
component.TileGas = air;
|
||||
|
||||
_checkPos.Remove(uid);
|
||||
}
|
||||
|
||||
private void BeforePacketRecv(EntityUid uid, AtmosMonitorComponent component, BeforePacketSentEvent args)
|
||||
{
|
||||
if (!component.NetEnabled) args.Cancel();
|
||||
}
|
||||
|
||||
private void OnPacketRecv(EntityUid uid, AtmosMonitorComponent component, PacketSentEvent args)
|
||||
{
|
||||
// sync the internal 'last alarm state' from
|
||||
// the other alarms, so that we can calculate
|
||||
// the highest network alarm state at any time
|
||||
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|
||||
|| !EntityManager.TryGetComponent(uid, out AtmosAlarmableComponent? alarmable)
|
||||
|| !EntityManager.TryGetComponent(uid, out DeviceNetworkComponent netConn))
|
||||
return;
|
||||
|
||||
// ignore packets from self, ignore from different frequency
|
||||
if (netConn.Address == args.SenderAddress) return;
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
// sync on alarm or explicit sync
|
||||
case AtmosMonitorAlarmCmd:
|
||||
case AtmosMonitorAlarmSyncCmd:
|
||||
if (args.Data.TryGetValue(AtmosMonitorAlarmSrc, out string? src)
|
||||
&& alarmable.AlarmedByPrototypes.Contains(src)
|
||||
&& args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out AtmosMonitorAlarmType state)
|
||||
&& !component.NetworkAlarmStates.TryAdd(args.SenderAddress, state))
|
||||
component.NetworkAlarmStates[args.SenderAddress] = state;
|
||||
break;
|
||||
case AtmosMonitorAlarmResetAllCmd:
|
||||
if (args.Data.TryGetValue(AtmosMonitorAlarmSrc, out string? resetSrc)
|
||||
&& alarmable.AlarmedByPrototypes.Contains(resetSrc))
|
||||
{
|
||||
component.LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||
component.NetworkAlarmStates.Clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (component.DisplayMaxAlarmInNet)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
|
||||
appearanceComponent.SetData("alarmType", component.HighestAlarmInNetwork);
|
||||
|
||||
if (component.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, component);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnPowerChangedEvent(EntityUid uid, AtmosMonitorComponent component, PowerChangedEvent args)
|
||||
{
|
||||
if (!args.Powered)
|
||||
{
|
||||
if (component.AtmosDeviceComponent != null
|
||||
&& component.AtmosDeviceComponent.JoinedGrid != null)
|
||||
{
|
||||
_atmosDeviceSystem.LeaveAtmosphere(component.AtmosDeviceComponent);
|
||||
component.TileGas = null;
|
||||
}
|
||||
|
||||
// clear memory when power cycled
|
||||
component.LastAlarmState = AtmosMonitorAlarmType.Normal;
|
||||
component.NetworkAlarmStates.Clear();
|
||||
}
|
||||
else if (args.Powered)
|
||||
{
|
||||
if (component.AtmosDeviceComponent != null
|
||||
&& component.AtmosDeviceComponent.JoinedGrid == null)
|
||||
{
|
||||
_atmosDeviceSystem.JoinAtmosphere(component.AtmosDeviceComponent);
|
||||
var coords = Transform(component.Owner).Coordinates;
|
||||
var air = _atmosphereSystem.GetTileMixture(coords);
|
||||
component.TileGas = air;
|
||||
}
|
||||
}
|
||||
|
||||
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData("powered", args.Powered);
|
||||
appearanceComponent.SetData("alarmType", component.LastAlarmState);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, TileFireEvent args)
|
||||
{
|
||||
if (component.PowerRecvComponent == null
|
||||
|| !component.PowerRecvComponent.Powered)
|
||||
return;
|
||||
|
||||
// if we're monitoring for atmos fire, then we make it similar to a smoke detector
|
||||
// and just outright trigger a danger event
|
||||
//
|
||||
// somebody else can reset it :sunglasses:
|
||||
if (component.MonitorFire
|
||||
&& component.LastAlarmState != AtmosMonitorAlarmType.Danger)
|
||||
Alert(uid, AtmosMonitorAlarmType.Danger, new []{ AtmosMonitorThresholdType.Temperature }, component); // technically???
|
||||
|
||||
// only monitor state elevation so that stuff gets alarmed quicker during a fire,
|
||||
// let the atmos update loop handle when temperature starts to reach different
|
||||
// thresholds and different states than normal -> warning -> danger
|
||||
if (component.TemperatureThreshold != null
|
||||
&& component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState)
|
||||
&& temperatureState > component.LastAlarmState)
|
||||
Alert(uid, AtmosMonitorAlarmType.Danger, new []{ AtmosMonitorThresholdType.Temperature }, component);
|
||||
}
|
||||
|
||||
private void OnAtmosUpdate(EntityUid uid, AtmosMonitorComponent component, AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (component.PowerRecvComponent == null
|
||||
|| !component.PowerRecvComponent.Powered)
|
||||
return;
|
||||
|
||||
// can't hurt
|
||||
// (in case something is making AtmosDeviceUpdateEvents
|
||||
// outside the typical device loop)
|
||||
if (component.AtmosDeviceComponent == null
|
||||
|| component.AtmosDeviceComponent.JoinedGrid == null)
|
||||
return;
|
||||
|
||||
// if we're not monitoring atmos, don't bother
|
||||
if (component.TemperatureThreshold == null
|
||||
&& component.PressureThreshold == null
|
||||
&& component.GasThresholds == null)
|
||||
return;
|
||||
|
||||
UpdateState(uid, component.TileGas, component);
|
||||
}
|
||||
|
||||
// Update checks the current air if it exceeds thresholds of
|
||||
// any kind.
|
||||
//
|
||||
// If any threshold exceeds the other, that threshold
|
||||
// immediately replaces the current recorded state.
|
||||
//
|
||||
// If the threshold does not match the current state
|
||||
// of the monitor, it is set in the Alert call.
|
||||
private void UpdateState(EntityUid uid, GasMixture? air, AtmosMonitorComponent? monitor = null)
|
||||
{
|
||||
if (air == null) return;
|
||||
|
||||
if (!Resolve(uid, ref monitor)) return;
|
||||
|
||||
AtmosMonitorAlarmType state = AtmosMonitorAlarmType.Normal;
|
||||
List<AtmosMonitorThresholdType> alarmTypes = new();
|
||||
|
||||
if (monitor.TemperatureThreshold != null
|
||||
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState)
|
||||
&& temperatureState > state)
|
||||
{
|
||||
state = temperatureState;
|
||||
alarmTypes.Add(AtmosMonitorThresholdType.Temperature);
|
||||
}
|
||||
|
||||
if (monitor.PressureThreshold != null
|
||||
&& monitor.PressureThreshold.CheckThreshold(air.Pressure, out var pressureState)
|
||||
&& pressureState > state)
|
||||
{
|
||||
state = pressureState;
|
||||
alarmTypes.Add(AtmosMonitorThresholdType.Pressure);
|
||||
}
|
||||
|
||||
if (monitor.GasThresholds != null)
|
||||
{
|
||||
foreach (var (gas, threshold) in monitor.GasThresholds)
|
||||
{
|
||||
var gasRatio = air.GetMoles(gas) / air.TotalMoles;
|
||||
if (threshold.CheckThreshold(gasRatio, out var gasState)
|
||||
&& gasState > state)
|
||||
{
|
||||
state = gasState;
|
||||
alarmTypes.Add(AtmosMonitorThresholdType.Gas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the state of the current air doesn't match the last alarm state,
|
||||
// we update the state
|
||||
if (state != monitor.LastAlarmState)
|
||||
{
|
||||
Alert(uid, state, alarmTypes, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alerts the network that the state of a monitor has changed.
|
||||
/// </summary>
|
||||
/// <param name="state">The alarm state to set this monitor to.</param>
|
||||
/// <param name="alarms">The alarms that caused this alarm state.</param>
|
||||
public void Alert(EntityUid uid, AtmosMonitorAlarmType state, IEnumerable<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)) return;
|
||||
monitor.LastAlarmState = state;
|
||||
if (EntityManager.TryGetComponent(monitor.Owner, out AppearanceComponent? appearanceComponent))
|
||||
appearanceComponent.SetData("alarmType", monitor.LastAlarmState);
|
||||
|
||||
BroadcastAlertPacket(monitor, alarms);
|
||||
|
||||
if (state == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, monitor);
|
||||
|
||||
if (EntityManager.TryGetComponent(monitor.Owner, out AtmosAlarmableComponent alarmable)
|
||||
&& !alarmable.IgnoreAlarms)
|
||||
RaiseLocalEvent(monitor.Owner, new AtmosMonitorAlarmEvent(monitor.LastAlarmState, monitor.HighestAlarmInNetwork));
|
||||
// TODO: Central system that grabs *all* alarms from wired network
|
||||
}
|
||||
|
||||
private void PlayAlertSound(EntityUid uid, AtmosMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)) return;
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(uid), monitor.AlarmSound.GetSound(), uid, AudioParams.Default.WithVolume(monitor.AlarmVolume));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets a single monitor's alarm.
|
||||
/// </summary>
|
||||
public void Reset(EntityUid uid) =>
|
||||
Alert(uid, AtmosMonitorAlarmType.Normal);
|
||||
|
||||
/// <summary>
|
||||
/// Resets a network's alarms, using this monitor as a source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The resulting packet will have this monitor set as the source, using its prototype ID if it has one - otherwise just sending an empty string.
|
||||
/// </remarks>
|
||||
public void ResetAll(EntityUid uid, AtmosMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)) return;
|
||||
|
||||
var prototype = Prototype(monitor.Owner);
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmResetAllCmd,
|
||||
[AtmosMonitorAlarmSrc] = prototype != null ? prototype.ID : string.Empty
|
||||
};
|
||||
|
||||
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||
monitor.NetworkAlarmStates.Clear();
|
||||
|
||||
Alert(uid, AtmosMonitorAlarmType.Normal, null, monitor);
|
||||
}
|
||||
|
||||
// (TODO: maybe just cache monitors in other monitors?)
|
||||
/// <summary>
|
||||
/// Syncs the current state of this monitor to the network (to avoid alerting other monitors).
|
||||
/// </summary>
|
||||
private void Sync(AtmosMonitorComponent monitor)
|
||||
{
|
||||
if (!monitor.NetEnabled) return;
|
||||
|
||||
var prototype = Prototype(monitor.Owner);
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmSyncCmd,
|
||||
[DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
|
||||
[AtmosMonitorAlarmSrc] = prototype != null ? prototype.ID : string.Empty
|
||||
};
|
||||
|
||||
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts an alert packet to all devices on the network,
|
||||
/// which consists of the current alarm types,
|
||||
/// the highest alarm currently cached by this monitor,
|
||||
/// and the current alarm state of the monitor (so other
|
||||
/// alarms can sync to it).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Alarmables use the highest alarm to ensure that a monitor's
|
||||
/// state doesn't override if the alarm is lower. The state
|
||||
/// is synced between monitors the moment a monitor sends out an alarm,
|
||||
/// or if it is explicitly synced (see ResetAll/Sync).
|
||||
/// </remarks>
|
||||
private void BroadcastAlertPacket(AtmosMonitorComponent monitor, IEnumerable<AtmosMonitorThresholdType>? alarms = null)
|
||||
{
|
||||
if (!monitor.NetEnabled) return;
|
||||
|
||||
string source = string.Empty;
|
||||
if (alarms == null) alarms = new List<AtmosMonitorThresholdType>();
|
||||
var prototype = Prototype(monitor.Owner);
|
||||
if (prototype != null) source = prototype.ID;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AtmosMonitorAlarmCmd,
|
||||
[DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
|
||||
[AtmosMonitorAlarmNetMax] = monitor.HighestAlarmInNetwork,
|
||||
[AtmosMonitorAlarmThresholdTypes] = alarms,
|
||||
[AtmosMonitorAlarmSrc] = source
|
||||
};
|
||||
|
||||
_deviceNetSystem.QueuePacket(monitor.Owner, string.Empty, AtmosMonitorApcFreq, payload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a monitor's threshold.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of threshold to change.</param>
|
||||
/// <param name="threshold">Threshold data.</param>
|
||||
/// <param name="gas">Gas, if applicable.</param>
|
||||
public void SetThreshold(EntityUid uid, AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null, AtmosMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)) return;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case AtmosMonitorThresholdType.Pressure:
|
||||
monitor.PressureThreshold = threshold;
|
||||
break;
|
||||
case AtmosMonitorThresholdType.Temperature:
|
||||
monitor.TemperatureThreshold = threshold;
|
||||
break;
|
||||
case AtmosMonitorThresholdType.Gas:
|
||||
if (gas == null || monitor.GasThresholds == null) return;
|
||||
monitor.GasThresholds[(Gas) gas] = threshold;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class AtmosMonitorAlarmEvent : EntityEventArgs
|
||||
{
|
||||
public AtmosMonitorAlarmType Type { get; }
|
||||
public AtmosMonitorAlarmType HighestNetworkType { get; }
|
||||
|
||||
public AtmosMonitorAlarmEvent(AtmosMonitorAlarmType type, AtmosMonitorAlarmType netMax)
|
||||
{
|
||||
Type = type;
|
||||
HighestNetworkType = netMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs
Normal file
42
Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems
|
||||
{
|
||||
public class FireAlarmSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosMonitorSystem _monitorSystem = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<FireAlarmComponent, InteractHandEvent>(OnInteractHand);
|
||||
}
|
||||
|
||||
private void OnInteractHand(EntityUid uid, FireAlarmComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
|
||||
return;
|
||||
|
||||
if (EntityManager.TryGetComponent(args.User, out ActorComponent? actor)
|
||||
&& EntityManager.TryGetComponent(uid, out AtmosMonitorComponent? monitor)
|
||||
&& EntityManager.TryGetComponent(uid, out ApcPowerReceiverComponent? power)
|
||||
&& power.Powered)
|
||||
{
|
||||
if (monitor.HighestAlarmInNetwork == AtmosMonitorAlarmType.Normal)
|
||||
{
|
||||
_monitorSystem.Alert(uid, AtmosMonitorAlarmType.Danger);
|
||||
}
|
||||
else
|
||||
{
|
||||
_monitorSystem.ResetAll(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user