]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Visualized regions for NavMapControl (#31910)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Wed, 23 Oct 2024 12:49:58 +0000 (07:49 -0500)
committerGitHub <noreply@github.com>
Wed, 23 Oct 2024 12:49:58 +0000 (14:49 +0200)
* Atmospheric alerts computer

* Moved components, restricted access to them

* Minor tweaks

* The screen will now turn off when the computer is not powered

* Bug fix

* Adjusted label

* Updated to latest master version

* Initial commit

* Tidy up

* Add firelocks to the nav map

* Add nav map regions to atmos alerts computer

* Added support for multiple region overlay sets per grid

* Fixed issue where console values were not updating correctly

* Fixing merge conflict

* Fixing merge conflicts

* Finished all major features

* Removed station map regions (to be re-added in a separate PR)

* Improved clarity

* Adjusted the color saturation of the regions displayed on the atmos alerts computer

Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
Content.Client/Pinpointer/NavMapSystem.Regions.cs [new file with mode: 0644]
Content.Client/Pinpointer/NavMapSystem.cs
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs
Content.Shared/Pinpointer/NavMapComponent.cs
Content.Shared/Pinpointer/SharedNavMapSystem.cs
Resources/Locale/en-US/atmos/atmos-alerts-console.ftl
Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml

index f0b7ffbe1199097c837ab5f2f78d47fbe5b41815..64068d6dbbf1f478e4cb4e350a623bd45a59acc7 100644 (file)
@@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
 {
     private readonly IEntityManager _entManager;
     private readonly SpriteSystem _spriteSystem;
+    private readonly SharedNavMapSystem _navMapSystem;
 
     private EntityUid? _owner;
     private NetEntity? _trackedEntity;
@@ -47,6 +48,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
         RobustXamlLoader.Load(this);
         _entManager = IoCManager.Resolve<IEntityManager>();
         _spriteSystem = _entManager.System<SpriteSystem>();
+        _navMapSystem = _entManager.System<SharedNavMapSystem>();
 
         // Pass the owner to nav map
         _owner = owner;
@@ -179,6 +181,9 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
         // Add tracked entities to the nav map
         foreach (var device in console.AtmosDevices)
         {
+            if (!device.NetEntity.Valid)
+                continue;
+
             if (!NavMap.Visible)
                 continue;
 
@@ -270,6 +275,34 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
         else
             MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
 
+        // Update sensor regions
+        NavMap.RegionOverlays.Clear();
+        var prioritizedRegionOverlays = new Dictionary<NavMapRegionOverlay, int>();
+
+        if (_owner != null &&
+            _entManager.TryGetComponent<TransformComponent>(_owner, out var xform) &&
+            _entManager.TryGetComponent<NavMapComponent>(xform.GridUid, out var navMap))
+        {
+            var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key);
+
+            foreach (var (regionOwner, regionOverlay) in regionOverlays)
+            {
+                var alarmState = GetAlarmState(regionOwner);
+
+                if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor))
+                    continue;
+
+                regionOverlay.Color = regionColor.Value;
+
+                var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState;
+                prioritizedRegionOverlays.Add(regionOverlay, priority);
+            }
+
+            // Sort overlays according to their priority
+            var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList();
+            NavMap.RegionOverlays = sortedOverlays;
+        }
+
         // Auto-scroll re-enable
         if (_autoScrollAwaitsUpdate)
         {
@@ -298,6 +331,24 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
         NavMap.TrackedEntities[metaData.NetEntity] = blip;
     }
 
