diff --git a/.github/labeler.yml b/.github/labeler.yml index 85843fa7b1a..d3758efe3de 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -17,6 +17,10 @@ - changed-files: - any-glob-to-any-file: '**/*.swsl' +"Changes: Audio": +- changed-files: + - any-glob-to-any-file: '**/*.ogg' + "Changes: Localization": - changed-files: - any-glob-to-any-file: '**/*.ftl' diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml index 35aed1a7f7f..398baddd843 100644 --- a/.github/workflows/build-map-renderer.yml +++ b/.github/workflows/build-map-renderer.yml @@ -2,11 +2,11 @@ on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ master ] + branches: [ master, staging, stable ] jobs: build: diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml index 17ba32fe2bd..4991531575e 100644 --- a/.github/workflows/build-test-debug.yml +++ b/.github/workflows/build-test-debug.yml @@ -2,11 +2,11 @@ name: Build & Test Debug on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ master ] + branches: [ master, staging, stable ] jobs: build: diff --git a/.github/workflows/labeler-size.yml b/.github/workflows/labeler-size.yml index ad6e35c8292..50f89c9bc87 100644 --- a/.github/workflows/labeler-size.yml +++ b/.github/workflows/labeler-size.yml @@ -14,7 +14,7 @@ jobs: { "0": "XS", "10": "S", - "30": "M", - "100": "L", - "1000": "XL" + "100": "M", + "1000": "L", + "5000": "XL" } diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index 9d9679f8136..f180b959f11 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -2,7 +2,7 @@ on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] paths: - '**.cs' - '**.csproj' @@ -16,7 +16,7 @@ on: merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ master ] + branches: [ master, staging, stable ] paths: - '**.cs' - '**.csproj' diff --git a/.github/workflows/validate-rgas.yml b/.github/workflows/validate-rgas.yml index 1132c01960d..cd0d304043b 100644 --- a/.github/workflows/validate-rgas.yml +++ b/.github/workflows/validate-rgas.yml @@ -1,7 +1,7 @@ name: RGA schema validator on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] diff --git a/.github/workflows/validate-rsis.yml b/.github/workflows/validate-rsis.yml index be37678b5a0..d84604b5bb8 100644 --- a/.github/workflows/validate-rsis.yml +++ b/.github/workflows/validate-rsis.yml @@ -2,7 +2,7 @@ name: RSI Validator on: push: - branches: [ staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: paths: diff --git a/.github/workflows/validate_mapfiles.yml b/.github/workflows/validate_mapfiles.yml index bb6eeaf411f..db7c1b6a363 100644 --- a/.github/workflows/validate_mapfiles.yml +++ b/.github/workflows/validate_mapfiles.yml @@ -1,7 +1,7 @@ name: Map file schema validator on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index dfe497b100c..deb0795ccd4 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -2,7 +2,7 @@ name: YAML Linter on: push: - branches: [ master, staging, trying ] + branches: [ master, staging, stable ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs index e533ef2dce0..f0e4b13356c 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -31,19 +31,6 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state", }; - private Dictionary _gasShorthands = new Dictionary() - { - [Gas.Ammonia] = "NH₃", - [Gas.CarbonDioxide] = "CO₂", - [Gas.Frezon] = "F", - [Gas.Nitrogen] = "N₂", - [Gas.NitrousOxide] = "N₂O", - [Gas.Oxygen] = "O₂", - [Gas.Plasma] = "P", - [Gas.Tritium] = "T", - [Gas.WaterVapor] = "H₂O", - }; - public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates) { RobustXamlLoader.Load(this); @@ -162,12 +149,11 @@ public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlert foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs) { FixedPoint2 gasPercent = percent * 100f; - - var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X"); + var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); var gasLabel = new Label() { - Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)), + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)), FontOverride = normalFont, FontColorOverride = GetAlarmStateColor(alert), HorizontalAlignment = HAlignment.Center, diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs new file mode 100644 index 00000000000..563122f962c --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleBoundUserInterface.cs @@ -0,0 +1,40 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosMonitoringConsoleBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private AtmosMonitoringConsoleWindow? _menu; + + public AtmosMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } + + protected override void Open() + { + base.Open(); + + _menu = new AtmosMonitoringConsoleWindow(this, Owner); + _menu.OpenCentered(); + _menu.OnClose += Close; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not AtmosMonitoringConsoleBoundInterfaceState castState) + return; + + EntMan.TryGetComponent(Owner, out var xform); + _menu?.UpdateUI(xform?.Coordinates, castState.AtmosNetworks); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs new file mode 100644 index 00000000000..c23ebb64355 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs @@ -0,0 +1,295 @@ +using Content.Client.Pinpointer.UI; +using Content.Shared.Atmos.Components; +using Content.Shared.Pinpointer; +using Robust.Client.Graphics; +using Robust.Shared.Collections; +using Robust.Shared.Map.Components; +using System.Linq; +using System.Numerics; + +namespace Content.Client.Atmos.Consoles; + +public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public bool ShowPipeNetwork = true; + public int? FocusNetId = null; + + private const int ChunkSize = 4; + + private readonly Color _basePipeNetColor = Color.LightGray; + private readonly Color _unfocusedPipeNetColor = Color.DimGray; + + private List _atmosPipeNetwork = new(); + private Dictionary _sRGBLookUp = new Dictionary(); + + // Look up tables for merging continuous lines. Indexed by line color + private Dictionary> _horizLines = new(); + private Dictionary> _horizLinesReversed = new(); + private Dictionary> _vertLines = new(); + private Dictionary> _vertLinesReversed = new(); + + public AtmosMonitoringConsoleNavMapControl() : base() + { + PostWallDrawingAction += DrawAllPipeNetworks; + } + + protected override void UpdateNavMap() + { + base.UpdateNavMap(); + + if (!_entManager.TryGetComponent(Owner, out var console)) + return; + + if (!_entManager.TryGetComponent(MapUid, out var grid)) + return; + + _atmosPipeNetwork = GetDecodedAtmosPipeChunks(console.AtmosPipeChunks, grid); + } + + private void DrawAllPipeNetworks(DrawingHandleScreen handle) + { + if (!ShowPipeNetwork) + return; + + // Draw networks + if (_atmosPipeNetwork != null && _atmosPipeNetwork.Any()) + DrawPipeNetwork(handle, _atmosPipeNetwork); + } + + private void DrawPipeNetwork(DrawingHandleScreen handle, List atmosPipeNetwork) + { + var offset = GetOffset(); + offset = offset with { Y = -offset.Y }; + + if (WorldRange / WorldMaxRange > 0.5f) + { + var pipeNetworks = new Dictionary>(); + + foreach (var chunkedLine in atmosPipeNetwork) + { + var start = ScalePosition(chunkedLine.Origin - offset); + var end = ScalePosition(chunkedLine.Terminus - offset); + + if (!pipeNetworks.TryGetValue(chunkedLine.Color, out var subNetwork)) + subNetwork = new ValueList(); + + subNetwork.Add(start); + subNetwork.Add(end); + + pipeNetworks[chunkedLine.Color] = subNetwork; + } + + foreach ((var color, var subNetwork) in pipeNetworks) + { + if (subNetwork.Count > 0) + handle.DrawPrimitives(DrawPrimitiveTopology.LineList, subNetwork.Span, color); + } + } + + else + { + var pipeVertexUVs = new Dictionary>(); + + foreach (var chunkedLine in atmosPipeNetwork) + { + var leftTop = ScalePosition(new Vector2 + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + - offset); + + var rightTop = ScalePosition(new Vector2 + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + - offset); + + var leftBottom = ScalePosition(new Vector2 + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + - offset); + + var rightBottom = ScalePosition(new Vector2 + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + - offset); + + if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV)) + pipeVertexUV = new ValueList(); + + pipeVertexUV.Add(leftBottom); + pipeVertexUV.Add(leftTop); + pipeVertexUV.Add(rightBottom); + pipeVertexUV.Add(leftTop); + pipeVertexUV.Add(rightBottom); + pipeVertexUV.Add(rightTop); + + pipeVertexUVs[chunkedLine.Color] = pipeVertexUV; + } + + foreach ((var color, var pipeVertexUV) in pipeVertexUVs) + { + if (pipeVertexUV.Count > 0) + handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, pipeVertexUV.Span, color); + } + } + } + + private List GetDecodedAtmosPipeChunks(Dictionary? chunks, MapGridComponent? grid) + { + var decodedOutput = new List(); + + if (chunks == null || grid == null) + return decodedOutput; + + // Clear stale look up table values + _horizLines.Clear(); + _horizLinesReversed.Clear(); + _vertLines.Clear(); + _vertLinesReversed.Clear(); + + // Generate masks + var northMask = (ulong)1 << 0; + var southMask = (ulong)1 << 1; + var westMask = (ulong)1 << 2; + var eastMask = (ulong)1 << 3; + + foreach ((var chunkOrigin, var chunk) in chunks) + { + var list = new List(); + + foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData) + { + // Determine the correct coloration for the pipe + var color = Color.FromHex(hexColor) * _basePipeNetColor; + + if (FocusNetId != null && FocusNetId != netId) + color *= _unfocusedPipeNetColor; + + // Get the associated line look up tables + if (!_horizLines.TryGetValue(color, out var horizLines)) + { + horizLines = new(); + _horizLines[color] = horizLines; + } + + if (!_horizLinesReversed.TryGetValue(color, out var horizLinesReversed)) + { + horizLinesReversed = new(); + _horizLinesReversed[color] = horizLinesReversed; + } + + if (!_vertLines.TryGetValue(color, out var vertLines)) + { + vertLines = new(); + _vertLines[color] = vertLines; + } + + if (!_vertLinesReversed.TryGetValue(color, out var vertLinesReversed)) + { + vertLinesReversed = new(); + _vertLinesReversed[color] = vertLinesReversed; + } + + // Loop over the chunk + for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++) + { + if (atmosPipeData == 0) + continue; + + var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions; + + if ((atmosPipeData & mask) == 0) + continue; + + var relativeTile = GetTileFromIndex(tileIdx); + var tile = (chunk.Origin * ChunkSize + relativeTile) * grid.TileSize; + tile = tile with { Y = -tile.Y }; + + // Calculate the draw point offsets + var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? + new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + + // Since we can have pipe lines that have a length of a half tile, + // double the vectors and convert to vector2i so we can merge them + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed); + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed); + } + } + } + + // Scale the vector2is back down and convert to vector2 + foreach (var (color, horizLines) in _horizLines) + { + // Get the corresponding sRBG color + var sRGB = GetsRGBColor(color); + + foreach (var (origin, terminal) in horizLines) + decodedOutput.Add(new AtmosMonitoringConsoleLine + (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + } + + foreach (var (color, vertLines) in _vertLines) + { + // Get the corresponding sRBG color + var sRGB = GetsRGBColor(color); + + foreach (var (origin, terminal) in vertLines) + decodedOutput.Add(new AtmosMonitoringConsoleLine + (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + } + + return decodedOutput; + } + + private Vector2 ConvertVector2iToVector2(Vector2i vector, float scale = 1f) + { + return new Vector2(vector.X * scale, vector.Y * scale); + } + + private Vector2i ConvertVector2ToVector2i(Vector2 vector, float scale = 1f) + { + return new Vector2i((int)MathF.Round(vector.X * scale), (int)MathF.Round(vector.Y * scale)); + } + + private Vector2i GetTileFromIndex(int index) + { + var x = index / ChunkSize; + var y = index % ChunkSize; + return new Vector2i(x, y); + } + + private Color GetsRGBColor(Color color) + { + if (!_sRGBLookUp.TryGetValue(color, out var sRGB)) + { + sRGB = Color.ToSrgb(color); + _sRGBLookUp[color] = sRGB; + } + + return sRGB; + } +} + +public struct AtmosMonitoringConsoleLine +{ + public readonly Vector2 Origin; + public readonly Vector2 Terminus; + public readonly Color Color; + + public AtmosMonitoringConsoleLine(Vector2 origin, Vector2 terminus, Color color) + { + Origin = origin; + Terminus = terminus; + Color = color; + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs new file mode 100644 index 00000000000..bfbb05d2ab1 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Consoles; +using Robust.Shared.GameStates; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args) + { + Dictionary> modifiedChunks; + Dictionary atmosDevices; + + switch (args.Current) + { + case AtmosMonitoringConsoleDeltaState delta: + { + modifiedChunks = delta.ModifiedChunks; + atmosDevices = delta.AtmosDevices; + + foreach (var index in component.AtmosPipeChunks.Keys) + { + if (!delta.AllChunks!.Contains(index)) + component.AtmosPipeChunks.Remove(index); + } + + break; + } + + case AtmosMonitoringConsoleState state: + { + modifiedChunks = state.Chunks; + atmosDevices = state.AtmosDevices; + + foreach (var index in component.AtmosPipeChunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + component.AtmosPipeChunks.Remove(index); + } + + break; + } + default: + return; + } + + foreach (var (origin, chunk) in modifiedChunks) + { + var newChunk = new AtmosPipeChunk(origin); + newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk); + + component.AtmosPipeChunks[origin] = newChunk; + } + + component.AtmosDevices.Clear(); + + foreach (var (nuid, atmosDevice) in atmosDevices) + { + component.AtmosDevices[nuid] = atmosDevice; + } + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml new file mode 100644 index 00000000000..b6fde7592fd --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs new file mode 100644 index 00000000000..515f91790f4 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs @@ -0,0 +1,455 @@ +using Content.Client.Pinpointer.UI; +using Content.Client.UserInterface.Controls; +using Content.Shared.Atmos.Components; +using Content.Shared.Prototypes; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow +{ + private readonly IEntityManager _entManager; + private readonly IPrototypeManager _protoManager; + private readonly SpriteSystem _spriteSystem; + + private EntityUid? _owner; + private NetEntity? _focusEntity; + private int? _focusNetId; + + private bool _autoScrollActive = false; + + private readonly Color _unfocusedDeviceColor = Color.DimGray; + private ProtoId _navMapConsoleProtoId = "NavMapConsole"; + private ProtoId _gasPipeSensorProtoId = "GasPipeSensor"; + + public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _protoManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + // Pass the owner to nav map + _owner = owner; + NavMap.Owner = _owner; + + // Set nav map grid uid + var stationName = Loc.GetString("atmos-monitoring-window-unknown-location"); + EntityCoordinates? consoleCoords = null; + + if (_entManager.TryGetComponent(owner, out var xform)) + { + consoleCoords = xform.Coordinates; + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData)) + stationName = stationMetaData.EntityName; + + var msg = new FormattedMessage(); + msg.TryAddMarkup(Loc.GetString("atmos-monitoring-window-station-name", ("stationName", stationName)), out _); + + StationName.SetMessage(msg); + } + + else + { + StationName.SetMessage(stationName); + NavMap.Visible = false; + } + + // Set trackable entity selected action + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + // Update nav map + NavMap.ForceNavMapUpdate(); + + // Set tab container headers + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-monitoring-window-tab-networks")); + + // Set UI toggles + ShowPipeNetwork.OnToggled += _ => OnShowPipeNetworkToggled(); + ShowGasPipeSensors.OnToggled += _ => OnShowGasPipeSensors(); + + // Set nav map colors + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + NavMap.TileColor = console.NavMapTileColor; + NavMap.WallColor = console.NavMapWallColor; + + // Initalize + UpdateUI(consoleCoords, Array.Empty()); + } + + #region Toggle handling + + private void OnShowPipeNetworkToggled() + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + NavMap.ShowPipeNetwork = ShowPipeNetwork.Pressed; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (device.NavMapBlip == _gasPipeSensorProtoId) + continue; + + if (ShowPipeNetwork.Pressed) + AddTrackedEntityToNavMap(device); + + else + NavMap.TrackedEntities.Remove(netEnt); + } + } + + private void OnShowGasPipeSensors() + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (device.NavMapBlip != _gasPipeSensorProtoId) + continue; + + if (ShowGasPipeSensors.Pressed) + AddTrackedEntityToNavMap(device, true); + + else + NavMap.TrackedEntities.Remove(netEnt); + } + } + + #endregion + + public void UpdateUI + (EntityCoordinates? consoleCoords, + AtmosMonitoringConsoleEntry[] atmosNetworks) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + // Reset nav map values + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + + if (_focusEntity != null && !console.AtmosDevices.Any(x => x.Key == _focusEntity)) + ClearFocus(); + + // Add tracked entities to the nav map + UpdateNavMapBlips(); + + // Show the monitor location + var consoleNetEnt = _entManager.GetNetEntity(_owner); + + if (consoleCoords != null && consoleNetEnt != null) + { + var proto = _protoManager.Index(_navMapConsoleProtoId); + + if (proto.TexturePaths != null && proto.TexturePaths.Length != 0) + { + var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(proto.TexturePaths[0])); + var blip = new NavMapBlip(consoleCoords.Value, texture, proto.Color, proto.Blinks, proto.Selectable); + NavMap.TrackedEntities[consoleNetEnt.Value] = blip; + } + } + + // Update the nav map + NavMap.ForceNavMapUpdate(); + + // Clear excess children from the tables + while (AtmosNetworksTable.ChildCount > atmosNetworks.Length) + AtmosNetworksTable.RemoveChild(AtmosNetworksTable.GetChild(AtmosNetworksTable.ChildCount - 1)); + + // Update all entries in each table + for (int index = 0; index < atmosNetworks.Length; index++) + { + var entry = atmosNetworks.ElementAt(index); + UpdateUIEntry(entry, index, AtmosNetworksTable, console); + } + } + + private void UpdateNavMapBlips() + { + if (_owner == null || !_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + if (NavMap.Visible) + { + foreach (var (netEnt, device) in console.AtmosDevices) + { + // Update the focus network ID, incase it has changed + if (_focusEntity == netEnt) + { + _focusNetId = device.NetId; + NavMap.FocusNetId = _focusNetId; + } + + var isSensor = device.NavMapBlip == _gasPipeSensorProtoId; + + // Skip network devices if the toggled is off + if (!ShowPipeNetwork.Pressed && !isSensor) + continue; + + // Skip gas pipe sensors if the toggle is off + if (!ShowGasPipeSensors.Pressed && isSensor) + continue; + + AddTrackedEntityToNavMap(device, isSensor); + } + } + } + + private void AddTrackedEntityToNavMap(AtmosDeviceNavMapData metaData, bool isSensor = false) + { + var proto = _protoManager.Index(metaData.NavMapBlip); + + if (proto.TexturePaths == null || proto.TexturePaths.Length == 0) + return; + + var idx = Math.Clamp((int)metaData.Direction / 2, 0, proto.TexturePaths.Length - 1); + var texture = proto.TexturePaths.Length > 0 ? proto.TexturePaths[idx] : proto.TexturePaths[0]; + var color = isSensor ? proto.Color : proto.Color * metaData.PipeColor; + + if (_focusNetId != null && metaData.NetId != _focusNetId) + color *= _unfocusedDeviceColor; + + var blinks = proto.Blinks || _focusEntity == metaData.NetEntity; + var coords = _entManager.GetCoordinates(metaData.NetCoordinates); + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale); + NavMap.TrackedEntities[metaData.NetEntity] = blip; + } + + private void UpdateUIEntry(AtmosMonitoringConsoleEntry data, int index, Control table, AtmosMonitoringConsoleComponent console) + { + // Make new UI entry if required + if (index >= table.ChildCount) + { + var newEntryContainer = new AtmosMonitoringEntryContainer(data); + + // On click + newEntryContainer.FocusButton.OnButtonUp += args => + { + if (_focusEntity == newEntryContainer.Data.NetEntity) + { + ClearFocus(); + } + + else + { + SetFocus(newEntryContainer.Data.NetEntity, newEntryContainer.Data.NetId); + + var coords = _entManager.GetCoordinates(newEntryContainer.Data.Coordinates); + NavMap.CenterToCoordinates(coords); + } + + // Update affected UI elements across all tables + UpdateConsoleTable(console, AtmosNetworksTable, _focusEntity); + }; + + // Add the entry to the current table + table.AddChild(newEntryContainer); + } + + // Update values and UI elements + var tableChild = table.GetChild(index); + + if (tableChild is not AtmosMonitoringEntryContainer) + { + table.RemoveChild(tableChild); + UpdateUIEntry(data, index, table, console); + + return; + } + + var entryContainer = (AtmosMonitoringEntryContainer)tableChild; + entryContainer.UpdateEntry(data, data.NetEntity == _focusEntity); + } + + private void UpdateConsoleTable(AtmosMonitoringConsoleComponent console, Control table, NetEntity? currTrackedEntity) + { + foreach (var tableChild in table.Children) + { + if (tableChild is not AtmosAlarmEntryContainer) + continue; + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + if (entryContainer.NetEntity != currTrackedEntity) + entryContainer.RemoveAsFocus(); + + else if (entryContainer.NetEntity == currTrackedEntity) + entryContainer.SetAsFocus(); + } + } + + private void SetTrackedEntityFromNavMap(NetEntity? focusEntity) + { + if (focusEntity == null) + return; + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + foreach (var (netEnt, device) in console.AtmosDevices) + { + if (netEnt != focusEntity) + continue; + + if (device.NavMapBlip != _gasPipeSensorProtoId) + return; + + // Set new focus + SetFocus(focusEntity.Value, device.NetId); + + // Get the scroll position of the selected entity on the selected button the UI + ActivateAutoScrollToFocus(); + + break; + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + AutoScrollToFocus(); + } + + private void ActivateAutoScrollToFocus() + { + _autoScrollActive = true; + } + + private void AutoScrollToFocus() + { + if (!_autoScrollActive) + return; + + var scroll = AtmosNetworksTable.Parent as ScrollContainer; + if (scroll == null) + return; + + if (!TryGetVerticalScrollbar(scroll, out var vScrollbar)) + return; + + if (!TryGetNextScrollPosition(out float? nextScrollPosition)) + return; + + vScrollbar.ValueTarget = nextScrollPosition.Value; + + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) + _autoScrollActive = false; + } + + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; + + foreach (var control in scroll.Children) + { + if (control is not VScrollBar) + continue; + + vScrollBar = (VScrollBar)control; + + return true; + } + + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = null; + + var scroll = AtmosNetworksTable.Parent as ScrollContainer; + if (scroll == null) + return false; + + var container = scroll.Children.ElementAt(0) as BoxContainer; + if (container == null || container.Children.Count() == 0) + return false; + + // Exit if the heights of the children haven't been initialized yet + if (!container.Children.Any(x => x.Height > 0)) + return false; + + nextScrollPosition = 0; + + foreach (var control in container.Children) + { + if (control is not AtmosMonitoringEntryContainer) + continue; + + var entry = (AtmosMonitoringEntryContainer)control; + + if (entry.Data.NetEntity == _focusEntity) + return true; + + nextScrollPosition += control.Height; + } + + // Failed to find control + nextScrollPosition = null; + + return false; + } + + private void SetFocus(NetEntity focusEntity, int focusNetId) + { + _focusEntity = focusEntity; + _focusNetId = focusNetId; + NavMap.FocusNetId = focusNetId; + + OnFocusChanged(); + } + + private void ClearFocus() + { + _focusEntity = null; + _focusNetId = null; + NavMap.FocusNetId = null; + + OnFocusChanged(); + } + + private void OnFocusChanged() + { + UpdateNavMapBlips(); + NavMap.ForceNavMapUpdate(); + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + for (int index = 0; index < AtmosNetworksTable.ChildCount; index++) + { + var entry = (AtmosMonitoringEntryContainer)AtmosNetworksTable.GetChild(index); + + if (entry == null) + continue; + + UpdateUIEntry(entry.Data, index, AtmosNetworksTable, console); + } + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml new file mode 100644 index 00000000000..6a19f0775f9 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml @@ -0,0 +1,74 @@ + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs new file mode 100644 index 00000000000..0ce0c9c880a --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringEntryContainer.xaml.cs @@ -0,0 +1,166 @@ +using Content.Client.Stylesheets; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Temperature; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosMonitoringEntryContainer : BoxContainer +{ + public AtmosMonitoringConsoleEntry Data; + + private readonly IEntityManager _entManager; + private readonly IResourceCache _cache; + + public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _cache = IoCManager.Resolve(); + + Data = data; + + // Modulate colored stripe + NetworkColorStripe.Modulate = data.Color; + + // Load fonts + var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11); + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Set fonts + TemperatureHeaderLabel.FontOverride = headerFont; + PressureHeaderLabel.FontOverride = headerFont; + TotalMolHeaderLabel.FontOverride = headerFont; + GasesHeaderLabel.FontOverride = headerFont; + + TemperatureLabel.FontOverride = normalFont; + PressureLabel.FontOverride = normalFont; + TotalMolLabel.FontOverride = normalFont; + + NoDataLabel.FontOverride = headerFont; + } + + public void UpdateEntry(AtmosMonitoringConsoleEntry updatedData, bool isFocus) + { + // Load fonts + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Update name and values + if (!string.IsNullOrEmpty(updatedData.Address)) + NetworkNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", updatedData.EntityName), ("address", updatedData.Address)); + + else + NetworkNameLabel.Text = Loc.GetString(updatedData.EntityName); + + Data = updatedData; + + // Modulate colored stripe + NetworkColorStripe.Modulate = Data.Color; + + // Focus updates + if (isFocus) + SetAsFocus(); + else + RemoveAsFocus(); + + // Check if powered + if (!updatedData.IsPowered) + { + MainDataContainer.Visible = false; + NoDataLabel.Visible = true; + + return; + } + + // Set container visibility + MainDataContainer.Visible = true; + NoDataLabel.Visible = false; + + // Update temperature + var isNotVacuum = updatedData.TotalMolData > 1e-6f; + var tempK = (FixedPoint2)updatedData.TemperatureData; + var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float()); + + TemperatureLabel.Text = isNotVacuum ? + Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)) : + Loc.GetString("atmos-alerts-window-invalid-value"); + + TemperatureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update pressure + PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)updatedData.PressureData)); + PressureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update total mol + TotalMolLabel.Text = Loc.GetString("atmos-alerts-window-total-mol-value", ("value", (FixedPoint2)updatedData.TotalMolData)); + TotalMolLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore; + + // Update other present gases + GasGridContainer.RemoveAllChildren(); + + if (updatedData.GasData.Count() == 0) + { + // No gases + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"), + FontOverride = normalFont, + FontColorOverride = StyleNano.DisabledFore, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + + else + { + // Add an entry for each gas + foreach (var (gas, percent) in updatedData.GasData) + { + var gasPercent = (FixedPoint2)0f; + gasPercent = percent * 100f; + + var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation")); + + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)), + FontOverride = normalFont, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + } + } + + public void SetAsFocus() + { + FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png"; + FocusContainer.Visible = true; + } + + public void RemoveAsFocus() + { + FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png"; + FocusContainer.Visible = false; + } +} diff --git a/Content.Client/Atmos/EntitySystems/GasPressurePumpSystem.cs b/Content.Client/Atmos/EntitySystems/GasPressurePumpSystem.cs new file mode 100644 index 00000000000..54e16bc8621 --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasPressurePumpSystem.cs @@ -0,0 +1,23 @@ +using Content.Client.Atmos.UI; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Piping.Binary.Components; + +namespace Content.Client.Atmos.EntitySystems; + +public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPumpUpdate); + } + + private void OnPumpUpdate(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, GasPressurePumpUiKey.Key, out var bui)) + { + bui.Update(); + } + } +} diff --git a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs index 220fdbe875c..0c07eec4025 100644 --- a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs @@ -1,65 +1,63 @@ using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Binary.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Localizations; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; -namespace Content.Client.Atmos.UI +namespace Content.Client.Atmos.UI; + +/// +/// Initializes a and updates it when new server messages are received. +/// +[UsedImplicitly] +public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface { - /// - /// Initializes a and updates it when new server messages are received. - /// - [UsedImplicitly] - public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface - { - [ViewVariables] - private const float MaxPressure = Atmospherics.MaxOutputPressure; + [ViewVariables] + private const float MaxPressure = Atmospherics.MaxOutputPressure; + + [ViewVariables] + private GasPressurePumpWindow? _window; - [ViewVariables] - private GasPressurePumpWindow? _window; + public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } - public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + protected override void Open() + { + base.Open(); - protected override void Open() - { - base.Open(); + _window = this.CreateWindow(); - _window = this.CreateWindow(); + _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; + _window.PumpOutputPressureChanged += OnPumpOutputPressurePressed; + Update(); + } - _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; - _window.PumpOutputPressureChanged += OnPumpOutputPressurePressed; - } + public void Update() + { + if (_window == null) + return; - private void OnToggleStatusButtonPressed() - { - if (_window is null) return; - SendMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus)); - } + _window.Title = Identity.Name(Owner, EntMan); - private void OnPumpOutputPressurePressed(string value) - { - var pressure = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f; - if (pressure > MaxPressure) pressure = MaxPressure; + if (!EntMan.TryGetComponent(Owner, out GasPressurePumpComponent? pump)) + return; - SendMessage(new GasPressurePumpChangeOutputPressureMessage(pressure)); - } + _window.SetPumpStatus(pump.Enabled); + _window.MaxPressure = pump.MaxTargetPressure; + _window.SetOutputPressure(pump.TargetPressure); + } - /// - /// Update the UI state based on server-sent info - /// - /// - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); - if (_window == null || state is not GasPressurePumpBoundUserInterfaceState cast) - return; + private void OnToggleStatusButtonPressed() + { + if (_window is null) return; + SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus)); + } - _window.Title = (cast.PumpLabel); - _window.SetPumpStatus(cast.Enabled); - _window.SetOutputPressure(cast.OutputPressure); - } + private void OnPumpOutputPressurePressed(float value) + { + SendPredictedMessage(new GasPressurePumpChangeOutputPressureMessage(value)); } } diff --git a/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml b/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml index a0896a7b41e..f2c2c7cec50 100644 --- a/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml +++ b/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml @@ -1,22 +1,18 @@ - + xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" + SetSize="340 110" MinSize="340 110" Title="Pressure Pump"> -