Visualized regions for NavMapControl (#31910)

* 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
This commit is contained in:
chromiumboy
2024-10-23 07:49:58 -05:00
committed by GitHub
parent 3b0d8e63a1
commit d2216835d8
9 changed files with 649 additions and 22 deletions

View File

@@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
{ {
private readonly IEntityManager _entManager; private readonly IEntityManager _entManager;
private readonly SpriteSystem _spriteSystem; private readonly SpriteSystem _spriteSystem;
private readonly SharedNavMapSystem _navMapSystem;
private EntityUid? _owner; private EntityUid? _owner;
private NetEntity? _trackedEntity; private NetEntity? _trackedEntity;
@@ -47,6 +48,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>(); _entManager = IoCManager.Resolve<IEntityManager>();
_spriteSystem = _entManager.System<SpriteSystem>(); _spriteSystem = _entManager.System<SpriteSystem>();
_navMapSystem = _entManager.System<SharedNavMapSystem>();
// Pass the owner to nav map // Pass the owner to nav map
_owner = owner; _owner = owner;
@@ -179,6 +181,9 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
// Add tracked entities to the nav map // Add tracked entities to the nav map
foreach (var device in console.AtmosDevices) foreach (var device in console.AtmosDevices)
{ {
if (!device.NetEntity.Valid)
continue;
if (!NavMap.Visible) if (!NavMap.Visible)
continue; continue;
@@ -270,6 +275,34 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
else else
MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount))); 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 // Auto-scroll re-enable
if (_autoScrollAwaitsUpdate) if (_autoScrollAwaitsUpdate)
{ {
@@ -298,6 +331,24 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
NavMap.TrackedEntities[metaData.NetEntity] = blip; 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) private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
{ {
// Make new UI entry if required // Make new UI entry if required

View File

@@ -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;
}
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
@@ -16,6 +17,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
{ {
Dictionary<Vector2i, int[]> modifiedChunks; Dictionary<Vector2i, int[]> modifiedChunks;
Dictionary<NetEntity, NavMapBeacon> beacons; Dictionary<NetEntity, NavMapBeacon> beacons;
Dictionary<NetEntity, NavMapRegionProperties> regions;
switch (args.Current) switch (args.Current)
{ {
@@ -23,6 +25,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
{ {
modifiedChunks = delta.ModifiedChunks; modifiedChunks = delta.ModifiedChunks;
beacons = delta.Beacons; beacons = delta.Beacons;
regions = delta.Regions;
foreach (var index in component.Chunks.Keys) foreach (var index in component.Chunks.Keys)
{ {
if (!delta.AllChunks!.Contains(index)) if (!delta.AllChunks!.Contains(index))
@@ -35,6 +39,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
{ {
modifiedChunks = state.Chunks; modifiedChunks = state.Chunks;
beacons = state.Beacons; beacons = state.Beacons;
regions = state.Regions;
foreach (var index in component.Chunks.Keys) foreach (var index in component.Chunks.Keys)
{ {
if (!state.Chunks.ContainsKey(index)) if (!state.Chunks.ContainsKey(index))
@@ -47,13 +53,54 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
return; 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) foreach (var (origin, chunk) in modifiedChunks)
{ {
var newChunk = new NavMapChunk(origin); var newChunk = new NavMapChunk(origin);
Array.Copy(chunk, newChunk.TileData, chunk.Length); Array.Copy(chunk, newChunk.TileData, chunk.Length);
component.Chunks[origin] = newChunk; 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(); component.Beacons.Clear();
foreach (var (nuid, beacon) in beacons) foreach (var (nuid, beacon) in beacons)
{ {

View File

@@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl
public List<(Vector2, Vector2)> TileLines = new(); public List<(Vector2, Vector2)> TileLines = new();
public List<(Vector2, Vector2)> TileRects = new(); public List<(Vector2, Vector2)> TileRects = new();
public List<(Vector2[], Color)> TilePolygons = new(); public List<(Vector2[], Color)> TilePolygons = new();
public List<NavMapRegionOverlay> RegionOverlays = new();
// Default colors // Default colors
public Color WallColor = new(102, 217, 102); public Color WallColor = new(102, 217, 102);
@@ -228,7 +229,7 @@ public partial class NavMapControl : MapGridControl
{ {
if (!blip.Selectable) if (!blip.Selectable)
continue; continue;
var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length(); var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length();
if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance) 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 // Draw map lines
if (TileLines.Any()) if (TileLines.Any())
{ {

View File

@@ -1,15 +1,19 @@
using Content.Server.Atmos.Monitor.Components; using Content.Server.Atmos.Monitor.Components;
using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Pinpointer;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Consoles; using Content.Shared.Atmos.Consoles;
using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components; using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Player; using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!; [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!; [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = 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; private const float UpdateTime = 1.0f;
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
// Grid events // Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit); SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
// Alarm events
SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged); SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
} }
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
} }
private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args) 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 xform = Transform(uid);
var gridUid = xform.GridUid; var gridUid = xform.GridUid;
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (gridUid == null) if (gridUid == null)
return; return;
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data)) if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
return; return;
var netEntity = EntityManager.GetNetEntity(uid); if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
return;
var netEntity = GetNetEntity(uid);
var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>(); var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform)) 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) if (gridUid != entXform.GridUid)
continue; continue;
if (args.Anchored) if (isAdding)
{
entConsole.AtmosDevices.Add(data.Value); entConsole.AtmosDevices.Add(data.Value);
}
else if (!args.Anchored) else
{
entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity); 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) if (entDevice.Group != group)
continue; 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 // If emagged, change the alarm type to normal
var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState; 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) if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
alarmState = AtmosAlarmType.Invalid; alarmState = AtmosAlarmType.Invalid;
// Create entry
var netEnt = GetNetEntity(ent);
var entry = new AtmosAlertsComputerEntry var entry = new AtmosAlertsComputerEntry
(GetNetEntity(ent), (netEnt,
GetNetCoordinates(entXform.Coordinates), GetNetCoordinates(entXform.Coordinates),
entDevice.Group, entDevice.Group,
alarmState, alarmState,
MetaData(ent).EntityName, MetaData(ent).EntityName,
entDeviceNetwork.Address); 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); alarmStateData.Add(entry);
} }
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>(); var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entComponent, out var entXform)) 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); atmosDeviceNavMapData.Add(data.Value);
} }
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
(EntityUid uid, (EntityUid uid,
AtmosAlertsDeviceComponent component, AtmosAlertsDeviceComponent component,
TransformComponent xform, TransformComponent xform,
EntityUid gridUid,
[NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output) [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
{ {
output = null; output = null;
if (xform.GridUid != gridUid)
return false;
if (!xform.Anchored) if (!xform.Anchored)
return false; return false;

View File

@@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
public Dictionary<NetEntity, SharedNavMapSystem.NavMapBeacon> Beacons = new(); 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] [Serializable, NetSerializable]
@@ -51,10 +95,30 @@ public sealed class NavMapChunk(Vector2i origin)
public GameTick LastUpdate; 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 public enum NavMapChunkType : byte
{ {
// Values represent bit shift offsets when retrieving data in the tile array. // 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? Floor = 0, // I believe floors have directional information for diagonal tiles?
Wall = SharedNavMapSystem.Directions, Wall = SharedNavMapSystem.Directions,
Airlock = 2 * SharedNavMapSystem.Directions, Airlock = 2 * SharedNavMapSystem.Directions,

View File

@@ -3,10 +3,9 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Content.Shared.Tag; using Content.Shared.Tag;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Pinpointer; 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 Directions = 4; // Not directly tied to number of atmos directions
public const int ChunkSize = 8; 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 AllDirMask = (1 << Directions) - 1;
public const int AirlockMask = AllDirMask << (int) NavMapChunkType.Airlock; 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; public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
[Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!; [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 static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
private EntityQuery<NavMapDoorComponent> _doorQuery; private EntityQuery<NavMapDoorComponent> _doorQuery;
@@ -57,7 +57,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public NavMapChunkType GetEntityType(EntityUid uid) public NavMapChunkType GetEntityType(EntityUid uid)
{ {
if (_doorQuery.HasComp(uid)) if (_doorQuery.HasComp(uid))
return NavMapChunkType.Airlock; return NavMapChunkType.Airlock;
if (_tagSystem.HasAnyTag(uid, WallTags)) if (_tagSystem.HasAnyTag(uid, WallTags))
return NavMapChunkType.Wall; return NavMapChunkType.Wall;
@@ -81,6 +81,57 @@ public abstract class SharedNavMapSystem : EntitySystem
return true; 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 #region: Event handling
private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args) private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
@@ -97,7 +148,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData); chunks.Add(origin, chunk.TileData);
} }
args.State = new NavMapState(chunks, component.Beacons); args.State = new NavMapState(chunks, component.Beacons, component.RegionProperties);
return; return;
} }
@@ -110,7 +161,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData); 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 #endregion
@@ -120,22 +171,26 @@ public abstract class SharedNavMapSystem : EntitySystem
[Serializable, NetSerializable] [Serializable, NetSerializable]
protected sealed class NavMapState( protected sealed class NavMapState(
Dictionary<Vector2i, int[]> chunks, Dictionary<Vector2i, int[]> chunks,
Dictionary<NetEntity, NavMapBeacon> beacons) Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions)
: ComponentState : ComponentState
{ {
public Dictionary<Vector2i, int[]> Chunks = chunks; public Dictionary<Vector2i, int[]> Chunks = chunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons; public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
protected sealed class NavMapDeltaState( protected sealed class NavMapDeltaState(
Dictionary<Vector2i, int[]> modifiedChunks, Dictionary<Vector2i, int[]> modifiedChunks,
Dictionary<NetEntity, NavMapBeacon> beacons, Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions,
HashSet<Vector2i> allChunks) HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<NavMapState> : ComponentState, IComponentDeltaState<NavMapState>
{ {
public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks; public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons; public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
public HashSet<Vector2i> AllChunks = allChunks; public HashSet<Vector2i> AllChunks = allChunks;
public void ApplyToFullState(NavMapState state) public void ApplyToFullState(NavMapState state)
@@ -159,11 +214,18 @@ public abstract class SharedNavMapSystem : EntitySystem
{ {
state.Beacons.Add(nuid, beacon); state.Beacons.Add(nuid, beacon);
} }
state.Regions.Clear();
foreach (var (nuid, region) in Regions)
{
state.Regions.Add(nuid, region);
}
} }
public NavMapState CreateNewFullState(NavMapState state) public NavMapState CreateNewFullState(NavMapState state)
{ {
var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count); var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count);
foreach (var (index, data) in state.Chunks) foreach (var (index, data) in state.Chunks)
{ {
if (!AllChunks!.Contains(index)) if (!AllChunks!.Contains(index))
@@ -177,12 +239,25 @@ public abstract class SharedNavMapSystem : EntitySystem
Array.Copy(newData, data, ArraySize); Array.Copy(newData, data, ArraySize);
} }
return new NavMapState(chunks, new(Beacons)); return new NavMapState(chunks, new(Beacons), new(Regions));
} }
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position); 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 #endregion
} }

View File

@@ -25,7 +25,7 @@ atmos-alerts-window-warning-state = Warning
atmos-alerts-window-danger-state = Danger! atmos-alerts-window-danger-state = Danger!
atmos-alerts-window-invalid-state = Inactive 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-no-data-available = No data available
atmos-alerts-window-alerts-being-silenced = Silencing alerts... atmos-alerts-window-alerts-being-silenced = Silencing alerts...

View File

@@ -115,6 +115,7 @@
color: Red color: Red
enabled: false enabled: false
castShadows: false castShadows: false
- type: NavMapDoor
- type: entity - type: entity
id: Firelock id: Firelock