]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmospheric network monitor (#32294)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Tue, 17 Dec 2024 03:53:17 +0000 (21:53 -0600)
committerGitHub <noreply@github.com>
Tue, 17 Dec 2024 03:53:17 +0000 (04:53 +0100)
* Updated to latest master version

* Added gas pipe analyzer

* Completed prototype

* Playing with UI display

* Refinement of the main UI

* Renamed gas pipe analyzer to gas pipe sensor

* Added focus network highlighting and map icons for gas pipe sensors

* Added construction graph for gas pipe sensor

* Improved efficiency of atmos pipe and focus pipe network data storage

* Added gas pipe sensor variants

* Fixed gas pipe sensor nav map icon not highlighting on focus

* Rendered pipe lines now get merged together

* Set up appearance handling for the gas pipe sensor, but setting the layers is bugged

* Gas pipe sensor lights turn off when the device is unpowered

* Renamed console

* The gas pipe sensor is now a pipe. Redistributed components between it and its assembly

* AtmosMonitors can now optionally monitor their internal pipe network instead of the surrounding atmosphere

* Massive code clean up

* Added delta states to handle pipe net updates, fixed entity deletion handling

* Nav map blip data has been replaced with prototypes

* Nav map blip fixes

* Nav map colors are now set by the console component

* Made the nav map more responsive to changes in focus

* Updated nav map icons

* Reverted unnecessary namespace changes

* Code tidy up

* Updated sprites and construction graph for gas pipe sensor

* Updated localization files

* Misc bug fixes

* Added missing comment

* Fixed issue with the circuit board for the monitor

* Embellished the background of the console network entries

* Updated console to account for PR #32273

* Removed gas pipe sensor

* Fixing merge conflict

* Update

* Addressing reviews part 1

* Addressing review part 2

* Addressing reviews part 3

* Removed unnecessary references

* Side panel values will be grayed out if there is no gas present in the pipe network

* Declaring colors at the start of some files

* Added a colored stripe to the side of the atmos network entries

* Fixed an issue with pipe sensor blip coloration

* Fixed delay that occurs when toggling gas sensors on/off

47 files changed:
Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml [new file with mode: 0644]
Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs [new file with mode: 0644]
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs [new file with mode: 0644]
Content.Server/Atmos/Piping/Components/AtmosPipeColorComponent.cs
Content.Server/Atmos/Piping/EntitySystems/AtmosPipeColorSystem.cs
Content.Shared/Atmos/Atmospherics.cs
Content.Shared/Atmos/Components/GasPipeSensorComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Consoles/Components/AtmosMonitoringConsoleDeviceComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs [new file with mode: 0644]
Content.Shared/Prototypes/NavMapBlipPrototype.cs [new file with mode: 0644]
Resources/Locale/en-US/atmos/atmos-alerts-console.ftl
Resources/Locale/en-US/atmos/gases.ftl [new file with mode: 0644]
Resources/Locale/en-US/components/atmos-monitoring-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/nav_map_blips.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/gas_pipe_sensor.yml
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml
Resources/Textures/Interface/NavMap/attributions.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_east.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_east.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_north.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_north.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_south.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_south.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_west.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_arrow_west.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond_east_west.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_diamond_north_south.png.yml [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_star.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_star.png.yml [new file with mode: 0644]

index e533ef2dce0a2aec13aae9bc9741ee93ed692383..f0e4b13356c3529e9ca8e2b8a5c0e9e645e96629 100644 (file)
@@ -31,19 +31,6 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
         [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state",
     };
 
-    private Dictionary<Gas, string> _gasShorthands = new Dictionary<Gas, string>()
-    {
-        [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 (file)
index 0000000..563122f
--- /dev/null
@@ -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<TransformComponent>(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 (file)
index 0000000..c23ebb6
--- /dev/null
@@ -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<AtmosMonitoringConsoleLine> _atmosPipeNetwork = new();
+    private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
+
+    // Look up tables for merging continuous lines. Indexed by line color
+    private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLines = new();
+    private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLinesReversed = new();
+    private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLines = new();
+    private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLinesReversed = new();
+
+    public AtmosMonitoringConsoleNavMapControl() : base()
+    {
+        PostWallDrawingAction += DrawAllPipeNetworks;
+    }
+
+    protected override void UpdateNavMap()
+    {
+        base.UpdateNavMap();
+
+        if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(Owner, out var console))
+            return;
+
+        if (!_entManager.TryGetComponent<MapGridComponent>(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<AtmosMonitoringConsoleLine> atmosPipeNetwork)
+    {
+        var offset = GetOffset();
+        offset = offset with { Y = -offset.Y };
+
+        if (WorldRange / WorldMaxRange > 0.5f)
+        {
+            var pipeNetworks = new Dictionary<Color, ValueList<Vector2>>();
+
+            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<Vector2>();
+
+                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<Color, ValueList<Vector2>>();
+
+            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<Vector2>();
+
+                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<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Vector2i, AtmosPipeChunk>? chunks, MapGridComponent? grid)
+    {
+        var decodedOutput = new List<AtmosMonitoringConsoleLine>();
+
+        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<AtmosMonitoringConsoleLine>();
+
+            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 (file)
index 0000000..bfbb05d
--- /dev/null
@@ -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<AtmosMonitoringConsoleComponent, ComponentHandleState>(OnHandleState);
+    }
+
+    private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
+    {
+        Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
+        Dictionary<NetEntity, AtmosDeviceNavMapData> 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 (file)
index 0000000..b6fde75
--- /dev/null
@@ -0,0 +1,99 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+               xmlns:ui="clr-namespace:Content.Client.Atmos.Consoles"
+               xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+               xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+               Title="{Loc 'atmos-monitoring-window-title'}"
+               Resizable="False"
+               SetSize="1120 750"
+               MinSize="1120 750">
+    <BoxContainer Orientation="Vertical">
+        <!-- Main display -->
+        <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
+            <!-- Nav map -->
+            <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
+                <ui:AtmosMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
+
+                    <!-- System warning -->
+                    <PanelContainer Name="SystemWarningPanel"
+                                    HorizontalAlignment="Center"
+                                    VerticalAlignment="Top"
+                                    HorizontalExpand="True"
+                                    Margin="0 48 0 0"
+                                    Visible="False">
+                        <RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
+                    </PanelContainer>
+
+                </ui:AtmosMonitoringConsoleNavMapControl>
+
+                <!-- Nav map legend -->
+                <BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
+                    <TextureRect Stretch="KeepAspectCentered"
+                                 TexturePath="/Textures/Interface/NavMap/beveled_square.png"
+                                 Modulate="#a9a9a9"
+                                 SetSize="16 16"
+                                 Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-monitoring-window-label-gas-opening'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                 TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
+                                 SetSize="16 16"
+                                 Modulate="#a9a9a9"
+                                 Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-monitoring-window-label-gas-scrubber'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                 TexturePath="/Textures/Interface/NavMap/beveled_arrow_east.png"
+                                 SetSize="16 16"
+                                 Modulate="#a9a9a9"
+                                 Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-monitoring-window-label-gas-flow-regulator'}"/>
+                    <TextureRect Stretch="KeepAspectCentered"
+                                 TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
+                                 SetSize="16 16"
+                                 Modulate="#a9a9a9"
+                                 Margin="20 0 5 0"/>
+                    <Label Text="{Loc 'atmos-monitoring-window-label-thermoregulator'}"/>
+                </BoxContainer>
+            </BoxContainer>
+
+            <!-- Atmosphere status -->
+            <BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
+
+                <!-- Station name -->
+                <controls:StripeBack>
+                    <PanelContainer>
+                        <RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
+                    </PanelContainer>
+                </controls:StripeBack>
+
+                <!-- Alarm status (entries added by C# code) -->
+                <TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
+                    <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+                        <BoxContainer Name="AtmosNetworksTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
+                    </ScrollContainer>
+                </TabContainer>
+
+                <!-- Overlay toggles -->
+                <BoxContainer Orientation="Vertical" Margin="0 10 0 0">
+                    <Label Text="{Loc 'atmos-monitoring-window-toggle-overlays'}" Margin="0 0 0 5"/>
+                    <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+                        <CheckBox Name="ShowPipeNetwork" Text="{Loc 'atmos-monitoring-window-show-pipe-network'}" Pressed="True" HorizontalExpand="True"/>
+                        <CheckBox Name="ShowGasPipeSensors" Text="{Loc 'atmos-monitoring-window-show-gas-pipe-sensors'}" Pressed="False" HorizontalExpand="True"/>
+                    </BoxContainer>
+                </BoxContainer>
+
+            </BoxContainer>
+
+        </BoxContainer>
+
+        <!-- Footer -->
+        <BoxContainer Orientation="Vertical">
+            <PanelContainer StyleClasses="LowDivider" />
+            <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+                <Label Text="{Loc 'atmos-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
+                <Label Text="{Loc 'atmos-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
+                        HorizontalAlignment="Right" HorizontalExpand="True"  Margin="0 0 5 0" />
+                <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+                        VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+            </BoxContainer>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs
new file mode 100644 (file)
index 0000000..515f917
--- /dev/null
@@ -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<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
+    private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
+
+    public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
+    {
+        RobustXamlLoader.Load(this);
+        _entManager = IoCManager.Resolve<IEntityManager>();
+        _protoManager = IoCManager.Resolve<IPrototypeManager>();
+        _spriteSystem = _entManager.System<SpriteSystem>();
+
+        // 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<TransformComponent>(owner, out var xform))
+        {
+            consoleCoords = xform.Coordinates;
+            NavMap.MapUid = xform.GridUid;
+
+            // Assign station name      
+            if (_entManager.TryGetComponent<MetaDataComponent>(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<AtmosMonitoringConsoleComponent>(_owner, out var console))
+            return;
+
+        NavMap.TileColor = console.NavMapTileColor;
+        NavMap.WallColor = console.NavMapWallColor;
+
+        // Initalize
+        UpdateUI(consoleCoords, Array.Empty<AtmosMonitoringConsoleEntry>());
+    }
+
+    #region Toggle handling
+
+    private void OnShowPipeNetworkToggled()
+    {
+        if (_owner == null)
+            return;
+
+        if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_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<AtmosMonitoringConsoleComponent>(_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<AtmosMonitoringConsoleComponent>(_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<AtmosMonitoringConsoleComponent>(_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<AtmosMonitoringConsoleComponent>(_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<AtmosMonitoringConsoleComponent>(_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 (file)
index 0000000..6a19f07
--- /dev/null
@@ -0,0 +1,74 @@
+<BoxContainer xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         xmlns:s="clr-namespace:Content.Client.Stylesheets"
+         xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+         xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+         Orientation="Vertical" HorizontalExpand ="True" Margin="0 0 0 3">
+
+    <!-- Network selection button -->
+    <Button Name="FocusButton" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 6 8" StyleClasses="OpenLeft" Access="Public">
+        <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+            <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal" SetHeight="32">
+                <PanelContainer Name="NetworkColorStripe" HorizontalAlignment="Left" SetWidth="8" VerticalExpand="True" Margin="-8 -2 0 0">
+                    <PanelContainer.PanelOverride>
+                        <gfx:StyleBoxFlat BackgroundColor="#d7d7d7"/>
+                    </PanelContainer.PanelOverride>
+                </PanelContainer>
+                <Label Name="NetworkNameLabel" Text="???" HorizontalExpand="True" HorizontalAlignment="Center"/>
+            </BoxContainer>
+
+            <!-- Panel that appears on selecting the device -->
+        
+            <PanelContainer HorizontalExpand="True" Margin="-8 0 -14 -4" Access="Public">
+                <PanelContainer.PanelOverride>
+                    <gfx:StyleBoxFlat BackgroundColor="#25252a"/>
+                </PanelContainer.PanelOverride>
+                <BoxContainer Name="MainDataContainer" HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+                    <Control>
+                        <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+                            <BoxContainer HorizontalExpand="True" Orientation="Horizontal">
+                                <Label Name="TemperatureHeaderLabel" Text="{Loc 'atmos-alerts-window-temperature-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                                <Label Name="PressureHeaderLabel" Text="{Loc 'atmos-alerts-window-pressure-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                                <Label Name="TotalMolHeaderLabel" Text="{Loc 'atmos-alerts-window-total-mol-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                            </BoxContainer>
+                            <PanelContainer HorizontalExpand="True">
+                                <PanelContainer.PanelOverride>
+                                    <gfx:StyleBoxFlat BackgroundColor="#202023"/>
+                                </PanelContainer.PanelOverride>
+                                <BoxContainer HorizontalExpand="True" Orientation="Horizontal">
+                                    <Label Name="TemperatureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                                    <Label Name="PressureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                                    <Label Name="TotalMolLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
+                                </BoxContainer>
+                            </PanelContainer>
+                            <BoxContainer HorizontalExpand="True" Orientation="Horizontal" Margin="8 0">
+                                <TextureRect Name="ArrowTexture" VerticalAlignment="Center" SetSize="12 12" Stretch="KeepAspectCentered" Margin="3 0" TexturePath="/Textures/Interface/Nano/triangle_right.png"></TextureRect>
+                                <Label Name="GasesHeaderLabel" Text="{Loc 'atmos-monitoring-window-label-gases'}" HorizontalAlignment="Left" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="4 0 0 0" SetHeight="24"></Label>
+                            </BoxContainer>
+                    
+                        </BoxContainer>
+                    </Control>
+            
+                    <!-- Atmosphere status -->
+                    <Control Name="FocusContainer" ReservesSpace="False"  Visible="False">
+                        <!-- Main container for displaying atmospheric data -->
+                        <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
+                            <PanelContainer HorizontalExpand="True">
+                                <PanelContainer.PanelOverride>
+                                    <gfx:StyleBoxFlat BackgroundColor="#202023"/>
+                                </PanelContainer.PanelOverride>
+
+                                <!-- Gas entries added via C# code -->
+                                <GridContainer Name="GasGridContainer" HorizontalExpand="True" Columns = "4"></GridContainer>
+                            </PanelContainer>
+                        </BoxContainer>
+                    </Control>
+                </BoxContainer>
+
+                <!-- If the alarm is inactive, this is label is displayed instead -->
+                <Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
+                
+            </PanelContainer>
+        </BoxContainer>
+    </Button>
+</BoxContainer>
diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs
new file mode 100644 (file)
index 0000000..0ce0c9c
--- /dev/null
@@ -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<IEntityManager>();
+        _cache = IoCManager.Resolve<IResourceCache>();
+
+        Data = data;
+
+        // Modulate colored stripe
+        NetworkColorStripe.Modulate = data.Color;
+
+        // Load fonts
+        var headerFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11);
+        var normalFont = new VectorFont(_cache.GetResource<FontResource>("/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<FontResource>("/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;
+    }
+}
index 90c2680c4a79f94f7d205ed1111ba564fe3a123e..b774b7d8b566e011f36e5fe33ccac1428e3bb3d5 100644 (file)
@@ -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<FontResource>("/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<FontResource>("/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 (file)
index 0000000..5ecadc7
--- /dev/null
@@ -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<EntityUid, Dictionary<Vector2i, AtmosPipeChunk>> _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<AtmosMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
+        SubscribeLocalEvent<AtmosMonitoringConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChanged);
+        SubscribeLocalEvent<AtmosMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
+
+        // Tracked device events
+        SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, NodeGroupsRebuilt>(OnEntityNodeGroupsRebuilt);
+        SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, AtmosPipeColorChangedEvent>(OnEntityPipeColorChanged);
+        SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, EntityTerminatingEvent>(OnEntityShutdown);
+
+        // Grid events
+        SubscribeLocalEvent<GridSplitEvent>(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<MapGridComponent>(ent, out var grid))
+                continue;
+
+            RebuildAtmosPipeGrid(ent, grid);
+        }
+
+        // Update atmos monitoring consoles that stand upon an updated grid
+        var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
+        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<AtmosMonitoringConsoleComponent, TransformComponent>();
+            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<MapGridComponent>(gridUid, out var mapGrid))
+            return;
+
+        if (!TryComp<GridAtmosphereComponent>(gridUid, out var atmosphere))
+            return;
+
+        // The grid must have a NavMapComponent to visualize the map in the UI
+        EnsureComp<NavMapComponent>(gridUid);
+
+        // Gathering data to be send to the client
+        var atmosNetworks = new List<AtmosMonitoringConsoleEntry>();
+        var query = AllEntityQuery<GasPipeSensorComponent, TransformComponent>();
+
+        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<AtmosPipeColorComponent>(uid, out var colorComponent) ? colorComponent.Color : Color.White;
+
+        // Name the entity based on its label, if available
+        if (TryComp<LabelComponent>(uid, out var label) && label.CurrentLabel != null)
+            name = label.CurrentLabel;
+
+        // Otherwise use its base name and network address
+        else if (TryComp<DeviceNetworkComponent>(uid, out var deviceNet))
+            address = deviceNet.Address;
+
+        // Entry for unpowered devices
+        if (TryComp<ApcPowerReceiverComponent>(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<Gas, float>();
+        var isAirPresent = pipeNode.Air.TotalMoles > 0;
+
+        if (isAirPresent)
+        {
+            foreach (var gas in Enum.GetValues<Gas>())
+            {
+                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<NetEntity, AtmosDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
+    {
+        var atmosDeviceNavMapData = new Dictionary<NetEntity, AtmosDeviceNavMapData>();
+
+        var query = AllEntityQuery<AtmosMonitoringConsoleDeviceComponent, TransformComponent>();
+        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<AtmosPipeColorComponent>(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<Vector2i, AtmosPipeChunk>();
+
+        // Adds all atmos pipes to the nav map via bit mask chunks
+        var queryPipes = AllEntityQuery<AtmosPipeColorComponent, NodeContainerComponent, TransformComponent>();
+        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<AtmosMonitoringConsoleComponent, TransformComponent>();
+        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<Vector2i, AtmosPipeChunk>();
+
+        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<AtmosPipeColorComponent>(ent, out var entAtmosPipeColor))
+                continue;
+
+            if (!TryComp<NodeContainerComponent>(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<AtmosMonitoringConsoleComponent, TransformComponent>();
+
+        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<NodeContainerComponent>(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<MapGridComponent>(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<MapGridComponent>(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<AtmosMonitoringConsoleComponent, TransformComponent>();
+
+        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<MapGridComponent>(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<AtmosMonitoringConsoleComponent>();
+
+        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;
+    }
+}
index 5b05668ad57e30dd78a766303a3f9e04f709105f..a8edb07d31880abc84c9c0fbf81ae86c65773ccb 100644 (file)
@@ -1,19 +1,24 @@
 using Content.Server.Atmos.Piping.EntitySystems;
 using JetBrains.Annotations;
 
-namespace Content.Server.Atmos.Piping.Components
+namespace Content.Server.Atmos.Piping.Components;
+
+[RegisterComponent]
+public sealed partial class AtmosPipeColorComponent : Component
 {
-    [RegisterComponent]
-    public sealed partial class AtmosPipeColorComponent : Component
-    {
-        [DataField("color")]
-        public Color Color { get; set; } = Color.White;
+    [DataField]
+    public Color Color { get; set; } = Color.White;
 
-        [ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
-        public Color ColorVV
-        {
-            get => Color;
-            set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
-        }
+    [ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
+    public Color ColorVV
+    {
+        get => Color;
+        set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
     }
 }
+
+[ByRefEvent]
+public record struct AtmosPipeColorChangedEvent(Color color)
+{
+    public Color Color = color;
+}
index b9ee6680329df9915ae5bd6f8b2314f82e7ba7be..dcb08dcd574e16af12bc7269302fa55ebd7d8a30 100644 (file)
@@ -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);
         }
     }
 }
index 19766d92e6efab6426b02c3a9dabb6d95d768e72..cb89f6c19920838ff6de6c9a44b2cb0c81f98a7b 100644 (file)
@@ -145,6 +145,22 @@ namespace Content.Shared.Atmos
         /// </summary>
         public const float SpaceHeatCapacity = 7000f;
 
+        /// <summary>
+        ///     Dictionary of chemical abbreviations for <see cref="Gas"/>
+        /// </summary>
+        public static Dictionary<Gas, string> GasAbbreviations = new Dictionary<Gas, string>()
+        {
+            [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
 
         /// <summary>
diff --git a/Content.Shared/Atmos/Components/GasPipeSensorComponent.cs b/Content.Shared/Atmos/Components/GasPipeSensorComponent.cs
new file mode 100644 (file)
index 0000000..3393948
--- /dev/null
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Entities with component will be queried against for their
+/// atmos monitoring data on atmos monitoring consoles
+/// </summary>
+[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 (file)
index 0000000..2ac0d2a
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Entities capable of opening the atmos monitoring console UI
+/// require this component to function correctly
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedAtmosMonitoringConsoleSystem))]
+public sealed partial class AtmosMonitoringConsoleComponent : Component
+{
+    /*
+     * Don't need DataFields as this can be reconstructed
+     */
+
+    /// <summary>
+    /// A dictionary of the all the nav map chunks that contain anchored atmos pipes
+    /// </summary>
+    [ViewVariables]
+    public Dictionary<Vector2i, AtmosPipeChunk> AtmosPipeChunks = new();
+
+    /// <summary>
+    /// A list of all the atmos devices that will be used to populate the nav map
+    /// </summary>
+    [ViewVariables]
+    public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = new();
+
+    /// <summary>
+    /// Color of the floor tiles on the nav map screen
+    /// </summary>
+    [DataField, ViewVariables]
+    public Color NavMapTileColor;
+
+    /// <summary>
+    /// Color of the wall lines on the nav map screen
+    /// </summary>
+    [DataField, ViewVariables]
+    public Color NavMapWallColor;
+
+    /// <summary>
+    /// 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
+    /// </summary>
+    [ViewVariables]
+    public bool ForceFullUpdate = false;
+}
+
+[Serializable, NetSerializable]
+public struct AtmosPipeChunk(Vector2i origin)
+{
+    /// <summary>
+    /// Chunk position
+    /// </summary>
+    [ViewVariables]
+    public readonly Vector2i Origin = origin;
+
+    /// <summary>
+    /// Bitmask look up for atmos pipes, 1 for occupied and 0 for empty.
+    /// Indexed by the color hexcode of the pipe
+    /// </summary>
+    [ViewVariables]
+    public Dictionary<(int, string), ulong> AtmosPipeData = new();
+
+    /// <summary>
+    /// The last game tick that the chunk was updated
+    /// </summary>
+    [NonSerialized]
+    public GameTick LastUpdate;
+}
+
+[Serializable, NetSerializable]
+public struct AtmosDeviceNavMapData
+{
+    /// <summary>
+    /// The entity in question
+    /// </summary>
+    public NetEntity NetEntity;
+
+    /// <summary>
+    /// Location of the entity
+    /// </summary>
+    public NetCoordinates NetCoordinates;
+
+    /// <summary>
+    /// The associated pipe network ID 
+    /// </summary>
+    public int NetId = -1;
+
+    /// <summary>
+    /// Prototype ID for the nav map blip
+    /// </summary>
+    public ProtoId<NavMapBlipPrototype> NavMapBlip;
+
+    /// <summary>
+    /// Direction of the entity
+    /// </summary>
+    public Direction Direction;
+
+    /// <summary>
+    /// Color of the attached pipe
+    /// </summary>
+    public Color PipeColor;
+
+    /// <summary>
+    /// Populate the atmos monitoring console nav map with a single entity
+    /// </summary>
+    public AtmosDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, int netId, ProtoId<NavMapBlipPrototype> navMapBlip, Direction direction, Color pipeColor)
+    {
+        NetEntity = netEntity;
+        NetCoordinates = netCoordinates;
+        NetId = netId;
+        NavMapBlip = navMapBlip;
+        Direction = direction;
+        PipeColor = pipeColor;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class AtmosMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
+{
+    /// <summary>
+    /// A list of all entries to populate the UI with
+    /// </summary>
+    public AtmosMonitoringConsoleEntry[] AtmosNetworks;
+
+    /// <summary>
+    /// Sends data from the server to the client to populate the atmos monitoring console UI
+    /// </summary>
+    public AtmosMonitoringConsoleBoundInterfaceState(AtmosMonitoringConsoleEntry[] atmosNetworks)
+    {
+        AtmosNetworks = atmosNetworks;
+    }
+}
+
+[Serializable, NetSerializable]
+public struct AtmosMonitoringConsoleEntry
+{
+    /// <summary>
+    /// The entity in question
+    /// </summary>
+    public NetEntity NetEntity;
+
+    /// <summary>
+    /// Location of the entity
+    /// </summary>
+    public NetCoordinates Coordinates;
+
+    /// <summary>
+    /// The associated pipe network ID 
+    /// </summary>
+    public int NetId = -1;
+
+    /// <summary>
+    /// Localised device name
+    /// </summary>
+    public string EntityName;
+
+    /// <summary>
+    /// Device network address
+    /// </summary>
+    public string Address;
+
+    /// <summary>
+    /// Temperature (K)
+    /// </summary>
+    public float TemperatureData;
+
+    /// <summary>
+    /// Pressure (kPA)
+    /// </summary>
+    public float PressureData;
+
+    /// <summary>
+    /// Total number of mols of gas
+    /// </summary>
+    public float TotalMolData;
+
+    /// <summary>
+    /// Mol and percentage for all detected gases 
+    /// </summary>
+    public Dictionary<Gas, float> GasData = new();
+
+    /// <summary>
+    /// The color to be associated with the pipe network
+    /// </summary>
+    public Color Color;
+
+    /// <summary>
+    /// Indicates whether the entity is powered
+    /// </summary>
+    public bool IsPowered = true;
+
+    /// <summary>
+    /// Used to populate the atmos monitoring console UI with data from a single air alarm
+    /// </summary>
+    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,
+}
+
+/// <summary>
+/// UI key associated with the atmos monitoring console
+/// </summary>
+[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 (file)
index 0000000..50c3abc
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Entities with this component appear on the 
+/// nav maps of atmos monitoring consoles
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AtmosMonitoringConsoleDeviceComponent : Component
+{
+    /// <summary>
+    /// 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)
+    /// </summary>
+    [DataField, ViewVariables]
+    public ProtoId<NavMapBlipPrototype>? NavMapBlip = null;
+}
diff --git a/Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs b/Content.Shared/Atmos/Consoles/SharedAtmosMonitoringConsoleSystem.cs
new file mode 100644 (file)
index 0000000..e6dd455
--- /dev/null
@@ -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<AtmosMonitoringConsoleComponent, ComponentGetState>(OnGetState);
+    }
+
+    private void OnGetState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentGetState args)
+    {
+        Dictionary<Vector2i, Dictionary<(int, string), ulong>> 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<Vector2i, Dictionary<(int, string), ulong>> chunks,
+        Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices)
+        : ComponentState
+    {
+        public Dictionary<Vector2i, Dictionary<(int, string), ulong>> Chunks = chunks;
+        public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = atmosDevices;
+    }
+
+    [Serializable, NetSerializable]
+    protected sealed class AtmosMonitoringConsoleDeltaState(
+        Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks,
+        Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices,
+        HashSet<Vector2i> allChunks)
+        : ComponentState, IComponentDeltaState<AtmosMonitoringConsoleState>
+    {
+        public Dictionary<Vector2i, Dictionary<(int, string), ulong>> ModifiedChunks = modifiedChunks;
+        public Dictionary<NetEntity, AtmosDeviceNavMapData> AtmosDevices = atmosDevices;
+        public HashSet<Vector2i> 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<Vector2i, Dictionary<(int, string), ulong>>(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 (file)
index 0000000..ede82d8
--- /dev/null
@@ -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!;
+
+    /// <summary>
+    /// Sets whether the associated entity can be selected when the blip is clicked
+    /// </summary>
+    [DataField]
+    public bool Selectable = false;
+
+    /// <summary>
+    /// Sets whether the blips is always blinking
+    /// </summary>
+    [DataField]
+    public bool Blinks = false;
+
+    /// <summary>
+    /// Sets the color of the blip
+    /// </summary>
+    [DataField]
+    public Color Color { get; private set; } = Color.LightGray;
+
+    /// <summary>
+    /// Texture paths associated with the blip
+    /// </summary>
+    [DataField]
+    public ResPath[]? TexturePaths { get; private set; }
+
+    /// <summary>
+    /// Sets the UI scaling of the blip
+    /// </summary>
+    [DataField]
+    public float Scale { get; private set; } = 1f;
+}
index 470a8f869529e51907175bc81020247484f69d3f..dd9b6a01282ea50ef74a6268c391c46acf713e9b 100644 (file)
@@ -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 (file)
index 0000000..5c540c4
--- /dev/null
@@ -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 (file)
index 0000000..eab6f50
--- /dev/null
@@ -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
index 54616724fbe46ba581138eeca49e9c6d336eae06..26f2881ae80126fd23c15af5ca846d84baa0020f 100644 (file)
   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
index 4cd596e9b448824f0dff7b8a005607ca824f19e4..8167229ae5b37f535222e0cb01105f6bcc5be670 100644 (file)
         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 (file)
index 0000000..bc51557
--- /dev/null
@@ -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
index ed5137c28f57844dad75f46861bdb478ec1e0a2f..3bc00fb1b61f1e6a18a5447ccc4594433c037bde 100644 (file)
@@ -79,6 +79,8 @@
         !type:PortablePipeNode
         nodeGroupID: Pipe
         pipeDirection: South
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: Thermoregulator
   - type: ItemSlots
     slots:
       beakerSlot:
index 90e48d8be67d7b9d2f136b198aaa3bbb530fc48e..8327937ba8656c4d2fc43386b8b217343bc4d9d5 100644 (file)
@@ -73,7 +73,9 @@
     range: 5
     sound:
       path: /Audio/Ambience/Objects/gas_pump.ogg
-
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: GasFlowRegulator
+    
 - type: entity
   parent: GasBinaryBase
   id: GasVolumePump
       examinableAddress: true
       prefix: device-address-prefix-volume-pump
     - type: WiredNetworkConnection
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasFlowRegulator
+    
 - type: entity
   parent: GasBinaryBase
   id: GasPassiveGate
       range: 5
       sound:
         path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasValve
+      
 - type: entity
   parent: GasBinaryBase
   id: GasValve
       range: 5
       sound:
         path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasValve
+      
 - type: entity
   parent: GasBinaryBase
   id: SignalControlledValve
     range: 5
     sound:
       path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: GasValve
+      
 - type: entity
   parent: GasBinaryBase
   id: GasPort
     - type: Construction
       graph: GasBinary
       node: port
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasVentOpening
+      
 - type: entity
   parent: GasVentPump
   id: GasDualPortVentPump
           pipeDirection: South
     - type: AmbientSound
       enabled: true
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasVentOpening
+      
 - type: entity
   parent: [ BaseMachine, ConstructibleMachine ]
   id: GasRecycler
         acts: ["Destruction"]
   - type: Machine
     board: GasRecyclerMachineCircuitboard
-
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: GasValve
+    
 - type: entity
   parent: GasBinaryBase
   id: HeatExchanger
   - type: Construction
     graph: GasBinary
     node: radiator
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: Thermoregulator
index 08015abe7d6595bd2d41faa2812a26ff53edb5e1..22b56908ea818d552acb429e6838e223b4aa79ea 100644 (file)
@@ -27,6 +27,9 @@
           True: { state: lights }
   - type: AtmosMonitor
     monitorsPipeNet: true
+  - type: GasPipeSensor
+  - type: AtmosMonitoringConsoleDevice
+    navMapBlip: GasPipeSensor
   - type: ApcPowerReceiver
   - type: ExtensionCableReceiver
   - type: Construction
index 4fe5463bff9674f5b6a630e5c0e22081a1bb91a1..ef436b4299c4939734000ee50b8b770c9ef8805a 100644 (file)
@@ -54,6 +54,7 @@
   - type: PipeRestrictOverlap
   - type: AtmosUnsafeUnanchor
   - type: AtmosPipeColor
+  - type: AtmosMonitoringConsoleDevice
   - type: Tag
     tags:
     - Pipe
index e8025556aa54445eec76e8846b2f9a3053496ec3..bde7136850f0e485d3e8f3932e436cbfb57816b4 100644 (file)
@@ -70,7 +70,9 @@
       range: 5
       sound:
         path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasFlowRegulator
+      
 - type: entity
   parent: GasFilter
   id: GasFilterFlipped
       range: 5
       sound:
         path: /Audio/Ambience/Objects/gas_hiss.ogg
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasFlowRegulator
+      
 - type: entity
   parent: GasMixer
   id: GasMixerFlipped
     - type: Construction
       graph: GasTrinary
       node: pneumaticvalve
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasFlowRegulator
\ No newline at end of file
index d0ec74dd40a234edffe9125508fa76d724162e95..5da85544fca78ea8ee90ff76fcc94d4f22d25fb4 100644 (file)
@@ -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
       sound:
         path: /Audio/Ambience/Objects/gas_vent.ogg
     - type: Weldable
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasVentScrubber
+      
 - type: entity
   parent: GasUnaryBase
   id: GasOutletInjector
       visibleLayers:
       - enum.SubfloorLayers.FirstLayer
       - enum.LightLayers.Unshaded
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: GasVentOpening
+      
 - type: entity
   parent: [ BaseMachinePowered, ConstructibleMachine ]
   id: BaseGasThermoMachine
       examinableAddress: true
     - type: WiredNetworkConnection
     - type: PowerSwitch
-
+    - type: AtmosMonitoringConsoleDevice
+      navMapBlip: Thermoregulator
+      
 - type: entity
   parent: BaseGasThermoMachine
   id: GasThermoMachineFreezer
   - 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 (file)
index 0000000..f624c0f
--- /dev/null
@@ -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 (file)
index 0000000..156685f
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..70ecd5a
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..0086c42
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..0dd40c2
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..31fbf68
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..9c88e7c
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..af27998
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -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 (file)
index 0000000..0b39d01
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 (file)
index 0000000..dabd660
--- /dev/null
@@ -0,0 +1,2 @@
+sample:
+  filter: true