Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atmospheric Alerts Computer Upgrades #1313

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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;
Expand All @@ -49,6 +50,7 @@ public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInter
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_spriteSystem = _entManager.System<SpriteSystem>();
_navMapSystem = _entManager.System<SharedNavMapSystem>();

// Pass the owner to nav map
_owner = owner;
Expand Down Expand Up @@ -181,6 +183,9 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[
// Add tracked entities to the nav map
foreach (var device in console.AtmosDevices)
{
if (!device.NetEntity.Valid)
continue;

if (!NavMap.Visible)
continue;

Expand Down Expand Up @@ -272,6 +277,34 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[
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;

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)
{
Expand Down Expand Up @@ -300,6 +333,24 @@ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, Atmo
NavMap.TrackedEntities[metaData.NetEntity] = blip;
}

private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color)
{
color = Color.White;

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
Expand Down
303 changes: 303 additions & 0 deletions Content.Client/Pinpointer/NavMapSystem.Regions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading