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;
///
/// Command to set device data within the air alarm's network.
///
public const string AirAlarmSetData = "air_alarm_set_device_data";
///
/// Command to request a sync from devices in an air alarm's network.
///
public const string AirAlarmSyncCmd = "air_alarm_sync_devices";
///
/// Command to set an air alarm's mode.
///
public const string AirAlarmSetMode = "air_alarm_set_mode";
// -- Packet Data --
///
/// Data response to an AirAlarmSetData command.
///
public const string AirAlarmSetDataStatus = "air_alarm_set_device_data_status";
///
/// Data response to an AirAlarmSync command. Contains
/// IAtmosDeviceData in this system's implementation.
///
public const string AirAlarmSyncData = "air_alarm_device_sync_data";
// -- API --
///
/// Set the data for an air alarm managed device.
///
/// The address of the device.
/// The data to send to the device.
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);
}
///
/// Broadcast a sync packet to an air alarm's local network.
///
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);
}
///
/// Send a sync packet to a specific device from an air alarm.
///
/// The address of the device.
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);
}
///
/// Sync this air alarm's mode with the rest of the network.
///
/// The mode to sync with the rest of the network.
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(OnPacketRecv);
SubscribeLocalEvent(OnAtmosUpdate);
SubscribeLocalEvent(OnAtmosAlarm);
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnResyncAll);
SubscribeLocalEvent(OnUpdateAlarmMode);
SubscribeLocalEvent(OnUpdateThreshold);
SubscribeLocalEvent(OnUpdateDeviceData);
SubscribeLocalEvent(OnClose);
SubscribeLocalEvent(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
///
/// Set a threshold on an air alarm.
///
/// New threshold data.
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));
}
///
/// Set an air alarm's mode.
///
/// The origin address of this mode set. Used for network sync.
/// The mode to set the alarm to.
/// Whether to sync this mode change to the network or not. Defaults to false.
/// Whether this change is for the UI only, or if it changes the air alarm's operating mode. Defaults to true.
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);
}
///
/// Sets device data. Practically a wrapper around the packet sending function, SetData.
///
/// The address to send the new data to.
/// The device data to be sent.
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 _activeUserInterfaces = new();
///
/// Adds an active interface to be updated.
///
public void AddActiveInterface(EntityUid uid) =>
_activeUserInterfaces.Add(uid);
///
/// Removes an active interface from the system update loop.
///
public void RemoveActiveInterface(EntityUid uid) =>
_activeUserInterfaces.Remove(uid);
///
/// Force closes all interfaces currently open related to this air alarm.
///
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));
}
///
/// 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.
///
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();
foreach (var gas in Enum.GetValues())
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));
}
}
///
/// Send an air alarm mode to any open interface related to an air alarm.
///
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));
}
///
/// Send all thresholds to any open interface related to a given air alarm.
///
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
}
}