+    private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, [NotNullWhen(true)] out Color? color)
+    {
+        color = null;
+
+        var blip = GetBlipTexture(alarmState);
+
+        if (blip == null)
+            return false;
+
+        // Color the region based on alarm state and entity tracking
+        color = blip.Value.Item2 * new Color(154, 154, 154);
+
+        if (_trackedEntity != null && _trackedEntity != regionOwner)
+            color *= Color.DimGray;
+
+        return true;
+    }
+
     private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
     {
         // Make new UI entry if required
diff --git a/Content.Client/Pinpointer/NavMapSystem.Regions.cs b/Content.Client/Pinpointer/NavMapSystem.Regions.cs
new file mode 100644 (file)
index 0000000..4cc7754
--- /dev/null
@@ -0,0 +1,303 @@
+using Content.Shared.Atmos;
+using Content.Shared.Pinpointer;
+using System.Linq;
+
+namespace Content.Client.Pinpointer;
+
+public sealed partial class NavMapSystem
+{
+    private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable =
+    {
+        (AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West),
+        (AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East),
+        (AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South),
+        (AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North),
+    };
+
+    public override void Update(float frameTime)
+    {
+        // To prevent compute spikes, only one region is flood filled per frame 
+        var query = AllEntityQuery<NavMapComponent>();
+
+        while (query.MoveNext(out var ent, out var entNavMapRegions))
+            FloodFillNextEnqueuedRegion(ent, entNavMapRegions);
+    }
+
+    private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component)
+    {
+        if (!component.QueuedRegionsToFlood.Any())
+            return;
+
+        var regionOwner = component.QueuedRegionsToFlood.Dequeue();
+
+        // If the region is no longer valid, flood the next one in the queue
+        if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) ||
+            !regionProperties.Seeds.Any())
+        {
+            FloodFillNextEnqueuedRegion(uid, component);
+            return;
+        }
+
+        // Flood fill the region, using the region seeds as starting points
+        var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties);
+
+        // Combine the flooded tiles into larger rectangles
+        var gridCoords = GetMergedRegionTiles(floodedTiles);
+
+        // Create and assign the new region overlay
+        var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords)
+        {
+            Color = regionProperties.Color
+        };
+
+        component.RegionOverlays[regionOwner] = regionOverlay;
+
+        // To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner
+
+        // First remove an old assignments
+        if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks))
+        {
+            foreach (var chunk in oldChunks)
+            {
+                if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners))
+                {
+                    oldOwners.Remove(regionOwner);
+                    component.ChunkToRegionOwnerTable[chunk] = oldOwners;
+                }
+            }
+        }
+
+        // Now update with the new assignments
+        component.RegionOwnerToChunkTable[regionOwner] = floodedChunks;
+
+        foreach (var chunk in floodedChunks)
+        {
+            if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners))
+                owners = new();
+
+            owners.Add(regionOwner);
+            component.ChunkToRegionOwnerTable[chunk] = owners;
+        }
+    }
+
+    private (HashSet<Vector2i>, HashSet<Vector2i>) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties)
+    {
+        if (!regionProperties.Seeds.Any())
+            return (new(), new());
+
+        var visitedChunks = new HashSet<Vector2i>();
+        var visitedTiles = new HashSet<Vector2i>();
+        var tilesToVisit = new Stack<Vector2i>();
+
+        foreach (var regionSeed in regionProperties.Seeds)
+        {
+            tilesToVisit.Push(regionSeed);
+
+            while (tilesToVisit.Count > 0)
+            {
+                // If the max region area is hit, exit
+                if (visitedTiles.Count > regionProperties.MaxArea)
+                    return (new(), new());
+
+                // Pop the top tile from the stack 
+                var current = tilesToVisit.Pop();
+
+                // If the current tile position has already been visited,
+                // or is too far away from the seed, continue
+                if ((regionSeed - current).Length > regionProperties.MaxRadius)
+                    continue;
+
+                if (visitedTiles.Contains(current))
+                    continue;
+
+                // Determine the tile's chunk index
+                var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize);
+                var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize);
+                var idx = GetTileIndex(relative);
+
+                // Extract the tile data
+                if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
+                    continue;
+
+                var flag = chunk.TileData[idx];
+
+                // If the current tile is entirely occupied, continue
+                if ((FloorMask & flag) == 0)
+                    continue;
+
+                if ((WallMask & flag) == WallMask)
+                    continue;
+
+                if ((AirlockMask & flag) == AirlockMask)
+                    continue;
+
+                // Otherwise the tile can be added to this region
+                visitedTiles.Add(current);
+                visitedChunks.Add(chunkOrigin);
+
+                // Determine if we can propagate the region into its cardinally adjacent neighbors
+                // To propagate to a neighbor, movement into the neighbors closest edge must not be 
+                // blocked, and vice versa
+
+                foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable)
+                {
+                    if (!RegionCanPropagateInDirection(chunk, current, direction))
+                        continue;
+
+                    var neighbor = current + tileOffset;
+                    var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize);
+
+                    if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk))
+                        continue;
+
+                    visitedChunks.Add(neighborOrigin);
+
+                    if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection))
+                        continue;
+
+                    tilesToVisit.Push(neighbor);
+                }
+            }
+        }
+
+        return (visitedTiles, visitedChunks);
+    }
+
+    private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction)
+    {
+        var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
+        var idx = GetTileIndex(relative);
+        var flag = chunk.TileData[idx];
+
+        if ((FloorMask & flag) == 0)
+            return false;
+
+        var directionMask = 1 << (int)direction;
+        var wallMask = (int)direction << (int)NavMapChunkType.Wall;
+        var airlockMask = (int)direction << (int)NavMapChunkType.Airlock;
+
+        if ((wallMask & flag) > 0)
+            return false;
+
+        if ((airlockMask & flag) > 0)
+            return false;
+
+        return true;
+    }
+
+    private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet<Vector2i> tiles)
+    {
+        if (!tiles.Any())
+            return new();
+
+        var x = tiles.Select(t => t.X);
+        var minX = x.Min();
+        var maxX = x.Max();
+
+        var y = tiles.Select(t => t.Y);
+        var minY = y.Min();
+        var maxY = y.Max();
+
+        var matrix = new int[maxX - minX + 1, maxY - minY + 1];
+
+        foreach (var tile in tiles)
+        {
+            var a = tile.X - minX;
+            var b = tile.Y - minY;
+
+            matrix[a, b] = 1;
+        }
+
+        return GetMergedRegionTiles(matrix, new Vector2i(minX, minY));
+    }
+
+    private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset)
+    {
+        var output = new List<(Vector2i, Vector2i)>();
+
+        var rows = matrix.GetLength(0);
+        var cols = matrix.GetLength(1);
+
+        var dp = new int[rows, cols];
+        var coords = (new Vector2i(), new Vector2i());
+        var maxArea = 0;
+
+        var count = 0;
+
+        while (!IsArrayEmpty(matrix))
+        {
+            count++;
+
+            if (count > rows * cols)
+                break;
+
+            // Clear old values
+            dp = new int[rows, cols];
+            coords = (new Vector2i(), new Vector2i());
+            maxArea = 0;
+
+            // Initialize the first row of dp
+            for (int j = 0; j < cols; j++)
+            {
+                dp[0, j] = matrix[0, j];
+            }
+
+            // Calculate dp values for remaining rows
+            for (int i = 1; i < rows; i++)
+            {
+                for (int j = 0; j < cols; j++)
+                    dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0;
+            }
+
+            // Find the largest rectangular area seeded for each position in the matrix
+            for (int i = 0; i < rows; i++)
+            {
+                for (int j = 0; j < cols; j++)
+                {
+                    int minWidth = dp[i, j];
+
+                    for (int k = j; k >= 0; k--)
+                    {
+                        if (dp[i, k] <= 0)
+                            break;
+
+                        minWidth = Math.Min(minWidth, dp[i, k]);
+                        var currArea = Math.Max(maxArea, minWidth * (j - k + 1));
+
+                        if (currArea > maxArea)
+                        {
+                            maxArea = currArea;
+                            coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j));
+                        }
+                    }
+                }
+            }
+
+            // Save the recorded rectangle vertices
+            output.Add((coords.Item1 + offset, coords.Item2 + offset));
+
+            // Removed the tiles covered by the rectangle from matrix
+            for (int i = coords.Item1.X; i <= coords.Item2.X; i++)
+            {
+                for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++)
+                    matrix[i, j] = 0;
+            }
+        }
+
+        return output;
+    }
+
+    private bool IsArrayEmpty(int[,] matrix)
+    {
+        for (int i = 0; i < matrix.GetLength(0); i++)
+        {
+            for (int j = 0; j < matrix.GetLength(1); j++)
+            {
+                if (matrix[i, j] == 1)
+                    return false;
+            }
+        }
+
+        return true;
+    }
+}
index 9aeb792a429f5ed274403c34480e994c03c1fef5..47469d4ea79a645bf204e0d807ed131c5c509ab0 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using Content.Shared.Pinpointer;
 using Robust.Shared.GameStates;
 
