diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 94d30a6d5f5..2e90f0eed03 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,8 +1,20 @@
# Last match in file takes precedence.
-/Content.*/SimpleStation14/ @DEATHB4DEFEAT
+# C# code
+/Content.*/ @DeltaV-Station/maintainers
-/Resources/*.yml @Colin-Tel
-/Resources/*/SimpleStation14/ @DEATHB4DEFEAT
-/Resources/Maps/ @IamVelcroboy
-/Resources/Prototypes/Maps/ @IamVelcroboy
+# YML files
+/Resources/*.yml @DeltaV-Station/yaml-maintainers
+/Resources/**/*.yml @DeltaV-Station/yaml-maintainers
+
+# Sprites
+/Resources/Textures/ @IamVelcroboy
+
+# Lobby art and music - automatically direction issues since its immediately visible to players
+/Resources/Audio/Lobby/ @DeltaV-Station/game-directors
+/Resources/Textures/LobbyScreens/ @DeltaV-Station/game-directors
+
+# Maps
+/Resources/Maps/ @DeltaV-Station/maptainers
+/Resources/Prototypes/Maps/ @DeltaV-Station/maptainers
+/Content.IntegrationTests/Tests/PostMapInitTest.cs @DeltaV-Station/maptainers
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7a8129df1a6..4029b093ccb 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 00000000000..79bb66560e3
--- /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 00000000000..08cae979b9b
--- /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 00000000000..8824a776ee6
--- /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 00000000000..f0b7ffbe119
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
@@ -0,0 +1,548 @@
+using Content.Client.Message;
+using Content.Client.Pinpointer.UI;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.Pinpointer;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlertsComputerWindow : FancyWindow
+{
+ private readonly IEntityManager _entManager;
+ private readonly SpriteSystem _spriteSystem;
+
+ private EntityUid? _owner;
+ private NetEntity? _trackedEntity;
+
+ private AtmosAlertsComputerEntry[]? _airAlarms = null;
+ private AtmosAlertsComputerEntry[]? _fireAlarms = null;
+ private IEnumerable? _allAlarms = null;
+
+ private IEnumerable? _activeAlarms = null;
+ private Dictionary _deviceSilencingProgress = new();
+
+ public event Action? SendFocusChangeMessageAction;
+ public event Action? SendDeviceSilencedMessageAction;
+
+ private bool _autoScrollActive = false;
+ private bool _autoScrollAwaitsUpdate = false;
+
+ private const float SilencingDuration = 2.5f;
+
+ public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+ _spriteSystem = _entManager.System();
+
+ // Pass the owner to nav map
+ _owner = owner;
+ NavMap.Owner = _owner;
+
+ // Set nav map colors
+ NavMap.WallColor = new Color(64, 64, 64);
+ NavMap.TileColor = Color.DimGray * NavMap.WallColor;
+
+ // Set nav map grid uid
+ var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
+
+ if (_entManager.TryGetComponent(owner, out var xform))
+ {
+ NavMap.MapUid = xform.GridUid;
+
+ // Assign station name
+ if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData))
+ stationName = stationMetaData.EntityName;
+
+ var msg = new FormattedMessage();
+ msg.TryAddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)), out _);
+
+ StationName.SetMessage(msg);
+ }
+
+ else
+ {
+ StationName.SetMessage(stationName);
+ NavMap.Visible = false;
+ }
+
+ // Set trackable entity selected action
+ NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
+
+ // Update nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Set tab container headers
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms"));
+ MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms"));
+
+ // Set UI toggles
+ ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid);
+ ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal);
+ ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning);
+ ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger);
+
+ // Set atmos monitoring message action
+ SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
+ SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage;
+ }
+
+ #region Toggle handling
+
+ private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ foreach (var device in console.AtmosDevices)
+ {
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (toggledAlarmState != alarmState)
+ continue;
+
+ if (toggle.Pressed)
+ AddTrackedEntityToNavMap(device, alarmState);
+
+ else
+ NavMap.TrackedEntities.Remove(device.NetEntity);
+ }
+ }
+
+ private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState)
+ {
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ if (toggleState)
+ _deviceSilencingProgress[netEntity] = SilencingDuration;
+
+ else
+ _deviceSilencingProgress.Remove(netEntity);
+
+ foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children)
+ {
+ if (entryContainer.NetEntity == netEntity)
+ entryContainer.SilenceAlarmProgressBar.Visible = toggleState;
+ }
+
+ SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState);
+ }
+
+ #endregion
+
+ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ if (_trackedEntity != focusData?.NetEntity)
+ {
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ focusData = null;
+ }
+
+ // Retain alarm data for use inbetween updates
+ _airAlarms = airAlarms;
+ _fireAlarms = fireAlarms;
+ _allAlarms = airAlarms.Concat(fireAlarms);
+
+ var silenced = console.SilencedDevices;
+
+ _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal &&
+ (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity)));
+
+ // Reset nav map data
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ // Add tracked entities to the nav map
+ foreach (var device in console.AtmosDevices)
+ {
+ if (!NavMap.Visible)
+ continue;
+
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (_trackedEntity != device.NetEntity)
+ {
+ // Skip air alarms if the appropriate overlay is off
+ if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid)
+ continue;
+
+ if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal)
+ continue;
+
+ if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning)
+ continue;
+
+ if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger)
+ continue;
+ }
+
+ AddTrackedEntityToNavMap(device, alarmState);
+ }
+
+ // Show the monitor location
+ var consoleUid = _entManager.GetNetEntity(_owner);
+
+ if (consoleCoords != null && consoleUid != null)
+ {
+ var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+ var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
+ NavMap.TrackedEntities[consoleUid.Value] = blip;
+ }
+
+ // Update the nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Clear excess children from the tables
+ var activeAlarmCount = _activeAlarms.Count();
+
+ while (AlertsTable.ChildCount > activeAlarmCount)
+ AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1));
+
+ while (AirAlarmsTable.ChildCount > airAlarms.Length)
+ AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1));
+
+ while (FireAlarmsTable.ChildCount > fireAlarms.Length)
+ FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1));
+
+ // Update all entries in each table
+ for (int index = 0; index < _activeAlarms.Count(); index++)
+ {
+ var entry = _activeAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AlertsTable, console, focusData);
+ }
+
+ for (int index = 0; index < airAlarms.Count(); index++)
+ {
+ var entry = airAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData);
+ }
+
+ for (int index = 0; index < fireAlarms.Count(); index++)
+ {
+ var entry = fireAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData);
+ }
+
+ // If no alerts are active, display a message
+ if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0)
+ {
+ var label = new RichTextLabel()
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ };
+
+ label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
+
+ AlertsTable.AddChild(label);
+ }
+
+ // Update the alerts tab with the number of active alerts
+ if (activeAlarmCount == 0)
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+
+ else
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
+
+ // Auto-scroll re-enable
+ if (_autoScrollAwaitsUpdate)
+ {
+ _autoScrollActive = true;
+ _autoScrollAwaitsUpdate = false;
+ }
+ }
+
+ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState)
+ {
+ var data = GetBlipTexture(alarmState);
+
+ if (data == null)
+ return;
+
+ var texture = data.Value.Item1;
+ var color = data.Value.Item2;
+ var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+
+ if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
+ color *= Color.DimGray;
+
+ var selectable = true;
+ var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
+
+ NavMap.TrackedEntities[metaData.NetEntity] = blip;
+ }
+
+ private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
+ {
+ // Make new UI entry if required
+ if (index >= table.ChildCount)
+ {
+ var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates));
+
+ // On click
+ newEntryContainer.FocusButton.OnButtonUp += args =>
+ {
+ if (_trackedEntity == newEntryContainer.NetEntity)
+ {
+ _trackedEntity = null;
+ }
+
+ else
+ {
+ _trackedEntity = newEntryContainer.NetEntity;
+
+ if (newEntryContainer.Coordinates != null)
+ NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value);
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+
+ // Update affected UI elements across all tables
+ UpdateConsoleTable(console, AlertsTable, _trackedEntity);
+ UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity);
+ UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity);
+ };
+
+ // On toggling the silence check box
+ newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed);
+
+ // Add the entry to the current table
+ table.AddChild(newEntryContainer);
+ }
+
+ // Update values and UI elements
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not AtmosAlarmEntryContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateUIEntry(entry, index, table, console, focusData);
+
+ return;
+ }
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
+
+ if (_trackedEntity != entry.NetEntity)
+ {
+ var silenced = console.SilencedDevices;
+ entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity)
+ {
+ foreach (var tableChild in table.Children)
+ {
+ if (tableChild is not AtmosAlarmEntryContainer)
+ continue;
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ if (entryContainer.NetEntity != currTrackedEntity)
+ entryContainer.RemoveAsFocus();
+
+ else if (entryContainer.NetEntity == currTrackedEntity)
+ entryContainer.SetAsFocus();
+ }
+ }
+
+ private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
+ {
+ if (netEntity == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ _trackedEntity = netEntity;
+
+ if (netEntity != null)
+ {
+ // Tab switching
+ if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false)
+ {
+ var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity);
+
+ switch (device?.Group)
+ {
+ case AtmosAlertsComputerGroup.AirAlarm:
+ MasterTabContainer.CurrentTab = 1; break;
+ case AtmosAlertsComputerGroup.FireAlarm:
+ MasterTabContainer.CurrentTab = 2; break;
+ }
+ }
+
+ // Get the scroll position of the selected entity on the selected button the UI
+ ActivateAutoScrollToFocus();
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ AutoScrollToFocus();
+
+ // Device silencing update
+ foreach ((var device, var remainingTime) in _deviceSilencingProgress)
+ {
+ var t = remainingTime - args.DeltaSeconds;
+
+ if (t <= 0)
+ {
+ _deviceSilencingProgress.Remove(device);
+
+ if (device == _trackedEntity)
+ _trackedEntity = null;
+ }
+
+ else
+ _deviceSilencingProgress[device] = t;
+ }
+ }
+
+ private void ActivateAutoScrollToFocus()
+ {
+ _autoScrollActive = false;
+ _autoScrollAwaitsUpdate = true;
+ }
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return;
+
+ if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
+ return;
+
+ if (!TryGetNextScrollPosition(out float? nextScrollPosition))
+ return;
+
+ vScrollbar.ValueTarget = nextScrollPosition.Value;
+
+ if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
+ _autoScrollActive = false;
+ }
+
+ private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
+ {
+ vScrollBar = null;
+
+ foreach (var child in scroll.Children)
+ {
+ if (child is not VScrollBar)
+ continue;
+
+ var castChild = child as VScrollBar;
+
+ if (castChild != null)
+ {
+ vScrollBar = castChild;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return false;
+
+ var container = scroll.Children.ElementAt(0) as BoxContainer;
+ if (container == null || container.Children.Count() == 0)
+ return false;
+
+ // Exit if the heights of the children haven't been initialized yet
+ if (!container.Children.Any(x => x.Height > 0))
+ return false;
+
+ nextScrollPosition = 0;
+
+ foreach (var control in container.Children)
+ {
+ if (control == null || control is not AtmosAlarmEntryContainer)
+ continue;
+
+ if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ // Failed to find control
+ nextScrollPosition = null;
+
+ return false;
+ }
+
+ private AtmosAlarmType GetAlarmState(NetEntity netEntity)
+ {
+ var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState;
+
+ if (alarmState == null)
+ return AtmosAlarmType.Invalid;
+
+ return alarmState.Value;
+ }
+
+ private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState)
+ {
+ (SpriteSpecifier.Texture, Color)? output = null;
+
+ switch (alarmState)
+ {
+ case AtmosAlarmType.Invalid:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
+ case AtmosAlarmType.Normal:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
+ case AtmosAlarmType.Warning:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
+ case AtmosAlarmType.Danger:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
+ }
+
+ return output;
+ }
+}
diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs
index ca6336b91b8..b525747aa9c 100644
--- a/Content.Client/Audio/AmbientSoundSystem.cs
+++ b/Content.Client/Audio/AmbientSoundSystem.cs
@@ -306,6 +306,9 @@ private void ProcessNearbyAmbience(TransformComponent playerXform)
.WithMaxDistance(comp.Range);
var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
+ if (stream == null)
+ continue;
+
_playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
playingCount++;
diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs
index 7c77865f741..50c3971d95a 100644
--- a/Content.Client/Audio/ClientGlobalSoundSystem.cs
+++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs
@@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent)
if(!_adminAudioEnabled) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
- _adminAudio.Add(stream.Value.Entity);
+ _adminAudio.Add(stream?.Entity);
}
private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
@@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
- _eventAudio.Add(soundEvent.Type, stream.Value.Entity);
+ _eventAudio.Add(soundEvent.Type, stream?.Entity);
}
private void PlayGameSound(GameGlobalSoundEvent soundEvent)
diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
index d60c978ccf5..bf7ab26cba2 100644
--- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
@@ -213,9 +213,9 @@ private void UpdateAmbientMusic()
false,
AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
- _ambientMusicStream = strim.Value.Entity;
+ _ambientMusicStream = strim?.Entity;
- if (_musicProto.FadeIn)
+ if (_musicProto.FadeIn && strim != null)
{
FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime);
}
diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
index 92c5b7a4191..9864dbcb2a9 100644
--- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
@@ -185,7 +185,7 @@ private void PlaySoundtrack(string soundtrackFilename)
false,
_lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
);
- if (playResult.Value.Entity == default)
+ if (playResult == null)
{
_sawmill.Warning(
$"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",
diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs
index b8f98c0d408..00abd642fe8 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/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs
index adb61d10e62..32e9f4ae9be 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 a3cedb5f2f3..7c7d824ee98 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 96bbcc54f2a..27d77eda496 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 dd489fd4d65..c155c7a9dea 100644
--- a/Content.Client/Commands/ActionsCommands.cs
+++ b/Content.Client/Commands/ActionsCommands.cs
@@ -1,10 +1,6 @@
-using System.IO;
-using Content.Client.Actions;
-using Content.Client.Mapping;
+using Content.Client.Actions;
using Content.Shared.Administration;
-using Robust.Client.UserInterface;
using Robust.Shared.Console;
-using YamlDotNet.RepresentationModel;
namespace Content.Client.Commands;
@@ -50,7 +46,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
- LoadActs(); // DeltaV - Load from a file dialogue instead
+ shell.WriteLine(Help);
return;
}
@@ -63,48 +59,4 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
}
}
-
- ///
- /// DeltaV - Load actions from a file stream instead
- ///
- private static async void LoadActs()
- {
- var fileMan = IoCManager.Resolve();
- var actMan = IoCManager.Resolve().GetEntitySystem();
-
- var stream = await fileMan.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
- if (stream is null)
- return;
-
- var reader = new StreamReader(stream);
- var yamlStream = new YamlStream();
- yamlStream.Load(reader);
-
- actMan.LoadActionAssignments(yamlStream);
- reader.Close();
- }
-}
-
-[AnyCommand]
-public sealed class LoadMappingActionsCommand : LocalizedCommands
-{
- [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
-
- public const string CommandName = "loadmapacts";
-
- public override string Command => CommandName;
-
- public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
-
- public override void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- try
- {
- _entitySystemManager.GetEntitySystem().LoadMappingActions();
- }
- catch
- {
- shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
- }
- }
}
diff --git a/Content.Client/Commands/MappingClientSideSetupCommand.cs b/Content.Client/Commands/MappingClientSideSetupCommand.cs
index 39268c62847..eb2d13c9540 100644
--- a/Content.Client/Commands/MappingClientSideSetupCommand.cs
+++ b/Content.Client/Commands/MappingClientSideSetupCommand.cs
@@ -1,6 +1,8 @@
+using Content.Client.Mapping;
using Content.Client.Markers;
using JetBrains.Annotations;
using Robust.Client.Graphics;
+using Robust.Client.State;
using Robust.Shared.Console;
namespace Content.Client.Commands;
@@ -10,6 +12,7 @@ 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";
@@ -21,8 +24,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystemManager.GetEntitySystem().MarkersVisible = true;
_lightManager.Enabled = false;
- shell.ExecuteCommand(ShowSubFloorForever.CommandName);
- shell.ExecuteCommand(LoadMappingActionsCommand.CommandName);
+ shell.ExecuteCommand("showsubfloorforever");
+ _stateManager.RequestStateChange();
}
}
}
diff --git a/Content.Client/Computer/ComputerBoundUserInterface.cs b/Content.Client/Computer/ComputerBoundUserInterface.cs
index 11c26b252e9..9f34eeda20f 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/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
index 5b156644a73..2d94034bb9c 100644
--- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
@@ -2,6 +2,7 @@
using System.Threading;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
+using Content.Client.Mapping;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer;
@@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI
///
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
///
- public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged
+ public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged, IOnStateEntered, IOnStateExited
{
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
@@ -42,18 +43,51 @@ public sealed class ContextMenuUIController : UIController, IOnStateEntered? OnSubMenuOpened;
public Action? OnContextKeyEvent;
+ private bool _setup;
+
public void OnStateEntered(GameplayState state)
{
+ Setup();
+ }
+
+ public void OnStateExited(GameplayState state)
+ {
+ Shutdown();
+ }
+
+ public void OnStateEntered(MappingState state)
+ {
+ Setup();
+ }
+
+ public void OnStateExited(MappingState state)
+ {
+ Shutdown();
+ }
+
+ public void Setup()
+ {
+ if (_setup)
+ return;
+
+ _setup = true;
+
RootMenu = new(this, null);
RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu);
}
- public void OnStateExited(GameplayState state)
+ public void Shutdown()
{
+ if (!_setup)
+ return;
+
+ _setup = false;
+
Close();
RootMenu.OnPopupHide -= Close;
RootMenu.Dispose();
+ RootMenu = default!;
}
///
diff --git a/Content.Client/Credits/CreditsWindow.xaml.cs b/Content.Client/Credits/CreditsWindow.xaml.cs
index d804246687b..a65f1e3a514 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 21aa54c9622..7cae290fe17 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/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
index 845bd7c03d2..07b6f57bdb9 100644
--- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
+++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
@@ -4,6 +4,7 @@
using Robust.Client.Input;
using Robust.Shared.Enums;
using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
namespace Content.Client.Decals.Overlays;
@@ -16,7 +17,7 @@ public sealed class DecalPlacementOverlay : Overlay
private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprite;
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite)
{
@@ -24,6 +25,7 @@ public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSyst
_placement = placement;
_transform = transform;
_sprite = sprite;
+ ZIndex = 1000;
}
protected override void Draw(in OverlayDrawArgs args)
@@ -55,7 +57,7 @@ protected override void Draw(in OverlayDrawArgs args)
if (snap)
{
- localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector;
+ localPos = localPos.Floored() + grid.TileSizeHalfVector;
}
// Nothing uses snap cardinals so probably don't need preview?
diff --git a/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs b/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs
new file mode 100644
index 00000000000..879a5efee55
--- /dev/null
+++ b/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs
@@ -0,0 +1,44 @@
+using Content.Shared.DeltaV.Abilities;
+using Content.Shared.Popups;
+using Robust.Client.GameObjects;
+using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
+
+namespace Content.Client.DeltaV.Abilities;
+
+public sealed partial class HideUnderTableAbilitySystem : SharedCrawlUnderObjectsSystem
+{
+ [Dependency] private readonly AppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAppearanceChange);
+ }
+
+ private void OnAppearanceChange(EntityUid uid,
+ CrawlUnderObjectsComponent component,
+ AppearanceChangeEvent args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ _appearance.TryGetData(uid, SneakMode.Enabled, out bool enabled);
+ if (enabled)
+ {
+ if (component.OriginalDrawDepth != null)
+ return;
+
+ component.OriginalDrawDepth = sprite.DrawDepth;
+ sprite.DrawDepth = (int) DrawDepth.SmallMobs;
+ }
+ else
+ {
+ if (component.OriginalDrawDepth == null)
+ return;
+
+ sprite.DrawDepth = (int) component.OriginalDrawDepth;
+ component.OriginalDrawDepth = null;
+ }
+ }
+}
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index 60d2b6a6ef6..1c1f1984de4 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -1,8 +1,12 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading;
using Content.Client.Verbs;
-using Content.Shared.Eye.Blinding;
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;
@@ -13,15 +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 Content.Shared.Eye.Blinding.Components;
-using Robust.Client;
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
@@ -38,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;
@@ -77,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();
}
@@ -117,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;
}
@@ -360,10 +355,7 @@ public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid?
FormattedMessage message;
- // Basically this just predicts that we can't make out the entity if we have poor vision.
- var canSeeClearly = !HasComp(playerEnt);
-
- OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget: canSeeClearly);
+ OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false);
// Always update tooltip info from client first.
// If we get it wrong, server will correct us later anyway.
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
index 87931bf8455..f8d1c7e9720 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 168f352d1ab..135dc5522ac 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 00000000000..f47ad6ef1bb
--- /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 00000000000..a725fd4e4b5
--- /dev/null
+++ b/Content.Client/Guidebook/Richtext/ProtodataTag.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Guidebook.RichText;
+
+///
+/// RichText tag that can display values extracted from entity prototypes.
+/// In order to be accessed by this tag, the desired field/property must
+/// be tagged with .
+///
+public sealed class ProtodataTag : IMarkupTag
+{
+ [Dependency] private readonly ILogManager _logMan = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public string Name => "protodata";
+ private ISawmill Log => _log ??= _logMan.GetSawmill("protodata_tag");
+ private ISawmill? _log;
+
+ public string TextBefore(MarkupNode node)
+ {
+ // Do nothing with an empty tag
+ if (!node.Value.TryGetString(out var prototype))
+ return string.Empty;
+
+ if (!node.Attributes.TryGetValue("comp", out var component))
+ return string.Empty;
+ if (!node.Attributes.TryGetValue("member", out var member))
+ return string.Empty;
+ node.Attributes.TryGetValue("format", out var format);
+
+ var guidebookData = _entMan.System();
+
+ // Try to get the value
+ if (!guidebookData.TryGetValue(prototype, component.StringValue!, member.StringValue!, out var value))
+ {
+ Log.Error($"Failed to find protodata for {component}.{member} in {prototype}");
+ return "???";
+ }
+
+ // If we have a format string and a formattable value, format it as requested
+ if (!string.IsNullOrEmpty(format.StringValue) && value is IFormattable formattable)
+ return formattable.ToString(format.StringValue, CultureInfo.CurrentCulture);
+
+ // No format string given, so just use default ToString
+ return value?.ToString() ?? "NULL";
+ }
+}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index 97968c4b990..19d00a0bbf8 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -21,6 +21,7 @@
Orientation="Vertical">
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 9b96f5d3fe9..d61267d002c 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -73,6 +73,8 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
// Patient Information
SpriteView.SetEntity(target.Value);
+ SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
+ NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);
diff --git a/Content.Client/Info/InfoSection.xaml.cs b/Content.Client/Info/InfoSection.xaml.cs
index ab9d352d32f..9e10a4d7b4b 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 23be7506267..901fc913374 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/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 328cf41d0d4..1fd237cf3e3 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -4,23 +4,23 @@
using Content.Client.Clickable;
using Content.Client.DebugMon;
using Content.Client.Eui;
+using Content.Client.Fullscreen;
using Content.Client.GhostKick;
+using Content.Client.Guidebook;
using Content.Client.Launcher;
+using Content.Client.Mapping;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
+using Content.Client.Replay;
using Content.Client.Screenshot;
-using Content.Client.Fullscreen;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Administration.Logs;
-using Content.Client.Guidebook;
using Content.Client.Lobby;
-using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
-
namespace Content.Client.IoC
{
internal static class ClientContentIoC
@@ -49,6 +49,7 @@ public static void Register()
collection.Register();
collection.Register();
collection.Register();
+ collection.Register();
collection.Register();
}
}
diff --git a/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs b/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs
index 6b656123412..b9b58f23220 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 6482cdc1cc2..7a0627b3e23 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 9df793ee93c..246863ba60f 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/MachineLinking/UI/SignalTimerBoundUserInterface.cs b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
index 11abe8c2451..0607c768315 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/Mapping/MappingActionsButton.xaml b/Content.Client/Mapping/MappingActionsButton.xaml
new file mode 100644
index 00000000000..099719a70e1
--- /dev/null
+++ b/Content.Client/Mapping/MappingActionsButton.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/Content.Client/Mapping/MappingActionsButton.xaml.cs b/Content.Client/Mapping/MappingActionsButton.xaml.cs
new file mode 100644
index 00000000000..1a2f2c069f6
--- /dev/null
+++ b/Content.Client/Mapping/MappingActionsButton.xaml.cs
@@ -0,0 +1,15 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingActionsButton : Button
+{
+ public MappingActionsButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
+
diff --git a/Content.Client/Mapping/MappingDoNotMeasure.xaml b/Content.Client/Mapping/MappingDoNotMeasure.xaml
new file mode 100644
index 00000000000..08909636ee5
--- /dev/null
+++ b/Content.Client/Mapping/MappingDoNotMeasure.xaml
@@ -0,0 +1,4 @@
+
+
diff --git a/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs b/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs
new file mode 100644
index 00000000000..c4cb560234c
--- /dev/null
+++ b/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs
@@ -0,0 +1,21 @@
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingDoNotMeasure : Control
+{
+ public MappingDoNotMeasure()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ return Vector2.Zero;
+ }
+}
+
diff --git a/Content.Client/Mapping/MappingManager.cs b/Content.Client/Mapping/MappingManager.cs
new file mode 100644
index 00000000000..1aac02be714
--- /dev/null
+++ b/Content.Client/Mapping/MappingManager.cs
@@ -0,0 +1,69 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Content.Shared.Mapping;
+using Robust.Client.UserInterface;
+using Robust.Shared.Network;
+
+namespace Content.Client.Mapping;
+
+public sealed class MappingManager : IPostInjectInit
+{
+ [Dependency] private readonly IFileDialogManager _file = default!;
+ [Dependency] private readonly IClientNetManager _net = default!;
+
+ private Stream? _saveStream;
+ private MappingMapDataMessage? _mapData;
+
+ public void PostInject()
+ {
+ _net.RegisterNetMessage();
+ _net.RegisterNetMessage(OnSaveError);
+ _net.RegisterNetMessage(OnMapData);
+ }
+
+ private void OnSaveError(MappingSaveMapErrorMessage message)
+ {
+ _saveStream?.DisposeAsync();
+ _saveStream = null;
+ }
+
+ private async void OnMapData(MappingMapDataMessage message)
+ {
+ if (_saveStream == null)
+ {
+ _mapData = message;
+ return;
+ }
+
+ await _saveStream.WriteAsync(Encoding.ASCII.GetBytes(message.Yml));
+ await _saveStream.DisposeAsync();
+
+ _saveStream = null;
+ _mapData = null;
+ }
+
+ public async Task SaveMap()
+ {
+ if (_saveStream != null)
+ await _saveStream.DisposeAsync();
+
+ var request = new MappingSaveMapMessage();
+ _net.ClientSendMessage(request);
+
+ var path = await _file.SaveFile();
+ if (path is not { fileStream: var stream })
+ return;
+
+ if (_mapData != null)
+ {
+ await stream.WriteAsync(Encoding.ASCII.GetBytes(_mapData.Yml));
+ _mapData = null;
+ await stream.FlushAsync();
+ await stream.DisposeAsync();
+ return;
+ }
+
+ _saveStream = stream;
+ }
+}
diff --git a/Content.Client/Mapping/MappingOverlay.cs b/Content.Client/Mapping/MappingOverlay.cs
new file mode 100644
index 00000000000..ef9f3e795e6
--- /dev/null
+++ b/Content.Client/Mapping/MappingOverlay.cs
@@ -0,0 +1,84 @@
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using static Content.Client.Mapping.MappingState;
+
+namespace Content.Client.Mapping;
+
+public sealed class MappingOverlay : Overlay
+{
+ [Dependency] private readonly IEntityManager _entities = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPrototypeManager _prototypes = default!;
+
+ // 1 off in case something else uses these colors since we use them to compare
+ private static readonly Color PickColor = new(1, 255, 0);
+ private static readonly Color DeleteColor = new(255, 1, 0);
+
+ private readonly Dictionary _oldColors = new();
+
+ private readonly MappingState _state;
+ private readonly ShaderInstance _shader;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+ public MappingOverlay(MappingState state)
+ {
+ IoCManager.InjectDependencies(this);
+
+ _state = state;
+ _shader = _prototypes.Index("unshaded").Instance();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ foreach (var (id, color) in _oldColors)
+ {
+ if (!_entities.TryGetComponent(id, out SpriteComponent? sprite))
+ continue;
+
+ if (sprite.Color == DeleteColor || sprite.Color == PickColor)
+ sprite.Color = color;
+ }
+
+ _oldColors.Clear();
+
+ if (_player.LocalEntity == null)
+ return;
+
+ var handle = args.WorldHandle;
+ handle.UseShader(_shader);
+
+ switch (_state.State)
+ {
+ case CursorState.Pick:
+ {
+ if (_state.GetHoveredEntity() is { } entity &&
+ _entities.TryGetComponent(entity, out SpriteComponent? sprite))
+ {
+ _oldColors[entity] = sprite.Color;
+ sprite.Color = PickColor;
+ }
+
+ break;
+ }
+ case CursorState.Delete:
+ {
+ if (_state.GetHoveredEntity() is { } entity &&
+ _entities.TryGetComponent(entity, out SpriteComponent? sprite))
+ {
+ _oldColors[entity] = sprite.Color;
+ sprite.Color = DeleteColor;
+ }
+
+ break;
+ }
+ }
+
+ handle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Mapping/MappingPrototype.cs b/Content.Client/Mapping/MappingPrototype.cs
new file mode 100644
index 00000000000..eff2dfab151
--- /dev/null
+++ b/Content.Client/Mapping/MappingPrototype.cs
@@ -0,0 +1,39 @@
+using Content.Shared.Decals;
+using Content.Shared.Maps;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Mapping;
+
+///
+/// Used to represent a button's data in the mapping editor.
+///
+public sealed class MappingPrototype
+{
+ ///
+ /// The prototype instance, if any.
+ /// Can be one of , or
+ /// If null, this is a top-level button (such as Entities, Tiles or Decals)
+ ///
+ public readonly IPrototype? Prototype;
+
+ ///
+ /// The text to display on the UI for this button.
+ ///
+ public readonly string Name;
+
+ ///
+ /// Which other prototypes (buttons) this one is nested inside of.
+ ///
+ public List? Parents;
+
+ ///
+ /// Which other prototypes (buttons) are nested inside this one.
+ ///
+ public List? Children;
+
+ public MappingPrototype(IPrototype? prototype, string name)
+ {
+ Prototype = prototype;
+ Name = name;
+ }
+}
diff --git a/Content.Client/Mapping/MappingPrototypeList.xaml b/Content.Client/Mapping/MappingPrototypeList.xaml
new file mode 100644
index 00000000000..de311240df1
--- /dev/null
+++ b/Content.Client/Mapping/MappingPrototypeList.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Mapping/MappingPrototypeList.xaml.cs b/Content.Client/Mapping/MappingPrototypeList.xaml.cs
new file mode 100644
index 00000000000..8b59e6eb6f1
--- /dev/null
+++ b/Content.Client/Mapping/MappingPrototypeList.xaml.cs
@@ -0,0 +1,170 @@
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingPrototypeList : Control
+{
+ private (int start, int end) _lastIndices;
+ private readonly List _prototypes = new();
+ private readonly List _insertTextures = new();
+ private readonly List _search = new();
+
+ public MappingSpawnButton? Selected;
+ public Action>? GetPrototypeData;
+ public event Action? SelectionChanged;
+ public event Action? CollapseToggled;
+
+ public MappingPrototypeList()
+ {
+ RobustXamlLoader.Load(this);
+
+ MeasureButton.Measure(Vector2Helpers.Infinity);
+
+ ScrollContainer.OnScrolled += UpdateSearch;
+ OnResized += UpdateSearch;
+ }
+
+ public void UpdateVisible(List prototypes)
+ {
+ _prototypes.Clear();
+
+ PrototypeList.DisposeAllChildren();
+
+ _prototypes.AddRange(prototypes);
+
+ Selected = null;
+ ScrollContainer.SetScrollValue(new Vector2(0, 0));
+
+ foreach (var prototype in _prototypes)
+ {
+ Insert(PrototypeList, prototype, true);
+ }
+ }
+
+ public MappingSpawnButton Insert(Container list, MappingPrototype mapping, bool includeChildren)
+ {
+ var prototype = mapping.Prototype;
+
+ _insertTextures.Clear();
+
+ if (prototype != null)
+ GetPrototypeData?.Invoke(prototype, _insertTextures);
+
+ var button = new MappingSpawnButton { Prototype = mapping };
+ button.Label.Text = mapping.Name;
+
+ if (_insertTextures.Count > 0)
+ {
+ button.Texture.Textures.AddRange(_insertTextures);
+ button.Texture.InvalidateMeasure();
+ }
+ else
+ {
+ button.Texture.Visible = false;
+ }
+
+ if (prototype != null && button.Prototype == Selected?.Prototype)
+ {
+ Selected = button;
+ button.Button.Pressed = true;
+ }
+
+ list.AddChild(button);
+
+ button.Button.OnToggled += _ => SelectionChanged?.Invoke(button, prototype);
+
+ if (includeChildren && mapping.Children?.Count > 0)
+ {
+ button.CollapseButton.Visible = true;
+ button.CollapseButton.OnToggled += args => CollapseToggled?.Invoke(button, args);
+ }
+ else
+ {
+ button.CollapseButtonWrapper.Visible = false;
+ button.CollapseButton.Visible = false;
+ }
+
+ return button;
+ }
+
+ public void Search(List prototypes)
+ {
+ _search.Clear();
+ SearchList.DisposeAllChildren();
+ _lastIndices = (0, -1);
+
+ _search.AddRange(prototypes);
+ SearchList.TotalItemCount = _search.Count;
+ ScrollContainer.SetScrollValue(new Vector2(0, 0));
+
+ UpdateSearch();
+ }
+
+ ///
+ /// Constructs a virtual list where not all buttons exist at one time, since there may be thousands of them.
+ ///
+ private void UpdateSearch()
+ {
+ if (!SearchList.Visible)
+ return;
+
+ var height = MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation;
+ var offset = Math.Max(-SearchList.Position.Y, 0);
+ var startIndex = (int) Math.Floor(offset / height);
+ SearchList.ItemOffset = startIndex;
+
+ var (prevStart, prevEnd) = _lastIndices;
+ var endIndex = startIndex - 1;
+ var spaceUsed = -height;
+
+ // calculate how far down we are scrolled
+ while (spaceUsed < SearchList.Parent!.Height)
+ {
+ spaceUsed += height;
+ endIndex += 1;
+ }
+
+ endIndex = Math.Min(endIndex, _search.Count - 1);
+
+ // nothing changed in terms of which buttons are visible now and before
+ if (endIndex == prevEnd && startIndex == prevStart)
+ return;
+
+ _lastIndices = (startIndex, endIndex);
+
+ // remove previously seen but now unseen buttons from the top
+ for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
+ {
+ var control = SearchList.GetChild(0);
+ SearchList.RemoveChild(control);
+ }
+
+ // remove previously seen but now unseen buttons from the bottom
+ for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
+ {
+ var control = SearchList.GetChild(SearchList.ChildCount - 1);
+ SearchList.RemoveChild(control);
+ }
+
+ // insert buttons that can now be seen, from the start
+ for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
+ {
+ Insert(SearchList, _search[i], false).SetPositionInParent(0);
+ }
+
+ // insert buttons that can now be seen, from the end
+ for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
+ {
+ Insert(SearchList, _search[i], false);
+ }
+ }
+}
diff --git a/Content.Client/Mapping/MappingScreen.xaml b/Content.Client/Mapping/MappingScreen.xaml
new file mode 100644
index 00000000000..9cc3e734f0e
--- /dev/null
+++ b/Content.Client/Mapping/MappingScreen.xaml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs
new file mode 100644
index 00000000000..46c0e51fad6
--- /dev/null
+++ b/Content.Client/Mapping/MappingScreen.xaml.cs
@@ -0,0 +1,213 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Decals;
+using Content.Client.Decals.UI;
+using Content.Client.UserInterface.Screens;
+using Content.Client.UserInterface.Systems.Chat.Widgets;
+using Content.Shared.Decals;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingScreen : InGameScreen
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public DecalPlacementSystem DecalSystem = default!;
+
+ private PaletteColorPicker? _picker;
+
+ private ProtoId? _id;
+ private Color _decalColor = Color.White;
+ private float _decalRotation;
+ private bool _decalSnap;
+ private int _decalZIndex;
+ private bool _decalCleanable;
+
+ private bool _decalAuto;
+
+ public override ChatBox ChatBox => GetWidget()!;
+
+ public event Func? IsDecalVisible;
+
+ public MappingScreen()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ AutoscaleMaxResolution = new Vector2i(1080, 770);
+
+ SetAnchorPreset(ScreenContainer, LayoutPreset.Wide);
+ SetAnchorPreset(ViewportContainer, LayoutPreset.Wide);
+ SetAnchorPreset(SpawnContainer, LayoutPreset.Wide);
+ SetAnchorPreset(MainViewport, LayoutPreset.Wide);
+ SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
+ SetAnchorAndMarginPreset(Actions, LayoutPreset.TopWide, margin: 5);
+
+ ScreenContainer.OnSplitResizeFinished += () =>
+ OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
+
+ var rotationSpinBox = new FloatSpinBox(90.0f, 0)
+ {
+ HorizontalExpand = true
+ };
+ DecalSpinBoxContainer.AddChild(rotationSpinBox);
+
+ DecalColorPicker.OnColorChanged += OnDecalColorPicked;
+ DecalPickerOpen.OnPressed += OnDecalPickerOpenPressed;
+ rotationSpinBox.OnValueChanged += args =>
+ {
+ _decalRotation = args.Value;
+ UpdateDecal();
+ };
+ DecalEnableAuto.OnToggled += args =>
+ {
+ _decalAuto = args.Pressed;
+ if (_id is { } id)
+ SelectDecal(id);
+ };
+ DecalEnableSnap.OnToggled += args =>
+ {
+ _decalSnap = args.Pressed;
+ UpdateDecal();
+ };
+ DecalEnableCleanable.OnToggled += args =>
+ {
+ _decalCleanable = args.Pressed;
+ UpdateDecal();
+ };
+ DecalZIndexSpinBox.ValueChanged += args =>
+ {
+ _decalZIndex = args.Value;
+ UpdateDecal();
+ };
+
+ for (var i = 0; i < EntitySpawnWindow.InitOpts.Length; i++)
+ {
+ EntityPlacementMode.AddItem(EntitySpawnWindow.InitOpts[i], i);
+ }
+
+ Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
+ Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png";
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
+ Flip.OnPressed += args => FlipSides();
+ }
+
+ public void FlipSides()
+ {
+ ScreenContainer.Flip();
+
+ if (SpawnContainer.GetPositionInParent() == 0)
+ {
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
+ }
+ else
+ {
+ Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
+ }
+ }
+
+ private void OnDecalColorPicked(Color color)
+ {
+ _decalColor = color;
+ DecalColorPicker.Color = color;
+ UpdateDecal();
+ }
+
+ private void OnDecalPickerOpenPressed(ButtonEventArgs obj)
+ {
+ if (_picker == null)
+ {
+ _picker = new PaletteColorPicker();
+ _picker.OpenToLeft();
+ _picker.PaletteList.OnItemSelected += args =>
+ {
+ var color = ((Color?) args.ItemList.GetSelected().First().Metadata)!.Value;
+ OnDecalColorPicked(color);
+ };
+
+ return;
+ }
+
+ if (_picker.IsOpen)
+ _picker.Close();
+ else
+ _picker.Open();
+ }
+
+ private void UpdateDecal()
+ {
+ if (_id is not { } id)
+ return;
+
+ DecalSystem.UpdateDecalInfo(id, _decalColor, _decalRotation, _decalSnap, _decalZIndex, _decalCleanable);
+ }
+
+ public void SelectDecal(string decalId)
+ {
+ if (!_prototype.TryIndex(decalId, out var decal))
+ return;
+
+ _id = decalId;
+
+ if (_decalAuto)
+ {
+ _decalColor = Color.White;
+ _decalCleanable = decal.DefaultCleanable;
+ _decalSnap = decal.DefaultSnap;
+
+ DecalColorPicker.Color = _decalColor;
+ DecalEnableCleanable.Pressed = _decalCleanable;
+ DecalEnableSnap.Pressed = _decalSnap;
+ }
+
+ UpdateDecal();
+ RefreshList();
+ }
+
+ private void RefreshList()
+ {
+ foreach (var control in Prototypes.Children)
+ {
+ if (control is not MappingSpawnButton button ||
+ button.Prototype?.Prototype is not DecalPrototype)
+ {
+ continue;
+ }
+
+ foreach (var child in button.Children)
+ {
+ if (child is not MappingSpawnButton { Prototype.Prototype: DecalPrototype } childButton)
+ {
+ continue;
+ }
+
+ childButton.Texture.Modulate = _decalColor;
+ childButton.Visible = IsDecalVisible?.Invoke(childButton) ?? true;
+ }
+ }
+ }
+
+ public override void SetChatSize(Vector2 size)
+ {
+ ScreenContainer.DesiredSplitCenter = size.X;
+ ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
+ }
+
+ public void UnPressActionsExcept(Control except)
+ {
+ Add.Pressed = Add == except;
+ Fill.Pressed = Fill == except;
+ Grab.Pressed = Grab == except;
+ Move.Pressed = Move == except;
+ Pick.Pressed = Pick == except;
+ Delete.Pressed = Delete == except;
+ }
+}
diff --git a/Content.Client/Mapping/MappingSpawnButton.xaml b/Content.Client/Mapping/MappingSpawnButton.xaml
new file mode 100644
index 00000000000..a944d5ec2fd
--- /dev/null
+++ b/Content.Client/Mapping/MappingSpawnButton.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Mapping/MappingSpawnButton.xaml.cs b/Content.Client/Mapping/MappingSpawnButton.xaml.cs
new file mode 100644
index 00000000000..29fb884ed65
--- /dev/null
+++ b/Content.Client/Mapping/MappingSpawnButton.xaml.cs
@@ -0,0 +1,16 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingSpawnButton : Control
+{
+ public MappingPrototype? Prototype;
+
+ public MappingSpawnButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Client/Mapping/MappingState.cs b/Content.Client/Mapping/MappingState.cs
new file mode 100644
index 00000000000..bcc739fe4fc
--- /dev/null
+++ b/Content.Client/Mapping/MappingState.cs
@@ -0,0 +1,936 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Administration.Managers;
+using Content.Client.ContextMenu.UI;
+using Content.Client.Decals;
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Controls;
+using Content.Client.UserInterface.Systems.Gameplay;
+using Content.Client.Verbs;
+using Content.Shared.Administration;
+using Content.Shared.Decals;
+using Content.Shared.Input;
+using Content.Shared.Maps;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Placement;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Enums;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Markdown.Sequence;
+using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using static System.StringComparison;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+using static Robust.Client.UserInterface.Controls.LineEdit;
+using static Robust.Client.UserInterface.Controls.OptionButton;
+using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
+
+namespace Content.Client.Mapping;
+
+public sealed class MappingState : GameplayStateBase
+{
+ [Dependency] private readonly IClientAdminManager _admin = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IEntityNetworkManager _entityNetwork = default!;
+ [Dependency] private readonly IInputManager _input = default!;
+ [Dependency] private readonly ILogManager _log = default!;
+ [Dependency] private readonly IMapManager _mapMan = default!;
+ [Dependency] private readonly MappingManager _mapping = default!;
+ [Dependency] private readonly IOverlayManager _overlays = default!;
+ [Dependency] private readonly IPlacementManager _placement = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private EntityMenuUIController _entityMenuController = default!;
+
+ private DecalPlacementSystem _decal = default!;
+ private SpriteSystem _sprite = default!;
+ private TransformSystem _transform = default!;
+ private VerbSystem _verbs = default!;
+
+ private readonly ISawmill _sawmill;
+ private readonly GameplayStateLoadController _loadController;
+ private bool _setup;
+ private readonly List _allPrototypes = new();
+ private readonly Dictionary _allPrototypesDict = new();
+ private readonly Dictionary> _idDict = new();
+ private readonly List _prototypes = new();
+ private (TimeSpan At, MappingSpawnButton Button)? _lastClicked;
+ private Control? _scrollTo;
+ private bool _updatePlacement;
+ private bool _updateEraseDecal;
+
+ private MappingScreen Screen => (MappingScreen) UserInterfaceManager.ActiveScreen!;
+ private MainViewport Viewport => UserInterfaceManager.ActiveScreen!.GetWidget()!;
+
+ public CursorState State { get; set; }
+
+ public MappingState()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _sawmill = _log.GetSawmill("mapping");
+ _loadController = UserInterfaceManager.GetUIController();
+ }
+
+ protected override void Startup()
+ {
+ EnsureSetup();
+ base.Startup();
+
+ UserInterfaceManager.LoadScreen();
+ _loadController.LoadScreen();
+
+ var context = _input.Contexts.GetContext("common");
+ context.AddFunction(ContentKeyFunctions.MappingUnselect);
+ context.AddFunction(ContentKeyFunctions.SaveMap);
+ context.AddFunction(ContentKeyFunctions.MappingEnablePick);
+ context.AddFunction(ContentKeyFunctions.MappingEnableDelete);
+ context.AddFunction(ContentKeyFunctions.MappingPick);
+ context.AddFunction(ContentKeyFunctions.MappingRemoveDecal);
+ context.AddFunction(ContentKeyFunctions.MappingCancelEraseDecal);
+ context.AddFunction(ContentKeyFunctions.MappingOpenContextMenu);
+
+ Screen.DecalSystem = _decal;
+ Screen.Prototypes.SearchBar.OnTextChanged += OnSearch;
+ Screen.Prototypes.CollapseAllButton.OnPressed += OnCollapseAll;
+ Screen.Prototypes.ClearSearchButton.OnPressed += OnClearSearch;
+ Screen.Prototypes.GetPrototypeData += OnGetData;
+ Screen.Prototypes.SelectionChanged += OnSelected;
+ Screen.Prototypes.CollapseToggled += OnCollapseToggled;
+ Screen.Pick.OnPressed += OnPickPressed;
+ Screen.Delete.OnPressed += OnDeletePressed;
+ Screen.EntityReplaceButton.OnToggled += OnEntityReplacePressed;
+ Screen.EntityPlacementMode.OnItemSelected += OnEntityPlacementSelected;
+ Screen.EraseEntityButton.OnToggled += OnEraseEntityPressed;
+ Screen.EraseDecalButton.OnToggled += OnEraseDecalPressed;
+ _placement.PlacementChanged += OnPlacementChanged;
+
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.MappingUnselect, new PointerInputCmdHandler(HandleMappingUnselect, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.SaveMap, new PointerInputCmdHandler(HandleSaveMap, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingEnablePick, new PointerStateInputCmdHandler(HandleEnablePick, HandleDisablePick, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingEnableDelete, new PointerStateInputCmdHandler(HandleEnableDelete, HandleDisableDelete, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingPick, new PointerInputCmdHandler(HandlePick, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingRemoveDecal, new PointerInputCmdHandler(HandleEditorCancelPlace, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingCancelEraseDecal, new PointerInputCmdHandler(HandleCancelEraseDecal, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.MappingOpenContextMenu, new PointerInputCmdHandler(HandleOpenContextMenu, outsidePrediction: true))
+ .Register();
+
+ _overlays.AddOverlay(new MappingOverlay(this));
+
+ _prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
+
+ Screen.Prototypes.UpdateVisible(_prototypes);
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.WasModified() &&
+ !obj.WasModified() &&
+ !obj.WasModified())
+ {
+ return;
+ }
+
+ ReloadPrototypes();
+ }
+
+ private bool HandleOpenContextMenu(in PointerInputCmdArgs args)
+ {
+ Deselect();
+
+ var coords = args.Coordinates.ToMap(_entityManager, _transform);
+ if (_verbs.TryGetEntityMenuEntities(coords, out var entities))
+ _entityMenuController.OpenRootMenu(entities);
+
+ return true;
+ }
+
+ protected override void Shutdown()
+ {
+ CommandBinds.Unregister();
+
+ Screen.Prototypes.SearchBar.OnTextChanged -= OnSearch;
+ Screen.Prototypes.CollapseAllButton.OnPressed -= OnCollapseAll;
+ Screen.Prototypes.ClearSearchButton.OnPressed -= OnClearSearch;
+ Screen.Prototypes.GetPrototypeData -= OnGetData;
+ Screen.Prototypes.SelectionChanged -= OnSelected;
+ Screen.Prototypes.CollapseToggled -= OnCollapseToggled;
+ Screen.Pick.OnPressed -= OnPickPressed;
+ Screen.Delete.OnPressed -= OnDeletePressed;
+ Screen.EntityReplaceButton.OnToggled -= OnEntityReplacePressed;
+ Screen.EntityPlacementMode.OnItemSelected -= OnEntityPlacementSelected;
+ Screen.EraseEntityButton.OnToggled -= OnEraseEntityPressed;
+ Screen.EraseDecalButton.OnToggled -= OnEraseDecalPressed;
+ _placement.PlacementChanged -= OnPlacementChanged;
+ _prototypeManager.PrototypesReloaded -= OnPrototypesReloaded;
+
+ UserInterfaceManager.ClearWindows();
+ _loadController.UnloadScreen();
+ UserInterfaceManager.UnloadScreen();
+
+ var context = _input.Contexts.GetContext("common");
+ context.RemoveFunction(ContentKeyFunctions.MappingUnselect);
+ context.RemoveFunction(ContentKeyFunctions.SaveMap);
+ context.RemoveFunction(ContentKeyFunctions.MappingEnablePick);
+ context.RemoveFunction(ContentKeyFunctions.MappingEnableDelete);
+ context.RemoveFunction(ContentKeyFunctions.MappingPick);
+ context.RemoveFunction(ContentKeyFunctions.MappingRemoveDecal);
+ context.RemoveFunction(ContentKeyFunctions.MappingCancelEraseDecal);
+ context.RemoveFunction(ContentKeyFunctions.MappingOpenContextMenu);
+
+ _overlays.RemoveOverlay();
+
+ base.Shutdown();
+ }
+
+ private void EnsureSetup()
+ {
+ if (_setup)
+ return;
+
+ _setup = true;
+
+ _entityMenuController = UserInterfaceManager.GetUIController();
+
+ _decal = _entityManager.System();
+ _sprite = _entityManager.System();
+ _transform = _entityManager.System();
+ _verbs = _entityManager.System();
+ ReloadPrototypes();
+ }
+
+ private void ReloadPrototypes()
+ {
+ var entities = new MappingPrototype(null, Loc.GetString("mapping-entities")) { Children = new List() };
+ _prototypes.Add(entities);
+
+ var mappings = new Dictionary();
+ foreach (var entity in _prototypeManager.EnumeratePrototypes())
+ {
+ Register(entity, entity.ID, entities);
+ }
+
+ Sort(mappings, entities);
+ mappings.Clear();
+
+ var tiles = new MappingPrototype(null, Loc.GetString("mapping-tiles")) { Children = new List() };
+ _prototypes.Add(tiles);
+
+ foreach (var tile in _prototypeManager.EnumeratePrototypes())
+ {
+ Register(tile, tile.ID, tiles);
+ }
+
+ Sort(mappings, tiles);
+ mappings.Clear();
+
+ var decals = new MappingPrototype(null, Loc.GetString("mapping-decals")) { Children = new List() };
+ _prototypes.Add(decals);
+
+ foreach (var decal in _prototypeManager.EnumeratePrototypes())
+ {
+ Register(decal, decal.ID, decals);
+ }
+
+ Sort(mappings, decals);
+ mappings.Clear();
+ }
+
+ private void Sort(Dictionary prototypes, MappingPrototype topLevel)
+ {
+ static int Compare(MappingPrototype a, MappingPrototype b)
+ {
+ return string.Compare(a.Name, b.Name, OrdinalIgnoreCase);
+ }
+
+ topLevel.Children ??= new List();
+
+ foreach (var prototype in prototypes.Values)
+ {
+ if (prototype.Parents == null && prototype != topLevel)
+ {
+ prototype.Parents = new List { topLevel };
+ topLevel.Children.Add(prototype);
+ }
+
+ prototype.Parents?.Sort(Compare);
+ prototype.Children?.Sort(Compare);
+ }
+
+ topLevel.Children.Sort(Compare);
+ }
+
+ private MappingPrototype? Register(T? prototype, string id, MappingPrototype topLevel) where T : class, IPrototype, IInheritingPrototype
+ {
+ {
+ if (prototype == null &&
+ _prototypeManager.TryIndex(id, out prototype) &&
+ prototype is EntityPrototype entity)
+ {
+ if (entity.HideSpawnMenu || entity.Abstract)
+ prototype = null;
+ }
+ }
+
+ if (prototype == null)
+ {
+ if (!_prototypeManager.TryGetMapping(typeof(T), id, out var node))
+ {
+ _sawmill.Error($"No {nameof(T)} found with id {id}");
+ return null;
+ }
+
+ var ids = _idDict.GetOrNew(typeof(T));
+ if (ids.TryGetValue(id, out var mapping))
+ {
+ return mapping;
+ }
+ else
+ {
+ var name = node.TryGet("name", out ValueDataNode? nameNode)
+ ? nameNode.Value
+ : id;
+
+ if (node.TryGet("suffix", out ValueDataNode? suffix))
+ name = $"{name} [{suffix.Value}]";
+
+ mapping = new MappingPrototype(prototype, name);
+ _allPrototypes.Add(mapping);
+ ids.Add(id, mapping);
+
+ if (node.TryGet("parent", out ValueDataNode? parentValue))
+ {
+ var parent = Register(null, parentValue.Value, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List();
+ parent.Children.Add(mapping);
+ }
+ }
+ else if (node.TryGet("parent", out SequenceDataNode? parentSequence))
+ {
+ foreach (var parentNode in parentSequence.Cast())
+ {
+ var parent = Register(null, parentNode.Value, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List();
+ parent.Children.Add(mapping);
+ }
+ }
+ }
+ else
+ {
+ topLevel.Children ??= new List();
+ topLevel.Children.Add(mapping);
+ mapping.Parents ??= new List();
+ mapping.Parents.Add(topLevel);
+ }
+
+ return mapping;
+ }
+ }
+ else
+ {
+ var ids = _idDict.GetOrNew(typeof(T));
+ if (ids.TryGetValue(id, out var mapping))
+ {
+ return mapping;
+ }
+ else
+ {
+ var entity = prototype as EntityPrototype;
+ var name = entity?.Name ?? prototype.ID;
+
+ if (!string.IsNullOrWhiteSpace(entity?.EditorSuffix))
+ name = $"{name} [{entity.EditorSuffix}]";
+
+ mapping = new MappingPrototype(prototype, name);
+ _allPrototypes.Add(mapping);
+ _allPrototypesDict.Add(prototype, mapping);
+ ids.Add(prototype.ID, mapping);
+ }
+
+ if (prototype.Parents == null)
+ {
+ topLevel.Children ??= new List();
+ topLevel.Children.Add(mapping);
+ mapping.Parents ??= new List();
+ mapping.Parents.Add(topLevel);
+ return mapping;
+ }
+
+ foreach (var parentId in prototype.Parents)
+ {
+ var parent = Register(null, parentId, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List();
+ parent.Children.Add(mapping);
+ }
+ }
+
+ return mapping;
+ }
+ }
+
+ private void OnPlacementChanged(object? sender, EventArgs e)
+ {
+ _updatePlacement = true;
+ }
+
+ protected override void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args)
+ {
+ if (args.Viewport == null)
+ base.OnKeyBindStateChanged(new ViewportBoundKeyEventArgs(args.KeyEventArgs, Viewport.Viewport));
+ else
+ base.OnKeyBindStateChanged(args);
+ }
+
+ private void OnSearch(LineEditEventArgs args)
+ {
+ if (string.IsNullOrEmpty(args.Text))
+ {
+ Screen.Prototypes.PrototypeList.Visible = true;
+ Screen.Prototypes.SearchList.Visible = false;
+ return;
+ }
+
+ var matches = new List();
+ foreach (var prototype in _allPrototypes)
+ {
+ if (prototype.Name.Contains(args.Text, OrdinalIgnoreCase))
+ matches.Add(prototype);
+ }
+
+ matches.Sort(static (a, b) => string.Compare(a.Name, b.Name, OrdinalIgnoreCase));
+
+ Screen.Prototypes.PrototypeList.Visible = false;
+ Screen.Prototypes.SearchList.Visible = true;
+ Screen.Prototypes.Search(matches);
+ }
+
+ private void OnCollapseAll(ButtonEventArgs args)
+ {
+ foreach (var child in Screen.Prototypes.PrototypeList.Children)
+ {
+ if (child is not MappingSpawnButton button)
+ continue;
+
+ Collapse(button);
+ }
+
+ Screen.Prototypes.ScrollContainer.SetScrollValue(new Vector2(0, 0));
+ }
+
+ private void OnClearSearch(ButtonEventArgs obj)
+ {
+ Screen.Prototypes.SearchBar.Text = string.Empty;
+ OnSearch(new LineEditEventArgs(Screen.Prototypes.SearchBar, string.Empty));
+ }
+
+ private void OnGetData(IPrototype prototype, List textures)
+ {
+ switch (prototype)
+ {
+ case EntityPrototype entity:
+ textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
+ break;
+ case DecalPrototype decal:
+ textures.Add(_sprite.Frame0(decal.Sprite));
+ break;
+ case ContentTileDefinition tile:
+ if (tile.Sprite?.ToString() is { } sprite)
+ textures.Add(_resources.GetResource(sprite).Texture);
+ break;
+ }
+ }
+
+ private void OnSelected(MappingPrototype mapping)
+ {
+ if (mapping.Prototype == null)
+ return;
+
+ var chain = new Stack();
+ chain.Push(mapping);
+
+ var parent = mapping.Parents?.FirstOrDefault();
+ while (parent != null)
+ {
+ chain.Push(parent);
+ parent = parent.Parents?.FirstOrDefault();
+ }
+
+ _lastClicked = null;
+
+ Control? last = null;
+ var children = Screen.Prototypes.PrototypeList.Children;
+ foreach (var prototype in chain)
+ {
+ foreach (var child in children)
+ {
+ if (child is MappingSpawnButton button &&
+ button.Prototype == prototype)
+ {
+ UnCollapse(button);
+ OnSelected(button, prototype.Prototype);
+ children = button.ChildrenPrototypes.Children;
+ last = child;
+ break;
+ }
+ }
+ }
+
+ if (last != null && Screen.Prototypes.PrototypeList.Visible)
+ _scrollTo = last;
+ }
+
+ private void OnSelected(MappingSpawnButton button, IPrototype? prototype)
+ {
+ var time = _timing.CurTime;
+ if (prototype is DecalPrototype)
+ Screen.SelectDecal(prototype.ID);
+
+ // Double-click functionality if it's collapsible.
+ if (_lastClicked is { } lastClicked &&
+ lastClicked.Button == button &&
+ lastClicked.At > time - TimeSpan.FromSeconds(0.333) &&
+ string.IsNullOrEmpty(Screen.Prototypes.SearchBar.Text) &&
+ button.CollapseButton.Visible)
+ {
+ button.CollapseButton.Pressed = !button.CollapseButton.Pressed;
+ ToggleCollapse(button);
+ button.Button.Pressed = true;
+ Screen.Prototypes.Selected = button;
+ _lastClicked = null;
+ return;
+ }
+
+ // Toggle if it's the same button (at least if we just unclicked it).
+ if (!button.Button.Pressed && button.Prototype?.Prototype != null && _lastClicked?.Button == button)
+ {
+ _lastClicked = null;
+ Deselect();
+ return;
+ }
+
+ _lastClicked = (time, button);
+
+ if (button.Prototype == null)
+ return;
+
+ if (Screen.Prototypes.Selected is { } oldButton &&
+ oldButton != button)
+ {
+ Deselect();
+ }
+
+ Screen.EntityContainer.Visible = false;
+ Screen.DecalContainer.Visible = false;
+
+ switch (prototype)
+ {
+ case EntityPrototype entity:
+ {
+ var placementId = Screen.EntityPlacementMode.SelectedId;
+
+ var placement = new PlacementInformation
+ {
+ PlacementOption = placementId > 0 ? EntitySpawnWindow.InitOpts[placementId] : entity.PlacementMode,
+ EntityType = entity.ID,
+ IsTile = false
+ };
+
+ Screen.EntityContainer.Visible = true;
+ _decal.SetActive(false);
+ _placement.BeginPlacing(placement);
+ break;
+ }
+ case DecalPrototype decal:
+ _placement.Clear();
+
+ _decal.SetActive(true);
+ _decal.UpdateDecalInfo(decal.ID, Color.White, 0, true, 0, false);
+ Screen.DecalContainer.Visible = true;
+ break;
+ case ContentTileDefinition tile:
+ {
+ var placement = new PlacementInformation
+ {
+ PlacementOption = "AlignTileAny",
+ TileType = tile.TileId,
+ IsTile = true
+ };
+
+ _decal.SetActive(false);
+ _placement.BeginPlacing(placement);
+ break;
+ }
+ default:
+ _placement.Clear();
+ break;
+ }
+
+ Screen.Prototypes.Selected = button;
+
+ button.Button.Pressed = true;
+ }
+
+ private void Deselect()
+ {
+ if (Screen.Prototypes.Selected is { } selected)
+ {
+ selected.Button.Pressed = false;
+ Screen.Prototypes.Selected = null;
+
+ if (selected.Prototype?.Prototype is DecalPrototype)
+ {
+ _decal.SetActive(false);
+ Screen.DecalContainer.Visible = false;
+ }
+
+ if (selected.Prototype?.Prototype is EntityPrototype)
+ {
+ _placement.Clear();
+ }
+
+ if (selected.Prototype?.Prototype is ContentTileDefinition)
+ {
+ _placement.Clear();
+ }
+ }
+ }
+
+ private void OnCollapseToggled(MappingSpawnButton button, ButtonToggledEventArgs args)
+ {
+ ToggleCollapse(button);
+ }
+
+ private void OnPickPressed(ButtonEventArgs args)
+ {
+ if (args.Button.Pressed)
+ EnablePick();
+ else
+ DisablePick();
+ }
+
+ private void OnDeletePressed(ButtonEventArgs obj)
+ {
+ if (obj.Button.Pressed)
+ EnableDelete();
+ else
+ DisableDelete();
+ }
+
+ private void OnEntityReplacePressed(ButtonToggledEventArgs args)
+ {
+ _placement.Replacement = args.Pressed;
+ }
+
+ private void OnEntityPlacementSelected(ItemSelectedEventArgs args)
+ {
+ Screen.EntityPlacementMode.SelectId(args.Id);
+
+ if (_placement.CurrentMode != null)
+ {
+ var placement = new PlacementInformation
+ {
+ PlacementOption = EntitySpawnWindow.InitOpts[args.Id],
+ EntityType = _placement.CurrentPermission!.EntityType,
+ TileType = _placement.CurrentPermission.TileType,
+ Range = 2,
+ IsTile = _placement.CurrentPermission.IsTile,
+ };
+
+ _placement.BeginPlacing(placement);
+ }
+ }
+
+ private void OnEraseEntityPressed(ButtonEventArgs args)
+ {
+ if (args.Button.Pressed == _placement.Eraser)
+ return;
+
+ if (args.Button.Pressed)
+ EnableEraser();
+ else
+ DisableEraser();
+ }
+
+ private void OnEraseDecalPressed(ButtonToggledEventArgs args)
+ {
+ _placement.Clear();
+ Deselect();
+ Screen.EraseEntityButton.Pressed = false;
+ _updatePlacement = true;
+ _updateEraseDecal = args.Pressed;
+ }
+
+ private void EnableEraser()
+ {
+ if (_placement.Eraser)
+ return;
+
+ _placement.Clear();
+ _placement.ToggleEraser();
+ Screen.EntityPlacementMode.Disabled = true;
+ Screen.EraseDecalButton.Pressed = false;
+ Deselect();
+ }
+
+ private void DisableEraser()
+ {
+ if (!_placement.Eraser)
+ return;
+
+ _placement.ToggleEraser();
+ Screen.EntityPlacementMode.Disabled = false;
+ }
+
+ private void EnablePick()
+ {
+ Screen.UnPressActionsExcept(Screen.Pick);
+ State = CursorState.Pick;
+ }
+
+ private void DisablePick()
+ {
+ Screen.Pick.Pressed = false;
+ State = CursorState.None;
+ }
+
+ private void EnableDelete()
+ {
+ Screen.UnPressActionsExcept(Screen.Delete);
+ State = CursorState.Delete;
+ EnableEraser();
+ }
+
+ private void DisableDelete()
+ {
+ Screen.Delete.Pressed = false;
+ State = CursorState.None;
+ DisableEraser();
+ }
+
+ private bool HandleMappingUnselect(in PointerInputCmdArgs args)
+ {
+ if (Screen.Prototypes.Selected is not { Prototype.Prototype: DecalPrototype })
+ return false;
+
+ Deselect();
+ return true;
+ }
+
+ private bool HandleSaveMap(in PointerInputCmdArgs args)
+ {
+#if FULL_RELEASE
+ return false;
+#endif
+ if (!_admin.IsAdmin(true) || !_admin.HasFlag(AdminFlags.Host))
+ return false;
+
+ SaveMap();
+ return true;
+ }
+
+ private bool HandleEnablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ EnablePick();
+ return true;
+ }
+
+ private bool HandleDisablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ DisablePick();
+ return true;
+ }
+
+ private bool HandleEnableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ EnableDelete();
+ return true;
+ }
+
+ private bool HandleDisableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ DisableDelete();
+ return true;
+ }
+
+ private bool HandlePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ if (State != CursorState.Pick)
+ return false;
+
+ MappingPrototype? button = null;
+
+ // Try and get tile under it
+ // TODO: Separate mode for decals.
+ if (!uid.IsValid())
+ {
+ var mapPos = _transform.ToMapCoordinates(coords);
+
+ if (_mapMan.TryFindGridAt(mapPos, out var gridUid, out var grid) &&
+ _entityManager.System().TryGetTileRef(gridUid, grid, coords, out var tileRef) &&
+ _allPrototypesDict.TryGetValue(tileRef.GetContentTileDefinition(), out button))
+ {
+ OnSelected(button);
+ return true;
+ }
+ }
+
+ if (button == null)
+ {
+ if (uid == EntityUid.Invalid ||
+ _entityManager.GetComponentOrNull(uid) is not { EntityPrototype: { } prototype } ||
+ !_allPrototypesDict.TryGetValue(prototype, out button))
+ {
+ // we always block other input handlers if pick mode is enabled
+ // this makes you not accidentally place something in space because you
+ // miss-clicked while holding down the pick hotkey
+ return true;
+ }
+
+ // Selected an entity
+ OnSelected(button);
+
+ // Match rotation
+ _placement.Direction = _entityManager.GetComponent(uid).LocalRotation.GetDir();
+ }
+
+ return true;
+ }
+
+ private bool HandleEditorCancelPlace(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ if (!Screen.EraseDecalButton.Pressed)
+ return false;
+
+ _entityNetwork.SendSystemNetworkMessage(new RequestDecalRemovalEvent(_entityManager.GetNetCoordinates(coords)));
+ return true;
+ }
+
+ private bool HandleCancelEraseDecal(in PointerInputCmdArgs args)
+ {
+ if (!Screen.EraseDecalButton.Pressed)
+ return false;
+
+ Screen.EraseDecalButton.Pressed = false;
+ return true;
+ }
+
+ private async void SaveMap()
+ {
+ await _mapping.SaveMap();
+ }
+
+ private void ToggleCollapse(MappingSpawnButton button)
+ {
+ if (button.CollapseButton.Pressed)
+ {
+ if (button.Prototype?.Children != null)
+ {
+ foreach (var child in button.Prototype.Children)
+ {
+ Screen.Prototypes.Insert(button.ChildrenPrototypes, child, true);
+ }
+ }
+
+ button.CollapseButton.Label.Text = "▼";
+ }
+ else
+ {
+ button.ChildrenPrototypes.DisposeAllChildren();
+ button.CollapseButton.Label.Text = "▶";
+ }
+ }
+
+ private void Collapse(MappingSpawnButton button)
+ {
+ if (!button.CollapseButton.Pressed)
+ return;
+
+ button.CollapseButton.Pressed = false;
+ ToggleCollapse(button);
+ }
+
+
+ private void UnCollapse(MappingSpawnButton button)
+ {
+ if (button.CollapseButton.Pressed)
+ return;
+
+ button.CollapseButton.Pressed = true;
+ ToggleCollapse(button);
+ }
+
+ public EntityUid? GetHoveredEntity()
+ {
+ if (UserInterfaceManager.CurrentlyHovered is not IViewportControl viewport ||
+ _input.MouseScreenPosition is not { IsValid: true } position)
+ {
+ return null;
+ }
+
+ var mapPos = viewport.PixelToMap(position.Position);
+ return GetClickedEntity(mapPos);
+ }
+
+ public override void FrameUpdate(FrameEventArgs e)
+ {
+ if (_updatePlacement)
+ {
+ _updatePlacement = false;
+
+ if (!_placement.IsActive && _decal.GetActiveDecal().Decal == null)
+ Deselect();
+
+ Screen.EraseEntityButton.Pressed = _placement.Eraser;
+ Screen.EraseDecalButton.Pressed = _updateEraseDecal;
+ Screen.EntityPlacementMode.Disabled = _placement.Eraser;
+ }
+
+ if (_scrollTo is not { } scrollTo)
+ return;
+
+ // this is not ideal but we wait until the control's height is computed to use
+ // its position to scroll to
+ if (scrollTo.Height > 0 && Screen.Prototypes.PrototypeList.Visible)
+ {
+ var y = scrollTo.GlobalPosition.Y - Screen.Prototypes.ScrollContainer.Height / 2 + scrollTo.Height;
+ var scroll = Screen.Prototypes.ScrollContainer;
+ scroll.SetScrollValue(scroll.GetScrollValue() + new Vector2(0, y));
+ _scrollTo = null;
+ }
+ }
+
+
+ // TODO this doesn't handle pressing down multiple state hotkeys at the moment
+ public enum CursorState
+ {
+ None,
+ Pick,
+ Delete
+ }
+}
diff --git a/Content.Client/Mapping/MappingSystem.cs b/Content.Client/Mapping/MappingSystem.cs
index 8daf193dfeb..80189fbdfc1 100644
--- a/Content.Client/Mapping/MappingSystem.cs
+++ b/Content.Client/Mapping/MappingSystem.cs
@@ -13,7 +13,6 @@ public sealed partial class MappingSystem : EntitySystem
{
[Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
- [Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
///
@@ -26,8 +25,6 @@ public sealed partial class MappingSystem : EntitySystem
///
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
- public string DefaultMappingActions = "/mapping_actions.yml";
-
public override void Initialize()
{
base.Initialize();
@@ -36,11 +33,6 @@ public override void Initialize()
SubscribeLocalEvent(OnStartPlacementAction);
}
- public void LoadMappingActions()
- {
- _actionsSystem.LoadActionAssignments(DefaultMappingActions, false);
- }
-
///
/// This checks if the placement manager is currently active, and attempts to copy the placement information for
/// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
index 2b600845cae..f4fb9da0622 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 5e068f1e9c5..90a66bec7f3 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 22e5bc452a0..4f21361990a 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 c059ce785af..af1f9a94414 100644
--- a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
+++ b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
@@ -4,6 +4,7 @@
using Content.Shared.MassMedia.Systems;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.MassMedia.Ui;
@@ -16,6 +17,8 @@ public sealed partial class NewsWriterMenu : FancyWindow
public event Action? DeleteButtonPressed;
+ public event Action? CreateButtonPressed;
+
public NewsWriterMenu()
{
RobustXamlLoader.Load(this);
@@ -31,7 +34,7 @@ public NewsWriterMenu()
ButtonCreate.OnPressed += OnCreate;
}
- public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish)
+ public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish, string draftTitle, string draftContent)
{
ArticlesContainer.Children.Clear();
ArticleCount.Text = Loc.GetString("news-write-ui-article-count-text", ("count", articles.Length));
@@ -54,6 +57,9 @@ public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextP
ButtonCreate.Disabled = !publishEnabled;
_nextPublish = nextPublish;
+
+ ArticleEditorPanel.TitleField.Text = draftTitle;
+ ArticleEditorPanel.ContentField.TextRope = new Rope.Leaf(draftContent);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -93,5 +99,6 @@ protected override void Dispose(bool disposing)
private void OnCreate(BaseButton.ButtonEventArgs buttonEventArgs)
{
ArticleEditorPanel.Visible = true;
+ CreateButtonPressed?.Invoke();
}
}
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
index 660f2e5e11f..dd40749d33b 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
@@ -15,6 +15,9 @@
+
+
departmentSens
// Populate departments
foreach (var sensor in departmentSensors)
{
+ if (!string.IsNullOrEmpty(SearchLineEdit.Text)
+ && !sensor.Name.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase)
+ && !sensor.Job.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// Add a button that will hold a username and other details
diff --git a/Content.Client/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs
index 7ff6390764b..ee3c00fa1b8 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 00000000000..b23835b36ee
--- /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 00000000000..294cab30ca8
--- /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 c662111c3ed..00000000000
--- 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 b498d0e3bbc..aa757584733 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 e571c5a856c..c708c6fe7d2 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 37ce9c4280f..2d4033390c3 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 8b26860332d..8c9b4ae2ee6 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 74a5e7afdcd..c97110b208e 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.Owner, 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.Owner, 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 e0b3499a974..9cc2831d212 100644
--- a/Content.Client/Physics/JointVisualsOverlay.cs
+++ b/Content.Client/Physics/JointVisualsOverlay.cs
@@ -1,9 +1,8 @@
+using System.Numerics;
using Content.Shared.Physics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Dynamics.Joints;
namespace Content.Client.Physics;
@@ -16,8 +15,6 @@ public sealed class JointVisualsOverlay : Overlay
private IEntityManager _entManager;
- private HashSet _drawn = new();
-
public JointVisualsOverlay(IEntityManager entManager)
{
_entManager = entManager;
@@ -25,7 +22,6 @@ public JointVisualsOverlay(IEntityManager entManager)
protected override void Draw(in OverlayDrawArgs args)
{
- _drawn.Clear();
var worldHandle = args.WorldHandle;
var spriteSystem = _entManager.System();
@@ -33,12 +29,14 @@ protected override void Draw(in OverlayDrawArgs args)
var joints = _entManager.EntityQueryEnumerator();
var xformQuery = _entManager.GetEntityQuery();
+ args.DrawingHandle.SetTransform(Matrix3x2.Identity);
+
while (joints.MoveNext(out var visuals, out var xform))
{
if (xform.MapID != args.MapId)
continue;
- var other = visuals.Target;
+ var other = _entManager.GetEntity(visuals.Target);
if (!xformQuery.TryGetComponent(other, out var otherXform))
continue;
diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
new file mode 100644
index 00000000000..e1c55131cd6
--- /dev/null
+++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
new file mode 100644
index 00000000000..a4d4055c7df
--- /dev/null
+++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
@@ -0,0 +1,50 @@
+using Content.Shared.Pinpointer;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+
+namespace Content.Client.Pinpointer.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationMapBeaconControl : Control, IComparable
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public readonly EntityCoordinates BeaconPosition;
+ public Action? OnPressed;
+ public string? Label => BeaconNameLabel.Text;
+ private StyleBoxFlat _styleBox;
+ public Color Color => _styleBox.BackgroundColor;
+
+ public StationMapBeaconControl(EntityUid mapUid, SharedNavMapSystem.NavMapBeacon beacon)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ BeaconPosition = new EntityCoordinates(mapUid, beacon.Position);
+
+ _styleBox = new StyleBoxFlat { BackgroundColor = beacon.Color };
+ ColorPanel.PanelOverride = _styleBox;
+ BeaconNameLabel.Text = beacon.Text;
+
+ MainButton.OnPressed += args => OnPressed?.Invoke(BeaconPosition);
+ }
+
+ public int CompareTo(StationMapBeaconControl? other)
+ {
+ if (other == null)
+ return 1;
+
+ // Group by color
+ var colorCompare = Color.ToArgb().CompareTo(other.Color.ToArgb());
+ if (colorCompare != 0)
+ {
+ return colorCompare;
+ }
+
+ // If same color, sort by text
+ return string.Compare(Label, other.Label);
+ }
+}
diff --git a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
index 91fb4ef71bd..3d1eb1723c3 100644
--- a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
+++ b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
@@ -24,9 +24,16 @@ protected override void Open()
_window = this.CreateWindow();
_window.Title = EntMan.GetComponent(Owner).EntityName;
+
+ string stationName = string.Empty;
+ if(EntMan.TryGetComponent(gridUid, out var gridMetaData))
+ {
+ stationName = gridMetaData.EntityName;
+ }
+
if (EntMan.TryGetComponent(Owner, out var comp) && comp.ShowLocation)
- _window.Set(gridUid, Owner);
+ _window.Set(stationName, gridUid, Owner);
else
- _window.Set(gridUid, null);
+ _window.Set(stationName, gridUid, null);
}
}
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
index 00424a3566a..c79fc8f9e7b 100644
--- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml
+++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
@@ -3,11 +3,28 @@
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
Title="{Loc 'station-map-window-title'}"
Resizable="False"
- SetSize="668 713"
- MinSize="668 713">
+ SetSize="868 748"
+ MinSize="868 748">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
index 7cbb8b7d0db..52ef2ab7da4 100644
--- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
+++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
@@ -3,24 +3,75 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
+using Content.Shared.Pinpointer;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapWindow : FancyWindow
{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ private readonly List _buttons = new();
+
public StationMapWindow()
{
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ FilterBar.OnTextChanged += (bar) => OnFilterChanged(bar.Text);
}
- public void Set(EntityUid? mapUid, EntityUid? trackedEntity)
+ public void Set(string stationName, EntityUid? mapUid, EntityUid? trackedEntity)
{
NavMapScreen.MapUid = mapUid;
if (trackedEntity != null)
NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Cyan));
+ if (!string.IsNullOrEmpty(stationName))
+ {
+ StationName.Text = stationName;
+ }
+
NavMapScreen.ForceNavMapUpdate();
+ UpdateBeaconList(mapUid);
+ }
+
+ public void OnFilterChanged(string newFilter)
+ {
+ foreach (var button in _buttons)
+ {
+ button.Visible = string.IsNullOrEmpty(newFilter) || (
+ !string.IsNullOrEmpty(button.Label) &&
+ button.Label.Contains(newFilter, StringComparison.OrdinalIgnoreCase)
+ );
+ };
+ }
+
+ public void UpdateBeaconList(EntityUid? mapUid)
+ {
+ BeaconButtons.Children.Clear();
+ _buttons.Clear();
+
+ if (!mapUid.HasValue)
+ return;
+
+ if (!_entMan.TryGetComponent(mapUid, out var navMap))
+ return;
+
+ foreach (var beacon in navMap.Beacons.Values)
+ {
+ var button = new StationMapBeaconControl(mapUid.Value, beacon);
+
+ button.OnPressed += NavMapScreen.CenterToCoordinates;
+
+ _buttons.Add(button);
+ }
+
+ _buttons.Sort();
+
+ foreach (var button in _buttons)
+ BeaconButtons.AddChild(button);
}
-}
+}
\ No newline at end of file
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 771d23cb081..7056a15e0e4 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;
@@ -134,7 +134,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 759a5949ba6..a790c5d984a 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 5a082485a5a..a6a20958f53 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/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
index d9952992070..3f7ccfb903b 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 f90731bfa75..b96eae44e9d 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 00000000000..223895eb29c
--- /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 00000000000..5f77a66e535
--- /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 00000000000..088c9a291a7
--- /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 87d7e62c392..06e5674d9cb 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 9c9f83a4275..7108e4cca8f 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/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 64ead32586d..2674343e059 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -199,7 +199,9 @@ protected override void Draw(DrawingHandleScreen handle)
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
- var color = _shuttles.GetIFFColor(grid, self: false, iff);
+
+ var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
+ var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
// Others default:
// Color.FromHex("#FFC000FF")
@@ -213,25 +215,52 @@ protected override void Draw(DrawingHandleScreen handle)
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
+
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
+ var mapCoords = _transform.GetWorldPosition(gUid);
+ var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
+
// yes 1.0 scale is intended here.
var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
+ var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f);
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
- // The actual position in the UI. We offset the matrix position to render it off by half its width
- // plus by the offset.
- var uiPosition = ScalePosition(gridCentre)- new Vector2(labelDimensions.X / 2f, -yOffset);
+ // The actual position in the UI. We centre the label by offsetting the matrix position
+ // by half the label's width, plus the y-offset
+ var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
- // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
- uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, PixelWidth - labelDimensions.X ),
- Math.Clamp(uiPosition.Y, 0f, PixelHeight - labelDimensions.Y));
+ // Normalize the grid position if it exceeds the viewport bounds
+ // normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
+ var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f);
+ var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f;
+ if (offsetMax > 1)
+ {
+ gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax);
+
+ gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize;
+ }
- handle.DrawString(Font, uiPosition, labelText, color);
+ var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0);
+ var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y);
+
+ // clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport
+ // coord label intentionally isn't clamped so we don't get ugly clutter at the edges
+ var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y);
+ labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents);
+
+ // draw IFF label
+ handle.DrawString(Font, labelUiPosition, labelText, labelColor);
+
+ // only draw coords label if close enough
+ if (offsetMax < 1)
+ {
+ handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor);
+ }
}
// Detailed view
@@ -241,7 +270,7 @@ protected override void Draw(DrawingHandleScreen handle)
if (!gridAABB.Intersects(viewAABB))
continue;
- DrawGrid(handle, matty, grid, color);
+ DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
}
}
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
index 24a802a60fe..b152f5ead8b 100644
--- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
+++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
@@ -15,7 +15,6 @@ public sealed partial class StationAiMenu : RadialMenu
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
- [Dependency] private readonly IEyeManager _eyeManager = default!;
public event Action? OnAiRadial;
diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
index 7ed67f7b5dd..8c48258de00 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 12b4d7b5b30..3142f1cb061 100644
--- a/Content.Client/Store/Ui/StoreListingControl.xaml
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml
@@ -2,6 +2,8 @@
+