diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs index f0b7ffbe11..64068d6dbb 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -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(); _spriteSystem = _entManager.System(); + _navMapSystem = _entManager.System(); // 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(); + + if (_owner != null && + _entManager.TryGetComponent(_owner, out var xform) && + _entManager.TryGetComponent(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 index 0000000000..4cc775418e --- /dev/null +++ b/Content.Client/Pinpointer/NavMapSystem.Regions.cs @@ -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(); + + 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, HashSet) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties) + { + if (!regionProperties.Seeds.Any()) + return (new(), new()); + + var visitedChunks = new HashSet(); + var visitedTiles = new HashSet(); + var tilesToVisit = new Stack(); + + 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 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; + } +} diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs index 9aeb792a42..47469d4ea7 100644 --- a/Content.Client/Pinpointer/NavMapSystem.cs +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -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 modifiedChunks; Dictionary beacons; + Dictionary 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(); + + 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) { diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 413b41c36a..90c2680c4a 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -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 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()) { diff --git a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs index d9a475dbfb..2d35ae5973 100644 --- a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs +++ b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs @@ -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(OnGridSplit); + + // Alarm events + SubscribeLocalEvent(OnDeviceTerminatingEvent); SubscribeLocalEvent(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(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(); 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(entXform.GridUid, out var mapGrid)) + continue; + + if (!TryComp(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(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(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null; + + if (sensorList?.Any() == true) + { + var alarmRegionSeeds = new HashSet(); + + // If valid and anchored, use the position of sensors as seeds for the region + foreach (var (address, sensorEnt) in sensorList) + { + if (!sensorEnt.IsValid() || !HasComp(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(); 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; diff --git a/Content.Shared/Pinpointer/NavMapComponent.cs b/Content.Shared/Pinpointer/NavMapComponent.cs index d77169d32e..b876cb20fe 100644 --- a/Content.Shared/Pinpointer/NavMapComponent.cs +++ b/Content.Shared/Pinpointer/NavMapComponent.cs @@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component /// [ViewVariables] public Dictionary Beacons = new(); + + /// + /// Describes the properties of a region on the station. + /// It is indexed by the entity assigned as the region owner. + /// + [ViewVariables(VVAccess.ReadOnly)] + public Dictionary RegionProperties = new(); + + /// + /// All flood filled regions, ready for display on a NavMapControl. + /// It is indexed by the entity assigned as the region owner. + /// + /// + /// For client use only + /// + [ViewVariables(VVAccess.ReadOnly)] + public Dictionary RegionOverlays = new(); + + /// + /// A queue of all region owners that are waiting their associated regions to be floodfilled. + /// + /// + /// For client use only + /// + [ViewVariables(VVAccess.ReadOnly)] + public Queue QueuedRegionsToFlood = new(); + + /// + /// A look up table to get a list of region owners associated with a flood filled chunk. + /// + /// + /// For client use only + /// + [ViewVariables(VVAccess.ReadOnly)] + public Dictionary> ChunkToRegionOwnerTable = new(); + + /// + /// A look up table to find flood filled chunks associated with a given region owner. + /// + /// + /// For client use only + /// + [ViewVariables(VVAccess.ReadOnly)] + public Dictionary> 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) +{ + /// + /// The key to the UI that will be displaying this region on its navmap + /// + public Enum UiKey = uiKey; + + /// + /// The local grid coordinates of the rectangles that make up the region + /// Item1 is the top left corner, Item2 is the bottom right corner + /// + public List<(Vector2i, Vector2i)> GridCoords = gridCoords; + + /// + /// Color of the region + /// + 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, diff --git a/Content.Shared/Pinpointer/SharedNavMapSystem.cs b/Content.Shared/Pinpointer/SharedNavMapSystem.cs index 3ced5f3c9e..37d60dec28 100644 --- a/Content.Shared/Pinpointer/SharedNavMapSystem.cs +++ b/Content.Shared/Pinpointer/SharedNavMapSystem.cs @@ -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[] WallTags = {"Wall", "Window"}; private EntityQuery _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 GetNavMapRegionOverlays(EntityUid uid, NavMapComponent component, Enum uiKey) + { + var regionOverlays = new Dictionary(); + + 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 chunks, - Dictionary beacons) + Dictionary beacons, + Dictionary regions) : ComponentState { public Dictionary Chunks = chunks; public Dictionary Beacons = beacons; + public Dictionary Regions = regions; } [Serializable, NetSerializable] protected sealed class NavMapDeltaState( Dictionary modifiedChunks, Dictionary beacons, + Dictionary regions, HashSet allChunks) : ComponentState, IComponentDeltaState { public Dictionary ModifiedChunks = modifiedChunks; public Dictionary Beacons = beacons; + public Dictionary Regions = regions; public HashSet 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(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 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 } diff --git a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl index a1640c5e9d..470a8f8695 100644 --- a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl +++ b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl @@ -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... diff --git a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml index bbff3cb43e..a4cb78e67b 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml @@ -115,6 +115,7 @@ color: Red enabled: false castShadows: false + - type: NavMapDoor - type: entity id: Firelock