@@ -16,6 +17,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
     {
         Dictionary<Vector2i, int[]> modifiedChunks;
         Dictionary<NetEntity, NavMapBeacon> beacons;
+        Dictionary<NetEntity, NavMapRegionProperties> regions;
 
         switch (args.Current)
         {
@@ -23,6 +25,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
             {
                 modifiedChunks = delta.ModifiedChunks;
                 beacons = delta.Beacons;
+                regions = delta.Regions;
+
                 foreach (var index in component.Chunks.Keys)
                 {
                     if (!delta.AllChunks!.Contains(index))
@@ -35,6 +39,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
             {
                 modifiedChunks = state.Chunks;
                 beacons = state.Beacons;
+                regions = state.Regions;
+
                 foreach (var index in component.Chunks.Keys)
                 {
                     if (!state.Chunks.ContainsKey(index))
@@ -47,13 +53,54 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
                 return;
         }
 
+        // Update region data and queue new regions for flooding
+        var prevRegionOwners = component.RegionProperties.Keys.ToList();
+        var validRegionOwners = new List<NetEntity>();
+
+        component.RegionProperties.Clear();
+
+        foreach (var (regionOwner, regionData) in regions)
+        {
+            if (!regionData.Seeds.Any())
+                continue;
+
+            component.RegionProperties[regionOwner] = regionData;
+            validRegionOwners.Add(regionOwner);
+
+            if (component.RegionOverlays.ContainsKey(regionOwner))
+                continue;
+
+            if (component.QueuedRegionsToFlood.Contains(regionOwner))
+                continue;
+
+            component.QueuedRegionsToFlood.Enqueue(regionOwner);
+        }
+
+        // Remove stale region owners
+        var regionOwnersToRemove = prevRegionOwners.Except(validRegionOwners);
+
+        foreach (var regionOwnerRemoved in regionOwnersToRemove)
+            RemoveNavMapRegion(uid, component, regionOwnerRemoved);
+
+        // Modify chunks
         foreach (var (origin, chunk) in modifiedChunks)
         {
             var newChunk = new NavMapChunk(origin);
             Array.Copy(chunk, newChunk.TileData, chunk.Length);
             component.Chunks[origin] = newChunk;
+
+            // If the affected chunk intersects one or more regions, re-flood them
+            if (!component.ChunkToRegionOwnerTable.TryGetValue(origin, out var affectedOwners))
+                continue;
+
+            foreach (var affectedOwner in affectedOwners)
+            {
+                if (!component.QueuedRegionsToFlood.Contains(affectedOwner))
+                    component.QueuedRegionsToFlood.Enqueue(affectedOwner);
+            }
         }
 
+        // Refresh beacons
         component.Beacons.Clear();
         foreach (var (nuid, beacon) in beacons)
         {
index 413b41c36a6f43e916a497e0882e4f8f0d0a1e63..90c2680c4a79f94f7d205ed1111ba564fe3a123e 100644 (file)
@@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl
     public List<(Vector2, Vector2)> TileLines = new();
     public List<(Vector2, Vector2)> TileRects = new();
     public List<(Vector2[], Color)> TilePolygons = new();
+    public List<NavMapRegionOverlay> RegionOverlays = new();
 
     // Default colors
     public Color WallColor = new(102, 217, 102);
@@ -228,7 +229,7 @@ public partial class NavMapControl : MapGridControl
             {
                 if (!blip.Selectable)
                     continue;
-                
+
                 var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length();
 
                 if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
@@ -319,6 +320,22 @@ public partial class NavMapControl : MapGridControl
             }
         }
 
+        // Draw region overlays
+        if (_grid != null)
+        {
+            foreach (var regionOverlay in RegionOverlays)
+            {
+                foreach (var gridCoords in regionOverlay.GridCoords)
+                {
+                    var positionTopLeft = ScalePosition(new Vector2(gridCoords.Item1.X, -gridCoords.Item1.Y) - new Vector2(offset.X, -offset.Y));
+                    var positionBottomRight = ScalePosition(new Vector2(gridCoords.Item2.X + _grid.TileSize, -gridCoords.Item2.Y - _grid.TileSize) - new Vector2(offset.X, -offset.Y));
+
+                    var box = new UIBox2(positionTopLeft, positionBottomRight);
+                    handle.DrawRect(box, regionOverlay.Color);
+                }
+            }
+        }
+
         // Draw map lines
         if (TileLines.Any())
         {
index d9a475dbfb73438db659129cf17a95acb8b540d4..2d35ae59731094edfa94a8c159d79bf691ba034e 100644 (file)
@@ -1,15 +1,19 @@
 using Content.Server.Atmos.Monitor.Components;
 using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Pinpointer;
 using Content.Server.Power.Components;
 using Content.Shared.Atmos;
 using Content.Shared.Atmos.Components;
 using Content.Shared.Atmos.Consoles;
 using Content.Shared.Atmos.Monitor;
 using Content.Shared.Atmos.Monitor.Components;
+using Content.Shared.DeviceNetwork.Components;
 using Content.Shared.Pinpointer;
+using Content.Shared.Tag;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
-using Robust.Shared.Player;
+using Robust.Shared.Timing;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
     [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
     [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly TagSystem _tagSystem = default!;
+    [Dependency] private readonly MapSystem _mapSystem = default!;
+    [Dependency] private readonly TransformSystem _transformSystem = default!;
+    [Dependency] private readonly NavMapSystem _navMapSystem = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
 
     private const float UpdateTime = 1.0f;
 
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
 
         // Grid events
         SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
+
+        // Alarm events
+        SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
         SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
     }
 
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
     }
 
     private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
+    {
+        OnDeviceAdditionOrRemoval(uid, component, args.Anchored);
+    }
+
+    private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args)
+    {
+        OnDeviceAdditionOrRemoval(uid, component, false);
+    }
+
+    private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding)
     {
         var xform = Transform(uid);
         var gridUid = xform.GridUid;
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
         if (gridUid == null)
             return;
 
-        if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
+        if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
             return;
 
-        var netEntity = EntityManager.GetNetEntity(uid);
+        if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
+            return;
+
+        var netEntity = GetNetEntity(uid);
 
         var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
         while (query.MoveNext(out var ent, out var entConsole, out var entXform))
@@ -99,11 +125,18 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
             if (gridUid != entXform.GridUid)
                 continue;
 
-            if (args.Anchored)
+            if (isAdding)
+            {
                 entConsole.AtmosDevices.Add(data.Value);
+            }
 
-            else if (!args.Anchored)
+            else
+            {
                 entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
+                _navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity);
+            }
+
+            Dirty(ent, entConsole);
         }
     }
 
@@ -209,6 +242,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
             if (entDevice.Group != group)
                 continue;
 
+            if (!TryComp<MapGridComponent>(entXform.GridUid, out var mapGrid))
+                continue;
+
+            if (!TryComp<NavMapComponent>(entXform.GridUid, out var navMap))
+                continue;
+
             // If emagged, change the alarm type to normal
             var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
 
@@ -216,14 +255,45 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
             if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
                 alarmState = AtmosAlarmType.Invalid;
 
+            // Create entry
+            var netEnt = GetNetEntity(ent);
+
             var entry = new AtmosAlertsComputerEntry
-                (GetNetEntity(ent),
+                (netEnt,
                 GetNetCoordinates(entXform.Coordinates),
                 entDevice.Group,
                 alarmState,
                 MetaData(ent).EntityName,
                 entDeviceNetwork.Address);
 
+            // Get the list of sensors attached to the alarm
+            var sensorList = TryComp<DeviceListComponent>(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null;
+
+            if (sensorList?.Any() == true)
+            {
+                var alarmRegionSeeds = new HashSet<Vector2i>();
+
+                // If valid and anchored, use the position of sensors as seeds for the region
+                foreach (var (address, sensorEnt) in sensorList)
+                {
+                    if (!sensorEnt.IsValid() || !HasComp<AtmosMonitorComponent>(sensorEnt))
+                        continue;
+
+                    var sensorXform = Transform(sensorEnt);
+
+                    if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid)
+                        alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform)));
+                }
+
+                var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds);
+                _navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties);
+            }
+
+            else
+            {
+                _navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt);
+            }
+
             alarmStateData.Add(entry);
         }
 
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
         var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
         while (query.MoveNext(out var ent, out var entComponent, out var entXform))
         {
-            if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
+            if (entXform.GridUid != gridUid)
+                continue;
+
+            if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data))
                 atmosDeviceNavMapData.Add(data.Value);
         }
 
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
         (EntityUid uid,
         AtmosAlertsDeviceComponent component,
         TransformComponent xform,
-        EntityUid gridUid,
         [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
     {
         output = null;
 
-        if (xform.GridUid != gridUid)
-            return false;
-
         if (!xform.Anchored)
             return false;
 
index d77169d32eddd3e9a706c1afb1eba9b9adac8d4d..b876cb20fe23fd3767cbd13662e363795bff08d5 100644 (file)
@@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component
     /// </summary>
     [ViewVariables]
     public Dictionary<NetEntity, SharedNavMapSystem.NavMapBeacon> Beacons = new();
+
+    /// <summary>
+    /// Describes the properties of a region on the station.
+    /// It is indexed by the entity assigned as the region owner.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Dictionary<NetEntity, SharedNavMapSystem.NavMapRegionProperties> RegionProperties = new();
+
+    /// <summary>
+    /// All flood filled regions, ready for display on a NavMapControl.
+    /// It is indexed by the entity assigned as the region owner.
+    /// </summary>
+    /// <remarks>
+    /// For client use only
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Dictionary<NetEntity, NavMapRegionOverlay> RegionOverlays = new();
+
+    /// <summary>
+    /// A queue of all region owners that are waiting their associated regions to be floodfilled.
+    /// </summary>
+    /// <remarks>
+    /// For client use only
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Queue<NetEntity> QueuedRegionsToFlood = new();
+
+    /// <summary>
+    /// A look up table to get a list of region owners associated with a flood filled chunk.
+    /// </summary>
+    /// <remarks>
+    /// For client use only
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Dictionary<Vector2i, HashSet<NetEntity>> ChunkToRegionOwnerTable = new();
+
+    /// <summary>
+    ///  A look up table to find flood filled chunks associated with a given region owner.
+    /// </summary>
+    /// <remarks>
+    /// For client use only
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Dictionary<NetEntity, HashSet<Vector2i>> RegionOwnerToChunkTable = new();
 }
 
 [Serializable, NetSerializable]
@@ -51,10 +95,30 @@ public sealed class NavMapChunk(Vector2i origin)
     public GameTick LastUpdate;
 }
 
+[Serializable, NetSerializable]
+public sealed class NavMapRegionOverlay(Enum uiKey, List<(Vector2i, Vector2i)> gridCoords)
+{
+    /// <summary>
+    /// The key to the UI that will be displaying this region on its navmap
+    /// </summary>
+    public Enum UiKey = uiKey;
+
+    /// <summary>
+    /// The local grid coordinates of the rectangles that make up the region
+    /// Item1 is the top left corner, Item2 is the bottom right corner
+    /// </summary>
+    public List<(Vector2i, Vector2i)> GridCoords = gridCoords;
+
+    /// <summary>
+    /// Color of the region
+    /// </summary>
+    public Color Color = Color.White;
+}
+
 public enum NavMapChunkType : byte
 {
     // Values represent bit shift offsets when retrieving data in the tile array.
-    Invalid  = byte.MaxValue,
+    Invalid = byte.MaxValue,
     Floor = 0, // I believe floors have directional information for diagonal tiles?
     Wall = SharedNavMapSystem.Directions,
     Airlock = 2 * SharedNavMapSystem.Directions,
index 3ced5f3c9ed52966cb71fbeb092421321f46a230..37d60dec28a7ebde5ff39d0885a23282a0aee516 100644 (file)
@@ -3,10 +3,9 @@ using System.Numerics;
 using System.Runtime.CompilerServices;
 using Content.Shared.Tag;
 using Robust.Shared.GameStates;
+using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
 
 namespace Content.Shared.Pinpointer;
 
@@ -16,7 +15,7 @@ public abstract class SharedNavMapSystem : EntitySystem
     public const int Directions = 4; // Not directly tied to number of atmos directions
 
     public const int ChunkSize = 8;
-    public const int ArraySize = ChunkSize* ChunkSize;
+    public const int ArraySize = ChunkSize * ChunkSize;
 
     public const int AllDirMask = (1 << Directions) - 1;
     public const int AirlockMask = AllDirMask << (int) NavMapChunkType.Airlock;
@@ -24,6 +23,7 @@ public abstract class SharedNavMapSystem : EntitySystem
     public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
 
     [Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!;
+    [Robust.Shared.IoC.Dependency] private readonly INetManager _net = default!;
 
     private static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
     private EntityQuery<NavMapDoorComponent> _doorQuery;
@@ -57,7 +57,7 @@ public abstract class SharedNavMapSystem : EntitySystem
     public NavMapChunkType GetEntityType(EntityUid uid)
     {
         if (_doorQuery.HasComp(uid))
-            return  NavMapChunkType.Airlock;
+            return NavMapChunkType.Airlock;
 
         if (_tagSystem.HasAnyTag(uid, WallTags))
             return NavMapChunkType.Wall;
@@ -81,6 +81,57 @@ public abstract class SharedNavMapSystem : EntitySystem
         return true;
     }
 
+    public void AddOrUpdateNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner, NavMapRegionProperties regionProperties)
+    {
+        // Check if a new region has been added or an existing one has been altered
+        var isDirty = !component.RegionProperties.TryGetValue(regionOwner, out var oldProperties) || oldProperties != regionProperties;
+
+        if (isDirty)
+        {
+            component.RegionProperties[regionOwner] = regionProperties;
+
+            if (_net.IsServer)
+                Dirty(uid, component);
+        }
+    }
+
+    public void RemoveNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner)
+    {
+        bool regionOwnerRemoved = component.RegionProperties.Remove(regionOwner) | component.RegionOverlays.Remove(regionOwner);
+
+        if (regionOwnerRemoved)
+        {
+            if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var affectedChunks))
+            {
+                foreach (var affectedChunk in affectedChunks)
+                {
+                    if (component.ChunkToRegionOwnerTable.TryGetValue(affectedChunk, out var regionOwners))
+                        regionOwners.Remove(regionOwner);
+                }
+
+                component.RegionOwnerToChunkTable.Remove(regionOwner);
+            }
+
+            if (_net.IsServer)
+                Dirty(uid, component);
+        }
+    }
+
+    public Dictionary<NetEntity, NavMapRegionOverlay> GetNavMapRegionOverlays(EntityUid uid, NavMapComponent component, Enum uiKey)
+    {
+        var regionOverlays = new Dictionary<NetEntity, NavMapRegionOverlay>();
+
+        foreach (var (regionOwner, regionOverlay) in component.RegionOverlays)
+        {
+            if (!regionOverlay.UiKey.Equals(uiKey))
+                continue;
+
+            regionOverlays.Add(regionOwner, regionOverlay);
+        }
+
+        return regionOverlays;
+    }
+
     #region: Event handling
 
     private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
