diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2e90f0eed03..f248264219c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -3,18 +3,36 @@
# C# code
/Content.*/ @DeltaV-Station/maintainers
+# Any assets
+/Resources/ @DeltaV-Station/maintainers
+
+# Server config files
+/Resources/ConfigPresets/ @MilonPL
+
# YML files
/Resources/*.yml @DeltaV-Station/yaml-maintainers
/Resources/**/*.yml @DeltaV-Station/yaml-maintainers
# Sprites
-/Resources/Textures/ @IamVelcroboy
+/Resources/Textures/ @DeltaV-Station/direction
# Lobby art and music - automatically direction issues since its immediately visible to players
-/Resources/Audio/Lobby/ @DeltaV-Station/game-directors
-/Resources/Textures/LobbyScreens/ @DeltaV-Station/game-directors
+/Resources/Audio/Lobby/ @DeltaV-Station/direction
+/Resources/Textures/LobbyScreens/ @DeltaV-Station/direction
# Maps
/Resources/Maps/ @DeltaV-Station/maptainers
/Resources/Prototypes/Maps/ @DeltaV-Station/maptainers
/Content.IntegrationTests/Tests/PostMapInitTest.cs @DeltaV-Station/maptainers
+
+# Server rules
+/Resources/ServerInfo/Guidebook/DeltaV/Rules/ @DeltaV-Station/head-administrators
+
+# Tools and scripts
+/Tools/ @deltanedas @MilonPL
+
+# Workflows, codeowners, templates, etc.
+/.github/ @deltanedas @MilonPL
+
+# Standalone files in the root repo
+/* @deltanedas
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ce9e7559682..e41221d98d4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,17 +9,16 @@ Upstream is the [space-wizards/space-station-14](https://github.com/space-wizard
# Content specific to Delta-V
-In general anything you create from scratch (not modifying something that exists from upstream) should go in a DeltaV subfolder.
+In general anything you create from scratch (not modifying something that exists from upstream) should go in a DeltaV subfolder, `_DV`.
Examples:
-- `Content.Server/DeltaV/Chapel/SacrificialAltarSystem.cs`
-- `Resources/Prototypes/DeltaV/ai_factions.yml`
-- `Resources/Audio/DeltaV/Items/gavel.ogg`
-- `Resources/Textures/DeltaV/Icons/cri.rsi`
-- `Resources/Locale/en-US/deltav/shipyard/shipyard-console.ftl`
- The locale subfolder is lowercase `deltav` instead of `DeltaV`.
-- `Resources/ServerInfo/Guidebook/DeltaV/AlertProcedure.xml`
- Note that guidebooks go in `ServerInfo/Guidebook/DeltaV` and not `ServerInfo/DeltaV`!
+- `Content.Server/_DV/Chapel/SacrificialAltarSystem.cs`
+- `Resources/Prototypes/_DV/ai_factions.yml`
+- `Resources/Audio/_DV/Items/gavel.ogg`
+- `Resources/Textures/_DV/Icons/cri.rsi`
+- `Resources/Locale/en-US/_DV/shipyard/shipyard-console.ftl`
+- `Resources/ServerInfo/Guidebook/_DV/AlertProcedure.xml`
+ Note that guidebooks go in `ServerInfo/Guidebook/_DV` and not `ServerInfo/_DV`!
# Changes to upstream files
diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
index 050756fcd14..93ce5538aa1 100644
--- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -26,6 +26,13 @@ protected override void Open()
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged;
+ _window.OnNumberChanged += OnNumberChanged; // DeltaV
+ }
+
+ // DeltaV - Add number change handler
+ private void OnNumberChanged(uint newNumber)
+ {
+ SendMessage(new AgentIDCardNumberChangedMessage(newNumber));
}
private void OnNameChanged(string newName)
@@ -56,6 +63,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
_window.SetAllowedIcons(cast.CurrentJobIconId);
+ _window.SetCurrentNumber(cast.CurrentNumber); // DeltaV
}
}
}
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml b/Content.Client/Access/UI/AgentIDCardWindow.xaml
index 7d091e4e165..a2ddd1c417d 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml
@@ -6,6 +6,10 @@
+
+
+
+
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index 320bb88a67e..a342013d314 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -21,9 +21,13 @@ public sealed partial class AgentIDCardWindow : DefaultWindow
private const int JobIconColumnCount = 10;
+ private const int MaxNumberLength = 4; // DeltaV - Same as NewChatPopup
+
public event Action? OnNameChanged;
public event Action? OnJobChanged;
+ public event Action? OnNumberChanged; // DeltaV - Add event for number changes
+
public event Action>? OnJobIconChanged;
public AgentIDCardWindow()
@@ -37,6 +41,37 @@ public AgentIDCardWindow()
JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
+
+ // DeltaV - Add handlers for number changes
+ NumberLineEdit.OnTextEntered += OnNumberEntered;
+ NumberLineEdit.OnFocusExit += OnNumberEntered;
+
+ // DeltaV - Filter to only allow digits
+ NumberLineEdit.OnTextChanged += args =>
+ {
+ if (args.Text.Length > MaxNumberLength)
+ {
+ NumberLineEdit.Text = args.Text[..MaxNumberLength];
+ }
+
+ // Filter to digits only
+ var newText = string.Concat(args.Text.Where(char.IsDigit));
+ if (newText != args.Text)
+ NumberLineEdit.Text = newText;
+ };
+ }
+
+ // DeltaV - Add number validation and event
+ private void OnNumberEntered(LineEdit.LineEditEventArgs args)
+ {
+ if (uint.TryParse(args.Text, out var number) && number > 0)
+ OnNumberChanged?.Invoke(number);
+ }
+
+ // DeltaV - Add setter for current number
+ public void SetCurrentNumber(uint? number)
+ {
+ NumberLineEdit.Text = number?.ToString("D4") ?? "";
}
public void SetAllowedIcons(string currentJobIconId)
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index 57c861bd20f..a00b58f54f9 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -259,13 +259,13 @@ public void UnlinkAllActions()
public void LinkAllActions(ActionsComponent? actions = null)
{
- if (_playerManager.LocalEntity is not { } user ||
- !Resolve(user, ref actions, false))
- {
- return;
- }
+ if (_playerManager.LocalEntity is not { } user ||
+ !Resolve(user, ref actions, false))
+ {
+ return;
+ }
- LinkActions?.Invoke(actions);
+ LinkActions?.Invoke(actions);
}
public override void Shutdown()
diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
index a412e47396b..c8e3afeb22c 100644
--- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
@@ -159,6 +159,7 @@ private void OnTypeChanged(OptionButton.ItemSelectedEventArgs args)
SecretCheckBox.Pressed = false;
SeverityOption.Disabled = false;
PermanentCheckBox.Pressed = true;
+ SubmitButton.Disabled = true;
UpdatePermanentCheckboxFields();
break;
case (int) NoteType.Message: // Message: these are shown to the player when they log on
diff --git a/Content.Client/Atmos/Components/PipeColorVisualsComponent.cs b/Content.Client/Atmos/Components/PipeColorVisualsComponent.cs
index 355b10cb4a4..9b24b1adc25 100644
--- a/Content.Client/Atmos/Components/PipeColorVisualsComponent.cs
+++ b/Content.Client/Atmos/Components/PipeColorVisualsComponent.cs
@@ -1,8 +1,4 @@
-using Robust.Shared.GameObjects;
-
namespace Content.Client.Atmos.Components;
[RegisterComponent]
-public sealed partial class PipeColorVisualsComponent : Component
-{
-}
+public sealed partial class PipeColorVisualsComponent : Component;
diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
index 6bdfb3989f9..3dbe14e6b6d 100644
--- a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
+++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
@@ -1,6 +1,5 @@
@@ -62,7 +61,7 @@
-
+
diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
index 79bb66560e3..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);
@@ -136,8 +123,9 @@ public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlert
GasGridContainer.RemoveAllChildren();
var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen);
+ var keyValuePairs = gasData.ToList();
- if (gasData.Count() == 0)
+ if (keyValuePairs.Count == 0)
{
// No other gases
var gasLabel = new Label()
@@ -158,17 +146,14 @@ public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlert
else
{
// Add an entry for each gas
- foreach ((var gas, (var mol, var percent, var alert)) in gasData)
+ foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
- var gasPercent = (FixedPoint2)0f;
- gasPercent = percent * 100f;
-
- if (!_gasShorthands.TryGetValue(gas, out var gasShorthand))
- gasShorthand = "X";
+ FixedPoint2 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", 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/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
index 08cae979b9b..6f0e7f80da1 100644
--- a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
@@ -14,8 +14,6 @@ protected override void Open()
_menu = new AtmosAlertsComputerWindow(this, Owner);
_menu.OpenCentered();
_menu.OnClose += Close;
-
- EntMan.TryGetComponent(Owner, out var xform);
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -24,9 +22,6 @@ protected override void UpdateState(BoundUserInterfaceState state)
var castState = (AtmosAlertsComputerBoundInterfaceState) state;
- if (castState == null)
- return;
-
EntMan.TryGetComponent(Owner, out var xform);
_menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData);
}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
index 8824a776ee6..e5ede1b92e3 100644
--- a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
@@ -1,7 +1,6 @@
(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/AtmosPipeAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
index adcbfac7f35..5c323d7dc18 100644
--- a/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
@@ -4,8 +4,6 @@
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Client.Atmos.EntitySystems;
@@ -19,7 +17,7 @@ public override void Initialize()
base.Initialize();
SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnAppearanceChanged, after: new[] { typeof(SubFloorHideSystem) });
+ SubscribeLocalEvent(OnAppearanceChanged, after: [typeof(SubFloorHideSystem)]);
}
private void OnInit(EntityUid uid, PipeAppearanceComponent component, ComponentInit args)
@@ -84,7 +82,8 @@ private void OnAppearanceChanged(EntityUid uid, PipeAppearanceComponent componen
layer.Visible &= visible;
- if (!visible) continue;
+ if (!visible)
+ continue;
layer.Color = color;
}
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/Monitor/AtmosAlarmableVisualsSystem.cs b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
index 019f25f376b..18ca2234752 100644
--- a/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
+++ b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
@@ -1,12 +1,7 @@
-using System.Collections.Generic;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Power;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Monitor;
@@ -27,7 +22,7 @@ protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsC
{
foreach (var visLayer in component.HideOnDepowered)
{
- if (args.Sprite.LayerMapTryGet(visLayer, out int powerVisibilityLayer))
+ if (args.Sprite.LayerMapTryGet(visLayer, out var powerVisibilityLayer))
args.Sprite.LayerSetVisible(powerVisibilityLayer, powered);
}
}
@@ -36,7 +31,7 @@ protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsC
{
foreach (var (setLayer, powerState) in component.SetOnDepowered)
{
- if (args.Sprite.LayerMapTryGet(setLayer, out int setStateLayer))
+ if (args.Sprite.LayerMapTryGet(setLayer, out var setStateLayer))
args.Sprite.LayerSetState(setStateLayer, new RSI.StateId(powerState));
}
}
diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs
index d9e94e373b4..650f96eec97 100644
--- a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs
+++ b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs
@@ -1,11 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
namespace Content.Client.Atmos.Monitor.UI;
@@ -78,6 +74,7 @@ protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (disposing) _window?.Dispose();
+ if (disposing)
+ _window?.Dispose();
}
}
diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs
index e1425ac491b..65164983865 100644
--- a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs
@@ -8,7 +8,6 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -59,7 +58,7 @@ public AirAlarmWindow()
AirAlarmMode.Fill => "air-alarm-ui-mode-fill",
AirAlarmMode.Panic => "air-alarm-ui-mode-panic",
AirAlarmMode.None => "air-alarm-ui-mode-none",
- _ => "error"
+ _ => "error",
};
_modes.AddItem(Loc.GetString(text));
}
@@ -70,7 +69,7 @@ public AirAlarmWindow()
AirAlarmModeChanged!.Invoke((AirAlarmMode) args.Id);
};
- _autoMode.OnToggled += args =>
+ _autoMode.OnToggled += _ =>
{
AutoModeChanged!.Invoke(_autoMode.Pressed);
};
@@ -176,22 +175,18 @@ public void UpdateDeviceData(string addr, IAtmosDeviceData device)
public static Color ColorForThreshold(float amount, AtmosAlarmThreshold threshold)
{
- threshold.CheckThreshold(amount, out AtmosAlarmType curAlarm);
+ threshold.CheckThreshold(amount, out var curAlarm);
return ColorForAlarm(curAlarm);
}
public static Color ColorForAlarm(AtmosAlarmType curAlarm)
{
- if(curAlarm == AtmosAlarmType.Danger)
+ return curAlarm switch
{
- return StyleNano.DangerousRedFore;
- }
- else if(curAlarm == AtmosAlarmType.Warning)
- {
- return StyleNano.ConcerningOrangeFore;
- }
-
- return StyleNano.GoodGreenFore;
+ AtmosAlarmType.Danger => StyleNano.DangerousRedFore,
+ AtmosAlarmType.Warning => StyleNano.ConcerningOrangeFore,
+ _ => StyleNano.GoodGreenFore,
+ };
}
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml.cs
index 17b03b84684..2cd51d6fc7f 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/PumpControl.xaml.cs
@@ -1,12 +1,8 @@
-using System;
-using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets;
@@ -25,7 +21,7 @@ public sealed partial class PumpControl : BoxContainer
private OptionButton _pressureCheck => CPressureCheck;
private FloatSpinBox _externalBound => CExternalBound;
private FloatSpinBox _internalBound => CInternalBound;
- private Button _copySettings => CCopySettings;
+ private Button _copySettings => CCopySettings;
public PumpControl(GasVentPumpData data, string address)
{
@@ -86,7 +82,7 @@ public PumpControl(GasVentPumpData data, string address)
_data.PressureChecks = (VentPressureBound) args.Id;
PumpDataChanged?.Invoke(_address, _data);
};
-
+
_copySettings.OnPressed += _ =>
{
PumpDataCopied?.Invoke(_data);
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml.cs
index f2241bcd8da..c16ff688c93 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ScrubberControl.xaml.cs
@@ -1,15 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using Content.Shared.Atmos;
-using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets;
@@ -27,7 +21,7 @@ public sealed partial class ScrubberControl : BoxContainer
private OptionButton _pumpDirection => CPumpDirection;
private FloatSpinBox _volumeRate => CVolumeRate;
private CheckBox _wideNet => CWideNet;
- private Button _copySettings => CCopySettings;
+ private Button _copySettings => CCopySettings;
private GridContainer _gases => CGasContainer;
private Dictionary _gasControls = new();
@@ -77,7 +71,7 @@ public ScrubberControl(GasVentScrubberData data, string address)
_data.PumpDirection = (ScrubberPumpDirection) args.Id;
ScrubberDataChanged?.Invoke(_address, _data);
};
-
+
_copySettings.OnPressed += _ =>
{
ScrubberDataCopied?.Invoke(_data);
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml.cs
index da602cd7479..9e60b6cea62 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/SensorInfo.xaml.cs
@@ -43,7 +43,8 @@ public SensorInfo(AtmosSensorData data, string address)
var label = new RichTextLabel();
var fractionGas = amount / data.TotalMoles;
- label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator", ("gas", $"{gas}"),
+ label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator",
+ ("gas", $"{gas}"),
("color", AirAlarmWindow.ColorForThreshold(fractionGas, data.GasThresholds[gas])),
("amount", $"{amount:0.####}"),
("percentage", $"{(100 * fractionGas):0.##}")));
@@ -53,9 +54,9 @@ public SensorInfo(AtmosSensorData data, string address)
var threshold = data.GasThresholds[gas];
var gasThresholdControl = new ThresholdControl(Loc.GetString($"air-alarm-ui-thresholds-gas-title", ("gas", $"{gas}")), threshold, AtmosMonitorThresholdType.Gas, gas, 100);
gasThresholdControl.Margin = new Thickness(20, 2, 2, 2);
- gasThresholdControl.ThresholdDataChanged += (type, threshold, arg3) =>
+ gasThresholdControl.ThresholdDataChanged += (type, alarmThreshold, arg3) =>
{
- OnThresholdUpdate!(_address, type, threshold, arg3);
+ OnThresholdUpdate!(_address, type, alarmThreshold, arg3);
};
_gasThresholds.Add(gas, gasThresholdControl);
@@ -64,7 +65,8 @@ public SensorInfo(AtmosSensorData data, string address)
_pressureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-pressure-title"), data.PressureThreshold, AtmosMonitorThresholdType.Pressure);
PressureThresholdContainer.AddChild(_pressureThreshold);
- _temperatureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"), data.TemperatureThreshold,
+ _temperatureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"),
+ data.TemperatureThreshold,
AtmosMonitorThresholdType.Temperature);
TemperatureThresholdContainer.AddChild(_temperatureThreshold);
@@ -103,7 +105,8 @@ public void ChangeData(AtmosSensorData data)
}
var fractionGas = amount / data.TotalMoles;
- label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator", ("gas", $"{gas}"),
+ label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator",
+ ("gas", $"{gas}"),
("color", AirAlarmWindow.ColorForThreshold(fractionGas, data.GasThresholds[gas])),
("amount", $"{amount:0.####}"),
("percentage", $"{(100 * fractionGas):0.##}")));
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
index 3612d84de4c..55f7c008987 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
@@ -1,7 +1,4 @@
-using Content.Client.Message;
-using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
-using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
diff --git a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml.cs
index 78c73fa573a..651620f3e25 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdControl.xaml.cs
@@ -1,12 +1,8 @@
-using System;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
-using Content.Shared.Atmos.Monitor.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
// holy FUCK
// this technically works because some of this you can *not* do in XAML but holy FUCK
@@ -115,29 +111,38 @@ public ThresholdControl(string name, AtmosAlarmThreshold threshold, AtmosMonitor
_enabled.Pressed = !_threshold.Ignore;
}
- private String LabelForBound(string boundType) //, DebugMessage)> state) =>
{
if (_system.TileData.TryGetValue(uid, out var data))
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">
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
-
+
diff --git a/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml.cs b/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml.cs
index b5ffcd10721..aa86a1aa03b 100644
--- a/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml.cs
+++ b/Content.Client/Atmos/UI/GasPressurePumpWindow.xaml.cs
@@ -1,14 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using Content.Client.Atmos.EntitySystems;
+using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
-using Content.Shared.Atmos.Prototypes;
using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
namespace Content.Client.Atmos.UI
{
@@ -16,12 +10,25 @@ namespace Content.Client.Atmos.UI
/// Client-side UI used to control a gas pressure pump.
///
[GenerateTypedNameReferences]
- public sealed partial class GasPressurePumpWindow : DefaultWindow
+ public sealed partial class GasPressurePumpWindow : FancyWindow
{
public bool PumpStatus = true;
public event Action? ToggleStatusButtonPressed;
- public event Action? PumpOutputPressureChanged;
+ public event Action? PumpOutputPressureChanged;
+
+ public float MaxPressure
+ {
+ get => _maxPressure;
+ set
+ {
+ _maxPressure = value;
+
+ PumpPressureOutputInput.Value = MathF.Min(value, PumpPressureOutputInput.Value);
+ }
+ }
+
+ private float _maxPressure = Atmospherics.MaxOutputPressure;
public GasPressurePumpWindow()
{
@@ -30,23 +37,25 @@ public GasPressurePumpWindow()
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
- PumpPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
+ PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
+
SetOutputPressureButton.OnPressed += _ =>
{
- PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Text ??= "");
+ PumpPressureOutputInput.Value = Math.Clamp(PumpPressureOutputInput.Value, 0f, _maxPressure);
+ PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Value);
SetOutputPressureButton.Disabled = true;
};
SetMaxPressureButton.OnPressed += _ =>
{
- PumpPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.CurrentCulture);
+ PumpPressureOutputInput.Value = _maxPressure;
SetOutputPressureButton.Disabled = false;
};
}
public void SetOutputPressure(float pressure)
{
- PumpPressureOutputInput.Text = pressure.ToString(CultureInfo.CurrentCulture);
+ PumpPressureOutputInput.Value = pressure;
}
public void SetPumpStatus(bool enabled)
diff --git a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs
index 44c40143d83..04075000f5b 100644
--- a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs
+++ b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs
@@ -39,6 +39,6 @@ protected override void UpdateState(BoundUserInterfaceState message)
if (message is not CargoBountyConsoleState state)
return;
- _menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
+ _menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip);
}
}
diff --git a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml
new file mode 100644
index 00000000000..eee8c5cc165
--- /dev/null
+++ b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
new file mode 100644
index 00000000000..f3c9bbfafb1
--- /dev/null
+++ b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
@@ -0,0 +1,54 @@
+using Content.Client.Message;
+using Content.Shared.Cargo;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Cargo.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class BountyHistoryEntry : BoxContainer
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public BountyHistoryEntry(CargoBountyHistoryData bounty)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
+ return;
+
+ var items = new List();
+ foreach (var entry in bountyPrototype.Entries)
+ {
+ items.Add(Loc.GetString("bounty-console-manifest-entry",
+ ("amount", entry.Amount),
+ ("item", Loc.GetString(entry.Name))));
+ }
+ ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
+ RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
+ IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id)));
+
+ var stationTime = bounty.Timestamp.ToString("hh\\:mm\\:ss");
+ if (bounty.ActorName == null)
+ {
+ StatusLabel.SetMarkup(Loc.GetString("bounty-console-history-completed-label"));
+ NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-completed-label", ("time", stationTime)));
+ }
+ else
+ {
+ StatusLabel.SetMarkup(Loc.GetString("bounty-console-history-skipped-label"));
+ NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-skipped-label",
+ ("id", bounty.ActorName),
+ ("time", stationTime)));
+ }
+ }
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ }
+}
diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml b/Content.Client/Cargo/UI/CargoBountyMenu.xaml
index bb263ff6c4a..0f093d5f8e7 100644
--- a/Content.Client/Cargo/UI/CargoBountyMenu.xaml
+++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml
@@ -11,15 +11,26 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
index 3767b45e4be..0717aacc5e6 100644
--- a/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
+++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
@@ -17,8 +17,11 @@ public CargoBountyMenu()
RobustXamlLoader.Load(this);
}
- public void UpdateEntries(List bounties, TimeSpan untilNextSkip)
+ public void UpdateEntries(List bounties, List history, TimeSpan untilNextSkip)
{
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("bounty-console-tab-available-label"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("bounty-console-tab-history-label"));
+
BountyEntriesContainer.Children.Clear();
foreach (var b in bounties)
{
@@ -32,5 +35,12 @@ public void UpdateEntries(List bounties, TimeSpan untilNextSkip
{
MinHeight = 10
});
+
+ BountyHistoryContainer.Children.Clear();
+ foreach (var h in history)
+ {
+ var entry = new BountyHistoryEntry(h);
+ BountyHistoryContainer.AddChild(entry);
+ }
}
}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
index d28d3228c94..aaf3900beee 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
@@ -23,6 +23,6 @@ public override void UpdateState(BoundUserInterfaceState state)
if (state is not LogProbeUiState logProbeUiState)
return;
- _fragment?.UpdateState(logProbeUiState.PulledLogs);
+ _fragment?.UpdateState(logProbeUiState); // DeltaV - just take the state
}
}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
index d12fb55cdce..a0769590e91 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
@@ -9,10 +9,30 @@
BorderColor="#5a5a5a"
BorderThickness="0 0 0 1"/>
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
index b22e0bc1964..ed3c9236e68 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
@@ -1,4 +1,7 @@
-using Content.Shared.CartridgeLoader.Cartridges;
+using System.Linq; // DeltaV
+using Content.Client._DV.CartridgeLoader.Cartridges; // DeltaV
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared._DV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -13,10 +16,112 @@ public LogProbeUiFragment()
RobustXamlLoader.Load(this);
}
- public void UpdateState(List logs)
+ // DeltaV begin - Update to handle both types of data
+ public void UpdateState(LogProbeUiState state)
{
ProbedDeviceContainer.RemoveAllChildren();
+ if (state.NanoChatData != null)
+ {
+ SetupNanoChatView(state.NanoChatData.Value);
+ DisplayNanoChatData(state.NanoChatData.Value);
+ }
+ else
+ {
+ SetupAccessLogView();
+ if (state.PulledLogs.Count > 0)
+ DisplayAccessLogs(state.PulledLogs);
+ }
+ }
+
+ private void SetupNanoChatView(NanoChatData data)
+ {
+ TitleLabel.Text = Loc.GetString("log-probe-header-nanochat");
+ ContentLabel.Text = Loc.GetString("log-probe-label-message");
+
+ // Show card info if available
+ var cardInfo = new List();
+ if (data.CardNumber != null)
+ cardInfo.Add(Loc.GetString("log-probe-card-number", ("number", $"#{data.CardNumber:D4}")));
+
+ // Add recipient count
+ cardInfo.Add(Loc.GetString("log-probe-recipients", ("count", data.Recipients.Count)));
+
+ CardNumberLabel.Text = string.Join(" | ", cardInfo);
+ CardNumberLabel.Visible = true;
+ }
+
+ private void SetupAccessLogView()
+ {
+ TitleLabel.Text = Loc.GetString("log-probe-header-access");
+ ContentLabel.Text = Loc.GetString("log-probe-label-accessor");
+ CardNumberLabel.Visible = false;
+ }
+
+ private void DisplayNanoChatData(NanoChatData data)
+ {
+ // First add a recipient list entry
+ var recipientsList = Loc.GetString("log-probe-recipient-list") + "\n" + string.Join("\n",
+ data.Recipients.Values
+ .OrderBy(r => r.Name)
+ .Select(r => $" {r.Name}" +
+ (string.IsNullOrEmpty(r.JobTitle) ? "" : $" ({r.JobTitle})") +
+ $" | #{r.Number:D4}"));
+
+ var recipientsEntry = new LogProbeUiEntry(0, "---", recipientsList);
+ ProbedDeviceContainer.AddChild(recipientsEntry);
+
+ var count = 1;
+ foreach (var (partnerId, messages) in data.Messages)
+ {
+ // Show only successfully delivered incoming messages
+ var incomingMessages = messages
+ .Where(msg => msg.SenderId == partnerId && !msg.DeliveryFailed)
+ .OrderByDescending(msg => msg.Timestamp);
+
+ foreach (var msg in incomingMessages)
+ {
+ var messageText = Loc.GetString("log-probe-message-format",
+ ("sender", $"#{msg.SenderId:D4}"),
+ ("recipient", $"#{data.CardNumber:D4}"),
+ ("content", msg.Content));
+
+ var entry = new NanoChatLogEntry(
+ count,
+ TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(),
+ messageText);
+
+ ProbedDeviceContainer.AddChild(entry);
+ count++;
+ }
+
+ // Show only successfully delivered outgoing messages
+ var outgoingMessages = messages
+ .Where(msg => msg.SenderId == data.CardNumber && !msg.DeliveryFailed)
+ .OrderByDescending(msg => msg.Timestamp);
+
+ foreach (var msg in outgoingMessages)
+ {
+ var messageText = Loc.GetString("log-probe-message-format",
+ ("sender", $"#{msg.SenderId:D4}"),
+ ("recipient", $"#{partnerId:D4}"),
+ ("content", msg.Content));
+
+ var entry = new NanoChatLogEntry(
+ count,
+ TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(),
+ messageText);
+
+ ProbedDeviceContainer.AddChild(entry);
+ count++;
+ }
+ }
+ }
+ // DeltaV end
+
+ // DeltaV - Handle this in a separate method
+ private void DisplayAccessLogs(List logs)
+ {
//Reverse the list so the oldest entries appear at the bottom
logs.Reverse();
diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs
index 32e9f4ae9be..aa61e73e31c 100644
--- a/Content.Client/Chat/UI/SpeechBubble.cs
+++ b/Content.Client/Chat/UI/SpeechBubble.cs
@@ -2,6 +2,7 @@
using Content.Client.Chat.Managers;
using Content.Shared.CCVar;
using Content.Shared.Chat;
+using Content.Shared.Speech;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -141,7 +142,12 @@ protected override void FrameUpdate(FrameEventArgs args)
Modulate = Color.White;
}
- var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
+ var baseOffset = 0f;
+
+ if (_entityManager.TryGetComponent(_senderEntity, out var speech))
+ baseOffset = speech.SpeechBubbleOffset;
+
+ var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
var worldPos = _transformSystem.GetWorldPosition(xform) + offset;
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
index 5eace08a7fd..20c61f10cb8 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
@@ -12,6 +12,7 @@
using System.Linq;
using System.Numerics;
using Content.Shared.FixedPoint;
+using Robust.Client.Graphics;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Chemistry.UI
@@ -90,10 +91,40 @@ public ChemMasterWindow()
private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
{
- var button = new ReagentButton(text, amount, id, isBuffer, styleClass);
- button.OnPressed += args
- => OnReagentButtonPressed?.Invoke(args, button);
- return button;
+ var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
+ reagentTransferButton.OnPressed += args
+ => OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
+ return reagentTransferButton;
+ }
+ ///
+ /// Conditionally generates a set of reagent buttons based on the supplied boolean argument.
+ /// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well.
+ ///
+ private List CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons)
+ {
+ if (!addReagentButtons)
+ return new List(); // Return an empty list if reagentTransferButton creation is disabled.
+
+ var buttonConfigs = new (string text, ChemMasterReagentAmount amount, string styleClass)[]
+ {
+ ("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
+ ("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
+ ("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
+ ("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
+ ("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
+ ("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
+ (Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
+ };
+
+ var buttons = new List();
+
+ foreach (var (text, amount, styleClass) in buttonConfigs)
+ {
+ var reagentTransferButton = MakeReagentButton(text, amount, reagent, isBuffer, styleClass);
+ buttons.Add(reagentTransferButton);
+ }
+
+ return buttons;
}
///
@@ -102,25 +133,36 @@ private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amo
/// State data sent by the server.
public void UpdateState(BoundUserInterfaceState state)
{
- var castState = (ChemMasterBoundUserInterfaceState) state;
+ var castState = (ChemMasterBoundUserInterfaceState)state;
+
if (castState.UpdateLabel)
LabelLine = GenerateLabel(castState);
- UpdatePanelInfo(castState);
-
- var output = castState.OutputContainerInfo;
+ // Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
+ UpdatePanelInfo(castState);
+
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
-
+
InputEjectButton.Disabled = castState.InputContainerInfo is null;
- OutputEjectButton.Disabled = output is null;
- CreateBottleButton.Disabled = output?.Reagents == null;
- CreatePillButton.Disabled = output?.Entities == null;
-
+ OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
+ CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
+ CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
+
+ UpdateDosageFields(castState);
+ }
+
+ //assign default values for pill and bottle fields.
+ private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
+ {
+ var output = castState.OutputContainerInfo;
var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
+ var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
+ PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
+
PillTypeButtons[castState.SelectedPillType].Pressed = true;
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
@@ -130,8 +172,19 @@ public void UpdateState(BoundUserInterfaceState state)
PillNumber.Value = pillNumberMax;
if (BottleDosage.Value > bottleAmountMax)
BottleDosage.Value = bottleAmountMax;
- }
+ // Avoid division by zero
+ if (PillDosage.Value > 0)
+ {
+ PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
+ }
+ else
+ {
+ PillNumber.Value = 0;
+ }
+
+ BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
+ }
///
/// Generate a product label based on reagents in the buffer.
///
@@ -178,46 +231,23 @@ private void UpdatePanelInfo(ChemMasterBoundUserInterfaceState state)
var bufferVol = new Label
{
Text = $"{state.BufferCurrentVolume}u",
- StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
+ StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
};
bufferHBox.AddChild(bufferVol);
+ // initialises rowCount to allow for striped rows
+
+ var rowCount = 0;
foreach (var (reagent, quantity) in state.BufferReagents)
{
- // Try to get the prototype for the given reagent. This gives us its name.
- _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
+ var reagentId = reagent;
+ _prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
-
- if (proto != null)
- {
- BufferInfo.Children.Add(new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- Children =
- {
- new Label {Text = $"{name}: "},
- new Label
- {
- Text = $"{quantity}u",
- StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
- },
-
- // Padding
- new Control {HorizontalExpand = true},
-
- MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent, true, StyleBase.ButtonOpenRight),
- MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent, true, StyleBase.ButtonOpenBoth),
- MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent, true, StyleBase.ButtonOpenBoth),
- MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent, true, StyleBase.ButtonOpenBoth),
- MakeReagentButton("50", ChemMasterReagentAmount.U50, reagent, true, StyleBase.ButtonOpenBoth),
- MakeReagentButton("100", ChemMasterReagentAmount.U100, reagent, true, StyleBase.ButtonOpenBoth),
- MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent, true, StyleBase.ButtonOpenLeft),
- }
- });
- }
+ var reagentColor = proto?.SubstanceColor ?? default(Color);
+ BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));
}
}
-
+
private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
{
control.Children.Clear();
@@ -228,104 +258,111 @@ private void BuildContainerUI(Control control, ContainerInfo? info, bool addReag
{
Text = Loc.GetString("chem-master-window-no-container-loaded-text")
});
+ return;
}
- else
+
+ // Name of the container and its fill status (Ex: 44/100u)
+ control.Children.Add(new BoxContainer
{
- // Name of the container and its fill status (Ex: 44/100u)
- control.Children.Add(new BoxContainer
+ Orientation = LayoutOrientation.Horizontal,
+ Children =
{
- Orientation = LayoutOrientation.Horizontal,
- Children =
+ new Label { Text = $"{info.DisplayName}: " },
+ new Label
{
- new Label {Text = $"{info.DisplayName}: "},
- new Label
- {
- Text = $"{info.CurrentVolume}/{info.MaxVolume}",
- StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
- }
+ Text = $"{info.CurrentVolume}/{info.MaxVolume}",
+ StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
}
- });
-
- IEnumerable<(string Name, ReagentId Id, FixedPoint2 Quantity)> contents;
+ }
+ });
+ // Initialises rowCount to allow for striped rows
+ var rowCount = 0;
- if (info.Entities != null)
+ // Handle entities if they are not null
+ if (info.Entities != null)
+ {
+ foreach (var (id, quantity) in info.Entities.Select(x => (x.Id, x.Quantity)))
{
- contents = info.Entities.Select(x => (x.Id, default(ReagentId), x.Quantity));
+ control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
}
- else if (info.Reagents != null)
- {
- contents = info.Reagents.Select(x =>
- {
- _prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
- var name = proto?.LocalizedName
- ?? Loc.GetString("chem-master-window-unknown-reagent-text");
+ }
- return (name, Id: x.Reagent, x.Quantity);
- })
- .OrderBy(r => r.Item1);
- }
- else
+ // Handle reagents if they are not null
+ if (info.Reagents != null)
+ {
+ foreach (var reagent in info.Reagents)
{
- return;
+ _prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto);
+ var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
+ var reagentColor = proto?.SubstanceColor ?? default(Color);
+
+ control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons));
}
-
-
- foreach (var (name, id, quantity) in contents)
+ }
+ }
+ ///
+ /// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites?
+ ///
+ private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons)
+ {
+ //Colors rows and sets fallback for reagentcolor to the same as background, this will hide colorPanel for entities hopefully
+ var rowColor1 = Color.FromHex("#1B1B1E");
+ var rowColor2 = Color.FromHex("#202025");
+ var currentRowColor = (rowCount % 2 == 1) ? rowColor1 : rowColor2;
+ if ((reagentColor == default(Color))|(!addReagentButtons))
+ {
+ reagentColor = currentRowColor;
+ }
+ //this calls the separated button builder, and stores the return to render after labels
+ var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
+
+ // Create the row layout with the color panel
+ var rowContainer = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ Children =
{
- var inner = new BoxContainer
+ new Label { Text = $"{name}: " },
+ new Label
{
- Orientation = LayoutOrientation.Horizontal,
- Children =
- {
- new Label { Text = $"{name}: " },
- new Label
- {
- Text = $"{quantity}u",
- StyleClasses = { StyleNano.StyleClassLabelSecondaryColor },
- }
- }
- };
-
- if (addReagentButtons)
+ Text = $"{quantity}u",
+ StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
+ },
+
+ // Padding
+ new Control { HorizontalExpand = true },
+ // Colored panels for reagents
+ new PanelContainer
{
- var cs = inner.Children;
-
- // Padding
- cs.Add(new Control { HorizontalExpand = true });
-
- cs.Add(MakeReagentButton(
- "1", ChemMasterReagentAmount.U1, id, false, StyleBase.ButtonOpenRight));
- cs.Add(MakeReagentButton(
- "5", ChemMasterReagentAmount.U5, id, false, StyleBase.ButtonOpenBoth));
- cs.Add(MakeReagentButton(
- "10", ChemMasterReagentAmount.U10, id, false, StyleBase.ButtonOpenBoth));
- cs.Add(MakeReagentButton(
- "25", ChemMasterReagentAmount.U25, id, false, StyleBase.ButtonOpenBoth));
- cs.Add(MakeReagentButton(
- "50", ChemMasterReagentAmount.U50, id, false, StyleBase.ButtonOpenBoth));
- cs.Add(MakeReagentButton(
- "100", ChemMasterReagentAmount.U100, id, false, StyleBase.ButtonOpenBoth));
- cs.Add(MakeReagentButton(
- Loc.GetString("chem-master-window-buffer-all-amount"),
- ChemMasterReagentAmount.All, id, false, StyleBase.ButtonOpenLeft));
+ Name = "colorPanel",
+ VerticalExpand = true,
+ MinWidth = 4,
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = reagentColor
+ },
+ Margin = new Thickness(0, 1)
}
-
- control.Children.Add(inner);
}
+ };
- }
- }
-
- public String LabelLine
- {
- get
+ // Add the reagent buttons after the color panel
+ foreach (var reagentTransferButton in reagentButtonConstructors)
{
- return LabelLineEdit.Text;
+ rowContainer.AddChild(reagentTransferButton);
}
- set
+ //Apply panencontainer to allow for striped rows
+ return new PanelContainer
{
- LabelLineEdit.Text = value;
- }
+ PanelOverride = new StyleBoxFlat(currentRowColor),
+ Children = { rowContainer }
+ };
+ }
+
+ public string LabelLine
+ {
+ get => LabelLineEdit.Text;
+ set => LabelLineEdit.Text = value;
}
}
diff --git a/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs b/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs
index 2c3afb0324f..1f09ae9eebb 100644
--- a/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs
+++ b/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs
@@ -7,7 +7,7 @@
namespace Content.Client.Clothing;
-public sealed class FlippableClothingVisualizerSystem : VisualizerSystem
+public sealed class FlippableClothingVisualizerSystem : VisualizerSystem
{
[Dependency] private readonly SharedItemSystem _itemSys = default!;
diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
index 83dc42c4a44..b74df979cf4 100644
--- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
+++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
@@ -1,17 +1,62 @@
-
-
-
-
+ MinSize="400 300">
-
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml b/Content.Client/Construction/UI/ConstructionMenu.xaml
index 6e4438cf6fd..a934967a533 100644
--- a/Content.Client/Construction/UI/ConstructionMenu.xaml
+++ b/Content.Client/Construction/UI/ConstructionMenu.xaml
@@ -1,15 +1,20 @@
-
+
+
+
+
-
-
+
+
+
+
+
diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
index f0cb8148762..9ab8a156005 100644
--- a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
+++ b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
@@ -25,11 +25,16 @@ public interface IConstructionMenuView : IDisposable
OptionButton OptionCategories { get; }
bool EraseButtonPressed { get; set; }
+ bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
ItemList Recipes { get; }
ItemList RecipeStepList { get; }
+
+ ScrollContainer RecipesGridScrollContainer { get; }
+ GridContainer RecipesGrid { get; }
+
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler RecipeSelected;
event EventHandler RecipeFavorited;
@@ -72,9 +77,16 @@ public bool EraseButtonPressed
set => EraseButton.Pressed = value;
}
+ public bool GridViewButtonPressed
+ {
+ get => MenuGridViewButton.Pressed;
+ set => MenuGridViewButton.Pressed = value;
+ }
+
public ConstructionMenu()
{
- SetSize = MinSize = new Vector2(720, 320);
+ SetSize = new Vector2(560, 450);
+ MinSize = new Vector2(560, 320);
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
@@ -102,6 +114,9 @@ public ConstructionMenu()
EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty);
+
+ MenuGridViewButton.OnPressed += _ =>
+ PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
}
public event EventHandler? ClearAllGhosts;
diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
index c315cdedb2c..d35e8fbe769 100644
--- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
+++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
@@ -1,7 +1,8 @@
using System.Linq;
+using System.Numerics;
+using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared.Construction.Prototypes;
-using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -11,7 +12,6 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Enums;
-using Robust.Shared.Graphics;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -33,10 +33,12 @@ internal sealed class ConstructionMenuPresenter : IDisposable
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
+ private readonly SpriteSystem _spriteSystem;
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List _favoritedRecipes = [];
+ private Dictionary _recipeButtons = new();
private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all";
@@ -85,6 +87,7 @@ public ConstructionMenuPresenter()
IoCManager.InjectDependencies(this);
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System();
+ _spriteSystem = _entManager.System();
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem(out var constructionSystem))
@@ -150,12 +153,24 @@ private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
PopulateInfo(_selected);
}
+ private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe)
+ {
+ if (recipe is null)
+ {
+ _selected = null;
+ _constructionView.ClearRecipeInfo();
+ return;
+ }
+
+ _selected = recipe;
+ if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
+ PopulateInfo(_selected);
+ }
+
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{
var (search, category) = args;
- var recipesList = _constructionView.Recipes;
- recipesList.Clear();
var recipes = new List();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
@@ -201,12 +216,73 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
- foreach (var recipe in recipes)
+ var recipesList = _constructionView.Recipes;
+ recipesList.Clear();
+
+ var recipesGrid = _constructionView.RecipesGrid;
+ recipesGrid.RemoveAllChildren();
+
+ _constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed;
+ _constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed;
+
+ if (_constructionView.GridViewButtonPressed)
+ {
+ foreach (var recipe in recipes)
+ {
+ var itemButton = new TextureButton
+ {
+ TextureNormal = _spriteSystem.Frame0(recipe.Icon),
+ VerticalAlignment = Control.VAlignment.Center,
+ Name = recipe.Name,
+ ToolTip = recipe.Name,
+ Scale = new Vector2(1.35f),
+ ToggleMode = true,
+ };
+ var itemButtonPanelContainer = new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
+ Children = { itemButton },
+ };
+
+ itemButton.OnToggled += buttonToggledEventArgs =>
+ {
+ SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
+
+ if (buttonToggledEventArgs.Pressed &&
+ _selected != null &&
+ _recipeButtons.TryGetValue(_selected.Name, out var oldButton))
+ {
+ oldButton.Pressed = false;
+ SelectGridButton(oldButton, false);
+ }
+
+ OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
+ };
+
+ recipesGrid.AddChild(itemButtonPanelContainer);
+ _recipeButtons[recipe.Name] = itemButton;
+ var isCurrentButtonSelected = _selected == recipe;
+ itemButton.Pressed = isCurrentButtonSelected;
+ SelectGridButton(itemButton, isCurrentButtonSelected);
+ }
+ }
+ else
{
- recipesList.Add(GetItem(recipe, recipesList));
+ foreach (var recipe in recipes)
+ {
+ recipesList.Add(GetItem(recipe, recipesList));
+ }
}
+ }
+
+ private void SelectGridButton(TextureButton button, bool select)
+ {
+ if (button.Parent is not PanelContainer buttonPanel)
+ return;
- // There is apparently no way to set which
+ button.Modulate = select ? Color.Green : Color.White;
+ var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
+ buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
private void PopulateCategories(string? selectCategory = null)
@@ -257,11 +333,10 @@ private void PopulateCategories(string? selectCategory = null)
private void PopulateInfo(ConstructionPrototype prototype)
{
- var spriteSys = _systemManager.GetEntitySystem();
_constructionView.ClearRecipeInfo();
_constructionView.SetRecipeInfo(
- prototype.Name, prototype.Description, spriteSys.Frame0(prototype.Icon),
+ prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon),
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
@@ -274,7 +349,6 @@ private void GenerateStepList(ConstructionPrototype prototype, ItemList stepList
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return;
- var spriteSys = _systemManager.GetEntitySystem();
foreach (var entry in guide.Entries)
{
@@ -290,20 +364,20 @@ private void GenerateStepList(ConstructionPrototype prototype, ItemList stepList
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
text = text.PadLeft(text.Length + entry.Padding);
- var icon = entry.Icon != null ? spriteSys.Frame0(entry.Icon) : Texture.Transparent;
+ var icon = entry.Icon != null ? _spriteSystem.Frame0(entry.Icon) : Texture.Transparent;
stepList.AddItem(text, icon, false);
}
}
- private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
+ private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = recipe.Name,
- Icon = recipe.Icon.Frame0(),
+ Icon = _spriteSystem.Frame0(recipe.Icon),
TooltipEnabled = true,
- TooltipText = recipe.Description
+ TooltipText = recipe.Description,
};
}
diff --git a/Content.Client/Crayon/CrayonSystem.cs b/Content.Client/Crayon/CrayonSystem.cs
index dc039794813..84749cf1f1b 100644
--- a/Content.Client/Crayon/CrayonSystem.cs
+++ b/Content.Client/Crayon/CrayonSystem.cs
@@ -57,6 +57,17 @@ protected override void FrameUpdate(FrameEventArgs args)
}
_parent.UIUpdateNeeded = false;
+
+ // Frontier: unlimited crayon, Delta V Port
+ if (_parent.Capacity == int.MaxValue)
+ {
+ _label.SetMarkup(Robust.Shared.Localization.Loc.GetString("crayon-drawing-label-unlimited",
+ ("color", _parent.Color),
+ ("state", _parent.SelectedState)));
+ return;
+ }
+ // End Frontier, Delta V Port
+
_label.SetMarkup(Robust.Shared.Localization.Loc.GetString("crayon-drawing-label",
("color",_parent.Color),
("state",_parent.SelectedState),
diff --git a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
index e5be0b1811f..44501767dd4 100644
--- a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
+++ b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
@@ -31,7 +31,7 @@ protected override void Open()
private void PopulateCrayons()
{
var crayonDecals = _protoManager.EnumeratePrototypes().Where(x => x.Tags.Contains("crayon"));
- _menu?.Populate(crayonDecals);
+ _menu?.Populate(crayonDecals.ToList());
}
public override void OnProtoReload(PrototypesReloadedEventArgs args)
@@ -44,6 +44,16 @@ public override void OnProtoReload(PrototypesReloadedEventArgs args)
PopulateCrayons();
}
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ base.ReceiveMessage(message);
+
+ if (_menu is null || message is not CrayonUsedMessage crayonMessage)
+ return;
+
+ _menu.AdvanceState(crayonMessage.DrawnDecal);
+ }
+
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml b/Content.Client/Crayon/UI/CrayonWindow.xaml
index 7729318ae7f..7acb22551b7 100644
--- a/Content.Client/Crayon/UI/CrayonWindow.xaml
+++ b/Content.Client/Crayon/UI/CrayonWindow.xaml
@@ -1,14 +1,13 @@
+ MinSize="450 500"
+ SetSize="450 500">
-
+
-
-
-
+
+
diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
index 6ef282d219a..88475562c67 100644
--- a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
+++ b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
+using System.Linq;
using Content.Client.Stylesheets;
using Content.Shared.Crayon;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -18,7 +20,12 @@ namespace Content.Client.Crayon.UI
[GenerateTypedNameReferences]
public sealed partial class CrayonWindow : DefaultWindow
{
- private Dictionary? _decals;
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ private readonly SpriteSystem _spriteSystem = default!;
+
+ private Dictionary>? _decals;
+ private List? _allDecals;
+ private string? _autoSelected;
private string? _selected;
private Color _color;
@@ -28,8 +35,10 @@ public sealed partial class CrayonWindow : DefaultWindow
public CrayonWindow()
{
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entitySystem.GetEntitySystem();
- Search.OnTextChanged += _ => RefreshList();
+ Search.OnTextChanged += SearchChanged;
ColorSelector.OnColorChanged += SelectColor;
}
@@ -44,51 +53,94 @@ private void SelectColor(Color color)
private void RefreshList()
{
// Clear
- Grid.DisposeAllChildren();
- if (_decals == null)
+ Grids.DisposeAllChildren();
+
+ if (_decals == null || _allDecals == null)
return;
var filter = Search.Text;
- foreach (var (decal, tex) in _decals)
+ var comma = filter.IndexOf(',');
+ var first = (comma == -1 ? filter : filter[..comma]).Trim();
+
+ var names = _decals.Keys.ToList();
+ names.Sort((a, b) => a == "random" ? 1 : b == "random" ? -1 : a.CompareTo(b));
+
+ if (_autoSelected != null && first != _autoSelected && _allDecals.Contains(first))
+ {
+ _selected = first;
+ _autoSelected = _selected;
+ OnSelected?.Invoke(_selected);
+ }
+
+ foreach (var categoryName in names)
{
- if (!decal.Contains(filter))
+ var locName = Loc.GetString("crayon-category-" + categoryName);
+ var category = _decals[categoryName].Where(d => locName.Contains(first) || d.Name.Contains(first)).ToList();
+
+ if (category.Count == 0)
continue;
- var button = new TextureButton()
+ var label = new Label
{
- TextureNormal = tex,
- Name = decal,
- ToolTip = decal,
- Modulate = _color,
+ Text = locName
};
- button.OnPressed += ButtonOnPressed;
- if (_selected == decal)
+
+ var grid = new GridContainer
{
- var panelContainer = new PanelContainer()
+ Columns = 6,
+ Margin = new Thickness(0, 0, 0, 16)
+ };
+
+ Grids.AddChild(label);
+ Grids.AddChild(grid);
+
+ foreach (var (name, texture) in category)
+ {
+ var button = new TextureButton()
{
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = StyleNano.ButtonColorDefault,
- },
- Children =
- {
- button,
- },
+ TextureNormal = texture,
+ Name = name,
+ ToolTip = name,
+ Modulate = _color,
+ Scale = new System.Numerics.Vector2(2, 2)
};
- Grid.AddChild(panelContainer);
- }
- else
- {
- Grid.AddChild(button);
+ button.OnPressed += ButtonOnPressed;
+
+ if (_selected == name)
+ {
+ var panelContainer = new PanelContainer()
+ {
+ PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = StyleNano.ButtonColorDefault,
+ },
+ Children =
+ {
+ button,
+ },
+ };
+ grid.AddChild(panelContainer);
+ }
+ else
+ {
+ grid.AddChild(button);
+ }
}
}
}
+ private void SearchChanged(LineEdit.LineEditEventArgs obj)
+ {
+ _autoSelected = ""; // Placeholder to kick off the auto-select in refreshlist()
+ RefreshList();
+ }
+
private void ButtonOnPressed(ButtonEventArgs obj)
{
if (obj.Button.Name == null) return;
_selected = obj.Button.Name;
+ _autoSelected = null;
OnSelected?.Invoke(_selected);
RefreshList();
}
@@ -107,12 +159,38 @@ public void UpdateState(CrayonBoundUserInterfaceState state)
RefreshList();
}
- public void Populate(IEnumerable prototypes)
+ public void AdvanceState(string drawnDecal)
{
- _decals = new Dictionary();
+ var filter = Search.Text;
+ if (!filter.Contains(',') || !filter.Contains(drawnDecal))
+ return;
+
+ var first = filter[..filter.IndexOf(',')].Trim();
+
+ if (first.Equals(drawnDecal, StringComparison.InvariantCultureIgnoreCase))
+ {
+ Search.Text = filter[(filter.IndexOf(',') + 1)..].Trim();
+ _autoSelected = first;
+ }
+
+ RefreshList();
+ }
+
+ public void Populate(List prototypes)
+ {
+ _decals = [];
+ _allDecals = [];
+
+ prototypes.Sort((a, b) => a.ID.CompareTo(b.ID));
+
foreach (var decalPrototype in prototypes)
{
- _decals.Add(decalPrototype.ID, decalPrototype.Sprite.Frame0());
+ var category = "random";
+ if (decalPrototype.Tags.Count > 1 && decalPrototype.Tags[1].StartsWith("crayon-"))
+ category = decalPrototype.Tags[1].Replace("crayon-", "");
+ var list = _decals.GetOrNew(category);
+ list.Add((decalPrototype.ID, _spriteSystem.Frame0(decalPrototype.Sprite)));
+ _allDecals.Add(decalPrototype.ID);
}
RefreshList();
diff --git a/Content.Client/DeltaV/AACTablet/UI/AACBoundUserInterface.cs b/Content.Client/DeltaV/AACTablet/UI/AACBoundUserInterface.cs
deleted file mode 100644
index 6a9330598fa..00000000000
--- a/Content.Client/DeltaV/AACTablet/UI/AACBoundUserInterface.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.DeltaV.AACTablet;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.DeltaV.AACTablet.UI;
-
-public sealed class AACBoundUserInterface : BoundUserInterface
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
- [ViewVariables]
- private AACWindow? _window;
-
- public AACBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- }
-
- protected override void Open()
- {
- base.Open();
- _window?.Close();
- _window = new AACWindow(this, _prototypeManager);
- _window.OpenCentered();
-
- _window.PhraseButtonPressed += OnPhraseButtonPressed;
- _window.OnClose += Close;
- }
-
- private void OnPhraseButtonPressed(string phraseId)
- {
- SendMessage(new AACTabletSendPhraseMessage(phraseId));
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- _window?.Dispose();
- }
-}
diff --git a/Content.Client/DeltaV/AACTablet/UI/AACWindow.xaml.cs b/Content.Client/DeltaV/AACTablet/UI/AACWindow.xaml.cs
deleted file mode 100644
index c9bc457b8df..00000000000
--- a/Content.Client/DeltaV/AACTablet/UI/AACWindow.xaml.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.DeltaV.QuickPhrase;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.DeltaV.AACTablet.UI;
-
-[GenerateTypedNameReferences]
-public sealed partial class AACWindow : FancyWindow
-{
- private IPrototypeManager _prototypeManager;
- public event Action? PhraseButtonPressed;
-
- public AACWindow(AACBoundUserInterface ui, IPrototypeManager prototypeManager)
- {
- RobustXamlLoader.Load(this);
- _prototypeManager = prototypeManager;
- PopulateGui(ui);
- }
-
- private void PopulateGui(AACBoundUserInterface ui)
- {
- var loc = IoCManager.Resolve();
- var phrases = _prototypeManager.EnumeratePrototypes().ToList();
-
- // take ALL phrases and turn them into tabs and groups, so the buttons are sorted and tabbed
- var sortedTabs = phrases
- .GroupBy(p => p.Tab)
- .OrderBy(g => g.Key)
- .ToDictionary(
- g => g.Key,
- g => g.GroupBy(p => p.Group)
- .OrderBy(gg => gg.Key)
- .ToDictionary(
- gg => gg.Key,
- gg => gg.OrderBy(p => loc.GetString(p.Text)).ToList()
- )
- );
-
- var tabContainer = CreateTabContainer(sortedTabs);
- WindowBody.AddChild(tabContainer);
- }
-
- private TabContainer CreateTabContainer(Dictionary>> sortedTabs)
- {
- var tabContainer = new TabContainer();
- var loc = IoCManager.Resolve();
-
- foreach (var tab in sortedTabs)
- {
- var tabName = loc.GetString(tab.Key);
- var boxContainer = CreateBoxContainerForTab(tab.Value);
- tabContainer.AddChild(boxContainer);
- tabContainer.SetTabTitle(tabContainer.ChildCount - 1, tabName);
- }
-
- return tabContainer;
- }
-
- private BoxContainer CreateBoxContainerForTab(Dictionary> groups)
- {
- var boxContainer = new BoxContainer()
- {
- HorizontalExpand = true,
- Orientation = BoxContainer.LayoutOrientation.Vertical
- };
-
- foreach (var group in groups)
- {
- var header = CreateHeaderForGroup(group.Key);
- var buttonContainer = CreateButtonContainerForGroup(group.Value);
- boxContainer.AddChild(header);
- boxContainer.AddChild(buttonContainer);
- }
-
- return boxContainer;
- }
-
- private Label CreateHeaderForGroup(string groupName)
- {
- var header = new Label
- {
- HorizontalExpand = true,
- Text = groupName,
- Margin = new Thickness(10, 10, 10, 0),
- StyleClasses = { "LabelBig" }
- };
-
- return header;
- }
-
- private GridContainer CreateButtonContainerForGroup(List phrases)
- {
- var loc = IoCManager.Resolve();
- var buttonContainer = CreateButtonContainer();
- foreach (var phrase in phrases)
- {
- var text = loc.GetString(phrase.Text);
- var button = CreatePhraseButton(text, phrase.StyleClass);
- button.OnPressed += _ => OnPhraseButtonPressed(phrase.ID);
- buttonContainer.AddChild(button);
- }
- return buttonContainer;
- }
-
- private static GridContainer CreateButtonContainer()
- {
- var buttonContainer = new GridContainer
- {
- Margin = new Thickness(10),
- Columns = 4
- };
-
- return buttonContainer;
- }
-
- private static Button CreatePhraseButton(string text, string styleClass)
- {
- var buttonWidth = GetButtonWidth();
- var phraseButton = new Button
- {
- Access = AccessLevel.Public,
- MaxSize = new Vector2(buttonWidth, buttonWidth),
- ClipText = false,
- HorizontalExpand = true,
- StyleClasses = { styleClass }
- };
-
- var buttonLabel = new RichTextLabel
- {
- Margin = new Thickness(0, 5),
- StyleClasses = { "WhiteText" }
- };
-
- buttonLabel.SetMessage(text);
- phraseButton.AddChild(buttonLabel);
- return phraseButton;
- }
-
- private static int GetButtonWidth()
- {
- var spaceWidth = 10;
- var parentWidth = 540;
- var columnCount = 4;
-
- var paddingSize = spaceWidth * 2;
- var gutterScale = (columnCount - 1) / columnCount;
- var columnWidth = (parentWidth - paddingSize) / columnCount;
- var buttonWidth = columnWidth - spaceWidth * gutterScale;
- return buttonWidth;
- }
-
- private void OnPhraseButtonPressed(string phraseId)
- {
- PhraseButtonPressed?.Invoke(phraseId);
- }
-}
diff --git a/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs b/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs
deleted file mode 100644
index 879a5efee55..00000000000
--- a/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using Content.Shared.DeltaV.Abilities;
-using Content.Shared.Popups;
-using Robust.Client.GameObjects;
-using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
-
-namespace Content.Client.DeltaV.Abilities;
-
-public sealed partial class HideUnderTableAbilitySystem : SharedCrawlUnderObjectsSystem
-{
- [Dependency] private readonly AppearanceSystem _appearance = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnAppearanceChange);
- }
-
- private void OnAppearanceChange(EntityUid uid,
- CrawlUnderObjectsComponent component,
- AppearanceChangeEvent args)
- {
- if (!TryComp(uid, out var sprite))
- return;
-
- _appearance.TryGetData(uid, SneakMode.Enabled, out bool enabled);
- if (enabled)
- {
- if (component.OriginalDrawDepth != null)
- return;
-
- component.OriginalDrawDepth = sprite.DrawDepth;
- sprite.DrawDepth = (int) DrawDepth.SmallMobs;
- }
- else
- {
- if (component.OriginalDrawDepth == null)
- return;
-
- sprite.DrawDepth = (int) component.OriginalDrawDepth;
- component.OriginalDrawDepth = null;
- }
- }
-}
diff --git a/Content.Client/DeltaV/Addictions/AddictionSystem.cs b/Content.Client/DeltaV/Addictions/AddictionSystem.cs
deleted file mode 100644
index 75ac6969a48..00000000000
--- a/Content.Client/DeltaV/Addictions/AddictionSystem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.DeltaV.Addictions;
-
-namespace Content.Client.DeltaV.Addictions;
-
-public sealed class AddictionSystem : SharedAddictionSystem
-{
- protected override void UpdateTime(EntityUid uid) {}
-}
diff --git a/Content.Client/DeltaV/Administration/UI/JobWhitelistsEui.cs b/Content.Client/DeltaV/Administration/UI/JobWhitelistsEui.cs
deleted file mode 100644
index c746f42f8f9..00000000000
--- a/Content.Client/DeltaV/Administration/UI/JobWhitelistsEui.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Content.Client.Eui;
-using Content.Shared.DeltaV.Administration;
-using Content.Shared.Eui;
-
-namespace Content.Client.DeltaV.Administration.UI;
-
-public sealed class JobWhitelistsEui : BaseEui
-{
- private JobWhitelistsWindow Window;
-
- public JobWhitelistsEui()
- {
- Window = new JobWhitelistsWindow();
- Window.OnClose += () => SendMessage(new CloseEuiMessage());
- Window.OnSetJob += (id, whitelisted) => SendMessage(new SetJobWhitelistedMessage(id, whitelisted));
- }
-
- public override void HandleState(EuiStateBase state)
- {
- if (state is not JobWhitelistsEuiState cast)
- return;
-
- Window.HandleState(cast);
- }
-
- public override void Opened()
- {
- base.Opened();
-
- Window.OpenCentered();
- }
-
- public override void Closed()
- {
- base.Closed();
-
- Window.Close();
- Window.Dispose();
- }
-}
diff --git a/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs b/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs
deleted file mode 100644
index 9cc7258d76f..00000000000
--- a/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Content.Shared.DeltaV.Biscuit;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.DeltaV.Biscuit;
-
-public sealed class BiscuitSystem : VisualizerSystem
-{
- [Dependency] private readonly AppearanceSystem _appearance = default!;
-
- protected override void OnAppearanceChange(EntityUid uid, BiscuitVisualsComponent component,
- ref AppearanceChangeEvent args)
- {
- if (args.Sprite == null)
- return;
-
- _appearance.TryGetData(uid, BiscuitStatus.Cracked, out bool cracked);
-
- args.Sprite.LayerSetVisible(BiscuitVisualLayers.Top, !cracked);
- }
-}
-
-public enum BiscuitVisualLayers : byte
-{
- Base,
- Top
-}
diff --git a/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs b/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs
deleted file mode 100644
index 42c745bcb4e..00000000000
--- a/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Content.Client.DeltaV.Biscuit;
-
-[RegisterComponent]
-public sealed partial class BiscuitVisualsComponent : Component
-{}
diff --git a/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs b/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs
deleted file mode 100644
index 7b9b3757e32..00000000000
--- a/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.DeltaV.Chapel;
-
-namespace Content.Client.DeltaV.Chapel;
-
-public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem;
diff --git a/Content.Client/DeltaV/Harpy/HarpyVisualsComponent.cs b/Content.Client/DeltaV/Harpy/HarpyVisualsComponent.cs
deleted file mode 100644
index 1c3253c74ef..00000000000
--- a/Content.Client/DeltaV/Harpy/HarpyVisualsComponent.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Content.Client.DeltaV.Harpy;
-
-[RegisterComponent]
-public sealed partial class HarpyVisualsComponent : Component
-{ }
diff --git a/Content.Client/DeltaV/Hologram/HologramSystem.cs b/Content.Client/DeltaV/Hologram/HologramSystem.cs
deleted file mode 100644
index 212a797fd87..00000000000
--- a/Content.Client/DeltaV/Hologram/HologramSystem.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using Content.Shared.DeltaV.Hologram;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.DeltaV.Hologram;
-
-public sealed class HologramSystem : SharedHologramSystem
-{
- [Dependency] private readonly IPrototypeManager _protoMan = default!;
- [Dependency] private readonly OccluderSystem _occluder = default!;
- [Dependency] private readonly EntityManager _entMan = default!;
-
- private ShaderInstance _shader = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- _shader = _protoMan.Index("Hologram").InstanceUnique();
- SubscribeLocalEvent(OnShutdown);
- SubscribeLocalEvent(OnStartup);
- }
-
- private void SetShader(EntityUid uid, bool enabled, HologramComponent? component = null, SpriteComponent? sprite = null)
- {
- if (!Resolve(uid, ref component, ref sprite, false))
- return;
-
- sprite.PostShader = enabled ? _shader : null;
- }
-
- private void OnStartup(EntityUid uid, HologramComponent component, ComponentStartup args)
- {
- SetShader(uid, true, component);
-
- component.Occludes = _entMan.TryGetComponent(uid, out var occluder) && occluder.Enabled;
- if (component.Occludes)
- _occluder.SetEnabled(uid, false);
- }
-
- private void OnShutdown(EntityUid uid, HologramComponent component, ComponentShutdown args)
- {
- SetShader(uid, false, component);
- if (component.Occludes)
- _occluder.SetEnabled(uid, true);
- }
-}
diff --git a/Content.Client/DeltaV/Implants/Radio/RadioImplantSystem.cs b/Content.Client/DeltaV/Implants/Radio/RadioImplantSystem.cs
deleted file mode 100644
index 27a021d19fe..00000000000
--- a/Content.Client/DeltaV/Implants/Radio/RadioImplantSystem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.DeltaV.Implants.Radio;
-
-namespace Content.Client.DeltaV.Implants.Radio;
-
-///
-public sealed class RadioImplantSystem : SharedRadioImplantSystem
-{
-}
diff --git a/Content.Client/DeltaV/Mail/MailComponent.cs b/Content.Client/DeltaV/Mail/MailComponent.cs
deleted file mode 100644
index 1603cf7d663..00000000000
--- a/Content.Client/DeltaV/Mail/MailComponent.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Shared.DeltaV.Mail;
-
-namespace Content.Client.DeltaV.Mail
-{
- [RegisterComponent]
- public sealed partial class MailComponent : SharedMailComponent
- {
- }
-}
diff --git a/Content.Client/DeltaV/Mail/MailSystem.cs b/Content.Client/DeltaV/Mail/MailSystem.cs
deleted file mode 100644
index b215192140f..00000000000
--- a/Content.Client/DeltaV/Mail/MailSystem.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Content.Shared.DeltaV.Mail;
-using Content.Shared.StatusIcon;
-using Robust.Client.GameObjects;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.DeltaV.Mail;
-
-///
-/// Display a cool stamp on the parcel based on the job of the recipient.
-///
-///
-/// GenericVisualizer is not powerful enough to handle setting a string on
-/// visual data then directly relaying that string to a layer's state.
-/// I.e. there is nothing like a regex capture group for visual data.
-/// Hence why this system exists.
-/// To do this with GenericVisualizer would require a separate condition
-/// for every job value, which would be extra mess to maintain.
-/// It would look something like this, multipled a couple dozen times.
-/// enum.MailVisuals.JobIcon:
-/// enum.MailVisualLayers.JobStamp:
-/// StationEngineer:
-/// state: StationEngineer
-/// SecurityOfficer:
-/// state: SecurityOfficer
-///
-public sealed class MailJobVisualizerSystem : VisualizerSystem
-{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly SpriteSystem _spriteSystem = default!;
-
- protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args)
- {
- if (args.Sprite == null)
- return;
-
- _appearance.TryGetData(uid, MailVisuals.JobIcon, out string job, args.Component);
-
- if (string.IsNullOrEmpty(job))
- job = "JobIconUnknown";
-
- if (!_prototypeManager.TryIndex(job, out var icon))
- {
- args.Sprite.LayerSetTexture(MailVisualLayers.JobStamp, _spriteSystem.Frame0(_prototypeManager.Index("JobIconUnknown")));
- return;
- }
-
- args.Sprite.LayerSetTexture(MailVisualLayers.JobStamp, _spriteSystem.Frame0(icon.Icon));
- }
-}
-
-public enum MailVisualLayers : byte
-{
- Icon,
- Lock,
- FragileStamp,
- JobStamp,
- PriorityTape,
- Breakage
-}
diff --git a/Content.Client/DeltaV/Overlays/PainSystem.cs b/Content.Client/DeltaV/Overlays/PainSystem.cs
deleted file mode 100644
index 9ad436027a2..00000000000
--- a/Content.Client/DeltaV/Overlays/PainSystem.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Content.Shared.DeltaV.Pain;
-using Robust.Client.Graphics;
-using Robust.Shared.Player;
-
-namespace Content.Client.DeltaV.Overlays;
-
-public sealed partial class PainSystem : EntitySystem
-{
- [Dependency] private readonly IOverlayManager _overlayMan = default!;
- [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
-
- private PainOverlay _overlay = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnPainInit);
- SubscribeLocalEvent(OnPainShutdown);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
-
- _overlay = new();
- }
-
- private void OnPainInit(Entity ent, ref ComponentInit args)
- {
- if (ent.Owner == _playerMan.LocalEntity && !ent.Comp.Suppressed)
- _overlayMan.AddOverlay(_overlay);
- }
-
- private void OnPainShutdown(Entity ent, ref ComponentShutdown args)
- {
- if (ent.Owner == _playerMan.LocalEntity)
- _overlayMan.RemoveOverlay(_overlay);
- }
-
- private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args)
- {
- if (!ent.Comp.Suppressed)
- _overlayMan.AddOverlay(_overlay);
- }
-
- private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args)
- {
- _overlayMan.RemoveOverlay(_overlay);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- // Handle showing/hiding overlay based on suppression status
- if (_playerMan.LocalEntity is not { } player)
- return;
-
- if (!TryComp(player, out var comp))
- return;
-
- if (comp.Suppressed && _overlayMan.HasOverlay())
- _overlayMan.RemoveOverlay(_overlay);
- else if (!comp.Suppressed && !_overlayMan.HasOverlay())
- _overlayMan.AddOverlay(_overlay);
- }
-}
diff --git a/Content.Client/DeltaV/Recruiter/RecruiterPenSystem.cs b/Content.Client/DeltaV/Recruiter/RecruiterPenSystem.cs
deleted file mode 100644
index 32fa6bf061a..00000000000
--- a/Content.Client/DeltaV/Recruiter/RecruiterPenSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.DeltaV.Recruiter;
-
-namespace Content.Client.DeltaV.Recruiter;
-
-public sealed class RecruiterPenSystem : SharedRecruiterPenSystem;
diff --git a/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs
new file mode 100644
index 00000000000..b95c0d585d7
--- /dev/null
+++ b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs
@@ -0,0 +1,95 @@
+using Content.Shared.Electrocution;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client.Electrocution;
+
+///
+/// Shows the Electrocution HUD to entities with the ShowElectrocutionHUDComponent.
+///
+public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem
+{
+ [Dependency] private readonly IPlayerManager _playerMan = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+ }
+
+ private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args)
+ {
+ ShowHUD();
+ }
+
+ private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args)
+ {
+ RemoveHUD();
+ }
+
+ private void OnInit(Entity ent, ref ComponentInit args)
+ {
+ if (_playerMan.LocalEntity == ent)
+ {
+ ShowHUD();
+ }
+ }
+
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if (_playerMan.LocalEntity == ent)
+ {
+ RemoveHUD();
+ }
+ }
+
+ // Show the HUD to the client.
+ // We have to look for all current entities that can be electrified and toggle the HUD layer on if they are.
+ private void ShowHUD()
+ {
+ var electrifiedQuery = AllEntityQuery();
+ while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
+ {
+ if (!AppearanceSystem.TryGetData(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp))
+ continue;
+
+ if (electrified)
+ spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, true);
+ else
+ spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
+ }
+ }
+
+ // Remove the HUD from the client.
+ // Find all current entities that can be electrified and hide the HUD layer.
+ private void RemoveHUD()
+ {
+ var electrifiedQuery = AllEntityQuery();
+ while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
+ {
+
+ spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
+ }
+ }
+
+ // Toggle the HUD layer if an entity becomes (de-)electrified
+ protected override void OnAppearanceChange(EntityUid uid, ElectrocutionHUDVisualsComponent comp, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (!AppearanceSystem.TryGetData(uid, ElectrifiedVisuals.IsElectrified, out var electrified, args.Component))
+ return;
+
+ var player = _playerMan.LocalEntity;
+ if (electrified && HasComp(player))
+ args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, true);
+ else
+ args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, false);
+ }
+}
diff --git a/Content.Client/Eui/BaseEui.cs b/Content.Client/Eui/BaseEui.cs
index 7f86ded7e48..c11ba5a9b69 100644
--- a/Content.Client/Eui/BaseEui.cs
+++ b/Content.Client/Eui/BaseEui.cs
@@ -55,7 +55,7 @@ public virtual void HandleMessage(EuiMessageBase msg)
///
protected void SendMessage(EuiMessageBase msg)
{
- var netMsg = _netManager.CreateNetMessage();
+ var netMsg = new MsgEuiMessage();
netMsg.Id = Id;
netMsg.Message = msg;
diff --git a/Content.Client/Explosion/ScatteringGrenadeSystem.cs b/Content.Client/Explosion/ScatteringGrenadeSystem.cs
new file mode 100644
index 00000000000..28976779153
--- /dev/null
+++ b/Content.Client/Explosion/ScatteringGrenadeSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Explosion.EntitySystems;
+
+namespace Content.Client.Explosion;
+
+public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
+{
+
+}
diff --git a/Content.Client/Holopad/HolopadBoundUserInterface.cs b/Content.Client/Holopad/HolopadBoundUserInterface.cs
new file mode 100644
index 00000000000..20b55ea8c76
--- /dev/null
+++ b/Content.Client/Holopad/HolopadBoundUserInterface.cs
@@ -0,0 +1,101 @@
+using Content.Shared.Holopad;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Shared.Player;
+using System.Numerics;
+
+namespace Content.Client.Holopad;
+
+public sealed class HolopadBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly ISharedPlayerManager _playerManager = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+
+ [ViewVariables]
+ private HolopadWindow? _window;
+
+ public HolopadBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.Title = Loc.GetString("holopad-window-title", ("title", EntMan.GetComponent(Owner).EntityName));
+
+ if (this.UiKey is not HolopadUiKey)
+ {
+ Close();
+ return;
+ }
+
+ var uiKey = (HolopadUiKey)this.UiKey;
+
+ // AIs will see a different holopad interface to crew when interacting with them in the world
+ if (uiKey == HolopadUiKey.InteractionWindow && EntMan.HasComponent(_playerManager.LocalEntity))
+ uiKey = HolopadUiKey.InteractionWindowForAi;
+
+ _window.SetState(Owner, uiKey);
+ _window.UpdateState(new Dictionary());
+
+ // Set message actions
+ _window.SendHolopadStartNewCallMessageAction += SendHolopadStartNewCallMessage;
+ _window.SendHolopadAnswerCallMessageAction += SendHolopadAnswerCallMessage;
+ _window.SendHolopadEndCallMessageAction += SendHolopadEndCallMessage;
+ _window.SendHolopadStartBroadcastMessageAction += SendHolopadStartBroadcastMessage;
+ _window.SendHolopadActivateProjectorMessageAction += SendHolopadActivateProjectorMessage;
+ _window.SendHolopadRequestStationAiMessageAction += SendHolopadRequestStationAiMessage;
+
+ // If this call is addressed to an AI, open the window in the bottom right hand corner of the screen
+ if (uiKey == HolopadUiKey.AiRequestWindow)
+ _window.OpenCenteredAt(new Vector2(1f, 1f));
+
+ // Otherwise offset to the left so the holopad can still be seen
+ else
+ _window.OpenCenteredAt(new Vector2(0.3333f, 0.50f));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ var castState = (HolopadBoundInterfaceState)state;
+ EntMan.TryGetComponent(Owner, out var xform);
+
+ _window?.UpdateState(castState.Holopads);
+ }
+
+ public void SendHolopadStartNewCallMessage(NetEntity receiver)
+ {
+ SendMessage(new HolopadStartNewCallMessage(receiver));
+ }
+
+ public void SendHolopadAnswerCallMessage()
+ {
+ SendMessage(new HolopadAnswerCallMessage());
+ }
+
+ public void SendHolopadEndCallMessage()
+ {
+ SendMessage(new HolopadEndCallMessage());
+ }
+
+ public void SendHolopadStartBroadcastMessage()
+ {
+ SendMessage(new HolopadStartBroadcastMessage());
+ }
+
+ public void SendHolopadActivateProjectorMessage()
+ {
+ SendMessage(new HolopadActivateProjectorMessage());
+ }
+
+ public void SendHolopadRequestStationAiMessage()
+ {
+ SendMessage(new HolopadStationAiRequestMessage());
+ }
+}
diff --git a/Content.Client/Holopad/HolopadSystem.cs b/Content.Client/Holopad/HolopadSystem.cs
new file mode 100644
index 00000000000..3bd556f1fc2
--- /dev/null
+++ b/Content.Client/Holopad/HolopadSystem.cs
@@ -0,0 +1,172 @@
+using Content.Shared.Chat.TypingIndicator;
+using Content.Shared.Holopad;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using System.Linq;
+
+namespace Content.Client.Holopad;
+
+public sealed class HolopadSystem : SharedHolopadSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnShaderRender);
+ SubscribeAllEvent(OnTypingChanged);
+
+ SubscribeNetworkEvent(OnPlayerSpriteStateRequest);
+ SubscribeNetworkEvent(OnPlayerSpriteStateMessage);
+ }
+
+ private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ UpdateHologramSprite(uid);
+ }
+
+ private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
+ {
+ if (ev.Sprite.PostShader == null)
+ return;
+
+ ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
+ }
+
+ private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
+ {
+ var uid = args.SenderSession.AttachedEntity;
+
+ if (!Exists(uid))
+ return;
+
+ if (!HasComp(uid))
+ return;
+
+ var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
+ RaiseNetworkEvent(netEv);
+ }
+
+ private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
+ {
+ var targetPlayer = GetEntity(ev.TargetPlayer);
+ var player = _playerManager.LocalSession?.AttachedEntity;
+
+ // Ignore the request if received by a player who isn't the target
+ if (targetPlayer != player)
+ return;
+
+ if (!TryComp(player, out var playerSprite))
+ return;
+
+ var spriteLayerData = new List();
+
+ if (playerSprite.Visible)
+ {
+ // Record the RSI paths, state names and shader paramaters of all visible layers
+ for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
+ {
+ if (!playerSprite.TryGetLayer(i, out var layer))
+ continue;
+
+ if (!layer.Visible ||
+ string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
+ string.IsNullOrEmpty(layer.State.Name))
+ continue;
+
+ var layerDatum = new PrototypeLayerData();
+ layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
+ layerDatum.State = layer.State.Name;
+
+ if (layer.CopyToShaderParameters != null)
+ {
+ var key = (string)layer.CopyToShaderParameters.LayerKey;
+
+ if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
+ playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
+ otherLayer.Visible)
+ {
+ layerDatum.MapKeys = new() { key };
+
+ layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
+ {
+ LayerKey = key,
+ ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
+ ParameterUV = layer.CopyToShaderParameters.ParameterUV
+ };
+ }
+ }
+
+ spriteLayerData.Add(layerDatum);
+ }
+ }
+
+ // Return the recorded data to the server
+ var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
+ RaiseNetworkEvent(evResponse);
+ }
+
+ private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
+ {
+ UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
+ }
+
+ private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
+ {
+ if (!TryComp(uid, out var hologramSprite))
+ return;
+
+ if (!TryComp(uid, out var holopadhologram))
+ return;
+
+ for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
+ hologramSprite.RemoveLayer(i);
+
+ if (layerData == null || layerData.Length == 0)
+ {
+ layerData = new PrototypeLayerData[1];
+ layerData[0] = new PrototypeLayerData()
+ {
+ RsiPath = holopadhologram.RsiPath,
+ State = holopadhologram.RsiState
+ };
+ }
+
+ for (int i = 0; i < layerData.Length; i++)
+ {
+ var layer = layerData[i];
+ layer.Shader = "unshaded";
+
+ hologramSprite.AddLayer(layerData[i], i);
+ }
+
+ UpdateHologramShader(uid, hologramSprite, holopadhologram);
+ }
+
+ private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
+ {
+ // Find the texture height of the largest layer
+ float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
+
+ var instance = _prototypeManager.Index(holopadHologram.ShaderName).InstanceUnique();
+ instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
+ instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
+ instance.SetParameter("alpha", holopadHologram.Alpha);
+ instance.SetParameter("intensity", holopadHologram.Intensity);
+ instance.SetParameter("texHeight", texHeight);
+ instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate);
+
+ sprite.PostShader = instance;
+ sprite.RaiseShaderEvent = true;
+ }
+}
diff --git a/Content.Client/Holopad/HolopadWindow.xaml b/Content.Client/Holopad/HolopadWindow.xaml
new file mode 100644
index 00000000000..9c3dfab1ea6
--- /dev/null
+++ b/Content.Client/Holopad/HolopadWindow.xaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Holopad/HolopadWindow.xaml.cs b/Content.Client/Holopad/HolopadWindow.xaml.cs
new file mode 100644
index 00000000000..bcab0d43df1
--- /dev/null
+++ b/Content.Client/Holopad/HolopadWindow.xaml.cs
@@ -0,0 +1,338 @@
+using Content.Client.Popups;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access.Systems;
+using Content.Shared.Holopad;
+using Content.Shared.Telephone;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Linq;
+
+namespace Content.Client.Holopad;
+
+[GenerateTypedNameReferences]
+public sealed partial class HolopadWindow : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private readonly SharedHolopadSystem _holopadSystem = default!;
+ private readonly SharedTelephoneSystem _telephoneSystem = default!;
+ private readonly AccessReaderSystem _accessReaderSystem = default!;
+ private readonly PopupSystem _popupSystem = default!;
+
+ private EntityUid? _owner = null;
+ private HolopadUiKey _currentUiKey;
+ private TelephoneState _currentState;
+ private TelephoneState _previousState;
+ private TimeSpan _buttonUnlockTime;
+ private float _updateTimer = 0.25f;
+
+ private const float UpdateTime = 0.25f;
+ private TimeSpan _buttonUnlockDelay = TimeSpan.FromSeconds(0.5f);
+
+ public event Action? SendHolopadStartNewCallMessageAction;
+ public event Action? SendHolopadAnswerCallMessageAction;
+ public event Action? SendHolopadEndCallMessageAction;
+ public event Action? SendHolopadStartBroadcastMessageAction;
+ public event Action? SendHolopadActivateProjectorMessageAction;
+ public event Action? SendHolopadRequestStationAiMessageAction;
+
+ public HolopadWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _holopadSystem = _entManager.System();
+ _telephoneSystem = _entManager.System();
+ _accessReaderSystem = _entManager.System();
+ _popupSystem = _entManager.System();
+
+ _buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
+
+ // Assign button actions
+ AnswerCallButton.OnPressed += args => { OnHolopadAnswerCallMessage(); };
+ EndCallButton.OnPressed += args => { OnHolopadEndCallMessage(); };
+ StartBroadcastButton.OnPressed += args => { OnHolopadStartBroadcastMessage(); };
+ ActivateProjectorButton.OnPressed += args => { OnHolopadActivateProjectorMessage(); };
+ RequestStationAiButton.OnPressed += args => { OnHolopadRequestStationAiMessage(); };
+
+ // XML formatting
+ AnswerCallButton.AddStyleClass("ButtonAccept");
+ EndCallButton.AddStyleClass("Caution");
+ StartBroadcastButton.AddStyleClass("Caution");
+
+ HolopadContactListPanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = new Color(47, 47, 59) * Color.DarkGray,
+ BorderColor = new Color(82, 82, 82), //new Color(70, 73, 102),
+ BorderThickness = new Thickness(2),
+ };
+
+ HolopadContactListHeaderPanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = new Color(82, 82, 82),
+ };
+
+ EmergencyBroadcastText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-emergency-broadcast-in-progress")));
+ SubtitleText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-subtitle")));
+ OptionsText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-options")));
+ }
+
+ #region: Button actions
+
+ private void OnSendHolopadStartNewCallMessage(NetEntity receiver)
+ {
+ SendHolopadStartNewCallMessageAction?.Invoke(receiver);
+ }
+
+ private void OnHolopadAnswerCallMessage()
+ {
+ SendHolopadAnswerCallMessageAction?.Invoke();
+ }
+
+ private void OnHolopadEndCallMessage()
+ {
+ SendHolopadEndCallMessageAction?.Invoke();
+
+ if (_currentUiKey == HolopadUiKey.AiRequestWindow)
+ Close();
+ }
+
+ private void OnHolopadStartBroadcastMessage()
+ {
+ if (_playerManager.LocalSession?.AttachedEntity == null || _owner == null)
+ return;
+
+ var player = _playerManager.LocalSession.AttachedEntity;
+
+ if (!_accessReaderSystem.IsAllowed(player.Value, _owner.Value))
+ {
+ _popupSystem.PopupClient(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
+ return;
+ }
+
+ SendHolopadStartBroadcastMessageAction?.Invoke();
+ }
+
+ private void OnHolopadActivateProjectorMessage()
+ {
+ SendHolopadActivateProjectorMessageAction?.Invoke();
+ }
+
+ private void OnHolopadRequestStationAiMessage()
+ {
+ SendHolopadRequestStationAiMessageAction?.Invoke();
+ }
+
+ #endregion
+
+ public void SetState(EntityUid owner, HolopadUiKey uiKey)
+ {
+ _owner = owner;
+ _currentUiKey = uiKey;
+
+ // Determines what UI containers are available to the user.
+ // Components of these will be toggled on and off when
+ // UpdateAppearance() is called
+
+ switch (uiKey)
+ {
+ case HolopadUiKey.InteractionWindow:
+ RequestStationAiContainer.Visible = true;
+ HolopadContactListContainer.Visible = true;
+ StartBroadcastContainer.Visible = true;
+ break;
+
+ case HolopadUiKey.InteractionWindowForAi:
+ ActivateProjectorContainer.Visible = true;
+ StartBroadcastContainer.Visible = true;
+ break;
+
+ case HolopadUiKey.AiActionWindow:
+ HolopadContactListContainer.Visible = true;
+ StartBroadcastContainer.Visible = true;
+ break;
+
+ case HolopadUiKey.AiRequestWindow:
+ break;
+ }
+ }
+
+ public void UpdateState(Dictionary holopads)
+ {
+ if (_owner == null || !_entManager.TryGetComponent(_owner.Value, out var telephone))
+ return;
+
+ // Caller ID text
+ var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
+
+ CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
+ LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
+
+ // Sort holopads alphabetically
+ var holopadArray = holopads.ToArray();
+ Array.Sort(holopadArray, AlphabeticalSort);
+
+ // Clear excess children from the contact list
+ while (ContactsList.ChildCount > holopadArray.Length)
+ ContactsList.RemoveChild(ContactsList.GetChild(ContactsList.ChildCount - 1));
+
+ // Make / update required children
+ for (int i = 0; i < holopadArray.Length; i++)
+ {
+ var (netEntity, label) = holopadArray[i];
+
+ if (i >= ContactsList.ChildCount)
+ {
+ var newContactButton = new HolopadContactButton();
+ newContactButton.OnPressed += args => { OnSendHolopadStartNewCallMessage(newContactButton.NetEntity); };
+
+ ContactsList.AddChild(newContactButton);
+ }
+
+ var child = ContactsList.GetChild(i);
+
+ if (child is not HolopadContactButton)
+ continue;
+
+ var contactButton = (HolopadContactButton)child;
+ contactButton.UpdateValues(netEntity, label);
+ }
+
+ // Update buttons
+ UpdateAppearance();
+ }
+
+ private void UpdateAppearance()
+ {
+ if (_owner == null || !_entManager.TryGetComponent(_owner.Value, out var telephone))
+ return;
+
+ if (_owner == null || !_entManager.TryGetComponent(_owner.Value, out var holopad))
+ return;
+
+ var hasBroadcastAccess = !_holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
+ var localPlayer = _playerManager.LocalSession?.AttachedEntity;
+
+ ControlsLockOutContainer.Visible = _holopadSystem.IsHolopadControlLocked((_owner.Value, holopad), localPlayer);
+ ControlsContainer.Visible = !ControlsLockOutContainer.Visible;
+
+ // Temporarily disable the interface buttons when the call state changes to prevent any misclicks
+ if (_currentState != telephone.CurrentState)
+ {
+ _previousState = _currentState;
+ _currentState = telephone.CurrentState;
+ _buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
+ }
+
+ var lockButtons = _timing.CurTime < _buttonUnlockTime;
+
+ // Make / update required children
+ foreach (var child in ContactsList.Children)
+ {
+ if (child is not HolopadContactButton)
+ continue;
+
+ var contactButton = (HolopadContactButton)child;
+ contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
+ }
+
+ // Update control text
+ var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
+ var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
+
+ StartBroadcastButton.Text = _holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad)) ?
+ Loc.GetString("holopad-window-emergency-broadcast-with-countdown", ("countdown", cooldownString)) :
+ Loc.GetString("holopad-window-emergency-broadcast");
+
+ var lockout = _holopadSystem.GetHolopadControlLockedPeriod((_owner.Value, holopad));
+ var lockoutString = $"{lockout.Minutes:00}:{lockout.Seconds:00}";
+
+ LockOutCountDownText.Text = Loc.GetString("holopad-window-controls-unlock-countdown", ("countdown", lockoutString));
+
+ switch (_currentState)
+ {
+ case TelephoneState.Idle:
+ CallStatusText.Text = Loc.GetString("holopad-window-no-calls-in-progress"); break;
+
+ case TelephoneState.Calling:
+ CallStatusText.Text = Loc.GetString("holopad-window-outgoing-call"); break;
+
+ case TelephoneState.Ringing:
+ CallStatusText.Text = (_currentUiKey == HolopadUiKey.AiRequestWindow) ?
+ Loc.GetString("holopad-window-ai-request") : Loc.GetString("holopad-window-incoming-call"); break;
+
+ case TelephoneState.InCall:
+ CallStatusText.Text = Loc.GetString("holopad-window-call-in-progress"); break;
+
+ case TelephoneState.EndingCall:
+ if (_previousState == TelephoneState.Calling || _previousState == TelephoneState.Idle)
+ CallStatusText.Text = Loc.GetString("holopad-window-call-rejected");
+ else
+ CallStatusText.Text = Loc.GetString("holopad-window-call-ending");
+ break;
+ }
+
+ // Update control disability
+ AnswerCallButton.Disabled = (_currentState != TelephoneState.Ringing || lockButtons);
+ EndCallButton.Disabled = (_currentState == TelephoneState.Idle || _currentState == TelephoneState.EndingCall || lockButtons);
+ StartBroadcastButton.Disabled = (_currentState != TelephoneState.Idle || !hasBroadcastAccess || lockButtons);
+ RequestStationAiButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
+ ActivateProjectorButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
+
+ // Update control visibility
+ FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
+ ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
+ CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
+ CallerIdText.Visible = (_currentState == TelephoneState.Ringing);
+ AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ _updateTimer += args.DeltaSeconds;
+
+ if (_updateTimer >= UpdateTime)
+ {
+ _updateTimer -= UpdateTime;
+ UpdateAppearance();
+ }
+ }
+
+ private sealed class HolopadContactButton : Button
+ {
+ public NetEntity NetEntity;
+
+ public HolopadContactButton()
+ {
+ HorizontalExpand = true;
+ SetHeight = 32;
+ Margin = new Thickness(0f, 1f, 0f, 1f);
+ }
+
+ public void UpdateValues(NetEntity netEntity, string label)
+ {
+ NetEntity = netEntity;
+ Text = Loc.GetString("holopad-window-contact-label", ("label", label));
+ }
+ }
+
+ private int AlphabeticalSort(KeyValuePair x, KeyValuePair y)
+ {
+ if (string.IsNullOrEmpty(x.Value))
+ return -1;
+
+ if (string.IsNullOrEmpty(y.Value))
+ return 1;
+
+ return x.Value.CompareTo(y.Value);
+ }
+}
diff --git a/Content.Client/Info/LinkBanner.cs b/Content.Client/Info/LinkBanner.cs
index a30aa413761..7366a8f8565 100644
--- a/Content.Client/Info/LinkBanner.cs
+++ b/Content.Client/Info/LinkBanner.cs
@@ -34,6 +34,7 @@ public LinkBanner()
AddInfoButton("server-info-website-button", CCVars.InfoLinksWebsite);
AddInfoButton("server-info-wiki-button", CCVars.InfoLinksWiki);
AddInfoButton("server-info-forum-button", CCVars.InfoLinksForum);
+ AddInfoButton("server-info-telegram-button", CCVars.InfoLinksTelegram);
var guidebookController = UserInterfaceManager.GetUIController();
var guidebookButton = new Button() { Text = Loc.GetString("server-info-guidebook-button") };
diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs
index 2ce07758c96..90e52d72837 100644
--- a/Content.Client/Inventory/StrippableBoundUserInterface.cs
+++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs
@@ -17,6 +17,7 @@
using Content.Shared.Strip.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
+using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@@ -29,10 +30,13 @@ namespace Content.Client.Inventory
[UsedImplicitly]
public sealed class StrippableBoundUserInterface : BoundUserInterface
{
+ [Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
+
private readonly ExamineSystem _examine;
private readonly InventorySystem _inv;
private readonly SharedCuffableSystem _cuffable;
+ private readonly StrippableSystem _strippable;
[ViewVariables]
private const int ButtonSeparation = 4;
@@ -51,6 +55,8 @@ public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u
_examine = EntMan.System();
_inv = EntMan.System();
_cuffable = EntMan.System();
+ _strippable = EntMan.System();
+
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
}
@@ -185,9 +191,15 @@ private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot)
return;
if (ev.Function == ContentKeyFunctions.ExamineEntity)
+ {
_examine.DoExamine(slot.Entity.Value);
+ ev.Handle();
+ }
else if (ev.Function == EngineKeyFunctions.UseSecondary)
+ {
_ui.GetUIController().OpenVerbMenu(slot.Entity.Value);
+ ev.Handle();
+ }
}
private void AddInventoryButton(EntityUid invUid, string slotId, InventoryComponent inv)
@@ -198,7 +210,8 @@ private void AddInventoryButton(EntityUid invUid, string slotId, InventoryCompon
var entity = container.ContainedEntity;
// If this is a full pocket, obscure the real entity
- if (entity != null && slotDef.StripHidden)
+ // this does not work for modified clients because they are still sent the real entity
+ if (entity != null && _strippable.IsStripHidden(slotDef, _player.LocalEntity))
entity = _virtualHiddenEntity;
var button = new SlotButton(new SlotData(slotDef, container));
diff --git a/Content.Client/Lathe/UI/LatheBoundUserInterface.cs b/Content.Client/Lathe/UI/LatheBoundUserInterface.cs
index a599f79152e..80ffe86ebfd 100644
--- a/Content.Client/Lathe/UI/LatheBoundUserInterface.cs
+++ b/Content.Client/Lathe/UI/LatheBoundUserInterface.cs
@@ -1,3 +1,4 @@
+using Content.Shared._DV.Salvage; // DeltaV
using Content.Shared.Lathe;
using Content.Shared.Research.Components;
using JetBrains.Annotations;
@@ -31,6 +32,8 @@ protected override void Open()
{
SendMessage(new LatheQueueRecipeMessage(recipe, amount));
};
+
+ _menu.OnClaimMiningPoints += () => SendMessage(new LatheClaimMiningPointsMessage()); // DeltaV
}
protected override void UpdateState(BoundUserInterfaceState state)
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml
index 5b21f0bae66..d84449ce43e 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml
@@ -132,6 +132,12 @@
HorizontalExpand="True">
+
+
+
+
+
+
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index 02464d22e12..151eef49437 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -1,16 +1,20 @@
using System.Linq;
using System.Text;
using Content.Client.Materials;
+using Content.Shared._DV.Salvage.Components; // DeltaV
+using Content.Shared._DV.Salvage.Systems; // DeltaV
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
+using Robust.Client.Player; // DeltaV
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
+using Robust.Shared.Timing; // DeltaV
namespace Content.Client.Lathe.UI;
@@ -18,14 +22,17 @@ namespace Content.Client.Lathe.UI;
public sealed partial class LatheMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!; // DeltaV
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly LatheSystem _lathe;
private readonly MaterialStorageSystem _materialStorage;
+ private readonly MiningPointsSystem _miningPoints; // DeltaV
public event Action? OnServerListButtonPressed;
public event Action? RecipeQueueAction;
+ public event Action? OnClaimMiningPoints; // DeltaV
public List> Recipes = new();
@@ -35,6 +42,8 @@ public sealed partial class LatheMenu : DefaultWindow
public EntityUid Entity;
+ private uint? _lastMiningPoints; // DeltaV: used to avoid Loc.GetString every frame
+
public LatheMenu()
{
RobustXamlLoader.Load(this);
@@ -43,6 +52,7 @@ public LatheMenu()
_spriteSystem = _entityManager.System();
_lathe = _entityManager.System();
_materialStorage = _entityManager.System();
+ _miningPoints = _entityManager.System(); // DeltaV
SearchBar.OnTextChanged += _ =>
{
@@ -70,9 +80,31 @@ public void SetEntity(EntityUid uid)
}
}
+ // Begin DeltaV Additions: Mining points UI
+ MiningPointsContainer.Visible = _entityManager.TryGetComponent(Entity, out var points);
+ MiningPointsClaimButton.OnPressed += _ => OnClaimMiningPoints?.Invoke();
+ if (points != null)
+ UpdateMiningPoints(points.Points);
+ // End DeltaV Additions
+
MaterialsList.SetOwner(Entity);
}
+ ///
+ /// DeltaV: Updates the UI elements for mining points.
+ ///
+ private void UpdateMiningPoints(uint points)
+ {
+ MiningPointsClaimButton.Disabled = points == 0 ||
+ _player.LocalSession?.AttachedEntity is not {} player ||
+ _miningPoints.TryFindIdCard(player) == null;
+ if (points == _lastMiningPoints)
+ return;
+
+ _lastMiningPoints = points;
+ MiningPointsLabel.Text = Loc.GetString("lathe-menu-mining-points", ("points", points));
+ }
+
protected override void Opened()
{
base.Opened();
@@ -83,6 +115,17 @@ protected override void Opened()
}
}
+ ///
+ /// DeltaV: Update mining points UI whenever it changes.
+ ///
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (_entityManager.TryGetComponent(Entity, out var points))
+ UpdateMiningPoints(points.Points);
+ }
+
///
/// Populates the list of all the recipes
///
diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs
index ddd99c7c483..d25b28756f8 100644
--- a/Content.Client/Light/HandheldLightSystem.cs
+++ b/Content.Client/Light/HandheldLightSystem.cs
@@ -21,6 +21,22 @@ public override void Initialize()
SubscribeLocalEvent(OnAppearanceChange);
}
+ ///
+ /// TODO: Not properly predicted yet. Don't call this function if you want a the actual return value!
+ ///
+ public override bool TurnOff(Entity ent, bool makeNoise = true)
+ {
+ return true;
+ }
+
+ ///
+ /// TODO: Not properly predicted yet. Don't call this function if you want a the actual return value!
+ ///
+ public override bool TurnOn(EntityUid user, Entity uid)
+ {
+ return true;
+ }
+
private void OnAppearanceChange(EntityUid uid, HandheldLightComponent? component, ref AppearanceChangeEvent args)
{
if (!Resolve(uid, ref component))
diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs
index 1aabc4ff381..1361ca57cd2 100644
--- a/Content.Client/Lobby/LobbyState.cs
+++ b/Content.Client/Lobby/LobbyState.cs
@@ -116,7 +116,7 @@ public override void FrameUpdate(FrameEventArgs e)
return;
}
- Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
+ Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
string text;
if (_gameTicker.Paused)
@@ -136,6 +136,10 @@ public override void FrameUpdate(FrameEventArgs e)
{
text = Loc.GetString(seconds < -5 ? "lobby-state-right-now-question" : "lobby-state-right-now-confirmation");
}
+ else if (difference.TotalHours >= 1)
+ {
+ text = $"{Math.Floor(difference.TotalHours)}:{difference.Minutes:D2}:{difference.Seconds:D2}";
+ }
else
{
text = $"{difference.Minutes}:{difference.Seconds:D2}";
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index 3cf98c98aba..56dc9d6d412 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -279,7 +279,7 @@ private void OpenSavePanel()
_profileEditor.OnOpenGuidebook += _guide.OpenHelp;
- _characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
+ _characterSetup = new CharacterSetupGui(_profileEditor);
_characterSetup.CloseButton.OnPressed += _ =>
{
@@ -455,7 +455,21 @@ public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobProtot
{
EntityUid dummyEnt;
- if (humanoid is not null)
+ EntProtoId? previewEntity = null;
+ if (humanoid != null && jobClothes)
+ {
+ job ??= GetPreferredJob(humanoid);
+
+ previewEntity = job.JobPreviewEntity ?? (EntProtoId?)job?.JobEntity;
+ }
+
+ if (previewEntity != null)
+ {
+ // Special type like borg or AI, do not spawn a human just spawn the entity.
+ dummyEnt = EntityManager.SpawnEntity(previewEntity, MapCoordinates.Nullspace);
+ return dummyEnt;
+ }
+ else if (humanoid is not null)
{
var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype;
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
@@ -469,7 +483,8 @@ public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobProtot
if (humanoid != null && jobClothes)
{
- job ??= GetPreferredJob(humanoid);
+ DebugTools.Assert(job != null);
+
GiveDummyJobClothes(dummyEnt, humanoid, job);
if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID)))
diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml b/Content.Client/Lobby/UI/CharacterPickerButton.xaml
index af1e640aadb..2edfd19a24e 100644
--- a/Content.Client/Lobby/UI/CharacterPickerButton.xaml
+++ b/Content.Client/Lobby/UI/CharacterPickerButton.xaml
@@ -6,6 +6,7 @@
SeparationOverride="0"
Name="InternalHBox">