diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7a8129df1a6686..4029b093ccb4a8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,47 +1,33 @@ - - + ## About the PR - + ## Why / Balance - + ## Technical details - + ## Media - + ## Requirements - -- [ ] I have read and I am following the [Pull Request Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). I understand that not doing so may get my pr closed at maintainer’s discretion -- [ ] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase + +- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). +- [ ] I have added media to this PR or it does not require an ingame showcase. + ## Breaking changes - + **Changelog** + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs new file mode 100644 index 00000000000000..79bb66560e3c33 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -0,0 +1,215 @@ +using Content.Client.Stylesheets; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Monitor; +using Content.Shared.FixedPoint; +using Content.Shared.Temperature; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosAlarmEntryContainer : BoxContainer +{ + public NetEntity NetEntity; + public EntityCoordinates? Coordinates; + + private readonly IEntityManager _entManager; + private readonly IResourceCache _cache; + + private Dictionary _alarmStrings = new Dictionary() + { + [AtmosAlarmType.Invalid] = "atmos-alerts-window-invalid-state", + [AtmosAlarmType.Normal] = "atmos-alerts-window-normal-state", + [AtmosAlarmType.Warning] = "atmos-alerts-window-warning-state", + [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state", + }; + + private Dictionary _gasShorthands = new Dictionary() + { + [Gas.Ammonia] = "NH₃", + [Gas.CarbonDioxide] = "CO₂", + [Gas.Frezon] = "F", + [Gas.Nitrogen] = "N₂", + [Gas.NitrousOxide] = "N₂O", + [Gas.Oxygen] = "O₂", + [Gas.Plasma] = "P", + [Gas.Tritium] = "T", + [Gas.WaterVapor] = "H₂O", + }; + + public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates) + { + RobustXamlLoader.Load(this); + + _entManager = IoCManager.Resolve(); + _cache = IoCManager.Resolve(); + + NetEntity = uid; + Coordinates = coordinates; + + // Load fonts + var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11); + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + var smallFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10); + + // Set fonts + TemperatureHeaderLabel.FontOverride = headerFont; + PressureHeaderLabel.FontOverride = headerFont; + OxygenationHeaderLabel.FontOverride = headerFont; + GasesHeaderLabel.FontOverride = headerFont; + + TemperatureLabel.FontOverride = normalFont; + PressureLabel.FontOverride = normalFont; + OxygenationLabel.FontOverride = normalFont; + + NoDataLabel.FontOverride = headerFont; + + SilenceCheckBox.Label.FontOverride = smallFont; + SilenceCheckBox.Label.FontColorOverride = Color.DarkGray; + } + + public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlertsFocusDeviceData? focusData = null) + { + NetEntity = entry.NetEntity; + Coordinates = _entManager.GetCoordinates(entry.Coordinates); + + // Load fonts + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Update alarm state + if (!_alarmStrings.TryGetValue(entry.AlarmState, out var alarmString)) + alarmString = "atmos-alerts-window-invalid-state"; + + AlarmStateLabel.Text = Loc.GetString(alarmString); + AlarmStateLabel.FontColorOverride = GetAlarmStateColor(entry.AlarmState); + + // Update alarm name + AlarmNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", entry.EntityName), ("address", entry.Address)); + + // Focus updates + FocusContainer.Visible = isFocus; + + if (isFocus) + SetAsFocus(); + else + RemoveAsFocus(); + + if (isFocus && entry.Group == AtmosAlertsComputerGroup.AirAlarm) + { + MainDataContainer.Visible = (entry.AlarmState != AtmosAlarmType.Invalid); + NoDataLabel.Visible = (entry.AlarmState == AtmosAlarmType.Invalid); + + if (focusData != null) + { + // Update temperature + var tempK = (FixedPoint2)focusData.Value.TemperatureData.Item1; + var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float()); + + TemperatureLabel.Text = Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)); + TemperatureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.TemperatureData.Item2); + + // Update pressure + PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)focusData.Value.PressureData.Item1)); + PressureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.PressureData.Item2); + + // Update oxygenation + var oxygenPercent = (FixedPoint2)0f; + var oxygenAlert = AtmosAlarmType.Invalid; + + if (focusData.Value.GasData.TryGetValue(Gas.Oxygen, out var oxygenData)) + { + oxygenPercent = oxygenData.Item2 * 100f; + oxygenAlert = oxygenData.Item3; + } + + OxygenationLabel.Text = Loc.GetString("atmos-alerts-window-oxygenation-value", ("value", oxygenPercent)); + OxygenationLabel.FontColorOverride = GetAlarmStateColor(oxygenAlert); + + // Update other present gases + GasGridContainer.RemoveAllChildren(); + + var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen); + + if (gasData.Count() == 0) + { + // No other gases + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"), + FontOverride = normalFont, + FontColorOverride = StyleNano.DisabledFore, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + + else + { + // Add an entry for each gas + foreach ((var gas, (var mol, var percent, var alert)) in gasData) + { + var gasPercent = (FixedPoint2)0f; + gasPercent = percent * 100f; + + if (!_gasShorthands.TryGetValue(gas, out var gasShorthand)) + gasShorthand = "X"; + + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)), + FontOverride = normalFont, + FontColorOverride = GetAlarmStateColor(alert), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + } + } + } + } + + public void SetAsFocus() + { + FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png"; + } + + public void RemoveAsFocus() + { + FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png"; + FocusContainer.Visible = false; + } + + private Color GetAlarmStateColor(AtmosAlarmType alarmType) + { + switch (alarmType) + { + case AtmosAlarmType.Normal: + return StyleNano.GoodGreenFore; + case AtmosAlarmType.Warning: + return StyleNano.ConcerningOrangeFore; + case AtmosAlarmType.Danger: + return StyleNano.DangerousRedFore; + } + + return StyleNano.DisabledFore; + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs new file mode 100644 index 00000000000000..08cae979b9b83a --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs @@ -0,0 +1,52 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private AtmosAlertsComputerWindow? _menu; + + public AtmosAlertsComputerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } + + protected override void Open() + { + _menu = new AtmosAlertsComputerWindow(this, Owner); + _menu.OpenCentered(); + _menu.OnClose += Close; + + EntMan.TryGetComponent(Owner, out var xform); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (AtmosAlertsComputerBoundInterfaceState) state; + + if (castState == null) + return; + + EntMan.TryGetComponent(Owner, out var xform); + _menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData); + } + + public void SendFocusChangeMessage(NetEntity? netEntity) + { + SendMessage(new AtmosAlertsComputerFocusChangeMessage(netEntity)); + } + + public void SendDeviceSilencedMessage(NetEntity netEntity, bool silenceDevice) + { + SendMessage(new AtmosAlertsComputerDeviceSilencedMessage(netEntity, silenceDevice)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml new file mode 100644 index 00000000000000..8824a776ee6825 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs new file mode 100644 index 00000000000000..f0b7ffbe119909 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -0,0 +1,548 @@ +using Content.Client.Message; +using Content.Client.Pinpointer.UI; +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Monitor; +using Content.Shared.Pinpointer; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosAlertsComputerWindow : FancyWindow +{ + private readonly IEntityManager _entManager; + private readonly SpriteSystem _spriteSystem; + + private EntityUid? _owner; + private NetEntity? _trackedEntity; + + private AtmosAlertsComputerEntry[]? _airAlarms = null; + private AtmosAlertsComputerEntry[]? _fireAlarms = null; + private IEnumerable? _allAlarms = null; + + private IEnumerable? _activeAlarms = null; + private Dictionary _deviceSilencingProgress = new(); + + public event Action? SendFocusChangeMessageAction; + public event Action? SendDeviceSilencedMessageAction; + + private bool _autoScrollActive = false; + private bool _autoScrollAwaitsUpdate = false; + + private const float SilencingDuration = 2.5f; + + public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + // Pass the owner to nav map + _owner = owner; + NavMap.Owner = _owner; + + // Set nav map colors + NavMap.WallColor = new Color(64, 64, 64); + NavMap.TileColor = Color.DimGray * NavMap.WallColor; + + // Set nav map grid uid + var stationName = Loc.GetString("atmos-alerts-window-unknown-location"); + + if (_entManager.TryGetComponent(owner, out var xform)) + { + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData)) + stationName = stationMetaData.EntityName; + + var msg = new FormattedMessage(); + msg.TryAddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)), out _); + + StationName.SetMessage(msg); + } + + else + { + StationName.SetMessage(stationName); + NavMap.Visible = false; + } + + // Set trackable entity selected action + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + // Update nav map + NavMap.ForceNavMapUpdate(); + + // Set tab container headers + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts")); + MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms")); + MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms")); + + // Set UI toggles + ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid); + ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal); + ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning); + ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger); + + // Set atmos monitoring message action + SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage; + SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage; + } + + #region Toggle handling + + private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + foreach (var device in console.AtmosDevices) + { + var alarmState = GetAlarmState(device.NetEntity); + + if (toggledAlarmState != alarmState) + continue; + + if (toggle.Pressed) + AddTrackedEntityToNavMap(device, alarmState); + + else + NavMap.TrackedEntities.Remove(device.NetEntity); + } + } + + private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState) + { + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + if (toggleState) + _deviceSilencingProgress[netEntity] = SilencingDuration; + + else + _deviceSilencingProgress.Remove(netEntity); + + foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children) + { + if (entryContainer.NetEntity == netEntity) + entryContainer.SilenceAlarmProgressBar.Visible = toggleState; + } + + SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState); + } + + #endregion + + public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + if (_trackedEntity != focusData?.NetEntity) + { + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + focusData = null; + } + + // Retain alarm data for use inbetween updates + _airAlarms = airAlarms; + _fireAlarms = fireAlarms; + _allAlarms = airAlarms.Concat(fireAlarms); + + var silenced = console.SilencedDevices; + + _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal && + (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity))); + + // Reset nav map data + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + + // Add tracked entities to the nav map + foreach (var device in console.AtmosDevices) + { + if (!NavMap.Visible) + continue; + + var alarmState = GetAlarmState(device.NetEntity); + + if (_trackedEntity != device.NetEntity) + { + // Skip air alarms if the appropriate overlay is off + if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid) + continue; + + if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal) + continue; + + if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning) + continue; + + if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger) + continue; + } + + AddTrackedEntityToNavMap(device, alarmState); + } + + // Show the monitor location + var consoleUid = _entManager.GetNetEntity(_owner); + + if (consoleCoords != null && consoleUid != null) + { + var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); + var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false); + NavMap.TrackedEntities[consoleUid.Value] = blip; + } + + // Update the nav map + NavMap.ForceNavMapUpdate(); + + // Clear excess children from the tables + var activeAlarmCount = _activeAlarms.Count(); + + while (AlertsTable.ChildCount > activeAlarmCount) + AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1)); + + while (AirAlarmsTable.ChildCount > airAlarms.Length) + AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1)); + + while (FireAlarmsTable.ChildCount > fireAlarms.Length) + FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1)); + + // Update all entries in each table + for (int index = 0; index < _activeAlarms.Count(); index++) + { + var entry = _activeAlarms.ElementAt(index); + UpdateUIEntry(entry, index, AlertsTable, console, focusData); + } + + for (int index = 0; index < airAlarms.Count(); index++) + { + var entry = airAlarms.ElementAt(index); + UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData); + } + + for (int index = 0; index < fireAlarms.Count(); index++) + { + var entry = fireAlarms.ElementAt(index); + UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData); + } + + // If no alerts are active, display a message + if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0) + { + var label = new RichTextLabel() + { + HorizontalExpand = true, + VerticalExpand = true, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + }; + + label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha()))); + + AlertsTable.AddChild(label); + } + + // Update the alerts tab with the number of active alerts + if (activeAlarmCount == 0) + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts")); + + else + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount))); + + // Auto-scroll re-enable + if (_autoScrollAwaitsUpdate) + { + _autoScrollActive = true; + _autoScrollAwaitsUpdate = false; + } + } + + private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState) + { + var data = GetBlipTexture(alarmState); + + if (data == null) + return; + + var texture = data.Value.Item1; + var color = data.Value.Item2; + var coords = _entManager.GetCoordinates(metaData.NetCoordinates); + + if (_trackedEntity != null && _trackedEntity != metaData.NetEntity) + color *= Color.DimGray; + + var selectable = true; + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable); + + NavMap.TrackedEntities[metaData.NetEntity] = blip; + } + + private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null) + { + // Make new UI entry if required + if (index >= table.ChildCount) + { + var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates)); + + // On click + newEntryContainer.FocusButton.OnButtonUp += args => + { + if (_trackedEntity == newEntryContainer.NetEntity) + { + _trackedEntity = null; + } + + else + { + _trackedEntity = newEntryContainer.NetEntity; + + if (newEntryContainer.Coordinates != null) + NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value); + } + + // Send message to console that the focus has changed + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + + // Update affected UI elements across all tables + UpdateConsoleTable(console, AlertsTable, _trackedEntity); + UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity); + UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity); + }; + + // On toggling the silence check box + newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed); + + // Add the entry to the current table + table.AddChild(newEntryContainer); + } + + // Update values and UI elements + var tableChild = table.GetChild(index); + + if (tableChild is not AtmosAlarmEntryContainer) + { + table.RemoveChild(tableChild); + UpdateUIEntry(entry, index, table, console, focusData); + + return; + } + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData); + + if (_trackedEntity != entry.NetEntity) + { + var silenced = console.SilencedDevices; + entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity)); + } + + entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity)); + } + + private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity) + { + foreach (var tableChild in table.Children) + { + if (tableChild is not AtmosAlarmEntryContainer) + continue; + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + if (entryContainer.NetEntity != currTrackedEntity) + entryContainer.RemoveAsFocus(); + + else if (entryContainer.NetEntity == currTrackedEntity) + entryContainer.SetAsFocus(); + } + } + + private void SetTrackedEntityFromNavMap(NetEntity? netEntity) + { + if (netEntity == null) + return; + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + _trackedEntity = netEntity; + + if (netEntity != null) + { + // Tab switching + if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false) + { + var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity); + + switch (device?.Group) + { + case AtmosAlertsComputerGroup.AirAlarm: + MasterTabContainer.CurrentTab = 1; break; + case AtmosAlertsComputerGroup.FireAlarm: + MasterTabContainer.CurrentTab = 2; break; + } + } + + // Get the scroll position of the selected entity on the selected button the UI + ActivateAutoScrollToFocus(); + } + + // Send message to console that the focus has changed + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + AutoScrollToFocus(); + + // Device silencing update + foreach ((var device, var remainingTime) in _deviceSilencingProgress) + { + var t = remainingTime - args.DeltaSeconds; + + if (t <= 0) + { + _deviceSilencingProgress.Remove(device); + + if (device == _trackedEntity) + _trackedEntity = null; + } + + else + _deviceSilencingProgress[device] = t; + } + } + + private void ActivateAutoScrollToFocus() + { + _autoScrollActive = false; + _autoScrollAwaitsUpdate = true; + } + + private void AutoScrollToFocus() + { + if (!_autoScrollActive) + return; + + var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer; + if (scroll == null) + return; + + if (!TryGetVerticalScrollbar(scroll, out var vScrollbar)) + return; + + if (!TryGetNextScrollPosition(out float? nextScrollPosition)) + return; + + vScrollbar.ValueTarget = nextScrollPosition.Value; + + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) + _autoScrollActive = false; + } + + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; + + foreach (var child in scroll.Children) + { + if (child is not VScrollBar) + continue; + + var castChild = child as VScrollBar; + + if (castChild != null) + { + vScrollBar = castChild; + return true; + } + } + + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = null; + + var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer; + if (scroll == null) + return false; + + var container = scroll.Children.ElementAt(0) as BoxContainer; + if (container == null || container.Children.Count() == 0) + return false; + + // Exit if the heights of the children haven't been initialized yet + if (!container.Children.Any(x => x.Height > 0)) + return false; + + nextScrollPosition = 0; + + foreach (var control in container.Children) + { + if (control == null || control is not AtmosAlarmEntryContainer) + continue; + + if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity) + return true; + + nextScrollPosition += control.Height; + } + + // Failed to find control + nextScrollPosition = null; + + return false; + } + + private AtmosAlarmType GetAlarmState(NetEntity netEntity) + { + var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState; + + if (alarmState == null) + return AtmosAlarmType.Invalid; + + return alarmState.Value; + } + + private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState) + { + (SpriteSpecifier.Texture, Color)? output = null; + + switch (alarmState) + { + case AtmosAlarmType.Invalid: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break; + case AtmosAlarmType.Normal: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break; + case AtmosAlarmType.Warning: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break; + case AtmosAlarmType.Danger: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break; + } + + return output; + } +} diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index ca6336b91b857c..b525747aa9cd61 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -306,6 +306,9 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) .WithMaxDistance(comp.Range); var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams); + if (stream == null) + continue; + _playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key); playingCount++; diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs index 7c77865f74156d..50c3971d95addd 100644 --- a/Content.Client/Audio/ClientGlobalSoundSystem.cs +++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs @@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent) if(!_adminAudioEnabled) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _adminAudio.Add(stream.Value.Entity); + _adminAudio.Add(stream?.Entity); } private void PlayStationEventMusic(StationEventMusicEvent soundEvent) @@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent) if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _eventAudio.Add(soundEvent.Type, stream.Value.Entity); + _eventAudio.Add(soundEvent.Type, stream?.Entity); } private void PlayGameSound(GameGlobalSoundEvent soundEvent) diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index d60c978ccf5c5e..bf7ab26cba25ba 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -213,9 +213,9 @@ private void UpdateAmbientMusic() false, AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider)); - _ambientMusicStream = strim.Value.Entity; + _ambientMusicStream = strim?.Entity; - if (_musicProto.FadeIn) + if (_musicProto.FadeIn && strim != null) { FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime); } diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs index 92c5b7a419153b..9864dbcb2a91bf 100644 --- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs @@ -185,7 +185,7 @@ private void PlaySoundtrack(string soundtrackFilename) false, _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))) ); - if (playResult.Value.Entity == default) + if (playResult == null) { _sawmill.Warning( $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!", diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index 6770899e0aa293..035e1300ca5394 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -15,7 +15,6 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnAppearanceChange); SubscribeLocalEvent(OnStrapMoveEvent); } @@ -57,21 +56,6 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE } } - private void OnHandleState(Entity ent, ref ComponentHandleState args) - { - if (args.Current is not BuckleState state) - return; - - ent.Comp.DontCollide = state.DontCollide; - ent.Comp.BuckleTime = state.BuckleTime; - var strapUid = EnsureEntity(state.BuckledTo, ent); - - SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null)); - - var (uid, component) = ent; - - } - private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { if (!TryComp(uid, out var rotVisuals)) diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs new file mode 100644 index 00000000000000..3c97b8b37d1545 --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs @@ -0,0 +1,30 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.CartridgeLoader.Cartridges; + +public sealed partial class WantedListUi : UIFragment +{ + private WantedListUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new WantedListUiFragment(); + } + + public override void UpdateState(BoundUserInterfaceState state) + { + switch (state) + { + case WantedListUiState cast: + _fragment?.UpdateState(cast.Records); + break; + } + } +} diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs new file mode 100644 index 00000000000000..4137f6c2af0cc0 --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs @@ -0,0 +1,240 @@ +using System.Linq; +using Content.Client.UserInterface.Controls; +using Content.Shared.CriminalRecords.Systems; +using Content.Shared.Security; +using Content.Shared.StatusIcon; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class WantedListUiFragment : BoxContainer +{ + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private readonly SpriteSystem _spriteSystem; + + private string? _selectedTargetName; + private List _wantedRecords = new(); + + public WantedListUiFragment() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _spriteSystem = _entitySystem.GetEntitySystem(); + + SearchBar.OnTextChanged += OnSearchBarTextChanged; + } + + private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) + { + var found = !String.IsNullOrWhiteSpace(args.Text) + ? _wantedRecords.FindAll(r => + r.TargetInfo.Name.Contains(args.Text) || + r.Status.ToString().Contains(args.Text, StringComparison.OrdinalIgnoreCase)) + : _wantedRecords; + + UpdateState(found, false); + } + + public void UpdateState(List records, bool refresh = true) + { + if (records.Count == 0) + { + NoRecords.Visible = true; + RecordsList.Visible = false; + RecordUnselected.Visible = false; + PersonContainer.Visible = false; + + _selectedTargetName = null; + if (refresh) + _wantedRecords.Clear(); + + RecordsList.PopulateList(new List()); + + return; + } + + NoRecords.Visible = false; + RecordsList.Visible = true; + RecordUnselected.Visible = true; + PersonContainer.Visible = false; + + var dataList = records.Select(r => new StatusListData(r)).ToList(); + + RecordsList.GenerateItem = GenerateItem; + RecordsList.ItemPressed = OnItemSelected; + RecordsList.PopulateList(dataList); + + if (refresh) + _wantedRecords = records; + } + + private void OnItemSelected(BaseButton.ButtonEventArgs args, ListData data) + { + if (data is not StatusListData(var record)) + return; + + FormattedMessage GetLoc(string fluentId, params (string,object)[] args) + { + var msg = new FormattedMessage(); + var fluent = Loc.GetString(fluentId, args); + msg.AddMarkupPermissive(fluent); + return msg; + } + + // Set personal info + PersonName.Text = record.TargetInfo.Name; + TargetAge.SetMessage(GetLoc( + "wanted-list-age-label", + ("age", record.TargetInfo.Age) + )); + TargetJob.SetMessage(GetLoc( + "wanted-list-job-label", + ("job", record.TargetInfo.JobTitle.ToLower()) + )); + TargetSpecies.SetMessage(GetLoc( + "wanted-list-species-label", + ("species", record.TargetInfo.Species.ToLower()) + )); + TargetGender.SetMessage(GetLoc( + "wanted-list-gender-label", + ("gender", record.TargetInfo.Gender) + )); + + // Set reason + WantedReason.SetMessage(GetLoc( + "wanted-list-reason-label", + ("reason", record.Reason ?? Loc.GetString("wanted-list-unknown-reason-label")) + )); + + // Set status + PersonState.SetMessage(GetLoc( + "wanted-list-status-label", + ("status", record.Status.ToString().ToLower()) + )); + + // Set initiator + InitiatorName.SetMessage(GetLoc( + "wanted-list-initiator-label", + ("initiator", record.Initiator ?? Loc.GetString("wanted-list-unknown-initiator-label")) + )); + + // History table + // Clear table if it exists + HistoryTable.RemoveAllChildren(); + + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-time-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + }); + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-reason-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + HorizontalExpand = true, + }); + + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-initiator-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + }); + + if (record.History.Count > 0) + { + HistoryTable.Visible = true; + + foreach (var history in record.History.OrderByDescending(h => h.AddTime)) + { + HistoryTable.AddChild(new Label() + { + Text = $"{history.AddTime.Hours:00}:{history.AddTime.Minutes:00}:{history.AddTime.Seconds:00}", + StyleClasses = { "LabelSmall" }, + VerticalAlignment = VAlignment.Top, + }); + + HistoryTable.AddChild(new RichTextLabel() + { + Text = $"[color=white]{history.Crime}[/color]", + HorizontalExpand = true, + VerticalAlignment = VAlignment.Top, + StyleClasses = { "LabelSubText" }, + Margin = new(10f, 0f), + }); + + HistoryTable.AddChild(new RichTextLabel() + { + Text = $"[color=white]{history.InitiatorName}[/color]", + StyleClasses = { "LabelSubText" }, + VerticalAlignment = VAlignment.Top, + }); + } + } + + RecordUnselected.Visible = false; + PersonContainer.Visible = true; + + // Save selected item + _selectedTargetName = record.TargetInfo.Name; + } + + private void GenerateItem(ListData data, ListContainerButton button) + { + if (data is not StatusListData(var record)) + return; + + var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true }; + var label = new Label() { Text = record.TargetInfo.Name }; + var rect = new TextureRect() + { + TextureScale = new(2.2f), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Margin = new(0f, 0f, 6f, 0f), + }; + + if (record.Status is not SecurityStatus.None) + { + var proto = "SecurityIcon" + record.Status switch + { + SecurityStatus.Detained => "Incarcerated", + _ => record.Status.ToString(), + }; + + if (_prototypeManager.TryIndex(proto, out var prototype)) + { + rect.Texture = _spriteSystem.Frame0(prototype.Icon); + } + } + + box.AddChild(rect); + box.AddChild(label); + button.AddChild(box); + button.AddStyleClass(ListContainer.StyleClassListContainerButton); + + if (record.TargetInfo.Name.Equals(_selectedTargetName)) + { + button.Pressed = true; + // For some reason the event is not called when `Pressed` changed, call it manually. + OnItemSelected( + new(button, new(new(), BoundKeyState.Down, new(), false, new(), new())), + data); + } + } +} + +internal record StatusListData(WantedRecord Record) : ListData; diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml new file mode 100644 index 00000000000000..7b5d116ad74b8e --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml @@ -0,0 +1,50 @@ + + + + + + + diff --git a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs index a3cedb5f2f3824..7c7d824ee981aa 100644 --- a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs +++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs @@ -1,5 +1,5 @@ using System.Linq; -using Content.Client.Chemistry.Containers.EntitySystems; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Atmos.Prototypes; using Content.Shared.Body.Part; using Content.Shared.Chemistry; @@ -16,7 +16,7 @@ namespace Content.Client.Chemistry.EntitySystems; /// public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem { - [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [ValidatePrototypeId] private const string DefaultMixingCategory = "DummyMix"; diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index a941f0acff97fd..1c1f1984de4e39 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -1,7 +1,12 @@ +using System.Linq; +using System.Numerics; +using System.Threading; using Content.Client.Verbs; using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Input; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -12,13 +17,8 @@ using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Utility; -using System.Linq; -using System.Numerics; -using System.Threading; using static Content.Shared.Interaction.SharedInteractionSystem; using static Robust.Client.UserInterface.Controls.BoxContainer; -using Content.Shared.Interaction.Events; -using Content.Shared.Item; using Direction = Robust.Shared.Maths.Direction; namespace Content.Client.Examine @@ -35,7 +35,6 @@ public sealed class ExamineSystem : ExamineSystemShared private EntityUid _examinedEntity; private EntityUid _lastExaminedEntity; - private EntityUid _playerEntity; private Popup? _examineTooltipOpen; private ScreenCoordinates _popupPos; private CancellationTokenSource? _requestCancelTokenSource; @@ -74,9 +73,9 @@ private void OnExaminedItemDropped(EntityUid item, ItemComponent comp, DroppedEv public override void Update(float frameTime) { if (_examineTooltipOpen is not {Visible: true}) return; - if (!_examinedEntity.Valid || !_playerEntity.Valid) return; + if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return; - if (!CanExamine(_playerEntity, _examinedEntity)) + if (!CanExamine(player, _examinedEntity)) CloseTooltip(); } @@ -114,9 +113,8 @@ private bool HandleExamine(in PointerInputCmdHandler.PointerInputCmdArgs args) return false; } - _playerEntity = _playerManager.LocalEntity ?? default; - - if (_playerEntity == default || !CanExamine(_playerEntity, entity)) + if (_playerManager.LocalEntity is not { } player || + !CanExamine(player, entity)) { return false; } diff --git a/Content.Client/Explosion/ExplosionSystem.cs b/Content.Client/Explosion/ExplosionSystem.cs index a2ed2d50e0d1ef..692782ded4b77b 100644 --- a/Content.Client/Explosion/ExplosionSystem.cs +++ b/Content.Client/Explosion/ExplosionSystem.cs @@ -2,7 +2,4 @@ namespace Content.Client.Explosion.EntitySystems; -public sealed class ExplosionSystem : SharedExplosionSystem -{ - -} +public sealed class ExplosionSystem : SharedExplosionSystem; diff --git a/Content.Client/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs index 046be2aa62142c..8e41c382dd7e7c 100644 --- a/Content.Client/Flash/FlashOverlay.cs +++ b/Content.Client/Flash/FlashOverlay.cs @@ -16,6 +16,7 @@ public sealed class FlashOverlay : Overlay [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IGameTiming _timing = default!; + private readonly SharedFlashSystem _flash; private readonly StatusEffectsSystem _statusSys; public override OverlaySpace Space => OverlaySpace.WorldSpace; @@ -27,6 +28,7 @@ public FlashOverlay() { IoCManager.InjectDependencies(this); _shader = _prototypeManager.Index("FlashedEffect").InstanceUnique(); + _flash = _entityManager.System(); _statusSys = _entityManager.System(); } @@ -41,7 +43,7 @@ protected override void FrameUpdate(FrameEventArgs args) || !_entityManager.TryGetComponent(playerEntity, out var status)) return; - if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time, status)) + if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status)) return; var curTime = _timing.CurTime; diff --git a/Content.Client/Guidebook/GuidebookDataSystem.cs b/Content.Client/Guidebook/GuidebookDataSystem.cs new file mode 100644 index 00000000000000..f47ad6ef1bb2b5 --- /dev/null +++ b/Content.Client/Guidebook/GuidebookDataSystem.cs @@ -0,0 +1,45 @@ +using Content.Shared.Guidebook; + +namespace Content.Client.Guidebook; + +/// +/// Client system for storing and retrieving values extracted from entity prototypes +/// for display in the guidebook (). +/// Requests data from the server on . +/// Can also be pushed new data when the server reloads prototypes. +/// +public sealed class GuidebookDataSystem : EntitySystem +{ + private GuidebookData? _data; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnServerUpdated); + + // Request data from the server + RaiseNetworkEvent(new RequestGuidebookDataEvent()); + } + + private void OnServerUpdated(UpdateGuidebookDataEvent args) + { + // Got new data from the server, either in response to our request, or because prototypes reloaded on the server + _data = args.Data; + _data.Freeze(); + } + + /// + /// Attempts to retrieve a value using the given identifiers. + /// See for more information. + /// + public bool TryGetValue(string prototype, string component, string field, out object? value) + { + if (_data == null) + { + value = null; + return false; + } + return _data.TryGetValue(prototype, component, field, out value); + } +} diff --git a/Content.Client/Guidebook/Richtext/ProtodataTag.cs b/Content.Client/Guidebook/Richtext/ProtodataTag.cs new file mode 100644 index 00000000000000..a725fd4e4b5994 --- /dev/null +++ b/Content.Client/Guidebook/Richtext/ProtodataTag.cs @@ -0,0 +1,49 @@ +using System.Globalization; +using Robust.Client.UserInterface.RichText; +using Robust.Shared.Utility; + +namespace Content.Client.Guidebook.RichText; + +/// +/// RichText tag that can display values extracted from entity prototypes. +/// In order to be accessed by this tag, the desired field/property must +/// be tagged with . +/// +public sealed class ProtodataTag : IMarkupTag +{ + [Dependency] private readonly ILogManager _logMan = default!; + [Dependency] private readonly IEntityManager _entMan = default!; + + public string Name => "protodata"; + private ISawmill Log => _log ??= _logMan.GetSawmill("protodata_tag"); + private ISawmill? _log; + + public string TextBefore(MarkupNode node) + { + // Do nothing with an empty tag + if (!node.Value.TryGetString(out var prototype)) + return string.Empty; + + if (!node.Attributes.TryGetValue("comp", out var component)) + return string.Empty; + if (!node.Attributes.TryGetValue("member", out var member)) + return string.Empty; + node.Attributes.TryGetValue("format", out var format); + + var guidebookData = _entMan.System(); + + // Try to get the value + if (!guidebookData.TryGetValue(prototype, component.StringValue!, member.StringValue!, out var value)) + { + Log.Error($"Failed to find protodata for {component}.{member} in {prototype}"); + return "???"; + } + + // If we have a format string and a formattable value, format it as requested + if (!string.IsNullOrEmpty(format.StringValue) && value is IFormattable formattable) + return formattable.ToString(format.StringValue, CultureInfo.CurrentCulture); + + // No format string given, so just use default ToString + return value?.ToString() ?? "NULL"; + } +} diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index 97968c4b990b3d..19d00a0bbf8b1d 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -21,6 +21,7 @@ Orientation="Vertical"> +