@@ -97,7 +148,7 @@ public abstract class SharedNavMapSystem : EntitySystem
                 chunks.Add(origin, chunk.TileData);
             }
 
-            args.State = new NavMapState(chunks, component.Beacons);
+            args.State = new NavMapState(chunks, component.Beacons, component.RegionProperties);
             return;
         }
 
@@ -110,7 +161,7 @@ public abstract class SharedNavMapSystem : EntitySystem
             chunks.Add(origin, chunk.TileData);
         }
 
-        args.State = new NavMapDeltaState(chunks, component.Beacons, new(component.Chunks.Keys));
+        args.State = new NavMapDeltaState(chunks, component.Beacons, component.RegionProperties, new(component.Chunks.Keys));
     }
 
     #endregion
@@ -120,22 +171,26 @@ public abstract class SharedNavMapSystem : EntitySystem
     [Serializable, NetSerializable]
     protected sealed class NavMapState(
         Dictionary<Vector2i, int[]> chunks,
-        Dictionary<NetEntity, NavMapBeacon> beacons)
+        Dictionary<NetEntity, NavMapBeacon> beacons,
+        Dictionary<NetEntity, NavMapRegionProperties> regions)
         : ComponentState
     {
         public Dictionary<Vector2i, int[]> Chunks = chunks;
         public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
+        public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
     }
 
     [Serializable, NetSerializable]
     protected sealed class NavMapDeltaState(
         Dictionary<Vector2i, int[]> modifiedChunks,
         Dictionary<NetEntity, NavMapBeacon> beacons,
+        Dictionary<NetEntity, NavMapRegionProperties> regions,
         HashSet<Vector2i> allChunks)
         : ComponentState, IComponentDeltaState<NavMapState>
     {
         public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
         public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
+        public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
         public HashSet<Vector2i> AllChunks = allChunks;
 
         public void ApplyToFullState(NavMapState state)
@@ -159,11 +214,18 @@ public abstract class SharedNavMapSystem : EntitySystem
             {
                 state.Beacons.Add(nuid, beacon);
             }
+
+            state.Regions.Clear();
+            foreach (var (nuid, region) in Regions)
+            {
+                state.Regions.Add(nuid, region);
+            }
         }
 
         public NavMapState CreateNewFullState(NavMapState state)
         {
             var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count);
