using Content.Server.Atmos.Monitor.Components; using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.Pinpointer; using Content.Server.Power.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Consoles; using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Monitor.Components; using Content.Shared.DeviceNetwork.Components; using Content.Shared.Pinpointer; using Robust.Server.GameObjects; using Robust.Shared.Map.Components; using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Content.Server.Atmos.Monitor.Systems; public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem { [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!; [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly NavMapSystem _navMapSystem = default!; [Dependency] private readonly DeviceListSystem _deviceListSystem = default!; private const float UpdateTime = 1.0f; // Note: this data does not need to be saved private float _updateTimer = 1.0f; public override void Initialize() { base.Initialize(); // Console events SubscribeLocalEvent(OnConsoleInit); SubscribeLocalEvent(OnConsoleParentChanged); SubscribeLocalEvent(OnFocusChangedMessage); // Grid events SubscribeLocalEvent(OnGridSplit); // Alarm events SubscribeLocalEvent(OnDeviceTerminatingEvent); SubscribeLocalEvent(OnDeviceAnchorChanged); } #region Event handling private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args) { InitalizeConsole(uid, component); } private void OnConsoleParentChanged(EntityUid uid, AtmosAlertsComputerComponent component, EntParentChangedMessage args) { InitalizeConsole(uid, component); } private void OnFocusChangedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerFocusChangeMessage args) { component.FocusDevice = args.FocusDevice; } private void OnGridSplit(ref GridSplitEvent args) { // Collect grids var allGrids = args.NewGrids.ToList(); if (!allGrids.Contains(args.Grid)) allGrids.Add(args.Grid); // Update atmos monitoring consoles that stand upon an updated grid var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var entConsole, out var entXform)) { if (entXform.GridUid == null) continue; if (!allGrids.Contains(entXform.GridUid.Value)) continue; InitalizeConsole(ent, entConsole); } } private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args) { OnDeviceAdditionOrRemoval(uid, component, args.Anchored); } private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args) { OnDeviceAdditionOrRemoval(uid, component, false); } private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding) { var xform = Transform(uid); var gridUid = xform.GridUid; if (gridUid == null) return; if (!TryComp(xform.GridUid, out var navMap)) return; if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data)) return; var netEntity = GetNetEntity(uid); var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var entConsole, out var entXform)) { if (gridUid != entXform.GridUid) continue; if (isAdding) { entConsole.AtmosDevices.Add(data.Value); } else { entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity); _navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity); } Dirty(ent, entConsole); } } #endregion public override void Update(float frameTime) { base.Update(frameTime); _updateTimer += frameTime; if (_updateTimer >= UpdateTime) { _updateTimer -= UpdateTime; // Keep a list of UI entries for each gridUid, in case multiple consoles stand on the same grid var airAlarmEntriesForEachGrid = new Dictionary(); var fireAlarmEntriesForEachGrid = new Dictionary(); var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var entConsole, out var entXform)) { if (entXform?.GridUid == null) continue; // Make a list of alarm state data for all the air and fire alarms on the grid if (!airAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var airAlarmEntries)) { airAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.AirAlarm).ToArray(); airAlarmEntriesForEachGrid[entXform.GridUid.Value] = airAlarmEntries; } if (!fireAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var fireAlarmEntries)) { fireAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.FireAlarm).ToArray(); fireAlarmEntriesForEachGrid[entXform.GridUid.Value] = fireAlarmEntries; } // Determine the highest level of alert for the console (based on non-silenced alarms) var highestAlert = AtmosAlarmType.Invalid; foreach (var entry in airAlarmEntries) { if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity)) highestAlert = entry.AlarmState; } foreach (var entry in fireAlarmEntries) { if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity)) highestAlert = entry.AlarmState; } // Update the appearance of the console based on the highest recorded level of alert if (TryComp(ent, out var entAppearance)) _appearance.SetData(ent, AtmosAlertsComputerVisuals.ComputerLayerScreen, (int) highestAlert, entAppearance); // If the console UI is open, send UI data to each subscribed session UpdateUIState(ent, airAlarmEntries, fireAlarmEntries, entConsole, entXform); } } } public void UpdateUIState (EntityUid uid, AtmosAlertsComputerEntry[] airAlarmStateData, AtmosAlertsComputerEntry[] fireAlarmStateData, AtmosAlertsComputerComponent component, TransformComponent xform) { if (!_userInterfaceSystem.IsUiOpen(uid, AtmosAlertsComputerUiKey.Key)) return; var gridUid = xform.GridUid!.Value; if (!HasComp(gridUid)) return; // The grid must have a NavMapComponent to visualize the map in the UI EnsureComp(gridUid); // Gathering remaining data to be send to the client var focusAlarmData = GetFocusAlarmData(uid, GetEntity(component.FocusDevice), gridUid); // Set the UI state _userInterfaceSystem.SetUiState(uid, AtmosAlertsComputerUiKey.Key, new AtmosAlertsComputerBoundInterfaceState(airAlarmStateData, fireAlarmStateData, focusAlarmData)); } private List GetAlarmStateData(EntityUid gridUid, AtmosAlertsComputerGroup group) { var alarmStateData = new List(); var queryAlarms = AllEntityQuery(); while (queryAlarms.MoveNext(out var ent, out var entDevice, out var entAtmosAlarmable, out var entDeviceNetwork, out var entXform)) { if (entXform.GridUid != gridUid) continue; if (!entXform.Anchored) continue; if (entDevice.Group != group) continue; if (!TryComp(entXform.GridUid, out var mapGrid)) continue; if (!TryComp(entXform.GridUid, out var navMap)) continue; // If emagged, change the alarm type to normal var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState; // Unpowered alarms can't sound if (TryComp(ent, out var entAPCPower) && !entAPCPower.Powered) alarmState = AtmosAlarmType.Invalid; // Create entry var netEnt = GetNetEntity(ent); var entry = new AtmosAlertsComputerEntry (netEnt, GetNetCoordinates(entXform.Coordinates), entDevice.Group, alarmState, MetaData(ent).EntityName, entDeviceNetwork.Address); // Get the list of sensors attached to the alarm var sensorList = TryComp(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null; if (sensorList?.Any() == true) { var alarmRegionSeeds = new HashSet(); // If valid and anchored, use the position of sensors as seeds for the region foreach (var (address, sensorEnt) in sensorList) { if (!sensorEnt.IsValid() || !HasComp(sensorEnt)) continue; var sensorXform = Transform(sensorEnt); if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid) alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform))); } var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds); _navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties); } else { _navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt); } alarmStateData.Add(entry); } return alarmStateData; } private AtmosAlertsFocusDeviceData? GetFocusAlarmData(EntityUid uid, EntityUid? focusDevice, EntityUid gridUid) { if (focusDevice == null) return null; var focusDeviceXform = Transform(focusDevice.Value); if (!focusDeviceXform.Anchored || focusDeviceXform.GridUid != gridUid || !TryComp(focusDevice.Value, out var focusDeviceAirAlarm)) { return null; } // Force update the sensors attached to the alarm if (!_userInterfaceSystem.IsUiOpen(focusDevice.Value, SharedAirAlarmInterfaceKey.Key)) { _atmosDevNet.Register(focusDevice.Value, null); _atmosDevNet.Sync(focusDevice.Value, null); foreach ((var address, var _) in focusDeviceAirAlarm.SensorData) _atmosDevNet.Register(uid, null); } // Get the sensor data var temperatureData = (_airAlarmSystem.CalculateTemperatureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal); var pressureData = (_airAlarmSystem.CalculatePressureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal); var gasData = new Dictionary(); foreach ((var address, var sensorData) in focusDeviceAirAlarm.SensorData) { if (sensorData.TemperatureThreshold.CheckThreshold(sensorData.Temperature, out var temperatureState) && (int) temperatureState > (int) temperatureData.Item2) { temperatureData = (temperatureData.Item1, temperatureState); } if (sensorData.PressureThreshold.CheckThreshold(sensorData.Pressure, out var pressureState) && (int) pressureState > (int) pressureData.Item2) { pressureData = (pressureData.Item1, pressureState); } if (focusDeviceAirAlarm.SensorData.Sum(g => g.Value.TotalMoles) > 1e-8) { foreach ((var gas, var threshold) in sensorData.GasThresholds) { if (!gasData.ContainsKey(gas)) { float mol = _airAlarmSystem.CalculateGasMolarConcentrationAverage(focusDeviceAirAlarm, gas, out var percentage); if (mol < 1e-8) continue; gasData[gas] = (mol, percentage, AtmosAlarmType.Normal); } if (threshold.CheckThreshold(gasData[gas].Item2, out var gasState) && (int) gasState > (int) gasData[gas].Item3) { gasData[gas] = (gasData[gas].Item1, gasData[gas].Item2, gasState); } } } } return new AtmosAlertsFocusDeviceData(GetNetEntity(focusDevice.Value), temperatureData, pressureData, gasData); } private HashSet GetAllAtmosDeviceNavMapData(EntityUid gridUid) { var atmosDeviceNavMapData = new HashSet(); var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var entComponent, out var entXform)) { if (entXform.GridUid != gridUid) continue; if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data)) atmosDeviceNavMapData.Add(data.Value); } return atmosDeviceNavMapData; } private bool TryGetAtmosDeviceNavMapData (EntityUid uid, AtmosAlertsDeviceComponent component, TransformComponent xform, [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output) { output = null; if (!xform.Anchored) return false; output = new AtmosAlertsDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), component.Group); return true; } private void InitalizeConsole(EntityUid uid, AtmosAlertsComputerComponent component) { var xform = Transform(uid); if (xform.GridUid == null) return; var grid = xform.GridUid.Value; component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid); Dirty(uid, component); } }