diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7a8129df1a6686..4029b093ccb4a8 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,47 +1,33 @@
-
-
+
## About the PR
-
+
## Why / Balance
-
+
## Technical details
-
+
## Media
-
+
## Requirements
-
-- [ ] I have read and I am following the [Pull Request Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). I understand that not doing so may get my pr closed at maintainer’s discretion
-- [ ] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase
+
+- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
+- [ ] I have added media to this PR or it does not require an ingame showcase.
+
## Breaking changes
-
+
**Changelog**
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
new file mode 100644
index 00000000000000..79bb66560e3c33
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
@@ -0,0 +1,215 @@
+using Content.Client.Stylesheets;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+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 Robust.Shared.Map;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlarmEntryContainer : BoxContainer
+{
+ public NetEntity NetEntity;
+ public EntityCoordinates? Coordinates;
+
+ private readonly IEntityManager _entManager;
+ private readonly IResourceCache _cache;
+
+ private Dictionary _alarmStrings = new Dictionary()
+ {
+ [AtmosAlarmType.Invalid] = "atmos-alerts-window-invalid-state",
+ [AtmosAlarmType.Normal] = "atmos-alerts-window-normal-state",
+ [AtmosAlarmType.Warning] = "atmos-alerts-window-warning-state",
+ [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);
+
+ _entManager = IoCManager.Resolve();
+ _cache = IoCManager.Resolve();
+
+ NetEntity = uid;
+ Coordinates = coordinates;
+
+ // 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);
+ var smallFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+
+ // Set fonts
+ TemperatureHeaderLabel.FontOverride = headerFont;
+ PressureHeaderLabel.FontOverride = headerFont;
+ OxygenationHeaderLabel.FontOverride = headerFont;
+ GasesHeaderLabel.FontOverride = headerFont;
+
+ TemperatureLabel.FontOverride = normalFont;
+ PressureLabel.FontOverride = normalFont;
+ OxygenationLabel.FontOverride = normalFont;
+
+ NoDataLabel.FontOverride = headerFont;
+
+ SilenceCheckBox.Label.FontOverride = smallFont;
+ SilenceCheckBox.Label.FontColorOverride = Color.DarkGray;
+ }
+
+ public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlertsFocusDeviceData? focusData = null)
+ {
+ NetEntity = entry.NetEntity;
+ Coordinates = _entManager.GetCoordinates(entry.Coordinates);
+
+ // Load fonts
+ var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+
+ // Update alarm state
+ if (!_alarmStrings.TryGetValue(entry.AlarmState, out var alarmString))
+ alarmString = "atmos-alerts-window-invalid-state";
+
+ AlarmStateLabel.Text = Loc.GetString(alarmString);
+ AlarmStateLabel.FontColorOverride = GetAlarmStateColor(entry.AlarmState);
+
+ // Update alarm name
+ AlarmNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", entry.EntityName), ("address", entry.Address));
+
+ // Focus updates
+ FocusContainer.Visible = isFocus;
+
+ if (isFocus)
+ SetAsFocus();
+ else
+ RemoveAsFocus();
+
+ if (isFocus && entry.Group == AtmosAlertsComputerGroup.AirAlarm)
+ {
+ MainDataContainer.Visible = (entry.AlarmState != AtmosAlarmType.Invalid);
+ NoDataLabel.Visible = (entry.AlarmState == AtmosAlarmType.Invalid);
+
+ if (focusData != null)
+ {
+ // Update temperature
+ var tempK = (FixedPoint2)focusData.Value.TemperatureData.Item1;
+ var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float());
+
+ TemperatureLabel.Text = Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK));
+ TemperatureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.TemperatureData.Item2);
+
+ // Update pressure
+ PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)focusData.Value.PressureData.Item1));
+ PressureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.PressureData.Item2);
+
+ // Update oxygenation
+ var oxygenPercent = (FixedPoint2)0f;
+ var oxygenAlert = AtmosAlarmType.Invalid;
+
+ if (focusData.Value.GasData.TryGetValue(Gas.Oxygen, out var oxygenData))
+ {
+ oxygenPercent = oxygenData.Item2 * 100f;
+ oxygenAlert = oxygenData.Item3;
+ }
+
+ OxygenationLabel.Text = Loc.GetString("atmos-alerts-window-oxygenation-value", ("value", oxygenPercent));
+ OxygenationLabel.FontColorOverride = GetAlarmStateColor(oxygenAlert);
+
+ // Update other present gases
+ GasGridContainer.RemoveAllChildren();
+
+ var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen);
+
+ if (gasData.Count() == 0)
+ {
+ // No other 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, (var mol, var percent, var alert)) in gasData)
+ {
+ var gasPercent = (FixedPoint2)0f;
+ gasPercent = percent * 100f;
+
+ if (!_gasShorthands.TryGetValue(gas, out var gasShorthand))
+ gasShorthand = "X";
+
+ var gasLabel = new Label()
+ {
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
+ FontOverride = normalFont,
+ FontColorOverride = GetAlarmStateColor(alert),
+ 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";
+ }
+
+ public void RemoveAsFocus()
+ {
+ FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+ ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
+ FocusContainer.Visible = false;
+ }
+
+ private Color GetAlarmStateColor(AtmosAlarmType alarmType)
+ {
+ switch (alarmType)
+ {
+ case AtmosAlarmType.Normal:
+ return StyleNano.GoodGreenFore;
+ case AtmosAlarmType.Warning:
+ return StyleNano.ConcerningOrangeFore;
+ case AtmosAlarmType.Danger:
+ return StyleNano.DangerousRedFore;
+ }
+
+ return StyleNano.DisabledFore;
+ }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
new file mode 100644
index 00000000000000..08cae979b9b83a
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
@@ -0,0 +1,52 @@
+using Content.Shared.Atmos.Components;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AtmosAlertsComputerWindow? _menu;
+
+ public AtmosAlertsComputerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ 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)
+ {
+ base.UpdateState(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);
+ }
+
+ public void SendFocusChangeMessage(NetEntity? netEntity)
+ {
+ SendMessage(new AtmosAlertsComputerFocusChangeMessage(netEntity));
+ }
+
+ public void SendDeviceSilencedMessage(NetEntity netEntity, bool silenceDevice)
+ {
+ SendMessage(new AtmosAlertsComputerDeviceSilencedMessage(netEntity, silenceDevice));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
new file mode 100644
index 00000000000000..8824a776ee6825
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
new file mode 100644
index 00000000000000..f0b7ffbe119909
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
@@ -0,0 +1,548 @@
+using Content.Client.Message;
+using Content.Client.Pinpointer.UI;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.Pinpointer;
+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.Timing;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlertsComputerWindow : FancyWindow
+{
+ private readonly IEntityManager _entManager;
+ private readonly SpriteSystem _spriteSystem;
+
+ private EntityUid? _owner;
+ private NetEntity? _trackedEntity;
+
+ private AtmosAlertsComputerEntry[]? _airAlarms = null;
+ private AtmosAlertsComputerEntry[]? _fireAlarms = null;
+ private IEnumerable? _allAlarms = null;
+
+ private IEnumerable? _activeAlarms = null;
+ private Dictionary _deviceSilencingProgress = new();
+
+ public event Action? SendFocusChangeMessageAction;
+ public event Action? SendDeviceSilencedMessageAction;
+
+ private bool _autoScrollActive = false;
+ private bool _autoScrollAwaitsUpdate = false;
+
+ private const float SilencingDuration = 2.5f;
+
+ public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+ _spriteSystem = _entManager.System();
+
+ // Pass the owner to nav map
+ _owner = owner;
+ NavMap.Owner = _owner;
+
+ // Set nav map colors
+ NavMap.WallColor = new Color(64, 64, 64);
+ NavMap.TileColor = Color.DimGray * NavMap.WallColor;
+
+ // Set nav map grid uid
+ var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
+
+ if (_entManager.TryGetComponent(owner, out var xform))
+ {
+ 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-alerts-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-alerts-window-tab-no-alerts"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms"));
+ MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms"));
+
+ // Set UI toggles
+ ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid);
+ ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal);
+ ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning);
+ ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger);
+
+ // Set atmos monitoring message action
+ SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
+ SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage;
+ }
+
+ #region Toggle handling
+
+ private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ foreach (var device in console.AtmosDevices)
+ {
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (toggledAlarmState != alarmState)
+ continue;
+
+ if (toggle.Pressed)
+ AddTrackedEntityToNavMap(device, alarmState);
+
+ else
+ NavMap.TrackedEntities.Remove(device.NetEntity);
+ }
+ }
+
+ private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState)
+ {
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ if (toggleState)
+ _deviceSilencingProgress[netEntity] = SilencingDuration;
+
+ else
+ _deviceSilencingProgress.Remove(netEntity);
+
+ foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children)
+ {
+ if (entryContainer.NetEntity == netEntity)
+ entryContainer.SilenceAlarmProgressBar.Visible = toggleState;
+ }
+
+ SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState);
+ }
+
+ #endregion
+
+ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ if (_trackedEntity != focusData?.NetEntity)
+ {
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ focusData = null;
+ }
+
+ // Retain alarm data for use inbetween updates
+ _airAlarms = airAlarms;
+ _fireAlarms = fireAlarms;
+ _allAlarms = airAlarms.Concat(fireAlarms);
+
+ var silenced = console.SilencedDevices;
+
+ _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal &&
+ (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity)));
+
+ // Reset nav map data
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ // Add tracked entities to the nav map
+ foreach (var device in console.AtmosDevices)
+ {
+ if (!NavMap.Visible)
+ continue;
+
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (_trackedEntity != device.NetEntity)
+ {
+ // Skip air alarms if the appropriate overlay is off
+ if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid)
+ continue;
+
+ if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal)
+ continue;
+
+ if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning)
+ continue;
+
+ if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger)
+ continue;
+ }
+
+ AddTrackedEntityToNavMap(device, alarmState);
+ }
+
+ // Show the monitor location
+ var consoleUid = _entManager.GetNetEntity(_owner);
+
+ if (consoleCoords != null && consoleUid != null)
+ {
+ var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+ var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
+ NavMap.TrackedEntities[consoleUid.Value] = blip;
+ }
+
+ // Update the nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Clear excess children from the tables
+ var activeAlarmCount = _activeAlarms.Count();
+
+ while (AlertsTable.ChildCount > activeAlarmCount)
+ AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1));
+
+ while (AirAlarmsTable.ChildCount > airAlarms.Length)
+ AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1));
+
+ while (FireAlarmsTable.ChildCount > fireAlarms.Length)
+ FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1));
+
+ // Update all entries in each table
+ for (int index = 0; index < _activeAlarms.Count(); index++)
+ {
+ var entry = _activeAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AlertsTable, console, focusData);
+ }
+
+ for (int index = 0; index < airAlarms.Count(); index++)
+ {
+ var entry = airAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData);
+ }
+
+ for (int index = 0; index < fireAlarms.Count(); index++)
+ {
+ var entry = fireAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData);
+ }
+
+ // If no alerts are active, display a message
+ if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0)
+ {
+ var label = new RichTextLabel()
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ };
+
+ label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
+
+ AlertsTable.AddChild(label);
+ }
+
+ // Update the alerts tab with the number of active alerts
+ if (activeAlarmCount == 0)
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+
+ else
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
+
+ // Auto-scroll re-enable
+ if (_autoScrollAwaitsUpdate)
+ {
+ _autoScrollActive = true;
+ _autoScrollAwaitsUpdate = false;
+ }
+ }
+
+ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState)
+ {
+ var data = GetBlipTexture(alarmState);
+
+ if (data == null)
+ return;
+
+ var texture = data.Value.Item1;
+ var color = data.Value.Item2;
+ var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+
+ if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
+ color *= Color.DimGray;
+
+ var selectable = true;
+ var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
+
+ NavMap.TrackedEntities[metaData.NetEntity] = blip;
+ }
+
+ private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
+ {
+ // Make new UI entry if required
+ if (index >= table.ChildCount)
+ {
+ var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates));
+
+ // On click
+ newEntryContainer.FocusButton.OnButtonUp += args =>
+ {
+ if (_trackedEntity == newEntryContainer.NetEntity)
+ {
+ _trackedEntity = null;
+ }
+
+ else
+ {
+ _trackedEntity = newEntryContainer.NetEntity;
+
+ if (newEntryContainer.Coordinates != null)
+ NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value);
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+
+ // Update affected UI elements across all tables
+ UpdateConsoleTable(console, AlertsTable, _trackedEntity);
+ UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity);
+ UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity);
+ };
+
+ // On toggling the silence check box
+ newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed);
+
+ // Add the entry to the current table
+ table.AddChild(newEntryContainer);
+ }
+
+ // Update values and UI elements
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not AtmosAlarmEntryContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateUIEntry(entry, index, table, console, focusData);
+
+ return;
+ }
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
+
+ if (_trackedEntity != entry.NetEntity)
+ {
+ var silenced = console.SilencedDevices;
+ entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ private void UpdateConsoleTable(AtmosAlertsComputerComponent 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? netEntity)
+ {
+ if (netEntity == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ _trackedEntity = netEntity;
+
+ if (netEntity != null)
+ {
+ // Tab switching
+ if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false)
+ {
+ var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity);
+
+ switch (device?.Group)
+ {
+ case AtmosAlertsComputerGroup.AirAlarm:
+ MasterTabContainer.CurrentTab = 1; break;
+ case AtmosAlertsComputerGroup.FireAlarm:
+ MasterTabContainer.CurrentTab = 2; break;
+ }
+ }
+
+ // Get the scroll position of the selected entity on the selected button the UI
+ ActivateAutoScrollToFocus();
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ AutoScrollToFocus();
+
+ // Device silencing update
+ foreach ((var device, var remainingTime) in _deviceSilencingProgress)
+ {
+ var t = remainingTime - args.DeltaSeconds;
+
+ if (t <= 0)
+ {
+ _deviceSilencingProgress.Remove(device);
+
+ if (device == _trackedEntity)
+ _trackedEntity = null;
+ }
+
+ else
+ _deviceSilencingProgress[device] = t;
+ }
+ }
+
+ private void ActivateAutoScrollToFocus()
+ {
+ _autoScrollActive = false;
+ _autoScrollAwaitsUpdate = true;
+ }
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) 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 child in scroll.Children)
+ {
+ if (child is not VScrollBar)
+ continue;
+
+ var castChild = child as VScrollBar;
+
+ if (castChild != null)
+ {
+ vScrollBar = castChild;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) 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 == null || control is not AtmosAlarmEntryContainer)
+ continue;
+
+ if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ // Failed to find control
+ nextScrollPosition = null;
+
+ return false;
+ }
+
+ private AtmosAlarmType GetAlarmState(NetEntity netEntity)
+ {
+ var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState;
+
+ if (alarmState == null)
+ return AtmosAlarmType.Invalid;
+
+ return alarmState.Value;
+ }
+
+ private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState)
+ {
+ (SpriteSpecifier.Texture, Color)? output = null;
+
+ switch (alarmState)
+ {
+ case AtmosAlarmType.Invalid:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
+ case AtmosAlarmType.Normal:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
+ case AtmosAlarmType.Warning:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
+ case AtmosAlarmType.Danger:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
+ }
+
+ return output;
+ }
+}
diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs
index ca6336b91b857c..b525747aa9cd61 100644
--- a/Content.Client/Audio/AmbientSoundSystem.cs
+++ b/Content.Client/Audio/AmbientSoundSystem.cs
@@ -306,6 +306,9 @@ private void ProcessNearbyAmbience(TransformComponent playerXform)
.WithMaxDistance(comp.Range);
var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
+ if (stream == null)
+ continue;
+
_playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
playingCount++;
diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs
index 7c77865f74156d..50c3971d95addd 100644
--- a/Content.Client/Audio/ClientGlobalSoundSystem.cs
+++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs
@@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent)
if(!_adminAudioEnabled) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
- _adminAudio.Add(stream.Value.Entity);
+ _adminAudio.Add(stream?.Entity);
}
private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
@@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
- _eventAudio.Add(soundEvent.Type, stream.Value.Entity);
+ _eventAudio.Add(soundEvent.Type, stream?.Entity);
}
private void PlayGameSound(GameGlobalSoundEvent soundEvent)
diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
index d60c978ccf5c5e..bf7ab26cba25ba 100644
--- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
@@ -213,9 +213,9 @@ private void UpdateAmbientMusic()
false,
AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
- _ambientMusicStream = strim.Value.Entity;
+ _ambientMusicStream = strim?.Entity;
- if (_musicProto.FadeIn)
+ if (_musicProto.FadeIn && strim != null)
{
FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime);
}
diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
index 92c5b7a419153b..9864dbcb2a91bf 100644
--- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
@@ -185,7 +185,7 @@ private void PlaySoundtrack(string soundtrackFilename)
false,
_lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
);
- if (playResult.Value.Entity == default)
+ if (playResult == null)
{
_sawmill.Warning(
$"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",
diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs
index 6770899e0aa293..035e1300ca5394 100644
--- a/Content.Client/Buckle/BuckleSystem.cs
+++ b/Content.Client/Buckle/BuckleSystem.cs
@@ -15,7 +15,6 @@ public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnHandleState);
SubscribeLocalEvent(OnAppearanceChange);
SubscribeLocalEvent(OnStrapMoveEvent);
}
@@ -57,21 +56,6 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE
}
}
- private void OnHandleState(Entity ent, ref ComponentHandleState args)
- {
- if (args.Current is not BuckleState state)
- return;
-
- ent.Comp.DontCollide = state.DontCollide;
- ent.Comp.BuckleTime = state.BuckleTime;
- var strapUid = EnsureEntity(state.BuckledTo, ent);
-
- SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null));
-
- var (uid, component) = ent;
-
- }
-
private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
{
if (!TryComp(uid, out var rotVisuals))
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs
new file mode 100644
index 00000000000000..3c97b8b37d1545
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs
@@ -0,0 +1,30 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed partial class WantedListUi : UIFragment
+{
+ private WantedListUiFragment? _fragment;
+
+ public override Control GetUIFragmentRoot()
+ {
+ return _fragment!;
+ }
+
+ public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+ {
+ _fragment = new WantedListUiFragment();
+ }
+
+ public override void UpdateState(BoundUserInterfaceState state)
+ {
+ switch (state)
+ {
+ case WantedListUiState cast:
+ _fragment?.UpdateState(cast.Records);
+ break;
+ }
+ }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs
new file mode 100644
index 00000000000000..4137f6c2af0cc0
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs
@@ -0,0 +1,240 @@
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.CriminalRecords.Systems;
+using Content.Shared.Security;
+using Content.Shared.StatusIcon;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class WantedListUiFragment : BoxContainer
+{
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ private readonly SpriteSystem _spriteSystem;
+
+ private string? _selectedTargetName;
+ private List _wantedRecords = new();
+
+ public WantedListUiFragment()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entitySystem.GetEntitySystem();
+
+ SearchBar.OnTextChanged += OnSearchBarTextChanged;
+ }
+
+ private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
+ {
+ var found = !String.IsNullOrWhiteSpace(args.Text)
+ ? _wantedRecords.FindAll(r =>
+ r.TargetInfo.Name.Contains(args.Text) ||
+ r.Status.ToString().Contains(args.Text, StringComparison.OrdinalIgnoreCase))
+ : _wantedRecords;
+
+ UpdateState(found, false);
+ }
+
+ public void UpdateState(List records, bool refresh = true)
+ {
+ if (records.Count == 0)
+ {
+ NoRecords.Visible = true;
+ RecordsList.Visible = false;
+ RecordUnselected.Visible = false;
+ PersonContainer.Visible = false;
+
+ _selectedTargetName = null;
+ if (refresh)
+ _wantedRecords.Clear();
+
+ RecordsList.PopulateList(new List());
+
+ return;
+ }
+
+ NoRecords.Visible = false;
+ RecordsList.Visible = true;
+ RecordUnselected.Visible = true;
+ PersonContainer.Visible = false;
+
+ var dataList = records.Select(r => new StatusListData(r)).ToList();
+
+ RecordsList.GenerateItem = GenerateItem;
+ RecordsList.ItemPressed = OnItemSelected;
+ RecordsList.PopulateList(dataList);
+
+ if (refresh)
+ _wantedRecords = records;
+ }
+
+ private void OnItemSelected(BaseButton.ButtonEventArgs args, ListData data)
+ {
+ if (data is not StatusListData(var record))
+ return;
+
+ FormattedMessage GetLoc(string fluentId, params (string,object)[] args)
+ {
+ var msg = new FormattedMessage();
+ var fluent = Loc.GetString(fluentId, args);
+ msg.AddMarkupPermissive(fluent);
+ return msg;
+ }
+
+ // Set personal info
+ PersonName.Text = record.TargetInfo.Name;
+ TargetAge.SetMessage(GetLoc(
+ "wanted-list-age-label",
+ ("age", record.TargetInfo.Age)
+ ));
+ TargetJob.SetMessage(GetLoc(
+ "wanted-list-job-label",
+ ("job", record.TargetInfo.JobTitle.ToLower())
+ ));
+ TargetSpecies.SetMessage(GetLoc(
+ "wanted-list-species-label",
+ ("species", record.TargetInfo.Species.ToLower())
+ ));
+ TargetGender.SetMessage(GetLoc(
+ "wanted-list-gender-label",
+ ("gender", record.TargetInfo.Gender)
+ ));
+
+ // Set reason
+ WantedReason.SetMessage(GetLoc(
+ "wanted-list-reason-label",
+ ("reason", record.Reason ?? Loc.GetString("wanted-list-unknown-reason-label"))
+ ));
+
+ // Set status
+ PersonState.SetMessage(GetLoc(
+ "wanted-list-status-label",
+ ("status", record.Status.ToString().ToLower())
+ ));
+
+ // Set initiator
+ InitiatorName.SetMessage(GetLoc(
+ "wanted-list-initiator-label",
+ ("initiator", record.Initiator ?? Loc.GetString("wanted-list-unknown-initiator-label"))
+ ));
+
+ // History table
+ // Clear table if it exists
+ HistoryTable.RemoveAllChildren();
+
+ HistoryTable.AddChild(new Label()
+ {
+ Text = Loc.GetString("wanted-list-history-table-time-col"),
+ StyleClasses = { "LabelSmall" },
+ HorizontalAlignment = HAlignment.Center,
+ });
+ HistoryTable.AddChild(new Label()
+ {
+ Text = Loc.GetString("wanted-list-history-table-reason-col"),
+ StyleClasses = { "LabelSmall" },
+ HorizontalAlignment = HAlignment.Center,
+ HorizontalExpand = true,
+ });
+
+ HistoryTable.AddChild(new Label()
+ {
+ Text = Loc.GetString("wanted-list-history-table-initiator-col"),
+ StyleClasses = { "LabelSmall" },
+ HorizontalAlignment = HAlignment.Center,
+ });
+
+ if (record.History.Count > 0)
+ {
+ HistoryTable.Visible = true;
+
+ foreach (var history in record.History.OrderByDescending(h => h.AddTime))
+ {
+ HistoryTable.AddChild(new Label()
+ {
+ Text = $"{history.AddTime.Hours:00}:{history.AddTime.Minutes:00}:{history.AddTime.Seconds:00}",
+ StyleClasses = { "LabelSmall" },
+ VerticalAlignment = VAlignment.Top,
+ });
+
+ HistoryTable.AddChild(new RichTextLabel()
+ {
+ Text = $"[color=white]{history.Crime}[/color]",
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Top,
+ StyleClasses = { "LabelSubText" },
+ Margin = new(10f, 0f),
+ });
+
+ HistoryTable.AddChild(new RichTextLabel()
+ {
+ Text = $"[color=white]{history.InitiatorName}[/color]",
+ StyleClasses = { "LabelSubText" },
+ VerticalAlignment = VAlignment.Top,
+ });
+ }
+ }
+
+ RecordUnselected.Visible = false;
+ PersonContainer.Visible = true;
+
+ // Save selected item
+ _selectedTargetName = record.TargetInfo.Name;
+ }
+
+ private void GenerateItem(ListData data, ListContainerButton button)
+ {
+ if (data is not StatusListData(var record))
+ return;
+
+ var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true };
+ var label = new Label() { Text = record.TargetInfo.Name };
+ var rect = new TextureRect()
+ {
+ TextureScale = new(2.2f),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Margin = new(0f, 0f, 6f, 0f),
+ };
+
+ if (record.Status is not SecurityStatus.None)
+ {
+ var proto = "SecurityIcon" + record.Status switch
+ {
+ SecurityStatus.Detained => "Incarcerated",
+ _ => record.Status.ToString(),
+ };
+
+ if (_prototypeManager.TryIndex(proto, out var prototype))
+ {
+ rect.Texture = _spriteSystem.Frame0(prototype.Icon);
+ }
+ }
+
+ box.AddChild(rect);
+ box.AddChild(label);
+ button.AddChild(box);
+ button.AddStyleClass(ListContainer.StyleClassListContainerButton);
+
+ if (record.TargetInfo.Name.Equals(_selectedTargetName))
+ {
+ button.Pressed = true;
+ // For some reason the event is not called when `Pressed` changed, call it manually.
+ OnItemSelected(
+ new(button, new(new(), BoundKeyState.Down, new(), false, new(), new())),
+ data);
+ }
+ }
+}
+
+internal record StatusListData(WantedRecord Record) : ListData;
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
new file mode 100644
index 00000000000000..7b5d116ad74b8e
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
index a3cedb5f2f3824..7c7d824ee981aa 100644
--- a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
+++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
@@ -1,5 +1,5 @@
using System.Linq;
-using Content.Client.Chemistry.Containers.EntitySystems;
+using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos.Prototypes;
using Content.Shared.Body.Part;
using Content.Shared.Chemistry;
@@ -16,7 +16,7 @@ namespace Content.Client.Chemistry.EntitySystems;
///
public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
{
- [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[ValidatePrototypeId]
private const string DefaultMixingCategory = "DummyMix";
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index a941f0acff97fd..1c1f1984de4e39 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -1,7 +1,12 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading;
using Content.Client.Verbs;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Input;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Item;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -12,13 +17,8 @@
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Utility;
-using System.Linq;
-using System.Numerics;
-using System.Threading;
using static Content.Shared.Interaction.SharedInteractionSystem;
using static Robust.Client.UserInterface.Controls.BoxContainer;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Item;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Examine
@@ -35,7 +35,6 @@ public sealed class ExamineSystem : ExamineSystemShared
private EntityUid _examinedEntity;
private EntityUid _lastExaminedEntity;
- private EntityUid _playerEntity;
private Popup? _examineTooltipOpen;
private ScreenCoordinates _popupPos;
private CancellationTokenSource? _requestCancelTokenSource;
@@ -74,9 +73,9 @@ private void OnExaminedItemDropped(EntityUid item, ItemComponent comp, DroppedEv
public override void Update(float frameTime)
{
if (_examineTooltipOpen is not {Visible: true}) return;
- if (!_examinedEntity.Valid || !_playerEntity.Valid) return;
+ if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return;
- if (!CanExamine(_playerEntity, _examinedEntity))
+ if (!CanExamine(player, _examinedEntity))
CloseTooltip();
}
@@ -114,9 +113,8 @@ private bool HandleExamine(in PointerInputCmdHandler.PointerInputCmdArgs args)
return false;
}
- _playerEntity = _playerManager.LocalEntity ?? default;
-
- if (_playerEntity == default || !CanExamine(_playerEntity, entity))
+ if (_playerManager.LocalEntity is not { } player ||
+ !CanExamine(player, entity))
{
return false;
}
diff --git a/Content.Client/Explosion/ExplosionSystem.cs b/Content.Client/Explosion/ExplosionSystem.cs
index a2ed2d50e0d1ef..692782ded4b77b 100644
--- a/Content.Client/Explosion/ExplosionSystem.cs
+++ b/Content.Client/Explosion/ExplosionSystem.cs
@@ -2,7 +2,4 @@
namespace Content.Client.Explosion.EntitySystems;
-public sealed class ExplosionSystem : SharedExplosionSystem
-{
-
-}
+public sealed class ExplosionSystem : SharedExplosionSystem;
diff --git a/Content.Client/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs
index 046be2aa62142c..8e41c382dd7e7c 100644
--- a/Content.Client/Flash/FlashOverlay.cs
+++ b/Content.Client/Flash/FlashOverlay.cs
@@ -16,6 +16,7 @@ public sealed class FlashOverlay : Overlay
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ private readonly SharedFlashSystem _flash;
private readonly StatusEffectsSystem _statusSys;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@@ -27,6 +28,7 @@ public FlashOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index("FlashedEffect").InstanceUnique();
+ _flash = _entityManager.System();
_statusSys = _entityManager.System();
}
@@ -41,7 +43,7 @@ protected override void FrameUpdate(FrameEventArgs args)
|| !_entityManager.TryGetComponent(playerEntity, out var status))
return;
- if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time, status))
+ if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status))
return;
var curTime = _timing.CurTime;
diff --git a/Content.Client/Guidebook/GuidebookDataSystem.cs b/Content.Client/Guidebook/GuidebookDataSystem.cs
new file mode 100644
index 00000000000000..f47ad6ef1bb2b5
--- /dev/null
+++ b/Content.Client/Guidebook/GuidebookDataSystem.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Guidebook;
+
+namespace Content.Client.Guidebook;
+
+///
+/// Client system for storing and retrieving values extracted from entity prototypes
+/// for display in the guidebook ().
+/// Requests data from the server on .
+/// Can also be pushed new data when the server reloads prototypes.
+///
+public sealed class GuidebookDataSystem : EntitySystem
+{
+ private GuidebookData? _data;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnServerUpdated);
+
+ // Request data from the server
+ RaiseNetworkEvent(new RequestGuidebookDataEvent());
+ }
+
+ private void OnServerUpdated(UpdateGuidebookDataEvent args)
+ {
+ // Got new data from the server, either in response to our request, or because prototypes reloaded on the server
+ _data = args.Data;
+ _data.Freeze();
+ }
+
+ ///
+ /// Attempts to retrieve a value using the given identifiers.
+ /// See for more information.
+ ///
+ public bool TryGetValue(string prototype, string component, string field, out object? value)
+ {
+ if (_data == null)
+ {
+ value = null;
+ return false;
+ }
+ return _data.TryGetValue(prototype, component, field, out value);
+ }
+}
diff --git a/Content.Client/Guidebook/Richtext/ProtodataTag.cs b/Content.Client/Guidebook/Richtext/ProtodataTag.cs
new file mode 100644
index 00000000000000..a725fd4e4b5994
--- /dev/null
+++ b/Content.Client/Guidebook/Richtext/ProtodataTag.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Guidebook.RichText;
+
+///
+/// RichText tag that can display values extracted from entity prototypes.
+/// In order to be accessed by this tag, the desired field/property must
+/// be tagged with .
+///
+public sealed class ProtodataTag : IMarkupTag
+{
+ [Dependency] private readonly ILogManager _logMan = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public string Name => "protodata";
+ private ISawmill Log => _log ??= _logMan.GetSawmill("protodata_tag");
+ private ISawmill? _log;
+
+ public string TextBefore(MarkupNode node)
+ {
+ // Do nothing with an empty tag
+ if (!node.Value.TryGetString(out var prototype))
+ return string.Empty;
+
+ if (!node.Attributes.TryGetValue("comp", out var component))
+ return string.Empty;
+ if (!node.Attributes.TryGetValue("member", out var member))
+ return string.Empty;
+ node.Attributes.TryGetValue("format", out var format);
+
+ var guidebookData = _entMan.System();
+
+ // Try to get the value
+ if (!guidebookData.TryGetValue(prototype, component.StringValue!, member.StringValue!, out var value))
+ {
+ Log.Error($"Failed to find protodata for {component}.{member} in {prototype}");
+ return "???";
+ }
+
+ // If we have a format string and a formattable value, format it as requested
+ if (!string.IsNullOrEmpty(format.StringValue) && value is IFormattable formattable)
+ return formattable.ToString(format.StringValue, CultureInfo.CurrentCulture);
+
+ // No format string given, so just use default ToString
+ return value?.ToString() ?? "NULL";
+ }
+}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index 97968c4b990b3d..19d00a0bbf8b1d 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -21,6 +21,7 @@
Orientation="Vertical">
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 9b96f5d3fe9284..d61267d002c6b5 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -73,6 +73,8 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
// Patient Information
SpriteView.SetEntity(target.Value);
+ SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
+ NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);
diff --git a/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs b/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs
index 6b6561234122de..b9b58f23220ace 100644
--- a/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs
+++ b/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs
@@ -26,6 +26,11 @@ protected override void Open()
_window = this.CreateWindow();
+ if (_entManager.TryGetComponent(Owner, out HandLabelerComponent? labeler))
+ {
+ _window.SetMaxLabelLength(labeler!.MaxLabelChars);
+ }
+
_window.OnLabelChanged += OnLabelChanged;
Reload();
}
diff --git a/Content.Client/Labels/UI/HandLabelerWindow.xaml.cs b/Content.Client/Labels/UI/HandLabelerWindow.xaml.cs
index 6482cdc1cc206f..7a0627b3e236e9 100644
--- a/Content.Client/Labels/UI/HandLabelerWindow.xaml.cs
+++ b/Content.Client/Labels/UI/HandLabelerWindow.xaml.cs
@@ -21,7 +21,7 @@ public HandLabelerWindow()
{
RobustXamlLoader.Load(this);
- LabelLineEdit.OnTextEntered += e =>
+ LabelLineEdit.OnTextChanged += e =>
{
_label = e.Text;
OnLabelChanged?.Invoke(_label);
@@ -33,6 +33,10 @@ public HandLabelerWindow()
_focused = false;
LabelLineEdit.Text = _label;
};
+
+ // Give the editor keybard focus, since that's the only
+ // thing the user will want to be doing with this UI
+ LabelLineEdit.GrabKeyboardFocus();
}
public void SetCurrentLabel(string label)
@@ -44,5 +48,10 @@ public void SetCurrentLabel(string label)
if (!_focused)
LabelLineEdit.Text = label;
}
+
+ public void SetMaxLabelLength(int maxLength)
+ {
+ LabelLineEdit.IsValid = s => s.Length <= maxLength;
+ }
}
}
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml b/Content.Client/Lobby/UI/LobbyGui.xaml
index c3bd0da642a634..4a158fd811f15e 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml
@@ -14,7 +14,7 @@
Stretch="KeepAspectCovered" />
-
+
diff --git a/Content.Client/Mapping/MappingScreen.xaml b/Content.Client/Mapping/MappingScreen.xaml
index b64136084788ac..9cc3e734f0e9ee 100644
--- a/Content.Client/Mapping/MappingScreen.xaml
+++ b/Content.Client/Mapping/MappingScreen.xaml
@@ -78,6 +78,7 @@
ToolTip="Pick (Hold 5)" />
+
diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs
index b2ad2fd83fbb8d..46c0e51fad69b4 100644
--- a/Content.Client/Mapping/MappingScreen.xaml.cs
+++ b/Content.Client/Mapping/MappingScreen.xaml.cs
@@ -96,6 +96,22 @@ public MappingScreen()
Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png";
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
+ Flip.OnPressed += args => FlipSides();
+ }
+
+ public void FlipSides()
+ {
+ ScreenContainer.Flip();
+
+ if (SpawnContainer.GetPositionInParent() == 0)
+ {
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
+ }
+ else
+ {
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
+ }
}
private void OnDecalColorPicked(Color color)
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
index 2b600845cae436..f4fb9da06220a7 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
@@ -47,8 +47,10 @@
+
+ StyleClasses="OpenBoth" Text="{Loc news-write-ui-preview-text}"/>
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 5e068f1e9c5346..90a66bec7f3079 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -14,6 +14,7 @@ namespace Content.Client.MassMedia.Ui;
public sealed partial class ArticleEditorPanel : Control
{
public event Action? PublishButtonPressed;
+ public event Action? ArticleDraftUpdated;
private bool _preview;
@@ -45,6 +46,7 @@ public ArticleEditorPanel()
ButtonPreview.OnPressed += OnPreview;
ButtonCancel.OnPressed += OnCancel;
ButtonPublish.OnPressed += OnPublish;
+ ButtonSaveDraft.OnPressed += OnDraftSaved;
TitleField.OnTextChanged += args => OnTextChanged(args.Text.Length, args.Control, SharedNewsSystem.MaxTitleLength);
ContentField.OnTextChanged += args => OnTextChanged(Rope.CalcTotalLength(args.TextRope), args.Control, SharedNewsSystem.MaxContentLength);
@@ -68,6 +70,9 @@ private void OnTextChanged(long length, Control control, long maxLength)
ButtonPublish.Disabled = false;
ButtonPreview.Disabled = false;
}
+
+ // save draft regardless; they can edit down the length later
+ ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
}
private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
@@ -92,6 +97,12 @@ private void OnPublish(BaseButton.ButtonEventArgs eventArgs)
Visible = false;
}
+ private void OnDraftSaved(BaseButton.ButtonEventArgs eventArgs)
+ {
+ ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
+ Visible = false;
+ }
+
private void Reset()
{
_preview = false;
@@ -100,6 +111,7 @@ private void Reset()
PreviewLabel.SetMarkup("");
TitleField.Text = "";
ContentField.TextRope = Rope.Leaf.Empty;
+ ArticleDraftUpdated?.Invoke(string.Empty, string.Empty);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
index 22e5bc452a0d45..4f21361990a951 100644
--- a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
+++ b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
@@ -25,6 +25,9 @@ protected override void Open()
_menu.ArticleEditorPanel.PublishButtonPressed += OnPublishButtonPressed;
_menu.DeleteButtonPressed += OnDeleteButtonPressed;
+ _menu.CreateButtonPressed += OnCreateButtonPressed;
+ _menu.ArticleEditorPanel.ArticleDraftUpdated += OnArticleDraftUpdated;
+
SendMessage(new NewsWriterArticlesRequestMessage());
}
@@ -34,7 +37,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
if (state is not NewsWriterBoundUserInterfaceState cast)
return;
- _menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish);
+ _menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish, cast.DraftTitle, cast.DraftContent);
}
private void OnPublishButtonPressed()
@@ -67,4 +70,14 @@ private void OnDeleteButtonPressed(int articleNum)
SendMessage(new NewsWriterDeleteMessage(articleNum));
}
+
+ private void OnCreateButtonPressed()
+ {
+ SendMessage(new NewsWriterRequestDraftMessage());
+ }
+
+ private void OnArticleDraftUpdated(string title, string content)
+ {
+ SendMessage(new NewsWriterSaveDraftMessage(title, content));
+ }
}
diff --git a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
index c059ce785af6c3..af1f9a94414cd1 100644
--- a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
+++ b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
@@ -4,6 +4,7 @@
using Content.Shared.MassMedia.Systems;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.MassMedia.Ui;
@@ -16,6 +17,8 @@ public sealed partial class NewsWriterMenu : FancyWindow
public event Action? DeleteButtonPressed;
+ public event Action? CreateButtonPressed;
+
public NewsWriterMenu()
{
RobustXamlLoader.Load(this);
@@ -31,7 +34,7 @@ public NewsWriterMenu()
ButtonCreate.OnPressed += OnCreate;
}
- public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish)
+ public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish, string draftTitle, string draftContent)
{
ArticlesContainer.Children.Clear();
ArticleCount.Text = Loc.GetString("news-write-ui-article-count-text", ("count", articles.Length));
@@ -54,6 +57,9 @@ public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextP
ButtonCreate.Disabled = !publishEnabled;
_nextPublish = nextPublish;
+
+ ArticleEditorPanel.TitleField.Text = draftTitle;
+ ArticleEditorPanel.ContentField.TextRope = new Rope.Leaf(draftContent);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -93,5 +99,6 @@ protected override void Dispose(bool disposing)
private void OnCreate(BaseButton.ButtonEventArgs buttonEventArgs)
{
ArticleEditorPanel.Visible = true;
+ CreateButtonPressed?.Invoke();
}
}
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
index 660f2e5e11f5ed..dd40749d33b2bd 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
@@ -15,6 +15,9 @@
+
+
departmentSens
// Populate departments
foreach (var sensor in departmentSensors)
{
+ if (!string.IsNullOrEmpty(SearchLineEdit.Text)
+ && !sensor.Name.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase)
+ && !sensor.Job.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// Add a button that will hold a username and other details
diff --git a/Content.Client/Mining/MiningOverlay.cs b/Content.Client/Mining/MiningOverlay.cs
new file mode 100644
index 00000000000000..b23835b36ee91c
--- /dev/null
+++ b/Content.Client/Mining/MiningOverlay.cs
@@ -0,0 +1,96 @@
+using System.Numerics;
+using Content.Shared.Mining.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Mining;
+
+public sealed class MiningOverlay : Overlay
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ private readonly EntityLookupSystem _lookup;
+ private readonly SpriteSystem _sprite;
+ private readonly TransformSystem _xform;
+
+ private readonly EntityQuery _spriteQuery;
+ private readonly EntityQuery _xformQuery;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ public override bool RequestScreenTexture => false;
+
+ private readonly HashSet> _viewableEnts = new();
+
+ public MiningOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _lookup = _entityManager.System();
+ _sprite = _entityManager.System();
+ _xform = _entityManager.System();
+
+ _spriteQuery = _entityManager.GetEntityQuery();
+ _xformQuery = _entityManager.GetEntityQuery();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var handle = args.WorldHandle;
+
+ if (_player.LocalEntity is not { } localEntity ||
+ !_entityManager.TryGetComponent(localEntity, out var viewerComp))
+ return;
+
+ if (viewerComp.LastPingLocation == null)
+ return;
+
+ var scaleMatrix = Matrix3Helpers.CreateScale(Vector2.One);
+
+ _viewableEnts.Clear();
+ _lookup.GetEntitiesInRange(viewerComp.LastPingLocation.Value, viewerComp.ViewRange, _viewableEnts);
+ foreach (var ore in _viewableEnts)
+ {
+ if (!_xformQuery.TryComp(ore, out var xform) ||
+ !_spriteQuery.TryComp(ore, out var sprite))
+ continue;
+
+ if (xform.MapID != args.MapId || !sprite.Visible)
+ continue;
+
+ if (!sprite.LayerMapTryGet(MiningScannerVisualLayers.Overlay, out var idx))
+ continue;
+ var layer = sprite[idx];
+
+ if (layer.ActualRsi?.Path == null || layer.RsiState.Name == null)
+ continue;
+
+ var gridRot = xform.GridUid == null ? 0 : _xformQuery.CompOrNull(xform.GridUid.Value)?.LocalRotation ?? 0;
+ var rotationMatrix = Matrix3Helpers.CreateRotation(gridRot);
+
+ var worldMatrix = Matrix3Helpers.CreateTranslation(_xform.GetWorldPosition(xform));
+ var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
+ var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
+ handle.SetTransform(matty);
+
+ var spriteSpec = new SpriteSpecifier.Rsi(layer.ActualRsi.Path, layer.RsiState.Name);
+ var texture = _sprite.GetFrame(spriteSpec, TimeSpan.FromSeconds(layer.AnimationTime));
+
+ var animTime = (viewerComp.NextPingTime - _timing.CurTime).TotalSeconds;
+
+
+ var alpha = animTime < viewerComp.AnimationDuration
+ ? 0
+ : (float) Math.Clamp((animTime - viewerComp.AnimationDuration) / viewerComp.AnimationDuration, 0f, 1f);
+ var color = Color.White.WithAlpha(alpha);
+
+ handle.DrawTexture(texture, -(Vector2) texture.Size / 2f / EyeManager.PixelsPerMeter, layer.Rotation, modulate: color);
+
+ }
+ handle.SetTransform(Matrix3x2.Identity);
+ }
+}
diff --git a/Content.Client/Mining/MiningOverlaySystem.cs b/Content.Client/Mining/MiningOverlaySystem.cs
new file mode 100644
index 00000000000000..294cab30ca8827
--- /dev/null
+++ b/Content.Client/Mining/MiningOverlaySystem.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Mining.Components;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client.Mining;
+
+///
+/// This handles the lifetime of the for a given entity.
+///
+public sealed class MiningOverlaySystem : EntitySystem
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+ private MiningOverlay _overlay = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ _overlay = new();
+ }
+
+ private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnInit(Entity ent, ref ComponentInit args)
+ {
+ if (_player.LocalEntity == ent)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ }
+ }
+
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if (_player.LocalEntity == ent)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+ }
+}
diff --git a/Content.Client/Mining/OreVeinVisualsComponent.cs b/Content.Client/Mining/OreVeinVisualsComponent.cs
deleted file mode 100644
index c662111c3ed075..00000000000000
--- a/Content.Client/Mining/OreVeinVisualsComponent.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Content.Client.Mining;
-
-public sealed class OreVeinVisualsComponent
-{
-
-}
diff --git a/Content.Client/Nuke/NukeMenu.xaml.cs b/Content.Client/Nuke/NukeMenu.xaml.cs
index b498d0e3bbcb4b..aa757584733046 100644
--- a/Content.Client/Nuke/NukeMenu.xaml.cs
+++ b/Content.Client/Nuke/NukeMenu.xaml.cs
@@ -107,7 +107,7 @@ public void UpdateState(NukeUiState state)
FirstStatusLabel.Text = firstMsg;
SecondStatusLabel.Text = secondMsg;
- EjectButton.Disabled = !state.DiskInserted || state.Status == NukeStatus.ARMED;
+ EjectButton.Disabled = !state.DiskInserted || state.Status == NukeStatus.ARMED || !state.IsAnchored;
AnchorButton.Disabled = state.Status == NukeStatus.ARMED;
AnchorButton.Pressed = state.IsAnchored;
ArmButton.Disabled = !state.AllowArm || !state.IsAnchored;
diff --git a/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs b/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs
index e571c5a856c866..c708c6fe7d2ef6 100644
--- a/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs
+++ b/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs
@@ -1,7 +1,6 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Client.GameObjects;
-using Robust.Shared.Utility;
namespace Content.Client.Nutrition.EntitySystems;
@@ -50,6 +49,7 @@ private void UpdateFoodVisuals(Entity start, Sp
sprite.AddBlankLayer(index);
sprite.LayerMapSet(keyCode, index);
sprite.LayerSetSprite(index, state.Sprite);
+ sprite.LayerSetScale(index, state.Scale);
//Offset the layer
var layerPos = start.Comp.StartPosition;
diff --git a/Content.Client/PDA/PdaBoundUserInterface.cs b/Content.Client/PDA/PdaBoundUserInterface.cs
index 37ce9c4280f2a0..2d4033390c3aaa 100644
--- a/Content.Client/PDA/PdaBoundUserInterface.cs
+++ b/Content.Client/PDA/PdaBoundUserInterface.cs
@@ -4,18 +4,20 @@
using Content.Shared.PDA;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
-using Robust.Shared.Configuration;
namespace Content.Client.PDA
{
[UsedImplicitly]
public sealed class PdaBoundUserInterface : CartridgeLoaderBoundUserInterface
{
+ private readonly PdaSystem _pdaSystem;
+
[ViewVariables]
private PdaMenu? _menu;
public PdaBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
+ _pdaSystem = EntMan.System();
}
protected override void Open()
@@ -92,7 +94,13 @@ protected override void UpdateState(BoundUserInterfaceState state)
if (state is not PdaUpdateState updateState)
return;
- _menu?.UpdateState(updateState);
+ if (_menu == null)
+ {
+ _pdaSystem.Log.Error("PDA state received before menu was created.");
+ return;
+ }
+
+ _menu.UpdateState(updateState);
}
protected override void AttachCartridgeUI(Control cartridgeUIFragment, string? title)
diff --git a/Content.Client/PDA/PdaMenu.xaml b/Content.Client/PDA/PdaMenu.xaml
index 8b26860332dd8e..8c9b4ae2ee6b4d 100644
--- a/Content.Client/PDA/PdaMenu.xaml
+++ b/Content.Client/PDA/PdaMenu.xaml
@@ -67,14 +67,17 @@
Description="{Loc 'comp-pda-ui-ringtone-button-description'}"/>
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 03df383eebce1b..c97110b208e58f 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -8,132 +8,131 @@
using Robust.Shared.Player;
using Robust.Shared.Timing;
-namespace Content.Client.Physics.Controllers
+namespace Content.Client.Physics.Controllers;
+
+public sealed class MoverController : SharedMoverController
{
- public sealed class MoverController : SharedMoverController
- {
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnRelayPlayerAttached);
- SubscribeLocalEvent(OnRelayPlayerDetached);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
-
- SubscribeLocalEvent(OnUpdatePredicted);
- SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
- SubscribeLocalEvent(OnUpdatePullablePredicted);
- }
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRelayPlayerAttached);
+ SubscribeLocalEvent(OnRelayPlayerDetached);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ SubscribeLocalEvent(OnUpdatePredicted);
+ SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
+ SubscribeLocalEvent(OnUpdatePullablePredicted);
+ }
- private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- // Enable prediction if an entity is controlled by the player
- if (entity.Owner == _playerManager.LocalEntity)
- args.IsPredicted = true;
- }
+ private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ // Enable prediction if an entity is controlled by the player
+ if (entity.Owner == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ }
- private void OnUpdateRelayTargetPredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- if (entity.Comp.Source == _playerManager.LocalEntity)
- args.IsPredicted = true;
- }
+ private void OnUpdateRelayTargetPredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ if (entity.Comp.Source == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ }
- private void OnUpdatePullablePredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- // Enable prediction if an entity is being pulled by the player.
- // Disable prediction if an entity is being pulled by some non-player entity.
+ private void OnUpdatePullablePredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ // Enable prediction if an entity is being pulled by the player.
+ // Disable prediction if an entity is being pulled by some non-player entity.
- if (entity.Comp.Puller == _playerManager.LocalEntity)
- args.IsPredicted = true;
- else if (entity.Comp.Puller != null)
- args.BlockPrediction = true;
+ if (entity.Comp.Puller == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ else if (entity.Comp.Puller != null)
+ args.BlockPrediction = true;
- // TODO recursive pulling checks?
- // What if the entity is being pulled by a vehicle controlled by the player?
- }
+ // TODO recursive pulling checks?
+ // What if the entity is being pulled by a vehicle controlled by the player?
+ }
- private void OnRelayPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
- {
- Physics.UpdateIsPredicted(entity.Owner);
- Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnRelayPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
+ {
+ Physics.UpdateIsPredicted(entity.Owner);
+ Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- private void OnRelayPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
- {
- Physics.UpdateIsPredicted(entity.Owner);
- Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnRelayPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
+ {
+ Physics.UpdateIsPredicted(entity.Owner);
+ Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- private void OnPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ private void OnPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- private void OnPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ private void OnPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
- {
- base.UpdateBeforeSolve(prediction, frameTime);
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
- if (_playerManager.LocalEntity is not {Valid: true} player)
- return;
+ if (_playerManager.LocalEntity is not {Valid: true} player)
+ return;
- if (RelayQuery.TryGetComponent(player, out var relayMover))
- HandleClientsideMovement(relayMover.RelayEntity, frameTime);
+ if (RelayQuery.TryGetComponent(player, out var relayMover))
+ HandleClientsideMovement(relayMover.RelayEntity, frameTime);
- HandleClientsideMovement(player, frameTime);
- }
+ HandleClientsideMovement(player, frameTime);
+ }
- private void HandleClientsideMovement(EntityUid player, float frameTime)
+ private void HandleClientsideMovement(EntityUid player, float frameTime)
+ {
+ if (!MoverQuery.TryGetComponent(player, out var mover) ||
+ !XformQuery.TryGetComponent(player, out var xform))
{
- if (!MoverQuery.TryGetComponent(player, out var mover) ||
- !XformQuery.TryGetComponent(player, out var xform))
- {
- return;
- }
-
- var physicsUid = player;
- PhysicsComponent? body;
- var xformMover = xform;
+ return;
+ }
- if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
- {
- if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
- !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
- {
- return;
- }
+ var physicsUid = player;
+ PhysicsComponent? body;
+ var xformMover = xform;
- physicsUid = xform.ParentUid;
- }
- else if (!PhysicsQuery.TryGetComponent(player, out body))
+ if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
+ {
+ if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
+ !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
{
return;
}
- // Server-side should just be handled on its own so we'll just do this shizznit
- HandleMobMovement(
- player,
- mover,
- physicsUid,
- body,
- xformMover,
- frameTime);
+ physicsUid = xform.ParentUid;
}
-
- protected override bool CanSound()
+ else if (!PhysicsQuery.TryGetComponent(player, out body))
{
- return _timing is { IsFirstTimePredicted: true, InSimulation: true };
+ return;
}
+
+ // Server-side should just be handled on its own so we'll just do this shizznit
+ HandleMobMovement(
+ player,
+ mover,
+ physicsUid,
+ body,
+ xformMover,
+ frameTime);
+ }
+
+ protected override bool CanSound()
+ {
+ return _timing is { IsFirstTimePredicted: true, InSimulation: true };
}
}
diff --git a/Content.Client/Physics/JointVisualsOverlay.cs b/Content.Client/Physics/JointVisualsOverlay.cs
index e0b3499a974fcc..9cc2831d2128ac 100644
--- a/Content.Client/Physics/JointVisualsOverlay.cs
+++ b/Content.Client/Physics/JointVisualsOverlay.cs
@@ -1,9 +1,8 @@
+using System.Numerics;
using Content.Shared.Physics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Dynamics.Joints;
namespace Content.Client.Physics;
@@ -16,8 +15,6 @@ public sealed class JointVisualsOverlay : Overlay
private IEntityManager _entManager;
- private HashSet _drawn = new();
-
public JointVisualsOverlay(IEntityManager entManager)
{
_entManager = entManager;
@@ -25,7 +22,6 @@ public JointVisualsOverlay(IEntityManager entManager)
protected override void Draw(in OverlayDrawArgs args)
{
- _drawn.Clear();
var worldHandle = args.WorldHandle;
var spriteSystem = _entManager.System();
@@ -33,12 +29,14 @@ protected override void Draw(in OverlayDrawArgs args)
var joints = _entManager.EntityQueryEnumerator();
var xformQuery = _entManager.GetEntityQuery();
+ args.DrawingHandle.SetTransform(Matrix3x2.Identity);
+
while (joints.MoveNext(out var visuals, out var xform))
{
if (xform.MapID != args.MapId)
continue;
- var other = visuals.Target;
+ var other = _entManager.GetEntity(visuals.Target);
if (!xformQuery.TryGetComponent(other, out var otherXform))
continue;
diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
new file mode 100644
index 00000000000000..e1c55131cd6b25
--- /dev/null
+++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
new file mode 100644
index 00000000000000..a4d4055c7dfe1b
--- /dev/null
+++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
@@ -0,0 +1,50 @@
+using Content.Shared.Pinpointer;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+
+namespace Content.Client.Pinpointer.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationMapBeaconControl : Control, IComparable
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public readonly EntityCoordinates BeaconPosition;
+ public Action? OnPressed;
+ public string? Label => BeaconNameLabel.Text;
+ private StyleBoxFlat _styleBox;
+ public Color Color => _styleBox.BackgroundColor;
+
+ public StationMapBeaconControl(EntityUid mapUid, SharedNavMapSystem.NavMapBeacon beacon)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ BeaconPosition = new EntityCoordinates(mapUid, beacon.Position);
+
+ _styleBox = new StyleBoxFlat { BackgroundColor = beacon.Color };
+ ColorPanel.PanelOverride = _styleBox;
+ BeaconNameLabel.Text = beacon.Text;
+
+ MainButton.OnPressed += args => OnPressed?.Invoke(BeaconPosition);
+ }
+
+ public int CompareTo(StationMapBeaconControl? other)
+ {
+ if (other == null)
+ return 1;
+
+ // Group by color
+ var colorCompare = Color.ToArgb().CompareTo(other.Color.ToArgb());
+ if (colorCompare != 0)
+ {
+ return colorCompare;
+ }
+
+ // If same color, sort by text
+ return string.Compare(Label, other.Label);
+ }
+}
diff --git a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
index 91fb4ef71bd6c9..3d1eb1723c31ec 100644
--- a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
+++ b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
@@ -24,9 +24,16 @@ protected override void Open()
_window = this.CreateWindow();
_window.Title = EntMan.GetComponent(Owner).EntityName;
+
+ string stationName = string.Empty;
+ if(EntMan.TryGetComponent(gridUid, out var gridMetaData))
+ {
+ stationName = gridMetaData.EntityName;
+ }
+
if (EntMan.TryGetComponent(Owner, out var comp) && comp.ShowLocation)
- _window.Set(gridUid, Owner);
+ _window.Set(stationName, gridUid, Owner);
else
- _window.Set(gridUid, null);
+ _window.Set(stationName, gridUid, null);
}
}
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
index 00424a3566aa42..c79fc8f9e7b8d0 100644
--- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml
+++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
@@ -3,11 +3,28 @@
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
Title="{Loc 'station-map-window-title'}"
Resizable="False"
- SetSize="668 713"
- MinSize="668 713">
+ SetSize="868 748"
+ MinSize="868 748">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
index 7cbb8b7d0dbc22..52ef2ab7da4c06 100644
--- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
+++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
@@ -3,24 +3,75 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
+using Content.Shared.Pinpointer;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapWindow : FancyWindow
{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ private readonly List _buttons = new();
+
public StationMapWindow()
{
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ FilterBar.OnTextChanged += (bar) => OnFilterChanged(bar.Text);
}
- public void Set(EntityUid? mapUid, EntityUid? trackedEntity)
+ public void Set(string stationName, EntityUid? mapUid, EntityUid? trackedEntity)
{
NavMapScreen.MapUid = mapUid;
if (trackedEntity != null)
NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Cyan));
+ if (!string.IsNullOrEmpty(stationName))
+ {
+ StationName.Text = stationName;
+ }
+
NavMapScreen.ForceNavMapUpdate();
+ UpdateBeaconList(mapUid);
+ }
+
+ public void OnFilterChanged(string newFilter)
+ {
+ foreach (var button in _buttons)
+ {
+ button.Visible = string.IsNullOrEmpty(newFilter) || (
+ !string.IsNullOrEmpty(button.Label) &&
+ button.Label.Contains(newFilter, StringComparison.OrdinalIgnoreCase)
+ );
+ };
+ }
+
+ public void UpdateBeaconList(EntityUid? mapUid)
+ {
+ BeaconButtons.Children.Clear();
+ _buttons.Clear();
+
+ if (!mapUid.HasValue)
+ return;
+
+ if (!_entMan.TryGetComponent(mapUid, out var navMap))
+ return;
+
+ foreach (var beacon in navMap.Beacons.Values)
+ {
+ var button = new StationMapBeaconControl(mapUid.Value, beacon);
+
+ button.OnPressed += NavMapScreen.CenterToCoordinates;
+
+ _buttons.Add(button);
+ }
+
+ _buttons.Sort();
+
+ foreach (var button in _buttons)
+ BeaconButtons.AddChild(button);
}
-}
+}
\ No newline at end of file
diff --git a/Content.Client/Power/APC/ApcBoundUserInterface.cs b/Content.Client/Power/APC/ApcBoundUserInterface.cs
index 759a5949ba625d..a790c5d984ad13 100644
--- a/Content.Client/Power/APC/ApcBoundUserInterface.cs
+++ b/Content.Client/Power/APC/ApcBoundUserInterface.cs
@@ -19,8 +19,8 @@ public ApcBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();
-
_menu = this.CreateWindow();
+ _menu.SetEntity(Owner);
_menu.OnBreaker += BreakerPressed;
}
diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
index 5a082485a5ac5a..a6a20958f536a8 100644
--- a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
+++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
@@ -18,9 +18,6 @@ protected override void OnActivate(Entity e
return;
}
- if (TryComp(ent.Owner, out var panel) && panel.Open)
- return;
-
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
diff --git a/Content.Client/Replay/ContentReplayPlaybackManager.cs b/Content.Client/Replay/ContentReplayPlaybackManager.cs
index f90731bfa75376..b96eae44e9d5ae 100644
--- a/Content.Client/Replay/ContentReplayPlaybackManager.cs
+++ b/Content.Client/Replay/ContentReplayPlaybackManager.cs
@@ -1,10 +1,8 @@
-using System.IO.Compression;
using Content.Client.Administration.Managers;
using Content.Client.Launcher;
using Content.Client.MainMenu;
using Content.Client.Replay.Spectator;
using Content.Client.Replay.UI.Loading;
-using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Chat;
using Content.Shared.Chat;
using Content.Shared.Effects;
@@ -26,8 +24,6 @@
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -60,7 +56,7 @@ public sealed class ContentReplayPlaybackManager
public bool IsScreenshotMode = false;
private bool _initialized;
-
+
///
/// Most recently loaded file, for re-attempting the load with error tolerance.
/// Required because the zip reader auto-disposes and I'm too lazy to change it so that
@@ -96,32 +92,17 @@ private void OnFinishedLoading(Exception? exception)
return;
}
- ReturnToDefaultState();
-
- // Show a popup window with the error message
- var text = Loc.GetString("replay-loading-failed", ("reason", exception));
- var box = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- Children = {new Label {Text = text}}
- };
+ if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
+ _client.StopSinglePlayer();
- var popup = new DefaultWindow { Title = "Error!" };
- popup.Contents.AddChild(box);
+ Action? retryAction = null;
+ Action? cancelAction = null;
- // Add button for attempting to re-load the replay while ignoring some errors.
- if (!_cfg.GetCVar(CVars.ReplayIgnoreErrors) && LastLoad is {} last)
+ if (!_cfg.GetCVar(CVars.ReplayIgnoreErrors) && LastLoad is { } last)
{
- var button = new Button
- {
- Text = Loc.GetString("replay-loading-retry"),
- StyleClasses = { StyleBase.ButtonCaution }
- };
-
- button.OnPressed += _ =>
+ retryAction = () =>
{
_cfg.SetCVar(CVars.ReplayIgnoreErrors, true);
- popup.Dispose();
IReplayFileReader reader = last.Zip == null
? new ReplayFileReaderResources(_resMan, last.Folder)
@@ -129,11 +110,20 @@ private void OnFinishedLoading(Exception? exception)
_loadMan.LoadAndStartReplay(reader);
};
-
- box.AddChild(button);
}
- popup.OpenCentered();
+ // If we have an explicit menu to get back to (e.g. replay browser UI), show a cancel button.
+ if (DefaultState != null)
+ {
+ cancelAction = () =>
+ {
+ _stateMan.RequestStateChange(DefaultState);
+ };
+ }
+
+ // Switch to a new game state to present the error and cancel/retry options.
+ var state = _stateMan.RequestStateChange();
+ state.SetData(exception, cancelAction, retryAction);
}
public void ReturnToDefaultState()
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs b/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
new file mode 100644
index 00000000000000..223895eb29cba3
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
@@ -0,0 +1,36 @@
+using Content.Client.Stylesheets;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.UI.Loading;
+
+///
+/// State used to display an error message if a replay failed to load.
+///
+///
+///
+public sealed class ReplayLoadingFailed : State
+{
+ [Dependency] private readonly IStylesheetManager _stylesheetManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _userInterface = default!;
+
+ private ReplayLoadingFailedControl? _control;
+
+ public void SetData(Exception exception, Action? cancelPressed, Action? retryPressed)
+ {
+ DebugTools.Assert(_control != null);
+ _control.SetData(exception, cancelPressed, retryPressed);
+ }
+
+ protected override void Startup()
+ {
+ _control = new ReplayLoadingFailedControl(_stylesheetManager);
+ _userInterface.StateRoot.AddChild(_control);
+ }
+
+ protected override void Shutdown()
+ {
+ _control?.Orphan();
+ }
+}
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml
new file mode 100644
index 00000000000000..5f77a66e535e69
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs
new file mode 100644
index 00000000000000..088c9a291a7faf
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs
@@ -0,0 +1,44 @@
+using Content.Client.Stylesheets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.UI.Loading;
+
+[GenerateTypedNameReferences]
+public sealed partial class ReplayLoadingFailedControl : Control
+{
+ public ReplayLoadingFailedControl(IStylesheetManager stylesheet)
+ {
+ RobustXamlLoader.Load(this);
+
+ Stylesheet = stylesheet.SheetSpace;
+ LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
+ }
+
+ public void SetData(Exception exception, Action? cancelPressed, Action? retryPressed)
+ {
+ ReasonLabel.SetMessage(
+ FormattedMessage.FromUnformatted(Loc.GetString("replay-loading-failed", ("reason", exception))));
+
+ if (cancelPressed != null)
+ {
+ CancelButton.Visible = true;
+ CancelButton.OnPressed += _ =>
+ {
+ cancelPressed();
+ };
+ }
+
+ if (retryPressed != null)
+ {
+ RetryButton.Visible = true;
+ RetryButton.OnPressed += _ =>
+ {
+ retryPressed();
+ };
+ }
+ }
+}
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 64ead32586d27e..2674343e059144 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -199,7 +199,9 @@ protected override void Draw(DrawingHandleScreen handle)
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
- var color = _shuttles.GetIFFColor(grid, self: false, iff);
+
+ var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
+ var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
// Others default:
// Color.FromHex("#FFC000FF")
@@ -213,25 +215,52 @@ protected override void Draw(DrawingHandleScreen handle)
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
+
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
+ var mapCoords = _transform.GetWorldPosition(gUid);
+ var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
+
// yes 1.0 scale is intended here.
var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
+ var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f);
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
- // The actual position in the UI. We offset the matrix position to render it off by half its width
- // plus by the offset.
- var uiPosition = ScalePosition(gridCentre)- new Vector2(labelDimensions.X / 2f, -yOffset);
+ // The actual position in the UI. We centre the label by offsetting the matrix position
+ // by half the label's width, plus the y-offset
+ var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
- // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
- uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, PixelWidth - labelDimensions.X ),
- Math.Clamp(uiPosition.Y, 0f, PixelHeight - labelDimensions.Y));
+ // Normalize the grid position if it exceeds the viewport bounds
+ // normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
+ var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f);
+ var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f;
+ if (offsetMax > 1)
+ {
+ gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax);
+
+ gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize;
+ }
- handle.DrawString(Font, uiPosition, labelText, color);
+ var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0);
+ var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y);
+
+ // clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport
+ // coord label intentionally isn't clamped so we don't get ugly clutter at the edges
+ var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y);
+ labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents);
+
+ // draw IFF label
+ handle.DrawString(Font, labelUiPosition, labelText, labelColor);
+
+ // only draw coords label if close enough
+ if (offsetMax < 1)
+ {
+ handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor);
+ }
}
// Detailed view
@@ -241,7 +270,7 @@ protected override void Draw(DrawingHandleScreen handle)
if (!gridAABB.Intersects(viewAABB))
continue;
- DrawGrid(handle, matty, grid, color);
+ DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
}
}
diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
index 7ed67f7b5dd9fd..8c48258de00b3d 100644
--- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs
+++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
@@ -19,7 +19,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
private string _search = string.Empty;
[ViewVariables]
- private HashSet _listings = new();
+ private HashSet _listings = new();
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
@@ -33,7 +33,7 @@ protected override void Open()
_menu.OnListingButtonPressed += (_, listing) =>
{
- SendMessage(new StoreBuyListingMessage(listing));
+ SendMessage(new StoreBuyListingMessage(listing.ID));
};
_menu.OnCategoryButtonPressed += (_, category) =>
@@ -68,6 +68,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
_listings = msg.Listings;
_menu?.UpdateBalance(msg.Balance);
+
UpdateListingsWithSearchFilter();
_menu?.SetFooterVisibility(msg.ShowFooter);
_menu?.UpdateRefund(msg.AllowRefund);
@@ -80,7 +81,7 @@ private void UpdateListingsWithSearchFilter()
if (_menu == null)
return;
- var filteredListings = new HashSet(_listings);
+ var filteredListings = new HashSet(_listings);
if (!string.IsNullOrEmpty(_search))
{
filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) &&
diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml b/Content.Client/Store/Ui/StoreListingControl.xaml
index 12b4d7b5b3002c..3142f1cb06115a 100644
--- a/Content.Client/Store/Ui/StoreListingControl.xaml
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml
@@ -2,6 +2,8 @@
+