+
             foreach (var (index, data) in state.Chunks)
             {
                 if (!AllChunks!.Contains(index))
@@ -177,12 +239,25 @@ public abstract class SharedNavMapSystem : EntitySystem
                     Array.Copy(newData, data, ArraySize);
             }
 
-            return new NavMapState(chunks, new(Beacons));
+            return new NavMapState(chunks, new(Beacons), new(Regions));
         }
     }
 
     [Serializable, NetSerializable]
     public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position);
 
+    [Serializable, NetSerializable]
+    public record struct NavMapRegionProperties(NetEntity Owner, Enum UiKey, HashSet<Vector2i> Seeds)
+    {
+        // Server defined color for the region
+        public Color Color = Color.White;
+
+        // The maximum number of tiles that can be assigned to this region
+        public int MaxArea = 625;
+
+        // The maximum distance this region can propagate from its seeds
+        public int MaxRadius = 25;
+    }
+
     #endregion
 }
index a1640c5e9d5b4f23235ebfe30dd039a8b8a411ed..470a8f869529e51907175bc81020247484f69d3f 100644 (file)
@@ -25,7 +25,7 @@ atmos-alerts-window-warning-state = Warning
 atmos-alerts-window-danger-state = Danger!
 atmos-alerts-window-invalid-state = Inactive
 
-atmos-alerts-window-no-active-alerts = [font size=16][color=white]No active alerts -[/color] [color={$color}]situation normal[/color][/font]
+atmos-alerts-window-no-active-alerts = [font size=16][color=white]No active alerts -[/color] [color={$color}]Situation normal[/color][/font]
 atmos-alerts-window-no-data-available = No data available
 atmos-alerts-window-alerts-being-silenced = Silencing alerts...
 
index bbff3cb43ec1b7d4c03083f42e15564469ca50d0..a4cb78e67b7448a67242b32aa01068c5f5af01db 100644 (file)
       color: Red
       enabled: false
       castShadows: false
+    - type: NavMapDoor
 
 - type: entity
   id: Firelock