diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs index e533ef2dce..f0e4b13356 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -31,19 +31,6 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state", }; - private Dictionary _gasShorthands = new Dictionary() - { - [Gas.Ammonia] = "NH₃", - [Gas.CarbonDioxide] = "CO₂", - [Gas.Frezon] = "F", - [Gas.Nitrogen] = "N₂", - [Gas.NitrousOxide] = "N₂O", - [Gas.Oxygen] = "O₂", - [Gas.Plasma] = "P", - [Gas.Tritium] = "T", - [Gas.WaterVapor] = "H₂O", - }; - public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates) { RobustXamlLoader.Load(this); @@ -162,12 +149,11 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs) { FixedPoint2 gasPercent = percent * 100f; - - var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X"); + var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); var gasLabel = new Label() { - Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)), + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)), FontOverride = normalFont, FontColorOverride = GetAlarmStateColor(alert), HorizontalAlignment = HAlignment.Center, diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs new file mode 100644 index 0000000000..563122f962 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs @@ -0,0 +1,40 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosMonitoringConsoleBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private AtmosMonitoringConsoleWindow? _menu; + + public AtmosMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } + + protected override void Open() + { + base.Open(); + + _menu = new AtmosMonitoringConsoleWindow(this, Owner); + _menu.OpenCentered(); + _menu.OnClose += Close; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not AtmosMonitoringConsoleBoundInterfaceState castState) + return; + + EntMan.TryGetComponent(Owner, out var xform); + _menu?.UpdateUI(xform?.Coordinates, castState.AtmosNetworks); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs new file mode 100644 index 0000000000..c23ebb6435 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs @@ -0,0 +1,295 @@ +using Content.Client.Pinpointer.UI; +using Content.Shared.Atmos.Components; +using Content.Shared.Pinpointer; +using Robust.Client.Graphics; +using Robust.Shared.Collections; +using Robust.Shared.Map.Components; +using System.Linq; +using System.Numerics; + +namespace Content.Client.Atmos.Consoles; + +public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public bool ShowPipeNetwork = true; + public int? FocusNetId = null; + + private const int ChunkSize = 4; + + private readonly Color _basePipeNetColor = Color.LightGray; + private readonly Color _unfocusedPipeNetColor = Color.DimGray; + + private List _atmosPipeNetwork = new(); + private Dictionary _sRGBLookUp = new Dictionary(); + + // Look up tables for merging continuous lines. Indexed by line color + private Dictionary> _horizLines = new(); + private Dictionary> _horizLinesReversed = new(); + private Dictionary> _vertLines = new(); + private Dictionary> _vertLinesReversed = new(); + + public AtmosMonitoringConsoleNavMapControl() : base() + { + PostWallDrawingAction += DrawAllPipeNetworks; + } + + protected override void UpdateNavMap() + { + base.UpdateNavMap(); + + if (!_entManager.TryGetComponent(Owner, out var console)) + return; + + if (!_entManager.TryGetComponent(MapUid, out var grid)) + return; + + _atmosPipeNetwork = GetDecodedAtmosPipeChunks(console.AtmosPipeChunks, grid); + } + + private void DrawAllPipeNetworks(DrawingHandleScreen handle) + { + if (!ShowPipeNetwork) + return; + + // Draw networks + if (_atmosPipeNetwork != null && _atmosPipeNetwork.Any()) + DrawPipeNetwork(handle, _atmosPipeNetwork); + } + + private void DrawPipeNetwork(DrawingHandleScreen handle, List atmosPipeNetwork) + { + var offset = GetOffset(); + offset = offset with { Y = -offset.Y }; + + if (WorldRange / WorldMaxRange > 0.5f) + { + var pipeNetworks = new Dictionary>(); + + foreach (var chunkedLine in atmosPipeNetwork) + { + var start = ScalePosition(chunkedLine.Origin - offset); + var end = ScalePosition(chunkedLine.Terminus - offset); + + if (!pipeNetworks.TryGetValue(chunkedLine.Color, out var subNetwork)) + subNetwork = new ValueList(); + + subNetwork.Add(start); + subNetwork.Add(end); + + pipeNetworks[chunkedLine.Color] = subNetwork; + } + + foreach ((var color, var subNetwork) in pipeNetworks) + { + if (subNetwork.Count > 0) + handle.DrawPrimitives(DrawPrimitiveTopology.LineList, subNetwork.Span, color); + } + } + + else + { + var pipeVertexUVs = new Dictionary>(); + + foreach (var chunkedLine in atmosPipeNetwork) + { + var leftTop = ScalePosition(new Vector2 + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + - offset); + + var rightTop = ScalePosition(new Vector2 + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + - offset); + + var leftBottom = ScalePosition(new Vector2 + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + - offset); + + var rightBottom = ScalePosition(new Vector2 + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + - offset); + + if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV)) + pipeVertexUV = new ValueList(); + + pipeVertexUV.Add(leftBottom); + pipeVertexUV.Add(leftTop); + pipeVertexUV.Add(rightBottom); + pipeVertexUV.Add(leftTop); + pipeVertexUV.Add(rightBottom); + pipeVertexUV.Add(rightTop); + + pipeVertexUVs[chunkedLine.Color] = pipeVertexUV; + } + + foreach ((var color, var pipeVertexUV) in pipeVertexUVs) + { + if (pipeVertexUV.Count > 0) + handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, pipeVertexUV.Span, color); + } + } + } + + private List GetDecodedAtmosPipeChunks(Dictionary? chunks, MapGridComponent? grid) + { + var decodedOutput = new List(); + + if (chunks == null || grid == null) + return decodedOutput; + + // Clear stale look up table values + _horizLines.Clear(); + _horizLinesReversed.Clear(); + _vertLines.Clear(); + _vertLinesReversed.Clear(); + + // Generate masks + var northMask = (ulong)1 << 0; + var southMask = (ulong)1 << 1; + var westMask = (ulong)1 << 2; + var eastMask = (ulong)1 << 3; + + foreach ((var chunkOrigin, var chunk) in chunks) + { + var list = new List(); + + foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData) + { + // Determine the correct coloration for the pipe + var color = Color.FromHex(hexColor) * _basePipeNetColor; + + if (FocusNetId != null && FocusNetId != netId) + color *= _unfocusedPipeNetColor; + + // Get the associated line look up tables + if (!_horizLines.TryGetValue(color, out var horizLines)) + { + horizLines = new(); + _horizLines[color] = horizLines; + } + + if (!_horizLinesReversed.TryGetValue(color, out var horizLinesReversed)) + { + horizLinesReversed = new(); + _horizLinesReversed[color] = horizLinesReversed; + } + + if (!_vertLines.TryGetValue(color, out var vertLines)) + { + vertLines = new(); + _vertLines[color] = vertLines; + } + + if (!_vertLinesReversed.TryGetValue(color, out var vertLinesReversed)) + { + vertLinesReversed = new(); + _vertLinesReversed[color] = vertLinesReversed; + } + + // Loop over the chunk + for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++) + { + if (atmosPipeData == 0) + continue; + + var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions; + + if ((atmosPipeData & mask) == 0) + continue; + + var relativeTile = GetTileFromIndex(tileIdx); + var tile = (chunk.Origin * ChunkSize + relativeTile) * grid.TileSize; + tile = tile with { Y = -tile.Y }; + + // Calculate the draw point offsets + var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + // Since we can have pipe lines that have a length of a half tile, + // double the vectors and convert to vector2i so we can merge them + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed); + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed); + } + } + } + + // Scale the vector2is back down and convert to vector2 + foreach (var (color, horizLines) in _horizLines) + { + // Get the corresponding sRBG color + var sRGB = GetsRGBColor(color); + + foreach (var (origin, terminal) in horizLines) + decodedOutput.Add(new AtmosMonitoringConsoleLine + (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + } + + foreach (var (color, vertLines) in _vertLines) + { + // Get the corresponding sRBG color + var sRGB = GetsRGBColor(color); + + foreach (var (origin, terminal) in vertLines) + decodedOutput.Add(new AtmosMonitoringConsoleLine + (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + } + + return decodedOutput; + } + + private Vector2 ConvertVector2iToVector2(Vector2i vector, float scale = 1f) + { + return new Vector2(vector.X * scale, vector.Y * scale); + } + + private Vector2i ConvertVector2ToVector2i(Vector2 vector, float scale = 1f) + { + return new Vector2i((int)MathF.Round(vector.X * scale), (int)MathF.Round(vector.Y * scale)); + } + + private Vector2i GetTileFromIndex(int index) + { + var x = index / ChunkSize; + var y = index % ChunkSize; + return new Vector2i(x, y); + } + + private Color GetsRGBColor(Color color) + { + if (!_sRGBLookUp.TryGetValue(color, out var sRGB)) + { + sRGB = Color.ToSrgb(color); + _sRGBLookUp[color] = sRGB; + } + + return sRGB; + } +} + +public struct AtmosMonitoringConsoleLine +{ + public readonly Vector2 Origin; + public readonly Vector2 Terminus; + public readonly Color Color; + + public AtmosMonitoringConsoleLine(Vector2 origin, Vector2 terminus, Color color) + { + Origin = origin; + Terminus = terminus; + Color = color; + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs new file mode 100644 index 0000000000..bfbb05d2ab --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Consoles; +using Robust.Shared.GameStates; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args) + { + Dictionary> modifiedChunks; + Dictionary atmosDevices; + + switch (args.Current) + { + case AtmosMonitoringConsoleDeltaState delta: + { + modifiedChunks = delta.ModifiedChunks; + atmosDevices = delta.AtmosDevices; + + foreach (var index in component.AtmosPipeChunks.Keys) + { + if (!delta.AllChunks!.Contains(index)) + component.AtmosPipeChunks.Remove(index); + } + + break; + } + + case AtmosMonitoringConsoleState state: + { + modifiedChunks = state.Chunks; + atmosDevices = state.AtmosDevices; + + foreach (var index in component.AtmosPipeChunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + component.AtmosPipeChunks.Remove(index); + } + + break; + } + default: + return; + } + + foreach (var (origin, chunk) in modifiedChunks) + { + var newChunk = new AtmosPipeChunk(origin); + newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk); + + component.AtmosPipeChunks[origin] = newChunk; + } + + component.AtmosDevices.Clear(); + + foreach (var (nuid, atmosDevice) in atmosDevices) + { + component.AtmosDevices[nuid] = atmosDevice; + } + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml new file mode 100644 index 0000000000..b6fde7592f --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs new file mode 100644 index 0000000000..515f91790f --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs @@ -0,0 +1,455 @@ +using Content.Client.Pinpointer.UI; +using Content.Client.UserInterface.Controls; +using Content.Shared.Atmos.Components; +using Content.Shared.Prototypes; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow +{ + private readonly IEntityManager _entManager; + private readonly IPrototypeManager _protoManager; + private readonly SpriteSystem _spriteSystem; + + private EntityUid? _owner; + private NetEntity? _focusEntity; + private int? _focusNetId; + + private bool _autoScrollActive = false; + + private readonly Color _unfocusedDeviceColor = Color.DimGray; + private ProtoId _navMapConsoleProtoId = "NavMapConsole"; + private ProtoId _gasPipeSensorProtoId = "GasPipeSensor"; + + public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _protoManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + // Pass the owner to nav map + _owner = owner; + NavMap.Owner = _owner; + + // Set nav map grid uid + var stationName = Loc.GetString("atmos-monitoring-window-unknown-location"); + EntityCoordinates? consoleCoords = null; + + if (_entManager.TryGetComponent(owner, out var xform)) + { + consoleCoords = xform.Coordinates; + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData)) + stationName = stationMetaData.EntityName; + + var msg = new FormattedMessage(); + msg.TryAddMarkup(Loc.GetString("atmos-monitoring-window-station-name", ("stationName", stationName)), out _); + + StationName.SetMessage(msg); + } + + else + { + StationName.SetMessage(stationName); + NavMap.Visible = false; + } + + // Set trackable entity selected action + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + // Update nav map + NavMap.ForceNavMapUpdate(); + + // Set tab container headers + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-monitoring-window-tab-networks")); + + // Set UI toggles + ShowPipeNetwork.OnToggled += _ => OnShowPipeNetworkToggled(); + ShowGasPipeSensors.OnToggled += _ => OnShowGasPipeSensors(); + + // Set nav map colors + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + NavMap.TileColor = console.NavMapTileColor; + NavMap.WallColor = console.NavMapWallColor; + + // Initalize + UpdateUI(consoleCoords, Array.Empty()); + } + + #region Toggle handling + + private void OnShowPipeNetworkToggled() + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + NavMap.ShowPipeNetwork = ShowPipeNetwork.Pressed; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (device.NavMapBlip == _gasPipeSensorProtoId) + continue; + + if (ShowPipeNetwork.Pressed) + AddTrackedEntityToNavMap(device); + + else + NavMap.TrackedEntities.Remove(netEnt); + } + } + + private void OnShowGasPipeSensors() + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (device.NavMapBlip != _gasPipeSensorProtoId) + continue; + + if (ShowGasPipeSensors.Pressed) + AddTrackedEntityToNavMap(device, true); + + else + NavMap.TrackedEntities.Remove(netEnt); + } + } + + #endregion + + public void UpdateUI + (EntityCoordinates? consoleCoords, + AtmosMonitoringConsoleEntry[] atmosNetworks) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + // Reset nav map values + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + + if (_focusEntity != null && !console.AtmosDevices.Any(x => x.Key == _focusEntity)) + ClearFocus(); + + // Add tracked entities to the nav map + UpdateNavMapBlips(); + + // Show the monitor location + var consoleNetEnt = _entManager.GetNetEntity(_owner); + + if (consoleCoords != null && consoleNetEnt != null) + { + var proto = _protoManager.Index(_navMapConsoleProtoId); + + if (proto.TexturePaths != null && proto.TexturePaths.Length != 0) + { + var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(proto.TexturePaths[0])); + var blip = new NavMapBlip(consoleCoords.Value, texture, proto.Color, proto.Blinks, proto.Selectable); + NavMap.TrackedEntities[consoleNetEnt.Value] = blip; + } + } + + // Update the nav map + NavMap.ForceNavMapUpdate(); + + // Clear excess children from the tables + while (AtmosNetworksTable.ChildCount > atmosNetworks.Length) + AtmosNetworksTable.RemoveChild(AtmosNetworksTable.GetChild(AtmosNetworksTable.ChildCount - 1)); + + // Update all entries in each table + for (int index = 0; index < atmosNetworks.Length; index++) + { + var entry = atmosNetworks.ElementAt(index); + UpdateUIEntry(entry, index, AtmosNetworksTable, console); + } + } + + private void UpdateNavMapBlips() + { + if (_owner == null || !_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + if (NavMap.Visible) + { + foreach (var (netEnt, device) in console.AtmosDevices) + { + // Update the focus network ID, incase it has changed + if (_focusEntity == netEnt) + { + _focusNetId = device.NetId; + NavMap.FocusNetId = _focusNetId; + } + + var isSensor = device.NavMapBlip == _gasPipeSensorProtoId; + + // Skip network devices if the toggled is off + if (!ShowPipeNetwork.Pressed && !isSensor) + continue; + + // Skip gas pipe sensors if the toggle is off + if (!ShowGasPipeSensors.Pressed && isSensor) + continue; + + AddTrackedEntityToNavMap(device, isSensor); + } + } + } + + private void AddTrackedEntityToNavMap(AtmosDeviceNavMapData metaData, bool isSensor = false) + { + var proto = _protoManager.Index(metaData.NavMapBlip); + + if (proto.TexturePaths == null || proto.TexturePaths.Length == 0) + return; + + var idx = Math.Clamp((int)metaData.Direction / 2, 0, proto.TexturePaths.Length - 1); + var texture = proto.TexturePaths.Length > 0 ? proto.TexturePaths[idx] : proto.TexturePaths[0]; + var color = isSensor ? proto.Color : proto.Color * metaData.PipeColor; + + if (_focusNetId != null && metaData.NetId != _focusNetId) + color *= _unfocusedDeviceColor; + + var blinks = proto.Blinks || _focusEntity == metaData.NetEntity; + var coords = _entManager.GetCoordinates(metaData.NetCoordinates); + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale); + NavMap.TrackedEntities[metaData.NetEntity] = blip; + } + + private void UpdateUIEntry(AtmosMonitoringConsoleEntry data, int index, Control table, AtmosMonitoringConsoleComponent console) + { + // Make new UI entry if required + if (index >= table.ChildCount) + { + var newEntryContainer = new AtmosMonitoringEntryContainer(data); + + // On click + newEntryContainer.FocusButton.OnButtonUp += args => + { + if (_focusEntity == newEntryContainer.Data.NetEntity) + { + ClearFocus(); + } + + else + { + SetFocus(newEntryContainer.Data.NetEntity, newEntryContainer.Data.NetId); + + var coords = _entManager.GetCoordinates(newEntryContainer.Data.Coordinates); + NavMap.CenterToCoordinates(coords); + } + + // Update affected UI elements across all tables + UpdateConsoleTable(console, AtmosNetworksTable, _focusEntity); + }; + + // Add the entry to the current table + table.AddChild(newEntryContainer); + } + + // Update values and UI elements + var tableChild = table.GetChild(index); + + if (tableChild is not AtmosMonitoringEntryContainer) + { + table.RemoveChild(tableChild); + UpdateUIEntry(data, index, table, console); + + return; + } + + var entryContainer = (AtmosMonitoringEntryContainer)tableChild; + entryContainer.UpdateEntry(data, data.NetEntity == _focusEntity); + } + + private void UpdateConsoleTable(AtmosMonitoringConsoleComponent console, Control table, NetEntity? currTrackedEntity) + { + foreach (var tableChild in table.Children) + { + if (tableChild is not AtmosAlarmEntryContainer) + continue; + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + if (entryContainer.NetEntity != currTrackedEntity) + entryContainer.RemoveAsFocus(); + + else if (entryContainer.NetEntity == currTrackedEntity) + entryContainer.SetAsFocus(); + } + } + + private void SetTrackedEntityFromNavMap(NetEntity? focusEntity) + { + if (focusEntity == null) + return; + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (netEnt != focusEntity) + continue; + + if (device.NavMapBlip != _gasPipeSensorProtoId) + return; + + // Set new focus + SetFocus(focusEntity.Value, device.NetId); + + // Get the scroll position of the selected entity on the selected button the UI + ActivateAutoScrollToFocus(); + + break; + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + AutoScrollToFocus(); + } + + private void ActivateAutoScrollToFocus() + { + _autoScrollActive = true; + } + + private void AutoScrollToFocus() + { + if (!_autoScrollActive) + return; + + var scroll = AtmosNetworksTable.Parent as ScrollContainer; + if (scroll == null) + return; + + if (!TryGetVerticalScrollbar(scroll, out var vScrollbar)) + return; + + if (!TryGetNextScrollPosition(out float? nextScrollPosition)) + return; + + vScrollbar.ValueTarget = nextScrollPosition.Value; + + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) + _autoScrollActive = false; + } + + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; + + foreach (var control in scroll.Children) + { + if (control is not VScrollBar) + continue; + + vScrollBar = (VScrollBar)control; + + return true; + } + + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = null; + + var scroll = AtmosNetworksTable.Parent as ScrollContainer; + if (scroll == null) + return false; + + var container = scroll.Children.ElementAt(0) as BoxContainer; + if (container == null || container.Children.Count() == 0) + return false; + + // Exit if the heights of the children haven't been initialized yet + if (!container.Children.Any(x => x.Height > 0)) + return false; + + nextScrollPosition = 0; + + foreach (var control in container.Children) + { + if (control is not AtmosMonitoringEntryContainer) + continue; + + var entry = (AtmosMonitoringEntryContainer)control; + + if (entry.Data.NetEntity == _focusEntity) + return true; + + nextScrollPosition += control.Height; + } + + // Failed to find control + nextScrollPosition = null; + + return false; + } + + private void SetFocus(NetEntity focusEntity, int focusNetId) + { + _focusEntity = focusEntity; + _focusNetId = focusNetId; + NavMap.FocusNetId = focusNetId; + + OnFocusChanged(); + } + + private void ClearFocus() + { + _focusEntity = null; + _focusNetId = null; + NavMap.FocusNetId = null; + + OnFocusChanged(); + } + + private void OnFocusChanged() + { + UpdateNavMapBlips(); + NavMap.ForceNavMapUpdate(); + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + for (int index = 0; index < AtmosNetworksTable.ChildCount; index++) + { + var entry = (AtmosMonitoringEntryContainer)AtmosNetworksTable.GetChild(index); + + if (entry == null) + continue; + + UpdateUIEntry(entry.Data, index, AtmosNetworksTable, console); + } + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml new file mode 100644 index 0000000000..6a19f0775f --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml @@ -0,0 +1,74 @@ + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs new file mode 100644 index 0000000000..0ce0c9c880 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs @@ -0,0 +1,166 @@ +using Content.Client.Stylesheets; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Temperature; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosMonitoringEntryContainer : BoxContainer +{ + public AtmosMonitoringConsoleEntry Data; + + private readonly IEntityManager _entManager; + private readonly IResourceCache _cache; + + public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _cache = IoCManager.Resolve(); + + Data = data; + + // Modulate colored stripe + NetworkColorStripe.Modulate = data.Color; + + // Load fonts + var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11); + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Set fonts + TemperatureHeaderLabel.FontOverride = headerFont; + PressureHeaderLabel.FontOverride = headerFont; + TotalMolHeaderLabel.FontOverride = headerFont; + GasesHeaderLabel.FontOverride = headerFont; + + TemperatureLabel.FontOverride = normalFont; + PressureLabel.FontOverride = normalFont; + TotalMolLabel.FontOverride = normalFont; + + NoDataLabel.FontOverride = headerFont; + } + + public void UpdateEntry(AtmosMonitoringConsoleEntry updatedData, bool isFocus) + { + // Load fonts + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Update name and values + if (!string.IsNullOrEmpty(updatedData.Address)) + NetworkNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", updatedData.EntityName), ("address", updatedData.Address)); + + else + NetworkNameLabel.Text = Loc.GetString(updatedData.EntityName); + + Data = updatedData; + + // Modulate colored stripe + NetworkColorStripe.Modulate = Data.Color; + + // Focus updates + if (isFocus) + SetAsFocus(); + else + RemoveAsFocus(); + + // Check if powered + if (!updatedData.IsPowered) + { + MainDataContainer.Visible = false; + NoDataLabel.Visible = true; + + return; + } + + // Set container visibility + MainDataContainer.Visible = true; + NoDataLabel.Visible = false; + + // Update temperature + var isNotVacuum = updatedData.TotalMolData > 1e-6f; + var tempK = (FixedPoint2)updatedData.TemperatureData; + var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float()); + + TemperatureLabel.Text = isNotVacuum ? + Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)) : + Loc.GetString("atmos-alerts-window-invalid-value"); + + TemperatureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update pressure + PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)updatedData.PressureData)); + PressureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update total mol + TotalMolLabel.Text = Loc.GetString("atmos-alerts-window-total-mol-value", ("value", (FixedPoint2)updatedData.TotalMolData)); + TotalMolLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update other present gases + GasGridContainer.RemoveAllChildren(); + + if (updatedData.GasData.Count() == 0) + { + // No gases + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"), + FontOverride = normalFont, + FontColorOverride = StyleNano.DisabledFore, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + + else + { + // Add an entry for each gas + foreach (var (gas, percent) in updatedData.GasData) + { + var gasPercent = (FixedPoint2)0f; + gasPercent = percent * 100f; + + var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); + + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)), + FontOverride = normalFont, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + } + } + + public void SetAsFocus() + { + FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png"; + FocusContainer.Visible = true; + } + + public void RemoveAsFocus() + { + FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png"; + FocusContainer.Visible = false; + } +} diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 90c2680c4a..b774b7d8b5 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -385,26 +385,6 @@ public partial class NavMapControl : MapGridControl if (PostWallDrawingAction != null) PostWallDrawingAction.Invoke(handle); - // Beacons - if (_beacons.Pressed) - { - var rectBuffer = new Vector2(5f, 3f); - - // Calculate font size for current zoom level - var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0); - var font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize); - - foreach (var beacon in _navMap.Beacons.Values) - { - var position = beacon.Position - offset; - position = ScalePosition(position with { Y = -position.Y }); - - var textDimensions = handle.GetDimensions(font, beacon.Text, 1f); - handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor); - handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color); - } - } - var curTime = Timing.RealTime; var blinkFrequency = 1f / 1f; var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f; @@ -443,11 +423,31 @@ public partial class NavMapControl : MapGridControl position = ScalePosition(new Vector2(position.X, -position.Y)); var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale); - var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height); + var positionOffset = new Vector2(scalingCoefficient * blip.Scale * blip.Texture.Width, scalingCoefficient * blip.Scale * blip.Texture.Height); handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color); } } + + // Beacons + if (_beacons.Pressed) + { + var rectBuffer = new Vector2(5f, 3f); + + // Calculate font size for current zoom level + var fontSize = (int)Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0); + var font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize); + + foreach (var beacon in _navMap.Beacons.Values) + { + var position = beacon.Position - offset; + position = ScalePosition(position with { Y = -position.Y }); + + var textDimensions = handle.GetDimensions(font, beacon.Text, 1f); + handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor); + handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color); + } + } } protected override void FrameUpdate(FrameEventArgs args) @@ -689,6 +689,9 @@ public partial class NavMapControl : MapGridControl Vector2i foundTermius; Vector2i foundOrigin; + if (origin == terminus) + return; + // Does our new line end at the beginning of an existing line? if (lookup.Remove(terminus, out foundTermius)) { @@ -739,13 +742,15 @@ public struct NavMapBlip public Color Color; public bool Blinks; public bool Selectable; + public float Scale; - public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true) + public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true, float scale = 1f) { Coordinates = coordinates; Texture = texture; Color = color; Blinks = blinks; Selectable = selectable; + Scale = scale; } } diff --git a/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs new file mode 100644 index 0000000000..5ecadc7154 --- /dev/null +++ b/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs @@ -0,0 +1,542 @@ +using Content.Server.Atmos.Components; +using Content.Server.Atmos.Piping.Components; +using Content.Server.DeviceNetwork.Components; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.NodeContainer.NodeGroups; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Consoles; +using Content.Shared.Labels.Components; +using Content.Shared.Pinpointer; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Timing; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Server.Atmos.Consoles; + +public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem +{ + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + // Private variables + // Note: this data does not need to be saved + private Dictionary> _gridAtmosPipeChunks = new(); + private float _updateTimer = 1.0f; + + // Constants + private const float UpdateTime = 1.0f; + private const int ChunkSize = 4; + + public override void Initialize() + { + base.Initialize(); + + // Console events + SubscribeLocalEvent(OnConsoleInit); + SubscribeLocalEvent(OnConsoleAnchorChanged); + SubscribeLocalEvent(OnConsoleParentChanged); + + // Tracked device events + SubscribeLocalEvent(OnEntityNodeGroupsRebuilt); + SubscribeLocalEvent(OnEntityPipeColorChanged); + SubscribeLocalEvent(OnEntityShutdown); + + // Grid events + SubscribeLocalEvent(OnGridSplit); + } + + #region Event handling + + private void OnConsoleInit(EntityUid uid, AtmosMonitoringConsoleComponent component, ComponentInit args) + { + InitializeAtmosMonitoringConsole(uid, component); + } + + private void OnConsoleAnchorChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, AnchorStateChangedEvent args) + { + InitializeAtmosMonitoringConsole(uid, component); + } + + private void OnConsoleParentChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, EntParentChangedMessage args) + { + component.ForceFullUpdate = true; + InitializeAtmosMonitoringConsole(uid, component); + } + + private void OnEntityNodeGroupsRebuilt(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, NodeGroupsRebuilt args) + { + InitializeAtmosMonitoringDevice(uid, component); + } + + private void OnEntityPipeColorChanged(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, AtmosPipeColorChangedEvent args) + { + InitializeAtmosMonitoringDevice(uid, component); + } + + private void OnEntityShutdown(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, EntityTerminatingEvent args) + { + ShutDownAtmosMonitoringEntity(uid, component); + } + + private void OnGridSplit(ref GridSplitEvent args) + { + // Collect grids + var allGrids = args.NewGrids.ToList(); + + if (!allGrids.Contains(args.Grid)) + allGrids.Add(args.Grid); + + // Rebuild the pipe networks on the affected grids + foreach (var ent in allGrids) + { + if (!TryComp(ent, out var grid)) + continue; + + RebuildAtmosPipeGrid(ent, 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; + + InitializeAtmosMonitoringConsole(ent, entConsole); + } + } + + #endregion + + #region UI updates + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _updateTimer += frameTime; + + if (_updateTimer >= UpdateTime) + { + _updateTimer -= UpdateTime; + + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (entXform?.GridUid == null) + continue; + + UpdateUIState(ent, entConsole, entXform); + } + } + } + + public void UpdateUIState + (EntityUid uid, + AtmosMonitoringConsoleComponent component, + TransformComponent xform) + { + if (!_userInterfaceSystem.IsUiOpen(uid, AtmosMonitoringConsoleUiKey.Key)) + return; + + var gridUid = xform.GridUid!.Value; + + if (!TryComp(gridUid, out var mapGrid)) + return; + + if (!TryComp(gridUid, out var atmosphere)) + return; + + // The grid must have a NavMapComponent to visualize the map in the UI + EnsureComp(gridUid); + + // Gathering data to be send to the client + var atmosNetworks = new List(); + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entSensor, out var entXform)) + { + if (entXform?.GridUid != xform.GridUid) + continue; + + if (!entXform.Anchored) + continue; + + var entry = CreateAtmosMonitoringConsoleEntry(ent, entXform); + + if (entry != null) + atmosNetworks.Add(entry.Value); + } + + // Set the UI state + _userInterfaceSystem.SetUiState(uid, AtmosMonitoringConsoleUiKey.Key, + new AtmosMonitoringConsoleBoundInterfaceState(atmosNetworks.ToArray())); + } + + private AtmosMonitoringConsoleEntry? CreateAtmosMonitoringConsoleEntry(EntityUid uid, TransformComponent xform) + { + AtmosMonitoringConsoleEntry? entry = null; + + var netEnt = GetNetEntity(uid); + var name = MetaData(uid).EntityName; + var address = string.Empty; + + if (xform.GridUid == null) + return null; + + if (!TryGettingFirstPipeNode(uid, out var pipeNode, out var netId) || + pipeNode == null || + netId == null) + return null; + + var pipeColor = TryComp(uid, out var colorComponent) ? colorComponent.Color : Color.White; + + // Name the entity based on its label, if available + if (TryComp(uid, out var label) && label.CurrentLabel != null) + name = label.CurrentLabel; + + // Otherwise use its base name and network address + else if (TryComp(uid, out var deviceNet)) + address = deviceNet.Address; + + // Entry for unpowered devices + if (TryComp(uid, out var apcPowerReceiver) && !apcPowerReceiver.Powered) + { + entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address) + { + IsPowered = false, + Color = pipeColor + }; + + return entry; + } + + // Entry for powered devices + var gasData = new Dictionary(); + var isAirPresent = pipeNode.Air.TotalMoles > 0; + + if (isAirPresent) + { + foreach (var gas in Enum.GetValues()) + { + if (pipeNode.Air[(int)gas] > 0) + gasData.Add(gas, pipeNode.Air[(int)gas] / pipeNode.Air.TotalMoles); + } + } + + entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address) + { + TemperatureData = isAirPresent ? pipeNode.Air.Temperature : 0f, + PressureData = pipeNode.Air.Pressure, + TotalMolData = pipeNode.Air.TotalMoles, + GasData = gasData, + Color = pipeColor + }; + + return entry; + } + + private Dictionary GetAllAtmosDeviceNavMapData(EntityUid gridUid) + { + var atmosDeviceNavMapData = new Dictionary(); + + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entComponent, out var entXform)) + { + if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data)) + atmosDeviceNavMapData.Add(data.Value.NetEntity, data.Value); + } + + return atmosDeviceNavMapData; + } + + private bool TryGetAtmosDeviceNavMapData + (EntityUid uid, + AtmosMonitoringConsoleDeviceComponent component, + TransformComponent xform, + EntityUid gridUid, + [NotNullWhen(true)] out AtmosDeviceNavMapData? device) + { + device = null; + + if (component.NavMapBlip == null) + return false; + + if (xform.GridUid != gridUid) + return false; + + if (!xform.Anchored) + return false; + + var direction = xform.LocalRotation.GetCardinalDir(); + + if (!TryGettingFirstPipeNode(uid, out var _, out var netId)) + netId = -1; + + var color = Color.White; + + if (TryComp(uid, out var atmosPipeColor)) + color = atmosPipeColor.Color; + + device = new AtmosDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), netId.Value, component.NavMapBlip.Value, direction, color); + + return true; + } + + #endregion + + #region Pipe net functions + + private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid) + { + var allChunks = new Dictionary(); + + // Adds all atmos pipes to the nav map via bit mask chunks + var queryPipes = AllEntityQuery(); + while (queryPipes.MoveNext(out var ent, out var entAtmosPipeColor, out var entNodeContainer, out var entXform)) + { + if (entXform.GridUid != gridUid) + continue; + + if (!entXform.Anchored) + continue; + + var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates); + var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize); + var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize); + + if (!allChunks.TryGetValue(chunkOrigin, out var chunk)) + { + chunk = new AtmosPipeChunk(chunkOrigin); + allChunks[chunkOrigin] = chunk; + } + + UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, GetTileIndex(relative), ref chunk); + } + + // Add or update the chunks on the associated grid + _gridAtmosPipeChunks[gridUid] = allChunks; + + // Update the consoles that are on the same grid + var queryConsoles = AllEntityQuery(); + while (queryConsoles.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (gridUid != entXform.GridUid) + continue; + + entConsole.AtmosPipeChunks = allChunks; + Dirty(ent, entConsole); + } + } + + private void RebuildSingleTileOfPipeNetwork(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords) + { + if (!_gridAtmosPipeChunks.TryGetValue(gridUid, out var allChunks)) + allChunks = new Dictionary(); + + var tile = _sharedMapSystem.GetTileRef(gridUid, grid, coords); + var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize); + var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize); + var tileIdx = GetTileIndex(relative); + + if (!allChunks.TryGetValue(chunkOrigin, out var chunk)) + chunk = new AtmosPipeChunk(chunkOrigin); + + // Remove all stale values for the tile + foreach (var (index, atmosPipeData) in chunk.AtmosPipeData) + { + var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions; + chunk.AtmosPipeData[index] = atmosPipeData & ~mask; + } + + // Rebuild the tile's pipe data + foreach (var ent in _sharedMapSystem.GetAnchoredEntities(gridUid, grid, coords)) + { + if (!TryComp(ent, out var entAtmosPipeColor)) + continue; + + if (!TryComp(ent, out var entNodeContainer)) + continue; + + UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, tileIdx, ref chunk); + } + + // Add or update the chunk on the associated grid + // Only the modified chunk will be sent to the client + chunk.LastUpdate = _gameTiming.CurTick; + allChunks[chunkOrigin] = chunk; + _gridAtmosPipeChunks[gridUid] = allChunks; + + // Update the components of the monitoring consoles that are attached to the same grid + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (gridUid != entXform.GridUid) + continue; + + entConsole.AtmosPipeChunks = allChunks; + Dirty(ent, entConsole); + } + } + + private void UpdateAtmosPipeChunk(EntityUid uid, NodeContainerComponent nodeContainer, AtmosPipeColorComponent pipeColor, int tileIdx, ref AtmosPipeChunk chunk) + { + // Entities that are actively being deleted are not to be drawn + if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating) + return; + + foreach ((var id, var node) in nodeContainer.Nodes) + { + if (node is not PipeNode) + continue; + + var pipeNode = (PipeNode)node; + var netId = GetPipeNodeNetId(pipeNode); + var pipeDirection = pipeNode.CurrentPipeDirection; + + chunk.AtmosPipeData.TryGetValue((netId, pipeColor.Color.ToHex()), out var atmosPipeData); + atmosPipeData |= (ulong)pipeDirection << tileIdx * SharedNavMapSystem.Directions; + chunk.AtmosPipeData[(netId, pipeColor.Color.ToHex())] = atmosPipeData; + } + } + + private bool TryGettingFirstPipeNode(EntityUid uid, [NotNullWhen(true)] out PipeNode? pipeNode, [NotNullWhen(true)] out int? netId) + { + pipeNode = null; + netId = null; + + if (!TryComp(uid, out var nodeContainer)) + return false; + + foreach (var node in nodeContainer.Nodes.Values) + { + if (node is PipeNode) + { + pipeNode = (PipeNode)node; + netId = GetPipeNodeNetId(pipeNode); + + return true; + } + } + + return false; + } + + private int GetPipeNodeNetId(PipeNode pipeNode) + { + if (pipeNode.NodeGroup is BaseNodeGroup) + { + var nodeGroup = (BaseNodeGroup)pipeNode.NodeGroup; + + return nodeGroup.NetId; + } + + return -1; + } + + #endregion + + #region Initialization functions + + private void InitializeAtmosMonitoringConsole(EntityUid uid, AtmosMonitoringConsoleComponent component) + { + var xform = Transform(uid); + + if (xform.GridUid == null) + return; + + var grid = xform.GridUid.Value; + + if (!TryComp(grid, out var map)) + return; + + component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid); + + if (!_gridAtmosPipeChunks.TryGetValue(grid, out var chunks)) + { + RebuildAtmosPipeGrid(grid, map); + } + + else + { + component.AtmosPipeChunks = chunks; + Dirty(uid, component); + } + } + + private void InitializeAtmosMonitoringDevice(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component) + { + // Rebuild tile + var xform = Transform(uid); + var gridUid = xform.GridUid; + + if (gridUid != null && TryComp(gridUid, out var grid)) + RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates); + + // Update blips on affected consoles + if (component.NavMapBlip == null) + return; + + var netEntity = EntityManager.GetNetEntity(uid); + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + var isDirty = entConsole.AtmosDevices.Remove(netEntity); + + if (gridUid != null && + gridUid == entXform.GridUid && + xform.Anchored && + TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data)) + { + entConsole.AtmosDevices.Add(netEntity, data.Value); + isDirty = true; + } + + if (isDirty) + Dirty(ent, entConsole); + } + } + + private void ShutDownAtmosMonitoringEntity(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component) + { + // Rebuild tile + var xform = Transform(uid); + var gridUid = xform.GridUid; + + if (gridUid != null && TryComp(gridUid, out var grid)) + RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates); + + // Update blips on affected consoles + if (component.NavMapBlip == null) + return; + + var netEntity = EntityManager.GetNetEntity(uid); + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entConsole)) + { + if (entConsole.AtmosDevices.Remove(netEntity)) + Dirty(ent, entConsole); + } + } + + #endregion + + private int GetTileIndex(Vector2i relativeTile) + { + return relativeTile.X * ChunkSize + relativeTile.Y; + } +} diff --git a/Content.Server/Atmos/Piping/Components/AtmosPipeColorComponent.cs b/Content.Server/Atmos/Piping/Components/AtmosPipeColorComponent.cs index 5b05668ad5..a8edb07d31 100644 --- a/Content.Server/Atmos/Piping/Components/AtmosPipeColorComponent.cs +++ b/Content.Server/Atmos/Piping/Components/AtmosPipeColorComponent.cs @@ -1,19 +1,24 @@ using Content.Server.Atmos.Piping.EntitySystems; using JetBrains.Annotations; -namespace Content.Server.Atmos.Piping.Components -{ - [RegisterComponent] - public sealed partial class AtmosPipeColorComponent : Component - { - [DataField("color")] - public Color Color { get; set; } = Color.White; +namespace Content.Server.Atmos.Piping.Components; - [ViewVariables(VVAccess.ReadWrite), UsedImplicitly] - public Color ColorVV - { - get => Color; - set => IoCManager.Resolve().System().SetColor(Owner, this, value); - } +[RegisterComponent] +public sealed partial class AtmosPipeColorComponent : Component +{ + [DataField] + public Color Color { get; set; } = Color.White; + + [ViewVariables(VVAccess.ReadWrite), UsedImplicitly] + public Color ColorVV + { + get => Color; + set => IoCManager.Resolve().System().SetColor(Owner, this, value); } } + +[ByRefEvent] +public record struct AtmosPipeColorChangedEvent(Color color) +{ + public Color Color = color; +} diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeColorSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeColorSystem.cs index b9ee668032..dcb08dcd57 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeColorSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeColorSystem.cs @@ -40,6 +40,9 @@ namespace Content.Server.Atmos.Piping.EntitySystems return; _appearance.SetData(uid, PipeColorVisuals.Color, color, appearance); + + var ev = new AtmosPipeColorChangedEvent(color); + RaiseLocalEvent(uid, ref ev); } } } diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 19766d92e6..cb89f6c199 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -145,6 +145,22 @@ namespace Content.Shared.Atmos /// public const float SpaceHeatCapacity = 7000f; + /// + /// Dictionary of chemical abbreviations for + /// + public static Dictionary GasAbbreviations = new Dictionary() + { + [Gas.Ammonia] = Loc.GetString("gas-ammonia-abbreviation"), + [Gas.CarbonDioxide] = Loc.GetString("gas-carbon-dioxide-abbreviation"), + [Gas.Frezon] = Loc.GetString("gas-frezon-abbreviation"), + [Gas.Nitrogen] = Loc.GetString("gas-nitrogen-abbreviation"), + [Gas.NitrousOxide] = Loc.GetString("gas-nitrous-oxide-abbreviation"), + [Gas.Oxygen] = Loc.GetString("gas-oxygen-abbreviation"), + [Gas.Plasma] = Loc.GetString("gas-plasma-abbreviation"), + [Gas.Tritium] = Loc.GetString("gas-tritium-abbreviation"), + [Gas.WaterVapor] = Loc.GetString("gas-water-vapor-abbreviation"), + }; + #region Excited Groups /// diff --git a/Content.Shared/Atmos/Components/GasPipeSensorComponent.cs b/Content.Shared/Atmos/Components/GasPipeSensorComponent.cs new file mode 100644 index 0000000000..3393948f4f --- /dev/null +++ b/Content.Shared/Atmos/Components/GasPipeSensorComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +/// +/// Entities with component will be queried against for their +/// atmos monitoring data on atmos monitoring consoles +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class GasPipeSensorComponent : Component; diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleComponent.cs new file mode 100644 index 0000000000..2ac0d2a9af --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleComponent.cs @@ -0,0 +1,235 @@ +using Content.Shared.Atmos.Consoles; +using Content.Shared.Pinpointer; +using Content.Shared.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; + +namespace Content.Shared.Atmos.Components; + +/// +/// Entities capable of opening the atmos monitoring console UI +/// require this component to function correctly +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedAtmosMonitoringConsoleSystem))] +public sealed partial class AtmosMonitoringConsoleComponent : Component +{ + /* + * Don't need DataFields as this can be reconstructed + */ + + /// + /// A dictionary of the all the nav map chunks that contain anchored atmos pipes + /// + [ViewVariables] + public Dictionary AtmosPipeChunks = new(); + + /// + /// A list of all the atmos devices that will be used to populate the nav map + /// + [ViewVariables] + public Dictionary AtmosDevices = new(); + + /// + /// Color of the floor tiles on the nav map screen + /// + [DataField, ViewVariables] + public Color NavMapTileColor; + + /// + /// Color of the wall lines on the nav map screen + /// + [DataField, ViewVariables] + public Color NavMapWallColor; + + /// + /// The next time this component is dirtied, it will force the full state + /// to be sent to the client, instead of just the delta state + /// + [ViewVariables] + public bool ForceFullUpdate = false; +} + +[Serializable, NetSerializable] +public struct AtmosPipeChunk(Vector2i origin) +{ + /// + /// Chunk position + /// + [ViewVariables] + public readonly Vector2i Origin = origin; + + /// + /// Bitmask look up for atmos pipes, 1 for occupied and 0 for empty. + /// Indexed by the color hexcode of the pipe + /// + [ViewVariables] + public Dictionary<(int, string), ulong> AtmosPipeData = new(); + + /// + /// The last game tick that the chunk was updated + /// + [NonSerialized] + public GameTick LastUpdate; +} + +[Serializable, NetSerializable] +public struct AtmosDeviceNavMapData +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates NetCoordinates; + + /// + /// The associated pipe network ID + /// + public int NetId = -1; + + /// + /// Prototype ID for the nav map blip + /// + public ProtoId NavMapBlip; + + /// + /// Direction of the entity + /// + public Direction Direction; + + /// + /// Color of the attached pipe + /// + public Color PipeColor; + + /// + /// Populate the atmos monitoring console nav map with a single entity + /// + public AtmosDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, int netId, ProtoId navMapBlip, Direction direction, Color pipeColor) + { + NetEntity = netEntity; + NetCoordinates = netCoordinates; + NetId = netId; + NavMapBlip = navMapBlip; + Direction = direction; + PipeColor = pipeColor; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState +{ + /// + /// A list of all entries to populate the UI with + /// + public AtmosMonitoringConsoleEntry[] AtmosNetworks; + + /// + /// Sends data from the server to the client to populate the atmos monitoring console UI + /// + public AtmosMonitoringConsoleBoundInterfaceState(AtmosMonitoringConsoleEntry[] atmosNetworks) + { + AtmosNetworks = atmosNetworks; + } +} + +[Serializable, NetSerializable] +public struct AtmosMonitoringConsoleEntry +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates Coordinates; + + /// + /// The associated pipe network ID + /// + public int NetId = -1; + + /// + /// Localised device name + /// + public string EntityName; + + /// + /// Device network address + /// + public string Address; + + /// + /// Temperature (K) + /// + public float TemperatureData; + + /// + /// Pressure (kPA) + /// + public float PressureData; + + /// + /// Total number of mols of gas + /// + public float TotalMolData; + + /// + /// Mol and percentage for all detected gases + /// + public Dictionary GasData = new(); + + /// + /// The color to be associated with the pipe network + /// + public Color Color; + + /// + /// Indicates whether the entity is powered + /// + public bool IsPowered = true; + + /// + /// Used to populate the atmos monitoring console UI with data from a single air alarm + /// + public AtmosMonitoringConsoleEntry + (NetEntity entity, + NetCoordinates coordinates, + int netId, + string entityName, + string address) + { + NetEntity = entity; + Coordinates = coordinates; + NetId = netId; + EntityName = entityName; + Address = address; + } +} + +public enum AtmosPipeChunkDataFacing : byte +{ + // Values represent bit shift offsets when retrieving data in the tile array. + North = 0, + South = SharedNavMapSystem.ArraySize, + East = SharedNavMapSystem.ArraySize * 2, + West = SharedNavMapSystem.ArraySize * 3, +} + +/// +/// UI key associated with the atmos monitoring console +/// +[Serializable, NetSerializable] +public enum AtmosMonitoringConsoleUiKey +{ + Key +} diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleDeviceComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleDeviceComponent.cs new file mode 100644 index 0000000000..50c3abcfca --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleDeviceComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Atmos.Components; + +/// +/// Entities with this component appear on the +/// nav maps of atmos monitoring consoles +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class AtmosMonitoringConsoleDeviceComponent : Component +{ + /// + /// Prototype ID for the blip used to represent this + /// entity on the atmos monitoring console nav map. + /// If null, no blip is drawn (i.e., null for pipes) + /// + [DataField, ViewVariables] + public ProtoId? NavMapBlip = null; +} diff --git a/Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs b/Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs new file mode 100644 index 0000000000..e6dd455be7 --- /dev/null +++ b/Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs @@ -0,0 +1,115 @@ +using Content.Shared.Atmos.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Atmos.Consoles; + +public abstract class SharedAtmosMonitoringConsoleSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetState); + } + + private void OnGetState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentGetState args) + { + Dictionary> chunks; + + // Should this be a full component state or a delta-state? + if (args.FromTick <= component.CreationTick || component.ForceFullUpdate) + { + component.ForceFullUpdate = false; + + // Full state + chunks = new(component.AtmosPipeChunks.Count); + + foreach (var (origin, chunk) in component.AtmosPipeChunks) + { + chunks.Add(origin, chunk.AtmosPipeData); + } + + args.State = new AtmosMonitoringConsoleState(chunks, component.AtmosDevices); + + return; + } + + chunks = new(); + + foreach (var (origin, chunk) in component.AtmosPipeChunks) + { + if (chunk.LastUpdate < args.FromTick) + continue; + + chunks.Add(origin, chunk.AtmosPipeData); + } + + args.State = new AtmosMonitoringConsoleDeltaState(chunks, component.AtmosDevices, new(component.AtmosPipeChunks.Keys)); + } + + #region: System messages + + [Serializable, NetSerializable] + protected sealed class AtmosMonitoringConsoleState( + Dictionary> chunks, + Dictionary atmosDevices) + : ComponentState + { + public Dictionary> Chunks = chunks; + public Dictionary AtmosDevices = atmosDevices; + } + + [Serializable, NetSerializable] + protected sealed class AtmosMonitoringConsoleDeltaState( + Dictionary> modifiedChunks, + Dictionary atmosDevices, + HashSet allChunks) + : ComponentState, IComponentDeltaState + { + public Dictionary> ModifiedChunks = modifiedChunks; + public Dictionary AtmosDevices = atmosDevices; + public HashSet AllChunks = allChunks; + + public void ApplyToFullState(AtmosMonitoringConsoleState state) + { + foreach (var key in state.Chunks.Keys) + { + if (!AllChunks!.Contains(key)) + state.Chunks.Remove(key); + } + + foreach (var (index, data) in ModifiedChunks) + { + state.Chunks[index] = new Dictionary<(int, string), ulong>(data); + } + + state.AtmosDevices.Clear(); + foreach (var (nuid, atmosDevice) in AtmosDevices) + { + state.AtmosDevices.Add(nuid, atmosDevice); + } + } + + public AtmosMonitoringConsoleState CreateNewFullState(AtmosMonitoringConsoleState state) + { + var chunks = new Dictionary>(state.Chunks.Count); + + foreach (var (index, data) in state.Chunks) + { + if (!AllChunks!.Contains(index)) + continue; + + if (ModifiedChunks.ContainsKey(index)) + chunks[index] = new Dictionary<(int, string), ulong>(ModifiedChunks[index]); + + else + chunks[index] = new Dictionary<(int, string), ulong>(state.Chunks[index]); + } + + return new AtmosMonitoringConsoleState(chunks, new(AtmosDevices)); + } + } + + #endregion +} diff --git a/Content.Shared/Prototypes/NavMapBlipPrototype.cs b/Content.Shared/Prototypes/NavMapBlipPrototype.cs new file mode 100644 index 0000000000..ede82d8e04 --- /dev/null +++ b/Content.Shared/Prototypes/NavMapBlipPrototype.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Prototypes; + +[Prototype("navMapBlip")] +public sealed partial class NavMapBlipPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// Sets whether the associated entity can be selected when the blip is clicked + /// + [DataField] + public bool Selectable = false; + + /// + /// Sets whether the blips is always blinking + /// + [DataField] + public bool Blinks = false; + + /// + /// Sets the color of the blip + /// + [DataField] + public Color Color { get; private set; } = Color.LightGray; + + /// + /// Texture paths associated with the blip + /// + [DataField] + public ResPath[]? TexturePaths { get; private set; } + + /// + /// Sets the UI scaling of the blip + /// + [DataField] + public float Scale { get; private set; } = 1f; +} diff --git a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl index 470a8f8695..dd9b6a0128 100644 --- a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl +++ b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl @@ -10,6 +10,9 @@ atmos-alerts-window-tab-fire-alarms = Fire alarms atmos-alerts-window-alarm-label = {CAPITALIZE($name)} ({$address}) atmos-alerts-window-temperature-label = Temperature atmos-alerts-window-temperature-value = {$valueInC} °C ({$valueInK} K) +atmos-alerts-window-invalid-value = N/A +atmos-alerts-window-total-mol-label = Total moles +atmos-alerts-window-total-mol-value = {$value} mol atmos-alerts-window-pressure-label = Pressure atmos-alerts-window-pressure-value = {$value} kPa atmos-alerts-window-oxygenation-label = Oxygenation diff --git a/Resources/Locale/en-US/atmos/gases.ftl b/Resources/Locale/en-US/atmos/gases.ftl new file mode 100644 index 0000000000..5c540c46df --- /dev/null +++ b/Resources/Locale/en-US/atmos/gases.ftl @@ -0,0 +1,10 @@ +gas-ammonia-abbreviation = NH₃ +gas-carbon-dioxide-abbreviation = CO₂ +gas-frezon-abbreviation = F +gas-nitrogen-abbreviation = N₂ +gas-nitrous-oxide-abbreviation = N₂O +gas-oxygen-abbreviation = O₂ +gas-plasma-abbreviation = P +gas-tritium-abbreviation = T +gas-water-vapor-abbreviation = H₂O +gas-unknown-abbreviation = X diff --git a/Resources/Locale/en-US/components/atmos-monitoring-component.ftl b/Resources/Locale/en-US/components/atmos-monitoring-component.ftl new file mode 100644 index 0000000000..eab6f50c60 --- /dev/null +++ b/Resources/Locale/en-US/components/atmos-monitoring-component.ftl @@ -0,0 +1,14 @@ +atmos-monitoring-window-title = Atmospheric Network Monitor +atmos-monitoring-window-station-name = [color=white][font size=14]{$stationName}[/font][/color] +atmos-monitoring-window-unknown-location = Unknown location +atmos-monitoring-window-label-gas-opening = Network opening +atmos-monitoring-window-label-gas-scrubber = Air scrubber +atmos-monitoring-window-label-gas-flow-regulator = Flow regulator +atmos-monitoring-window-label-thermoregulator = Thermoregulator +atmos-monitoring-window-tab-networks = Atmospheric networks +atmos-monitoring-window-toggle-overlays = Toggle map overlays +atmos-monitoring-window-show-pipe-network = Pipe network +atmos-monitoring-window-show-gas-pipe-sensors = Gas pipe sensors +atmos-monitoring-window-label-gases = Present gases +atmos-monitoring-window-flavor-left = Contact an atmospheric technician for assistance +atmos-monitoring-window-flavor-right = v1.1 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 54616724fb..26f2881ae8 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -26,6 +26,15 @@ components: - type: ComputerBoard prototype: ComputerAlert + +- type: entity + parent: BaseComputerCircuitboard + id: AtmosMonitoringComputerCircuitboard + name: atmospheric network monitor board + description: A computer printed circuit board for an atmospheric network monitor. + components: + - type: ComputerBoard + prototype: ComputerAtmosMonitoring - type: entity parent: BaseComputerCircuitboard diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 4cd596e9b4..8167229ae5 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -49,6 +49,39 @@ enum.WiresUiKey.Key: type: WiresBoundUserInterface +- type: entity + parent: BaseComputerAiAccess + id: ComputerAtmosMonitoring + name: atmospheric network monitor + description: Used to monitor the station's atmospheric networks. + components: + - type: Computer + board: AtmosMonitoringComputerCircuitboard + - type: Sprite + layers: + - map: ["computerLayerBody"] + state: computer + - map: ["computerLayerKeyboard"] + state: generic_keyboard + - map: ["computerLayerScreen"] + state: tank + - map: ["computerLayerKeys"] + state: atmos_key + - map: [ "enum.WiresVisualLayers.MaintenancePanel" ] + state: generic_panel_open + - type: AtmosMonitoringConsole + navMapTileColor: "#1a1a1a" + navMapWallColor: "#404040" + - type: ActivatableUI + singleUser: true + key: enum.AtmosMonitoringConsoleUiKey.Key + - type: UserInterface + interfaces: + enum.AtmosMonitoringConsoleUiKey.Key: + type: AtmosMonitoringConsoleBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface + - type: entity parent: BaseComputer id: ComputerEmergencyShuttle diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/nav_map_blips.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/nav_map_blips.yml new file mode 100644 index 0000000000..bc51557186 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/nav_map_blips.yml @@ -0,0 +1,56 @@ +# All consoles +- type: navMapBlip + id: NavMapConsole + blinks: true + color: Cyan + texturePaths: + - "/Textures/Interface/NavMap/beveled_circle.png" + +# Atmos monitoring console +- type: navMapBlip + id: GasPipeSensor + selectable: true + color: "#ffcd00" + texturePaths: + - "/Textures/Interface/NavMap/beveled_star.png" + +- type: navMapBlip + id: GasVentOpening + scale: 0.6667 + color: LightGray + texturePaths: + - "/Textures/Interface/NavMap/beveled_square.png" + +- type: navMapBlip + id: GasVentScrubber + scale: 0.6667 + color: LightGray + texturePaths: + - "/Textures/Interface/NavMap/beveled_circle.png" + +- type: navMapBlip + id: GasFlowRegulator + scale: 0.75 + color: LightGray + texturePaths: + - "/Textures/Interface/NavMap/beveled_arrow_south.png" + - "/Textures/Interface/NavMap/beveled_arrow_east.png" + - "/Textures/Interface/NavMap/beveled_arrow_north.png" + - "/Textures/Interface/NavMap/beveled_arrow_west.png" + +- type: navMapBlip + id: GasValve + scale: 0.6667 + color: LightGray + texturePaths: + - "/Textures/Interface/NavMap/beveled_diamond_north_south.png" + - "/Textures/Interface/NavMap/beveled_diamond_east_west.png" + - "/Textures/Interface/NavMap/beveled_diamond_north_south.png" + - "/Textures/Interface/NavMap/beveled_diamond_east_west.png" + +- type: navMapBlip + id: Thermoregulator + scale: 0.6667 + color: LightGray + texturePaths: + - "/Textures/Interface/NavMap/beveled_hexagon.png" \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml index ed5137c28f..3bc00fb1b6 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml @@ -79,6 +79,8 @@ !type:PortablePipeNode nodeGroupID: Pipe pipeDirection: South + - type: AtmosMonitoringConsoleDevice + navMapBlip: Thermoregulator - type: ItemSlots slots: beakerSlot: diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml index 90e48d8be6..8327937ba8 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml @@ -73,7 +73,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_pump.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasFlowRegulator + - type: entity parent: GasBinaryBase id: GasVolumePump @@ -130,7 +132,9 @@ examinableAddress: true prefix: device-address-prefix-volume-pump - type: WiredNetworkConnection - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasFlowRegulator + - type: entity parent: GasBinaryBase id: GasPassiveGate @@ -159,7 +163,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_hiss.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasValve + - type: entity parent: GasBinaryBase id: GasValve @@ -207,7 +213,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_hiss.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasValve + - type: entity parent: GasBinaryBase id: SignalControlledValve @@ -266,7 +274,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_hiss.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasValve + - type: entity parent: GasBinaryBase id: GasPort @@ -295,7 +305,9 @@ - type: Construction graph: GasBinary node: port - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentOpening + - type: entity parent: GasVentPump id: GasDualPortVentPump @@ -351,7 +363,9 @@ pipeDirection: South - type: AmbientSound enabled: true - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentOpening + - type: entity parent: [ BaseMachine, ConstructibleMachine ] id: GasRecycler @@ -413,7 +427,9 @@ acts: ["Destruction"] - type: Machine board: GasRecyclerMachineCircuitboard - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasValve + - type: entity parent: GasBinaryBase id: HeatExchanger @@ -453,3 +469,5 @@ - type: Construction graph: GasBinary node: radiator + - type: AtmosMonitoringConsoleDevice + navMapBlip: Thermoregulator diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/gas_pipe_sensor.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/gas_pipe_sensor.yml index 08015abe7d..22b56908ea 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/gas_pipe_sensor.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/gas_pipe_sensor.yml @@ -27,6 +27,9 @@ True: { state: lights } - type: AtmosMonitor monitorsPipeNet: true + - type: GasPipeSensor + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasPipeSensor - type: ApcPowerReceiver - type: ExtensionCableReceiver - type: Construction diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml index 4fe5463bff..ef436b4299 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml @@ -54,6 +54,7 @@ - type: PipeRestrictOverlap - type: AtmosUnsafeUnanchor - type: AtmosPipeColor + - type: AtmosMonitoringConsoleDevice - type: Tag tags: - Pipe diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml index e8025556aa..bde7136850 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml @@ -70,7 +70,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_hiss.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasFlowRegulator + - type: entity parent: GasFilter id: GasFilterFlipped @@ -158,7 +160,9 @@ range: 5 sound: path: /Audio/Ambience/Objects/gas_hiss.ogg - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasFlowRegulator + - type: entity parent: GasMixer id: GasMixerFlipped @@ -257,3 +261,5 @@ - type: Construction graph: GasTrinary node: pneumaticvalve + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasFlowRegulator \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml index d0ec74dd40..5da85544fc 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml @@ -68,7 +68,9 @@ sound: path: /Audio/Ambience/Objects/gas_vent.ogg - type: Weldable - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentOpening + - type: entity parent: GasUnaryBase id: GasPassiveVent @@ -92,7 +94,9 @@ - type: Construction graph: GasUnary node: passivevent - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentOpening + - type: entity parent: [GasUnaryBase, AirSensorBase] id: GasVentScrubber @@ -141,7 +145,9 @@ sound: path: /Audio/Ambience/Objects/gas_vent.ogg - type: Weldable - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentScrubber + - type: entity parent: GasUnaryBase id: GasOutletInjector @@ -180,7 +186,9 @@ visibleLayers: - enum.SubfloorLayers.FirstLayer - enum.LightLayers.Unshaded - + - type: AtmosMonitoringConsoleDevice + navMapBlip: GasVentOpening + - type: entity parent: [ BaseMachinePowered, ConstructibleMachine ] id: BaseGasThermoMachine @@ -224,7 +232,9 @@ examinableAddress: true - type: WiredNetworkConnection - type: PowerSwitch - + - type: AtmosMonitoringConsoleDevice + navMapBlip: Thermoregulator + - type: entity parent: BaseGasThermoMachine id: GasThermoMachineFreezer @@ -429,3 +439,5 @@ - type: ExaminableSolution solution: tank - type: PowerSwitch + - type: AtmosMonitoringConsoleDevice + navMapBlip: Thermoregulator diff --git a/Resources/Textures/Interface/NavMap/attributions.yml b/Resources/Textures/Interface/NavMap/attributions.yml new file mode 100644 index 0000000000..f624c0f448 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/attributions.yml @@ -0,0 +1,59 @@ +- files: ["beveled_arrow_east.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_arrow_north.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_arrow_south.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_arrow_west.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_circle.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_diamond.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_diamond_east_west.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_diamond_north_south.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_hexagon.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_square.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_star.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" + +- files: ["beveled_triangle.png"] + license: "CC-BY-SA-3.0" + copyright: "Created by chromiumboy" + source: "https://github.com/chromiumboy" diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_east.png b/Resources/Textures/Interface/NavMap/beveled_arrow_east.png new file mode 100644 index 0000000000..156685fe14 Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_arrow_east.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_east.png.yml b/Resources/Textures/Interface/NavMap/beveled_arrow_east.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_arrow_east.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_north.png b/Resources/Textures/Interface/NavMap/beveled_arrow_north.png new file mode 100644 index 0000000000..70ecd5afb9 Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_arrow_north.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_north.png.yml b/Resources/Textures/Interface/NavMap/beveled_arrow_north.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_arrow_north.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_south.png b/Resources/Textures/Interface/NavMap/beveled_arrow_south.png new file mode 100644 index 0000000000..0086c42c0e Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_arrow_south.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_south.png.yml b/Resources/Textures/Interface/NavMap/beveled_arrow_south.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_arrow_south.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_west.png b/Resources/Textures/Interface/NavMap/beveled_arrow_west.png new file mode 100644 index 0000000000..0dd40c204b Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_arrow_west.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_arrow_west.png.yml b/Resources/Textures/Interface/NavMap/beveled_arrow_west.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_arrow_west.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond.png b/Resources/Textures/Interface/NavMap/beveled_diamond.png new file mode 100644 index 0000000000..31fbf68db3 Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_diamond.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond.png.yml b/Resources/Textures/Interface/NavMap/beveled_diamond.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_diamond.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png b/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png new file mode 100644 index 0000000000..9c88e7c51d Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png.yml b/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png b/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png new file mode 100644 index 0000000000..af27998af8 Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png.yml b/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_star.png b/Resources/Textures/Interface/NavMap/beveled_star.png new file mode 100644 index 0000000000..0b39d01bd4 Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_star.png differ diff --git a/Resources/Textures/Interface/NavMap/beveled_star.png.yml b/Resources/Textures/Interface/NavMap/beveled_star.png.yml new file mode 100644 index 0000000000..dabd6601f7 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_star.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true