Skip to content

Commit

Permalink
Merge branch 'space-wizards:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Merrokitsune authored Oct 23, 2024
2 parents 6f16d2f + 05a0051 commit 38f2c42
Show file tree
Hide file tree
Showing 107 changed files with 9,584 additions and 5,918 deletions.
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 @@ -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;
Expand All @@ -47,6 +48,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 @@ -179,6 +181,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 @@ -270,6 +275,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.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)
{
Expand Down Expand Up @@ -298,6 +331,24 @@ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, Atmo
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
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

0 comments on commit 38f2c42

Please sign in to comment.