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/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml
index bd5879408ef536..fd5a7bb1617b20 100644
--- a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml
+++ b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml
@@ -26,7 +26,7 @@
Text="{Loc 'news-read-ui-next-text'}"
ToolTip="{Loc 'news-read-ui-next-tooltip'}"/>
-
+
diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs
index 8fbeaab5f40d14..ca86f8a6b2e4cb 100644
--- a/Content.Client/Changelog/ChangelogTab.xaml.cs
+++ b/Content.Client/Changelog/ChangelogTab.xaml.cs
@@ -131,13 +131,13 @@ public void PopulateChangelog(ChangelogManager.Changelog changelog)
Margin = new Thickness(6, 0, 0, 0),
};
authorLabel.SetMessage(
- FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author))));
+ FormattedMessage.FromMarkupOrThrow(Loc.GetString("changelog-author-changed", ("author", author))));
ChangelogBody.AddChild(authorLabel);
foreach (var change in groupedEntry.SelectMany(c => c.Changes))
{
var text = new RichTextLabel();
- text.SetMessage(FormattedMessage.FromMarkup(change.Message));
+ text.SetMessage(FormattedMessage.FromMarkupOrThrow(change.Message));
ChangelogBody.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
index 3340755343890b..f3b7837f21a59c 100644
--- a/Content.Client/Chat/UI/EmotesMenu.xaml.cs
+++ b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
@@ -19,9 +19,6 @@ public sealed partial class EmotesMenu : RadialMenu
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
- private readonly SpriteSystem _spriteSystem;
- private readonly EntityWhitelistSystem _whitelistSystem;
-
public event Action>? OnPlayEmote;
public EmotesMenu()
@@ -29,8 +26,8 @@ public EmotesMenu()
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
- _spriteSystem = _entManager.System();
- _whitelistSystem = _entManager.System();
+ var spriteSystem = _entManager.System();
+ var whitelistSystem = _entManager.System();
var main = FindControl("Main");
@@ -40,8 +37,8 @@ public EmotesMenu()
var player = _playerManager.LocalSession?.AttachedEntity;
if (emote.Category == EmoteCategory.Invalid ||
emote.ChatTriggers.Count == 0 ||
- !(player.HasValue && _whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
- _whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
+ !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
+ whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
continue;
if (!emote.Available &&
@@ -63,7 +60,7 @@ public EmotesMenu()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
- Texture = _spriteSystem.Frame0(emote.Icon),
+ Texture = spriteSystem.Frame0(emote.Icon),
TextureScale = new Vector2(2f, 2f),
};
diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs
index adb61d10e62712..32e9f4ae9be530 100644
--- a/Content.Client/Chat/UI/SpeechBubble.cs
+++ b/Content.Client/Chat/UI/SpeechBubble.cs
@@ -180,7 +180,7 @@ protected FormattedMessage FormatSpeech(string message, Color? fontColor = null)
var msg = new FormattedMessage();
if (fontColor != null)
msg.PushColor(fontColor.Value);
- msg.AddMarkup(message);
+ msg.AddMarkupOrThrow(message);
return msg;
}
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/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index 96bbcc54f2a249..27d77eda496f3e 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -50,7 +50,6 @@ public sealed class ClientClothingSystem : ClothingSystem
};
[Dependency] private readonly IResourceCache _cache = default!;
- [Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
diff --git a/Content.Client/Commands/ActionsCommands.cs b/Content.Client/Commands/ActionsCommands.cs
index 593b8e8256977e..c155c7a9deac17 100644
--- a/Content.Client/Commands/ActionsCommands.cs
+++ b/Content.Client/Commands/ActionsCommands.cs
@@ -1,6 +1,4 @@
using Content.Client.Actions;
-using Content.Client.Actions;
-using Content.Client.Mapping;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Client/Commands/MappingClientSideSetupCommand.cs b/Content.Client/Commands/MappingClientSideSetupCommand.cs
index 3255e85e18f893..d17f1fccaf85d4 100644
--- a/Content.Client/Commands/MappingClientSideSetupCommand.cs
+++ b/Content.Client/Commands/MappingClientSideSetupCommand.cs
@@ -13,7 +13,6 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
- [Dependency] private readonly IStateManager _stateManager = default!;
public override string Command => "mappingclientsidesetup";
diff --git a/Content.Client/Computer/ComputerBoundUserInterface.cs b/Content.Client/Computer/ComputerBoundUserInterface.cs
index 11c26b252e9d31..9f34eeda20f2da 100644
--- a/Content.Client/Computer/ComputerBoundUserInterface.cs
+++ b/Content.Client/Computer/ComputerBoundUserInterface.cs
@@ -11,8 +11,6 @@ namespace Content.Client.Computer
[Virtual]
public class ComputerBoundUserInterface : ComputerBoundUserInterfaceBase where TWindow : BaseWindow, IComputerWindow, new() where TState : BoundUserInterfaceState
{
- [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
-
[ViewVariables]
private TWindow? _window;
diff --git a/Content.Client/Credits/CreditsWindow.xaml.cs b/Content.Client/Credits/CreditsWindow.xaml.cs
index 60ac57984546e3..ba240209533a2f 100644
--- a/Content.Client/Credits/CreditsWindow.xaml.cs
+++ b/Content.Client/Credits/CreditsWindow.xaml.cs
@@ -145,7 +145,7 @@ void AddSection(string title, string path, bool markup = false)
var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}");
if (markup)
{
- label.SetMessage(FormattedMessage.FromMarkup(text.Trim()));
+ label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
}
else
{
diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
index 21aa54c9622f85..7cae290fe1788c 100644
--- a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
+++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
@@ -227,7 +227,7 @@ private void PopulateRecordContainer(GeneralStationRecord stationRecord, Crimina
StatusOptionButton.SelectId((int) criminalRecord.Status);
if (criminalRecord.Reason is {} reason)
{
- var message = FormattedMessage.FromMarkup(Loc.GetString("criminal-records-console-wanted-reason"));
+ var message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-wanted-reason"));
message.AddText($": {reason}");
WantedReason.SetMessage(message);
WantedReason.Visible = true;
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 6caefb9a7e9518..c4c18f154a3d93 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -108,6 +108,7 @@ public override void Init()
_prototypeManager.RegisterIgnore("lobbyBackground");
_prototypeManager.RegisterIgnore("gamePreset");
_prototypeManager.RegisterIgnore("noiseChannel");
+ _prototypeManager.RegisterIgnore("playerConnectionWhitelist");
_prototypeManager.RegisterIgnore("spaceBiome");
_prototypeManager.RegisterIgnore("worldgenConfig");
_prototypeManager.RegisterIgnore("gameRule");
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/Gravity/GravitySystem.cs b/Content.Client/Gravity/GravitySystem.cs
index 3e87f76ba2bf36..dd51436a1f5e1c 100644
--- a/Content.Client/Gravity/GravitySystem.cs
+++ b/Content.Client/Gravity/GravitySystem.cs
@@ -1,4 +1,5 @@
using Content.Shared.Gravity;
+using Content.Shared.Power;
using Robust.Client.GameObjects;
namespace Content.Client.Gravity;
@@ -21,7 +22,7 @@ private void OnAppearanceChange(EntityUid uid, SharedGravityGeneratorComponent c
if (args.Sprite == null)
return;
- if (_appearanceSystem.TryGetData(uid, GravityGeneratorVisuals.State, out var state, args.Component))
+ if (_appearanceSystem.TryGetData(uid, PowerChargeVisuals.State, out var state, args.Component))
{
if (comp.SpriteMap.TryGetValue(state, out var spriteState))
{
@@ -30,7 +31,7 @@ private void OnAppearanceChange(EntityUid uid, SharedGravityGeneratorComponent c
}
}
- if (_appearanceSystem.TryGetData(uid, GravityGeneratorVisuals.Charge, out var charge, args.Component))
+ if (_appearanceSystem.TryGetData(uid, PowerChargeVisuals.Charge, out var charge, args.Component))
{
var layer = args.Sprite.LayerMapGet(GravityGeneratorVisualLayers.Core);
switch (charge)
diff --git a/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs b/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs
deleted file mode 100644
index 32b40747d55945..00000000000000
--- a/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.Gravity;
-using JetBrains.Annotations;
-using Robust.Client.UserInterface;
-
-namespace Content.Client.Gravity.UI
-{
- [UsedImplicitly]
- public sealed class GravityGeneratorBoundUserInterface : BoundUserInterface
- {
- [ViewVariables]
- private GravityGeneratorWindow? _window;
-
- public GravityGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- }
-
- protected override void Open()
- {
- base.Open();
-
- _window = this.CreateWindow();
- _window.SetEntity(Owner);
- }
-
- protected override void UpdateState(BoundUserInterfaceState state)
- {
- base.UpdateState(state);
-
- var castState = (SharedGravityGeneratorComponent.GeneratorState) state;
- _window?.UpdateState(castState);
- }
-
- public void SetPowerSwitch(bool on)
- {
- SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on));
- }
- }
-}
diff --git a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs b/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs
deleted file mode 100644
index 6f04133b594cff..00000000000000
--- a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using Content.Shared.Gravity;
-using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
-
-namespace Content.Client.Gravity.UI
-{
- [GenerateTypedNameReferences]
- public sealed partial class GravityGeneratorWindow : FancyWindow
- {
- private readonly ButtonGroup _buttonGroup = new();
-
- public event Action? OnPowerSwitch;
-
- public GravityGeneratorWindow()
- {
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
-
- OnButton.Group = _buttonGroup;
- OffButton.Group = _buttonGroup;
-
- OnButton.OnPressed += _ => OnPowerSwitch?.Invoke(true);
- OffButton.OnPressed += _ => OnPowerSwitch?.Invoke(false);
- }
-
- public void SetEntity(EntityUid uid)
- {
- EntityView.SetEntity(uid);
- }
-
- public void UpdateState(SharedGravityGeneratorComponent.GeneratorState state)
- {
- if (state.On)
- OnButton.Pressed = true;
- else
- OffButton.Pressed = true;
-
- PowerLabel.Text = Loc.GetString(
- "gravity-generator-window-power-label",
- ("draw", state.PowerDraw),
- ("max", state.PowerDrawMax));
-
- PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution");
-
- ChargeBar.Value = state.Charge;
- ChargeText.Text = (state.Charge / 255f).ToString("P0");
- StatusLabel.Text = Loc.GetString(state.PowerStatus switch
- {
- GravityGeneratorPowerStatus.Off => "gravity-generator-window-status-off",
- GravityGeneratorPowerStatus.Discharging => "gravity-generator-window-status-discharging",
- GravityGeneratorPowerStatus.Charging => "gravity-generator-window-status-charging",
- GravityGeneratorPowerStatus.FullyCharged => "gravity-generator-window-status-fully-charged",
- _ => throw new ArgumentOutOfRangeException()
- });
-
- StatusLabel.SetOnlyStyleClass(state.PowerStatus switch
- {
- GravityGeneratorPowerStatus.Off => "Danger",
- GravityGeneratorPowerStatus.Discharging => "Caution",
- GravityGeneratorPowerStatus.Charging => "Caution",
- GravityGeneratorPowerStatus.FullyCharged => "Good",
- _ => throw new ArgumentOutOfRangeException()
- });
-
- EtaLabel.Text = state.EtaSeconds >= 0
- ? Loc.GetString("gravity-generator-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds)))
- : Loc.GetString("gravity-generator-window-eta-none");
-
- EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled");
- }
- }
-}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
index 87931bf8455994..f8d1c7e9720ef7 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
@@ -140,7 +140,7 @@ private void GenerateControl(ReagentPrototype reagent)
var i = 0;
foreach (var effectString in effect.EffectDescriptions)
{
- descMsg.AddMarkup(effectString);
+ descMsg.AddMarkupOrThrow(effectString);
i++;
if (i < descriptionsCount)
descMsg.PushNewline();
@@ -174,7 +174,7 @@ private void GenerateControl(ReagentPrototype reagent)
var i = 0;
foreach (var effectString in guideEntryRegistryPlant.PlantMetabolisms)
{
- descMsg.AddMarkup(effectString);
+ descMsg.AddMarkupOrThrow(effectString);
i++;
if (i < descriptionsCount)
descMsg.PushNewline();
@@ -195,7 +195,7 @@ private void GenerateControl(ReagentPrototype reagent)
FormattedMessage description = new();
description.AddText(reagent.LocalizedDescription);
description.PushNewline();
- description.AddMarkup(Loc.GetString("guidebook-reagent-physical-description",
+ description.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-physical-description",
("description", reagent.LocalizedPhysicalDescription)));
ReagentDescription.SetMessage(description);
}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs
index 168f352d1ab51c..135dc5522acfd9 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs
@@ -155,7 +155,7 @@ private void SetReagents(Dictionary reagents, ref RichTextL
var i = 0;
foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value))
{
- msg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display",
+ msg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-recipes-reagent-display",
("reagent", protoMan.Index(product).LocalizedName), ("ratio", amount)));
i++;
if (i < reagentCount)
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/Info/InfoSection.xaml.cs b/Content.Client/Info/InfoSection.xaml.cs
index ab9d352d32f822..9e10a4d7b4b717 100644
--- a/Content.Client/Info/InfoSection.xaml.cs
+++ b/Content.Client/Info/InfoSection.xaml.cs
@@ -1,4 +1,4 @@
-using Robust.Client.AutoGenerated;
+using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -18,7 +18,7 @@ public void SetText(string title, string text, bool markup = false)
{
TitleLabel.Text = title;
if (markup)
- Content.SetMessage(FormattedMessage.FromMarkup(text.Trim()));
+ Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
else
Content.SetMessage(text);
}
diff --git a/Content.Client/Info/ServerInfo.cs b/Content.Client/Info/ServerInfo.cs
index 23be7506267c24..901fc913374143 100644
--- a/Content.Client/Info/ServerInfo.cs
+++ b/Content.Client/Info/ServerInfo.cs
@@ -24,7 +24,7 @@ public ServerInfo()
}
public void SetInfoBlob(string markup)
{
- _richTextLabel.SetMessage(FormattedMessage.FromMarkup(markup));
+ _richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
}
}
}
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/Light/Components/LightBehaviourComponent.cs b/Content.Client/Light/Components/LightBehaviourComponent.cs
index 9df793ee93ca6f..246863ba60f971 100644
--- a/Content.Client/Light/Components/LightBehaviourComponent.cs
+++ b/Content.Client/Light/Components/LightBehaviourComponent.cs
@@ -359,9 +359,6 @@ void ISerializationHooks.AfterDeserialization()
[RegisterComponent]
public sealed partial class LightBehaviourComponent : SharedLightBehaviourComponent, ISerializationHooks
{
- [Dependency] private readonly IEntityManager _entMan = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
-
public const string KeyPrefix = nameof(LightBehaviourComponent);
public sealed class AnimationContainer
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml
index afa783c7aa9c03..87d11005be8a98 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml
@@ -1,10 +1,24 @@
+ MinSize="800 128">
+
+
+ VerticalExpand="True"
+ HorizontalExpand="True">
+
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
index d029eb1223d8df..aab2a56ff6819e 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
@@ -5,6 +6,7 @@
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
namespace Content.Client.Lobby.UI.Loadouts;
@@ -24,27 +26,36 @@ public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, Role
Profile = profile;
var protoManager = collection.Resolve();
- foreach (var group in proto.Groups)
+ // Hide if no groups
+ if (proto.Groups.Count == 0)
{
- if (!protoManager.TryIndex(group, out var groupProto))
- continue;
+ LoadoutGroupsContainer.Visible = false;
+ SetSize = Vector2.Zero;
+ }
+ else
+ {
+ foreach (var group in proto.Groups)
+ {
+ if (!protoManager.TryIndex(group, out var groupProto))
+ continue;
- if (groupProto.Hidden)
- continue;
+ if (groupProto.Hidden)
+ continue;
- var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
- LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
- _groups.Add(container);
+ var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
+ LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
+ _groups.Add(container);
- container.OnLoadoutPressed += args =>
- {
- OnLoadoutPressed?.Invoke(group, args);
- };
+ container.OnLoadoutPressed += args =>
+ {
+ OnLoadoutPressed?.Invoke(group, args);
+ };
- container.OnLoadoutUnpressed += args =>
- {
- OnLoadoutUnpressed?.Invoke(group, args);
- };
+ container.OnLoadoutUnpressed += args =>
+ {
+ OnLoadoutUnpressed?.Invoke(group, args);
+ };
+ }
}
}
diff --git a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
index 11abe8c2451f93..0607c7683154ef 100644
--- a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
+++ b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
@@ -7,8 +7,6 @@ namespace Content.Client.MachineLinking.UI;
public sealed class SignalTimerBoundUserInterface : BoundUserInterface
{
- [Dependency] private readonly IGameTiming _gameTiming = default!;
-
[ViewVariables]
private SignalTimerWindow? _window;
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/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs
index 7ff6390764b21e..ee3c00fa1b8721 100644
--- a/Content.Client/Message/RichTextLabelExt.cs
+++ b/Content.Client/Message/RichTextLabelExt.cs
@@ -15,7 +15,7 @@ public static class RichTextLabelExt
///
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
- label.SetMessage(FormattedMessage.FromMarkup(markup));
+ label.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
return label;
}
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/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 8ce22489c7181a..6d52c50290e5bb 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using Content.Client.Lobby;
using Content.Shared.CCVar;
using Content.Shared.Players;
@@ -133,7 +133,7 @@ public bool CheckRoleRequirements(HashSet? requirements, Humanoi
reasons.Add(jobReason.ToMarkup());
}
- reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons));
+ reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkupOrThrow(string.Join('\n', reasons));
return reason == null;
}
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/Power/PowerCharge/PowerChargeBoundUserInterface.cs b/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs
new file mode 100644
index 00000000000000..7a36b8ddf59a55
--- /dev/null
+++ b/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Power;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Power.PowerCharge;
+
+public sealed class PowerChargeBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private PowerChargeWindow? _window;
+
+ public PowerChargeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ public void SetPowerSwitch(bool on)
+ {
+ SendMessage(new SwitchChargingMachineMessage(on));
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ if (!EntMan.TryGetComponent(Owner, out PowerChargeComponent? component))
+ return;
+
+ _window = this.CreateWindow();
+ _window.UpdateWindow(this, Loc.GetString(component.WindowTitle));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (state is not PowerChargeState chargeState)
+ return;
+
+ _window?.UpdateState(chargeState);
+ }
+}
diff --git a/Content.Client/Power/PowerCharge/PowerChargeComponent.cs b/Content.Client/Power/PowerCharge/PowerChargeComponent.cs
new file mode 100644
index 00000000000000..ab5baa4e2f51a8
--- /dev/null
+++ b/Content.Client/Power/PowerCharge/PowerChargeComponent.cs
@@ -0,0 +1,10 @@
+using Content.Shared.Power;
+
+namespace Content.Client.Power.PowerCharge;
+
+///
+[RegisterComponent]
+public sealed partial class PowerChargeComponent : SharedPowerChargeComponent
+{
+
+}
diff --git a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml
similarity index 60%
rename from Content.Client/Gravity/UI/GravityGeneratorWindow.xaml
rename to Content.Client/Power/PowerCharge/PowerChargeWindow.xaml
index 853f437a2bf539..4e61255326ef36 100644
--- a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml
+++ b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml
@@ -1,27 +1,26 @@
-
+
-
-
+
+
-
-
+
+
-
+
-
+
@@ -31,5 +30,4 @@
-
diff --git a/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs
new file mode 100644
index 00000000000000..6739e24c208452
--- /dev/null
+++ b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs
@@ -0,0 +1,72 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Power;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Power.PowerCharge;
+
+[GenerateTypedNameReferences]
+public sealed partial class PowerChargeWindow : FancyWindow
+{
+ private readonly ButtonGroup _buttonGroup = new();
+
+ public PowerChargeWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ OnButton.Group = _buttonGroup;
+ OffButton.Group = _buttonGroup;
+ }
+
+ public void UpdateWindow(PowerChargeBoundUserInterface bui, string title)
+ {
+ Title = title;
+
+ OnButton.OnPressed += _ => bui.SetPowerSwitch(true);
+ OffButton.OnPressed += _ => bui.SetPowerSwitch(false);
+
+ EntityView.SetEntity(bui.Owner);
+ }
+
+ public void UpdateState(PowerChargeState state)
+ {
+ if (state.On)
+ OnButton.Pressed = true;
+ else
+ OffButton.Pressed = true;
+
+ PowerLabel.Text = Loc.GetString(
+ "power-charge-window-power-label",
+ ("draw", state.PowerDraw),
+ ("max", state.PowerDrawMax));
+
+ PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution");
+
+ ChargeBar.Value = state.Charge;
+ ChargeText.Text = (state.Charge / 255f).ToString("P0");
+ StatusLabel.Text = Loc.GetString(state.PowerStatus switch
+ {
+ PowerChargePowerStatus.Off => "power-charge-window-status-off",
+ PowerChargePowerStatus.Discharging => "power-charge-window-status-discharging",
+ PowerChargePowerStatus.Charging => "power-charge-window-status-charging",
+ PowerChargePowerStatus.FullyCharged => "power-charge-window-status-fully-charged",
+ _ => throw new ArgumentOutOfRangeException()
+ });
+
+ StatusLabel.SetOnlyStyleClass(state.PowerStatus switch
+ {
+ PowerChargePowerStatus.Off => "Danger",
+ PowerChargePowerStatus.Discharging => "Caution",
+ PowerChargePowerStatus.Charging => "Caution",
+ PowerChargePowerStatus.FullyCharged => "Good",
+ _ => throw new ArgumentOutOfRangeException()
+ });
+
+ EtaLabel.Text = state.EtaSeconds >= 0
+ ? Loc.GetString("power-charge-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds)))
+ : Loc.GetString("power-charge-window-eta-none");
+
+ EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled");
+ }
+}
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
index d995299207051b..3f7ccfb903b753 100644
--- a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
@@ -309,7 +309,7 @@ private void UpdateWarningLabel(PowerMonitoringFlags flags)
BorderThickness = new Thickness(2),
};
- msg.AddMarkup(Loc.GetString("power-monitoring-window-rogue-power-consumer"));
+ msg.AddMarkupOrThrow(Loc.GetString("power-monitoring-window-rogue-power-consumer"));
SystemWarningPanel.Visible = true;
}
@@ -322,7 +322,7 @@ private void UpdateWarningLabel(PowerMonitoringFlags flags)
BorderThickness = new Thickness(2),
};
- msg.AddMarkup(Loc.GetString("power-monitoring-window-power-net-abnormalities"));
+ msg.AddMarkupOrThrow(Loc.GetString("power-monitoring-window-power-net-abnormalities"));
SystemWarningPanel.Visible = true;
}
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/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
index 87d7e62c392ba9..06e5674d9cb33b 100644
--- a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
+++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
@@ -128,12 +128,12 @@ private void PopulateData()
};
var text = new FormattedMessage();
- text.PushMarkup(Loc.GetString("robotics-console-model", ("name", model)));
- text.AddMarkup(Loc.GetString("robotics-console-designation"));
+ text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-model", ("name", model))}\n");
+ text.AddMarkupOrThrow(Loc.GetString("robotics-console-designation"));
text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
- text.PushMarkup(Loc.GetString("robotics-console-battery", ("charge", (int) (data.Charge * 100f)), ("color", batteryColor)));
- text.PushMarkup(Loc.GetString("robotics-console-brain", ("brain", data.HasBrain)));
- text.AddMarkup(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
+ text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-battery", ("charge", (int)(data.Charge * 100f)), ("color", batteryColor))}\n");
+ text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-brain", ("brain", data.HasBrain))}\n");
+ text.AddMarkupOrThrow(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
BorgInfo.SetMessage(text);
// how the turntables
diff --git a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
index 9c9f83a4275a65..7108e4cca8f70a 100644
--- a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
+++ b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
@@ -61,9 +61,9 @@ private BoxContainer MakeRoundEndSummaryTab(string gamemode, string roundEnd, Ti
//Gamemode Name
var gamemodeLabel = new RichTextLabel();
var gamemodeMessage = new FormattedMessage();
- gamemodeMessage.AddMarkup(Loc.GetString("round-end-summary-window-round-id-label", ("roundId", roundId)));
+ gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-round-id-label", ("roundId", roundId)));
gamemodeMessage.AddText(" ");
- gamemodeMessage.AddMarkup(Loc.GetString("round-end-summary-window-gamemode-name-label", ("gamemode", gamemode)));
+ gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-gamemode-name-label", ("gamemode", gamemode)));
gamemodeLabel.SetMessage(gamemodeMessage);
roundEndSummaryContainer.AddChild(gamemodeLabel);
diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs
index a4d59d1f3150f3..03c74032f73bd3 100644
--- a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs
+++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs
@@ -6,7 +6,7 @@ namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
public sealed class SiliconLawEui : BaseEui
{
- public readonly EntityManager _entityManager = default!;
+ private readonly EntityManager _entityManager;
private SiliconLawUi _siliconLawUi;
private EntityUid _target;
diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
new file mode 100644
index 00000000000000..68318305a0c9e4
--- /dev/null
+++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed class StationAiBoundUserInterface : BoundUserInterface
+{
+ private StationAiMenu? _menu;
+
+ public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _menu = this.CreateWindow();
+ _menu.Track(Owner);
+
+ _menu.OnAiRadial += args =>
+ {
+ SendPredictedMessage(new StationAiRadialMessage()
+ {
+ Event = args,
+ });
+ };
+ }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml
new file mode 100644
index 00000000000000..d56fc832898490
--- /dev/null
+++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
new file mode 100644
index 00000000000000..b152f5ead8b177
--- /dev/null
+++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
@@ -0,0 +1,127 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Silicons.StationAi;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAiMenu : RadialMenu
+{
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ public event Action? OnAiRadial;
+
+ private EntityUid _tracked;
+
+ public StationAiMenu()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ }
+
+ public void Track(EntityUid owner)
+ {
+ _tracked = owner;
+
+ if (!_entManager.EntityExists(_tracked))
+ {
+ Close();
+ return;
+ }
+
+ BuildButtons();
+ UpdatePosition();
+ }
+
+ private void BuildButtons()
+ {
+ var ev = new GetStationAiRadialEvent();
+ _entManager.EventBus.RaiseLocalEvent(_tracked, ref ev);
+
+ var main = FindControl("Main");
+ main.DisposeAllChildren();
+ var sprites = _entManager.System();
+
+ foreach (var action in ev.Actions)
+ {
+ // TODO: This radial boilerplate is quite annoying
+ var button = new StationAiMenuButton(action.Event)
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64f, 64f),
+ ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
+ };
+
+ if (action.Sprite != null)
+ {
+ var texture = sprites.Frame0(action.Sprite);
+ var scale = Vector2.One;
+
+ if (texture.Width <= 32)
+ {
+ scale *= 2;
+ }
+
+ var tex = new TextureRect
+ {
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Texture = texture,
+ TextureScale = scale,
+ };
+
+ button.AddChild(tex);
+ }
+
+ button.OnPressed += args =>
+ {
+ OnAiRadial?.Invoke(action.Event);
+ Close();
+ };
+ main.AddChild(button);
+ }
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ UpdatePosition();
+ }
+
+ private void UpdatePosition()
+ {
+ if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform))
+ {
+ Close();
+ return;
+ }
+
+ if (!xform.Coordinates.IsValid(_entManager))
+ {
+ Close();
+ return;
+ }
+
+ var coords = _entManager.System().GetSpriteScreenCoordinates((_tracked, null, xform));
+
+ if (!coords.IsValid)
+ {
+ Close();
+ return;
+ }
+
+ OpenScreenAt(coords.Position, _clyde);
+ }
+}
+
+public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton
+{
+ public BaseStationAiAction Action = action;
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiOverlay.cs b/Content.Client/Silicons/StationAi/StationAiOverlay.cs
index efa1b8dbeff10a..15a8a3a63fe90a 100644
--- a/Content.Client/Silicons/StationAi/StationAiOverlay.cs
+++ b/Content.Client/Silicons/StationAi/StationAiOverlay.cs
@@ -4,7 +4,9 @@
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
namespace Content.Client.Silicons.StationAi;
@@ -12,6 +14,7 @@ public sealed class StationAiOverlay : Overlay
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
@@ -22,6 +25,9 @@ public sealed class StationAiOverlay : Overlay
private IRenderTexture? _staticTexture;
private IRenderTexture? _stencilTexture;
+ private float _updateRate = 1f / 30f;
+ private float _accumulator;
+
public StationAiOverlay()
{
IoCManager.InjectDependencies(this);
@@ -47,19 +53,22 @@ protected override void Draw(in OverlayDrawArgs args)
_entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform);
var gridUid = playerXform?.GridUid ?? EntityUid.Invalid;
_entManager.TryGetComponent(gridUid, out MapGridComponent? grid);
+ _entManager.TryGetComponent(gridUid, out BroadphaseComponent? broadphase);
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
+ _accumulator -= (float) _timing.FrameTime.TotalSeconds;
- if (grid != null)
+ if (grid != null && broadphase != null)
{
- // TODO: Pass in attached entity's grid.
- // TODO: Credit OD on the moved to code
- // TODO: Call the moved-to code here.
-
- _visibleTiles.Clear();
var lookups = _entManager.System();
var xforms = _entManager.System();
- _entManager.System().GetView((gridUid, grid), worldBounds, _visibleTiles);
+
+ if (_accumulator <= 0f)
+ {
+ _accumulator = MathF.Max(0f, _accumulator + _updateRate);
+ _visibleTiles.Clear();
+ _entManager.System().GetView((gridUid, broadphase, grid), worldBounds, _visibleTiles);
+ }
var gridMatrix = xforms.GetWorldMatrix(gridUid);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs
new file mode 100644
index 00000000000000..bf6b65a9697b5a
--- /dev/null
+++ b/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Doors.Components;
+using Content.Shared.Silicons.StationAi;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiSystem
+{
+ private void InitializeAirlock()
+ {
+ SubscribeLocalEvent(OnDoorBoltGetRadial);
+ }
+
+ private void OnDoorBoltGetRadial(Entity ent, ref GetStationAiRadialEvent args)
+ {
+ args.Actions.Add(new StationAiRadial()
+ {
+ Sprite = ent.Comp.BoltsDown ?
+ new SpriteSpecifier.Rsi(
+ new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "open") :
+ new SpriteSpecifier.Rsi(
+ new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "closed"),
+ Tooltip = ent.Comp.BoltsDown ? Loc.GetString("bolt-open") : Loc.GetString("bolt-close"),
+ Event = new StationAiBoltEvent()
+ {
+ Bolted = !ent.Comp.BoltsDown,
+ }
+ });
+ }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs
new file mode 100644
index 00000000000000..cf2f6136207d35
--- /dev/null
+++ b/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Item.ItemToggle.Components;
+using Content.Shared.Light.Components;
+using Content.Shared.Silicons.StationAi;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiSystem
+{
+ // Used for surveillance camera lights
+
+ private void InitializePowerToggle()
+ {
+ SubscribeLocalEvent(OnLightGetRadial);
+ }
+
+ private void OnLightGetRadial(Entity ent, ref GetStationAiRadialEvent args)
+ {
+ if (!TryComp(ent.Owner, out ItemToggleComponent? toggle))
+ return;
+
+ args.Actions.Add(new StationAiRadial()
+ {
+ Tooltip = Loc.GetString("toggle-light"),
+ Sprite = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/light.svg.192dpi.png")),
+ Event = new StationAiLightEvent()
+ {
+ Enabled = !toggle.Activated
+ }
+ });
+ }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs
index 2ed06175252985..ab9ace3c1d583d 100644
--- a/Content.Client/Silicons/StationAi/StationAiSystem.cs
+++ b/Content.Client/Silicons/StationAi/StationAiSystem.cs
@@ -5,7 +5,7 @@
namespace Content.Client.Silicons.StationAi;
-public sealed partial class StationAiSystem : EntitySystem
+public sealed partial class StationAiSystem : SharedStationAiSystem
{
[Dependency] private readonly IOverlayManager _overlayMgr = default!;
[Dependency] private readonly IPlayerManager _player = default!;
@@ -15,8 +15,8 @@ public sealed partial class StationAiSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
- // InitializeAirlock();
- // InitializePowerToggle();
+ InitializeAirlock();
+ InitializePowerToggle();
SubscribeLocalEvent(OnAiAttached);
SubscribeLocalEvent(OnAiDetached);
diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs
index 899df30f7fc01d..b90977cbb4da14 100644
--- a/Content.Client/Storage/StorageBoundUserInterface.cs
+++ b/Content.Client/Storage/StorageBoundUserInterface.cs
@@ -11,6 +11,8 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
private readonly StorageSystem _storage;
+ [Obsolete] public override bool DeferredClose => false;
+
public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
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 @@
+