From b70c0845d0adfb22f915a6868954031f9a9653f8 Mon Sep 17 00:00:00 2001 From: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:38:50 -0600 Subject: [PATCH] Crew monitor revisit (#22240) --- .../CrewMonitoringBoundUserInterface.cs | 71 +- .../CrewMonitoringNavMapControl.cs | 79 +++ .../CrewMonitoring/CrewMonitoringWindow.xaml | 76 +- .../CrewMonitoringWindow.xaml.cs | 562 +++++++++------ Content.Client/Pinpointer/UI/NavMapControl.cs | 535 ++++++++++---- .../Pinpointer/UI/StationMapWindow.xaml.cs | 2 + .../Components/PresetIdCardComponent.cs | 17 +- Content.Server/Access/Systems/IdCardSystem.cs | 322 +++++---- .../Access/Systems/PresetIdCardSystem.cs | 116 +-- .../CrewMonitoringConsoleComponent.cs | 42 +- .../CrewMonitoringConsoleSystem.cs | 115 +-- .../SuitSensors/SuitSensorComponent.cs | 119 ++-- .../Medical/SuitSensors/SuitSensorSystem.cs | 664 +++++++++--------- .../Access/Components/IdCardComponent.cs | 47 +- .../CrewMonitoring/CrewMonitoringShared.cs | 32 +- .../Medical/SuitSensor/SharedSuitSensor.cs | 109 +-- .../components/crew-monitoring-component.ftl | 7 +- Resources/Locale/en-US/ui/navmap.ftl | 3 + .../human_crew_monitoring.rsi/alive.png | Bin 0 -> 615 bytes .../human_crew_monitoring.rsi/critical.png | Bin 0 -> 659 bytes .../Alerts/human_crew_monitoring.rsi/dead.png | Bin 0 -> 615 bytes .../human_crew_monitoring.rsi/health0.png | Bin 0 -> 614 bytes .../human_crew_monitoring.rsi/health1.png | Bin 0 -> 623 bytes .../human_crew_monitoring.rsi/health2.png | Bin 0 -> 611 bytes .../human_crew_monitoring.rsi/health3.png | Bin 0 -> 617 bytes .../human_crew_monitoring.rsi/health4.png | Bin 0 -> 617 bytes .../human_crew_monitoring.rsi/meta.json | 41 ++ .../Interface/NavMap/beveled_circle.png | Bin 0 -> 1365 bytes 28 files changed, 1764 insertions(+), 1195 deletions(-) create mode 100644 Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs create mode 100644 Resources/Locale/en-US/ui/navmap.ftl create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/alive.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/critical.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/dead.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health0.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health1.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health2.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health3.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health4.png create mode 100644 Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/meta.json create mode 100644 Resources/Textures/Interface/NavMap/beveled_circle.png diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs index fc632575c7..3978880987 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs @@ -1,53 +1,56 @@ using Content.Shared.Medical.CrewMonitoring; -using Robust.Client.GameObjects; -namespace Content.Client.Medical.CrewMonitoring +namespace Content.Client.Medical.CrewMonitoring; + +public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface { - public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface + [ViewVariables] + private CrewMonitoringWindow? _menu; + + public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { - [ViewVariables] - private CrewMonitoringWindow? _menu; + } - public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + protected override void Open() + { + EntityUid? gridUid = null; + string stationName = string.Empty; - protected override void Open() + if (EntMan.TryGetComponent(Owner, out var xform)) { - EntityUid? gridUid = null; + gridUid = xform.GridUid; - if (EntMan.TryGetComponent(Owner, out var xform)) + if (EntMan.TryGetComponent(gridUid, out var metaData)) { - gridUid = xform.GridUid; + stationName = metaData.EntityName; } - - _menu = new CrewMonitoringWindow(gridUid); - - _menu.OpenCentered(); - _menu.OnClose += Close; } - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); + _menu = new CrewMonitoringWindow(stationName, gridUid); - switch (state) - { - case CrewMonitoringState st: - EntMan.TryGetComponent(Owner, out var xform); + _menu.OpenCentered(); + _menu.OnClose += Close; + } - _menu?.ShowSensors(st.Sensors, xform?.Coordinates, st.Snap, st.Precision); - break; - } - } + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); - protected override void Dispose(bool disposing) + switch (state) { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); + case CrewMonitoringState st: + EntMan.TryGetComponent(Owner, out var xform); + _menu?.ShowSensors(st.Sensors, Owner, xform?.Coordinates); + break; } } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } } diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs new file mode 100644 index 0000000000..f4ae82970c --- /dev/null +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs @@ -0,0 +1,79 @@ +using Content.Client.Pinpointer.UI; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Medical.CrewMonitoring; + +public sealed partial class CrewMonitoringNavMapControl : NavMapControl +{ + public NetEntity? Focus; + public Dictionary LocalizedNames = new(); + + private Color _backgroundColor; + private Label _trackedEntityLabel; + private PanelContainer _trackedEntityPanel; + + public CrewMonitoringNavMapControl() : base() + { + WallColor = new Color(250, 146, 255); + TileColor = new(71, 42, 72); + + _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); + + _trackedEntityLabel = new Label + { + Margin = new Thickness(10f, 8f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + Modulate = Color.White, + }; + + _trackedEntityPanel = new PanelContainer + { + PanelOverride = new StyleBoxFlat + { + BackgroundColor = _backgroundColor, + }, + + Margin = new Thickness(5f, 10f), + HorizontalAlignment = HAlignment.Left, + VerticalAlignment = VAlignment.Bottom, + Visible = false, + }; + + _trackedEntityPanel.AddChild(_trackedEntityLabel); + this.AddChild(_trackedEntityPanel); + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (Focus == null) + { + _trackedEntityLabel.Text = string.Empty; + _trackedEntityPanel.Visible = false; + + return; + } + + foreach ((var netEntity, var blip) in TrackedEntities) + { + if (netEntity != Focus) + continue; + + if (!LocalizedNames.TryGetValue(netEntity, out var name)) + name = "Unknown"; + + var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]"; + + _trackedEntityLabel.Text = message; + _trackedEntityPanel.Visible = true; + + return; + } + + _trackedEntityLabel.Text = string.Empty; + _trackedEntityPanel.Visible = false; + } +} diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml index 559a12d63b..80bf5a3f8b 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml @@ -1,39 +1,49 @@ - - - - - - - + + + + + + diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs index ff08af6bb6..d8c87899db 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs @@ -1,275 +1,437 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using Content.Client.Pinpointer.UI; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Medical.SuitSensor; +using Content.Shared.StatusIcon; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; -namespace Content.Client.Medical.CrewMonitoring +namespace Content.Client.Medical.CrewMonitoring; + +[GenerateTypedNameReferences] +public sealed partial class CrewMonitoringWindow : FancyWindow { - [GenerateTypedNameReferences] - public sealed partial class CrewMonitoringWindow : FancyWindow + private List _rowsContent = new(); + private readonly IEntityManager _entManager; + private readonly IPrototypeManager _prototypeManager; + private readonly SpriteSystem _spriteSystem; + + private NetEntity? _trackedEntity; + private bool _tryToScrollToListFocus; + private Texture? _blipTexture; + + public CrewMonitoringWindow(string stationName, EntityUid? mapUid) + { + RobustXamlLoader.Load(this); + + _entManager = IoCManager.Resolve(); + _prototypeManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); + + if (_entManager.TryGetComponent(mapUid, out var xform)) + NavMap.MapUid = xform.GridUid; + + else + NavMap.Visible = false; + + StationName.AddStyleClass("LabelBig"); + StationName.Text = stationName; + + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + NavMap.ForceNavMapUpdate(); + } + + protected override void FrameUpdate(FrameEventArgs args) { - private List _rowsContent = new(); - private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new(); - private readonly IEntityManager _entManager; - private readonly IEyeManager _eye; - private EntityUid? _stationUid; - private CrewMonitoringButton? _trackedButton; + base.FrameUpdate(args); - public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row. + if (_tryToScrollToListFocus) + TryToScrollToFocus(); + } - public CrewMonitoringWindow(EntityUid? mapUid) + public void ShowSensors(List sensors, EntityUid monitor, EntityCoordinates? monitorCoords) + { + ClearOutDatedData(); + + // No server label + if (sensors.Count == 0) { - RobustXamlLoader.Load(this); - _eye = IoCManager.Resolve(); - _entManager = IoCManager.Resolve(); - _stationUid = mapUid; + NoServerLabel.Visible = true; + return; + } + + NoServerLabel.Visible = false; + + // Order sensor data + var orderedSensors = sensors.OrderBy(n => n.Name).OrderBy(j => j.Job); + var assignedSensors = new HashSet(); + var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n); - if (_entManager.TryGetComponent(mapUid, out var xform)) + // Create department labels and populate lists + foreach (var department in departments) + { + var departmentSensors = orderedSensors.Where(d => d.JobDepartments.Contains(department)); + + if (departmentSensors == null || !departmentSensors.Any()) + continue; + + foreach (var sensor in departmentSensors) + assignedSensors.Add(sensor); + + if (SensorsTable.ChildCount > 0) { - NavMap.MapUid = xform.GridUid; + var spacer = new Control() + { + SetHeight = 20, + }; + + SensorsTable.AddChild(spacer); + _rowsContent.Add(spacer); } - else + + var deparmentLabel = new RichTextLabel() { - NavMap.Visible = false; - SetSize = new Vector2(775, 400); - MinSize = SetSize; - } + Margin = new Thickness(10, 0), + HorizontalExpand = true, + }; + + deparmentLabel.SetMessage(department); + deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription); + + SensorsTable.AddChild(deparmentLabel); + _rowsContent.Add(deparmentLabel); + + PopulateDepartmentList(departmentSensors); } - public void ShowSensors(List stSensors, EntityCoordinates? monitorCoords, bool snap, float precision) + // Account for any non-station users + var remainingSensors = orderedSensors.Except(assignedSensors); + + if (remainingSensors.Any()) { - ClearAllSensors(); + var spacer = new Control() + { + SetHeight = 20, + }; - var monitorCoordsInStationSpace = _stationUid != null ? monitorCoords?.WithEntityId(_stationUid.Value, _entManager).Position : null; + SensorsTable.AddChild(spacer); + _rowsContent.Add(spacer); - // TODO scroll container - // TODO filter by name & occupation - // TODO make each row a xaml-control. Get rid of some of this c# control creation. - if (stSensors.Count == 0) + var deparmentLabel = new RichTextLabel() { - NoServerLabel.Visible = true; - return; - } - NoServerLabel.Visible = false; + Margin = new Thickness(10, 0), + HorizontalExpand = true, + }; + + deparmentLabel.SetMessage(Loc.GetString("crew-monitoring-user-interface-no-department")); + deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription); - // add a row for each sensor - foreach (var sensor in stSensors.OrderBy(a => a.Name)) + SensorsTable.AddChild(deparmentLabel); + _rowsContent.Add(deparmentLabel); + + PopulateDepartmentList(remainingSensors); + } + + // Show monitor on nav map + if (monitorCoords != null && _blipTexture != null) + { + NavMap.TrackedEntities[_entManager.GetNetEntity(monitor)] = new NavMapBlip(monitorCoords.Value, _blipTexture, Color.Cyan, true, false); + } + } + + private void PopulateDepartmentList(IEnumerable departmentSensors) + { + // Populate departments + foreach (var sensor in departmentSensors) + { + var coordinates = _entManager.GetCoordinates(sensor.Coordinates); + + // Add a button that will hold a username and other details + NavMap.LocalizedNames.TryAdd(sensor.SuitSensorUid, sensor.Name + ", " + sensor.Job); + + var sensorButton = new CrewMonitoringButton() { - var sensorEntity = _entManager.GetEntity(sensor.SuitSensorUid); - var coordinates = _entManager.GetCoordinates(sensor.Coordinates); + SuitSensorUid = sensor.SuitSensorUid, + Coordinates = coordinates, + Disabled = (coordinates == null), + HorizontalExpand = true, + }; - // add button with username - var nameButton = new CrewMonitoringButton() - { - SuitSensorUid = sensorEntity, - Coordinates = coordinates, - Text = sensor.Name, - Margin = new Thickness(5f, 5f), - }; - if (sensorEntity == _trackedButton?.SuitSensorUid) - nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - SetColorLabel(nameButton.Label, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(nameButton); - _rowsContent.Add(nameButton); - - // add users job - // format: JobName - var jobLabel = new Label() - { - Text = sensor.Job, - HorizontalExpand = true - }; - SetColorLabel(jobLabel, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(jobLabel); - _rowsContent.Add(jobLabel); - - // add users status and damage - // format: IsAlive (TotalDamage) - var statusText = Loc.GetString(sensor.IsAlive ? - "crew-monitoring-user-interface-alive" : - "crew-monitoring-user-interface-dead"); - if (sensor.TotalDamage != null) - { - statusText += $" ({sensor.TotalDamage})"; - } - var statusLabel = new Label() - { - Text = statusText - }; - SetColorLabel(statusLabel, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(statusLabel); - _rowsContent.Add(statusLabel); + if (sensor.SuitSensorUid == _trackedEntity) + sensorButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - // add users positions - // format: (x, y) - var box = GetPositionBox(sensor, monitorCoordsInStationSpace ?? Vector2.Zero, snap, precision); + SensorsTable.AddChild(sensorButton); + _rowsContent.Add(sensorButton); - SensorsTable.AddChild(box); - _rowsContent.Add(box); + // Primary container to hold the button UI elements + var mainContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; - if (coordinates != null && NavMap.Visible) - { - NavMap.TrackedCoordinates.TryAdd(coordinates.Value, - (true, sensorEntity == _trackedButton?.SuitSensorUid ? StyleNano.PointGreen : StyleNano.PointRed)); + sensorButton.AddChild(mainContainer); - nameButton.OnButtonUp += args => - { - if (_trackedButton != null && _trackedButton?.Coordinates != null) - //Make previous point red - NavMap.TrackedCoordinates[_trackedButton.Coordinates.Value] = (true, StyleNano.PointRed); + // User status container + var statusContainer = new BoxContainer() + { + SizeFlagsStretchRatio = 1.25f, + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; - NavMap.TrackedCoordinates[coordinates.Value] = (true, StyleNano.PointGreen); - NavMap.CenterToCoordinates(coordinates.Value); + mainContainer.AddChild(statusContainer); - nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - if (_trackedButton != null) - { //Make previous button default - var previosButton = SensorsTable.GetChild(_trackedButton.IndexInTable); - previosButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); - } - _trackedButton = nameButton; - _trackedButton.IndexInTable = nameButton.GetPositionInParent(); - }; - } - } - // Show monitor point - if (monitorCoords != null) - NavMap.TrackedCoordinates.Add(monitorCoords.Value, (true, StyleNano.PointMagenta)); - } + // Suit coords indicator + var suitCoordsIndicator = new TextureRect() + { + Texture = _blipTexture, + TextureScale = new Vector2(0.25f, 0.25f), + Modulate = coordinates != null ? Color.LimeGreen : Color.DarkRed, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + }; - private BoxContainer GetPositionBox(SuitSensorStatus sensor, Vector2 monitorCoordsInStationSpace, bool snap, float precision) - { - EntityCoordinates? coordinates = _entManager.GetCoordinates(sensor.Coordinates); - var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal }; + statusContainer.AddChild(suitCoordsIndicator); + + // Specify texture for the user status icon + var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive"); - if (coordinates == null || _stationUid == null) + if (!sensor.IsAlive) { - var dirIcon = new DirectionIcon() - { - SetSize = new Vector2(IconSize, IconSize), - Margin = new(0, 0, 4, 0) - }; - box.AddChild(dirIcon); - box.AddChild(new Label() { Text = Loc.GetString("crew-monitoring-user-interface-no-info") }); + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead"); } - else + + else if (sensor.TotalDamage != null) { - var local = coordinates.Value.WithEntityId(_stationUid.Value, _entManager).Position; + var index = MathF.Round(4f * (sensor.TotalDamage.Value / 100f)); - var displayPos = local.Floored(); - var dirIcon = new DirectionIcon(snap, precision) - { - SetSize = new Vector2(IconSize, IconSize), - Margin = new(0, 0, 4, 0) - }; - box.AddChild(dirIcon); - Label label = new Label() { Text = displayPos.ToString() }; - SetColorLabel(label, sensor.TotalDamage, sensor.IsAlive); - box.AddChild(label); - _directionIcons.Add((dirIcon, local - monitorCoordsInStationSpace)); + if (index >= 5) + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical"); + + else + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index); } - return box; - } + // Status icon + var statusIcon = new AnimatedTextureRect + { + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 1, 3, 0), + }; - protected override void FrameUpdate(FrameEventArgs args) - { - // the window is separate from any specific viewport, so there is no real way to get an eye-rotation without - // using IEyeManager. Eventually this will have to be reworked for a station AI with multi-viewports. - // (From the future: Or alternatively, just disable the angular offset for station AIs?) - - // An offsetAngle of zero here perfectly aligns directions to the station map. - // Note that the "relative angle" does this weird inverse-inverse thing. - // Could recalculate it all in world coordinates and then pass in eye directly... or do this. - var offsetAngle = Angle.Zero; - if (_entManager.TryGetComponent(_stationUid, out var xform)) + statusIcon.SetFromSpriteSpecifier(specifier); + statusIcon.DisplayRect.TextureScale = new Vector2(2f, 2f); + + statusContainer.AddChild(statusIcon); + + // User name + var nameLabel = new Label() + { + Text = sensor.Name, + HorizontalExpand = true, + ClipText = true, + }; + + statusContainer.AddChild(nameLabel); + + // User job container + var jobContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + mainContainer.AddChild(jobContainer); + + // Job icon + if (_prototypeManager.TryIndex(sensor.JobIcon, out var proto)) { - // Apply the offset relative to the eye. - // For a station at 45 degrees rotation, the current eye rotation is -45 degrees. - // TODO: This feels sketchy. Is there something underlying wrong with eye rotation? - offsetAngle = -(_eye.CurrentEye.Rotation + xform.WorldRotation); + var jobIcon = new TextureRect() + { + TextureScale = new Vector2(2f, 2f), + Stretch = TextureRect.StretchMode.KeepCentered, + Texture = _spriteSystem.Frame0(proto.Icon), + Margin = new Thickness(5, 0, 5, 0), + }; + + jobContainer.AddChild(jobIcon); } - foreach (var (icon, pos) in _directionIcons) + // Job name + var jobLabel = new Label() { - icon.UpdateDirection(pos, offsetAngle); + Text = sensor.Job, + HorizontalExpand = true, + ClipText = true, + }; + + jobContainer.AddChild(jobLabel); + + // Add user coordinates to the navmap + if (coordinates != null && NavMap.Visible && _blipTexture != null) + { + NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid, + new NavMapBlip + (coordinates.Value, + _blipTexture, + (_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray, + sensor.SuitSensorUid == _trackedEntity)); + + NavMap.Focus = _trackedEntity; + + // On button up + sensorButton.OnButtonUp += args => + { + var prevTrackedEntity = _trackedEntity; + + if (_trackedEntity == sensor.SuitSensorUid) + { + _trackedEntity = null; + } + + else + { + _trackedEntity = sensor.SuitSensorUid; + NavMap.CenterToCoordinates(coordinates.Value); + } + + NavMap.Focus = _trackedEntity; + + UpdateSensorsTable(_trackedEntity, prevTrackedEntity); + }; } } + } + + private void SetTrackedEntityFromNavMap(NetEntity? netEntity) + { + var prevTrackedEntity = _trackedEntity; + _trackedEntity = netEntity; + + if (_trackedEntity == prevTrackedEntity) + prevTrackedEntity = null; + + NavMap.Focus = _trackedEntity; + _tryToScrollToListFocus = true; + + UpdateSensorsTable(_trackedEntity, prevTrackedEntity); + } - private void ClearAllSensors() + private void UpdateSensorsTable(NetEntity? currTrackedEntity, NetEntity? prevTrackedEntity) + { + foreach (var sensor in SensorsTable.Children) { - foreach (var child in _rowsContent) + if (sensor is not CrewMonitoringButton) + continue; + + var castSensor = (CrewMonitoringButton) sensor; + + if (castSensor.SuitSensorUid == prevTrackedEntity) + castSensor.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + + else if (castSensor.SuitSensorUid == currTrackedEntity) + castSensor.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + + if (castSensor?.Coordinates == null) + continue; + + if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data)) { - SensorsTable.RemoveChild(child); + data = new NavMapBlip + (data.Coordinates, + data.Texture, + (currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray, + castSensor.SuitSensorUid == currTrackedEntity); + + NavMap.TrackedEntities[castSensor.SuitSensorUid] = data; } - _rowsContent.Clear(); - _directionIcons.Clear(); - NavMap.TrackedCoordinates.Clear(); } + } - private void SetColorLabel(Label label, int? totalDamage, bool isAlive) + private void TryToScrollToFocus() + { + if (!_tryToScrollToListFocus) + return; + + if (!TryGetVerticalScrollbar(SensorScroller, out var vScrollbar)) + return; + + if (TryGetNextScrollPosition(out float? nextScrollPosition)) { - var startColor = Color.White; - var critColor = Color.Yellow; - var endColor = Color.Red; + vScrollbar.ValueTarget = nextScrollPosition.Value; - if (!isAlive) + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) { - label.FontColorOverride = endColor; + _tryToScrollToListFocus = false; return; } + } + } - //Convert from null to regular int - int damage; - if (totalDamage == null) return; - else damage = (int) totalDamage; + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; - if (damage <= 0) - { - label.FontColorOverride = startColor; - } - else if (damage >= 200) - { - label.FontColorOverride = endColor; - } - else if (damage >= 0 && damage <= 100) - { - label.FontColorOverride = GetColorLerp(startColor, critColor, damage); - } - else if (damage >= 100 && damage <= 200) - { - //We need a number from 0 to 100. Divide the number from 100 to 200 by 2 - damage /= 2; - label.FontColorOverride = GetColorLerp(critColor, endColor, damage); - } + foreach (var child in scroll.Children) + { + if (child is not VScrollBar) + continue; + + vScrollBar = (VScrollBar) child; + return true; } - private Color GetColorLerp(Color startColor, Color endColor, int damage) + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = 0; + + foreach (var sensor in SensorsTable.Children) { - //Smooth transition from one color to another depending on the percentage - var t = damage / 100f; - var r = MathHelper.Lerp(startColor.R, endColor.R, t); - var g = MathHelper.Lerp(startColor.G, endColor.G, t); - var b = MathHelper.Lerp(startColor.B, endColor.B, t); - var a = MathHelper.Lerp(startColor.A, endColor.A, t); - - return new Color(r, g, b, a); + if (sensor is CrewMonitoringButton && + ((CrewMonitoringButton) sensor).SuitSensorUid == _trackedEntity) + return true; + + nextScrollPosition += sensor.Height; } + + // Failed to find control + nextScrollPosition = null; + + return false; } - public sealed class CrewMonitoringButton : Button + private void ClearOutDatedData() { - public int IndexInTable; - public EntityUid? SuitSensorUid; - public EntityCoordinates? Coordinates; + SensorsTable.RemoveAllChildren(); + _rowsContent.Clear(); + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + NavMap.LocalizedNames.Clear(); } } + +public sealed class CrewMonitoringButton : Button +{ + public int IndexInTable; + public NetEntity SuitSensorUid; + public EntityCoordinates? Coordinates; +} diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 04d8cc76f9..cae5e15037 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -1,40 +1,66 @@ -using System.Numerics; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Pinpointer; -using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Collections; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; +using Robust.Shared.Timing; +using System.Numerics; +using JetBrains.Annotations; namespace Content.Client.Pinpointer.UI; /// /// Displays the nav map data of the specified grid. /// -public sealed class NavMapControl : MapGridControl +[UsedImplicitly, Virtual] +public partial class NavMapControl : MapGridControl { [Dependency] private readonly IEntityManager _entManager = default!; - private SharedTransformSystem _transform; + private readonly SharedTransformSystem _transformSystem = default!; public EntityUid? MapUid; + // Actions + public event Action? TrackedEntitySelectedAction; + + // Tracked data public Dictionary TrackedCoordinates = new(); + public Dictionary TrackedEntities = new(); + public Dictionary> TileGrid = default!; + + // Default colors + public Color WallColor = new(102, 217, 102); + public Color TileColor = new(30, 67, 30); + + // Constants + protected float UpdateTime = 1.0f; + protected float MaxSelectableDistance = 10f; + protected float RecenterMinimum = 0.05f; + // Local variables private Vector2 _offset; private bool _draggin; private bool _recentering = false; - private readonly float _recenterMinimum = 0.05f; private readonly Font _font; - private static readonly Color TileColor = new(30, 67, 30); - private static readonly Color BeaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); + private float _updateTimer = 0.25f; + private Dictionary _sRGBLookUp = new Dictionary(); + private Color _beaconColor; + + // Components + private NavMapComponent? _navMap; + private MapGridComponent? _grid; + private TransformComponent? _xform; + private PhysicsComponent? _physics; + private FixturesComponent? _fixtures; // TODO: https://github.com/space-wizards/RobustToolbox/issues/3818 private readonly Label _zoom = new() @@ -45,20 +71,30 @@ public sealed class NavMapControl : MapGridControl private readonly Button _recenter = new() { - Text = "Recentre", + Text = Loc.GetString("navmap-recenter"), VerticalAlignment = VAlignment.Top, HorizontalAlignment = HAlignment.Right, Margin = new Thickness(8f, 4f), Disabled = true, }; + private readonly CheckBox _beacons = new() + { + Text = Loc.GetString("navmap-toggle-beacons"), + Margin = new Thickness(4f, 0f), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Pressed = false, + }; + public NavMapControl() : base(8f, 128f, 48f) { IoCManager.InjectDependencies(this); - - _transform = _entManager.System(); var cache = IoCManager.Resolve(); - _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16); + + _transformSystem = _entManager.System(); + _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 12); + _beaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); RectClipContent = true; HorizontalExpand = true; @@ -75,6 +111,7 @@ public sealed class NavMapControl : MapGridControl Children = { _zoom, + _beacons, _recenter, } }; @@ -101,14 +138,28 @@ public sealed class NavMapControl : MapGridControl { _recentering = true; }; + + ForceNavMapUpdate(); + } + + public void ForceRecenter() + { + _recentering = true; + } + + public void ForceNavMapUpdate() + { + _entManager.TryGetComponent(MapUid, out _navMap); + _entManager.TryGetComponent(MapUid, out _grid); + + UpdateNavMap(); } public void CenterToCoordinates(EntityCoordinates coordinates) { - if (_entManager.TryGetComponent(MapUid, out var physics)) - { - _offset = new Vector2(coordinates.X, coordinates.Y) - physics.LocalCenter; - } + if (_physics != null) + _offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter; + _recenter.Disabled = false; } @@ -117,18 +168,62 @@ public sealed class NavMapControl : MapGridControl base.KeyBindDown(args); if (args.Function == EngineKeyFunctions.Use) - { _draggin = true; - } } protected override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); + if (TrackedEntitySelectedAction == null) + return; + if (args.Function == EngineKeyFunctions.Use) { _draggin = false; + + if (_xform == null || _physics == null || TrackedEntities.Count == 0) + return; + + // Get the clicked position + var offset = _offset + _physics.LocalCenter; + var localPosition = args.PointerLocation.Position - GlobalPixelPosition; + + // Convert to a world position + var unscaledPosition = (localPosition - MidpointVector) / MinimapScale; + var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset); + + // Find closest tracked entity in range + var closestEntity = NetEntity.Invalid; + var closestCoords = new EntityCoordinates(); + var closestDistance = float.PositiveInfinity; + + foreach ((var currentEntity, var blip) in TrackedEntities) + { + if (!blip.Selectable) + continue; + + var currentDistance = (blip.Coordinates.ToMapPos(_entManager, _transformSystem) - worldPosition).Length(); + + if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance) + continue; + + closestEntity = currentEntity; + closestCoords = blip.Coordinates; + closestDistance = currentDistance; + } + + if (closestDistance > MaxSelectableDistance || !closestEntity.IsValid()) + return; + + TrackedEntitySelectedAction.Invoke(closestEntity); + } + + else if (args.Function == EngineKeyFunctions.UIRightClick) + { + // Clear current selection with right click + if (TrackedEntitySelectedAction != null) + TrackedEntitySelectedAction.Invoke(null); } } @@ -143,25 +238,30 @@ public sealed class NavMapControl : MapGridControl _offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange; if (_offset != Vector2.Zero) - { _recenter.Disabled = false; - } + else - { _recenter.Disabled = true; - } } protected override void Draw(DrawingHandleScreen handle) { base.Draw(handle); + // Get the components necessary for drawing the navmap + _entManager.TryGetComponent(MapUid, out _navMap); + _entManager.TryGetComponent(MapUid, out _grid); + _entManager.TryGetComponent(MapUid, out _xform); + _entManager.TryGetComponent(MapUid, out _physics); + _entManager.TryGetComponent(MapUid, out _fixtures); + + // Map re-centering if (_recentering) { var frameTime = Timing.FrameTime; var diff = _offset * (float) frameTime.TotalSeconds; - if (_offset.LengthSquared() < _recenterMinimum) + if (_offset.LengthSquared() < RecenterMinimum) { _offset = Vector2.Zero; _recentering = false; @@ -173,29 +273,22 @@ public sealed class NavMapControl : MapGridControl } } - _zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%"; + _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(WorldRange / WorldMaxRange * 100f):0.00}")); - if (!_entManager.TryGetComponent(MapUid, out var navMap) || - !_entManager.TryGetComponent(MapUid, out var xform) || - !_entManager.TryGetComponent(MapUid, out var grid)) - { + if (_navMap == null || _xform == null) return; - } var offset = _offset; - var lineColor = new Color(102, 217, 102); - if (_entManager.TryGetComponent(MapUid, out var physics)) - { - offset += physics.LocalCenter; - } + if (_physics != null) + offset += _physics.LocalCenter; // Draw tiles - if (_entManager.TryGetComponent(MapUid, out var manager)) + if (_fixtures != null) { Span verts = new Vector2[8]; - foreach (var fixture in manager.Fixtures.Values) + foreach (var fixture in _fixtures.Fixtures.Values) { if (fixture.Shape is not PolygonShape poly) continue; @@ -211,157 +304,305 @@ public sealed class NavMapControl : MapGridControl } } - // Draw the wall data var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset); - var tileSize = new Vector2(grid.TileSize, -grid.TileSize); - for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize) + // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order + // to figure out where they should be drawn. However, we don't *need* to do check these every frame. + // Instead, lets periodically update where to draw each line and then store these points in a list. + // Then we can just run through the list each frame and draw the lines without any extra computation. + + // Draw walls + if (TileGrid != null && TileGrid.Count > 0) { - for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize) + var walls = new ValueList(); + + foreach ((var chunk, var chunkedLines) in TileGrid) { - var floored = new Vector2i((int) x, (int) y); + var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize; - var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize); + if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right) + continue; - if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk)) + if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top) continue; - // TODO: Okay maybe I should just use ushorts lmao... - for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++) + foreach (var chunkedLine in chunkedLines) { - var value = (int) Math.Pow(2, i); - - var mask = chunk.TileData & value; - - if (mask == 0x0) - continue; - - // Alright now we'll work out our edges - var relativeTile = SharedNavMapSystem.GetTile(mask); - var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset; - var position = new Vector2(tile.X, -tile.Y); - NavMapChunk? neighborChunk; - bool neighbor; - - // North edge - if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor); - } - - // East edge - if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor); - } - - // South edge - if (relativeTile.Y == 0) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor); - } - - // West edge - if (relativeTile.X == 0) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor); - } - - // Draw a diagonal line for interiors. - handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor); + var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y)); + var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y)); + + walls.Add(start); + walls.Add(end); } } + + if (walls.Count > 0) + { + if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB)) + { + sRGB = Color.ToSrgb(WallColor); + _sRGBLookUp[WallColor] = sRGB; + } + + handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB); + } + } + + // Beacons + if (_beacons.Pressed) + { + var rectBuffer = new Vector2(5f, 3f); + + foreach (var beacon in _navMap.Beacons) + { + var position = beacon.Position - offset; + position = Scale(position with { Y = -position.Y }); + + var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f); + handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), _beaconColor); + handle.DrawString(_font, position - textDimensions / 2, beacon.Text, beacon.Color); + } } var curTime = Timing.RealTime; var blinkFrequency = 1f / 1f; var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f; + // Tracked coordinates (simple dot, legacy) foreach (var (coord, value) in TrackedCoordinates) { if (lit && value.Visible) { - var mapPos = coord.ToMap(_entManager); + var mapPos = coord.ToMap(_entManager, _transformSystem); if (mapPos.MapId != MapId.Nullspace) { - var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset; + var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset; position = Scale(new Vector2(position.X, -position.Y)); - handle.DrawCircle(position, MinimapScale / 2f, value.Color); + handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color); } } } - // Beacons - var labelOffset = new Vector2(0.5f, 0.5f) * MinimapScale; - var rectBuffer = new Vector2(5f, 3f); + // Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually) + var iconVertexUVs = new Dictionary<(Texture, Color), ValueList>(); - foreach (var beacon in navMap.Beacons) + foreach (var blip in TrackedEntities.Values) { - var position = beacon.Position - offset; + if (blip.Blinks && !lit) + continue; + + if (blip.Texture == null) + continue; - position = Scale(position with { Y = -position.Y }); + if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs)) + vertexUVs = new(); - handle.DrawCircle(position, MinimapScale / 2f, beacon.Color); - var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f); + var mapPos = blip.Coordinates.ToMap(_entManager, _transformSystem); - var labelPosition = position + labelOffset; - handle.DrawRect(new UIBox2(labelPosition, labelPosition + textDimensions + rectBuffer * 2), BeaconColor); - handle.DrawString(_font, labelPosition + rectBuffer, beacon.Text, beacon.Color); + if (mapPos.MapId != MapId.Nullspace) + { + var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset; + position = Scale(new Vector2(position.X, -position.Y)); + + var scalingCoefficient = 2.5f; + var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale); + + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f))); + } + + iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs; } + + foreach ((var (texture, color), var vertexUVs) in iconVertexUVs) + { + if (!_sRGBLookUp.TryGetValue(color, out var sRGB)) + { + sRGB = Color.ToSrgb(color); + _sRGBLookUp[color] = sRGB; + } + + handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB); + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + // Update the timer + _updateTimer += args.DeltaSeconds; + + if (_updateTimer >= UpdateTime) + { + _updateTimer -= UpdateTime; + + UpdateNavMap(); + } + } + + private void UpdateNavMap() + { + if (_navMap == null || _grid == null) + return; + + TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid); + } + + public Dictionary> GetDecodedWallChunks + (Dictionary chunks, + MapGridComponent grid) + { + var decodedOutput = new Dictionary>(); + + foreach ((var chunkOrigin, var chunk) in chunks) + { + var list = new List(); + + // TODO: Okay maybe I should just use ushorts lmao... + for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++) + { + var value = (int) Math.Pow(2, i); + + var mask = chunk.TileData & value; + + if (mask == 0x0) + continue; + + // Alright now we'll work out our edges + var relativeTile = SharedNavMapSystem.GetTile(mask); + var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize; + var position = new Vector2(tile.X, -tile.Y); + NavMapChunk? neighborChunk; + bool neighbor; + + // North edge + if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize))); + } + + // East edge + if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f))); + } + + // South edge + if (relativeTile.Y == 0) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position)); + } + + // West edge + if (relativeTile.X == 0) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add point + list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize))); + } + + // Draw a diagonal line for interiors. + list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f))); + } + + decodedOutput.Add(chunkOrigin, list); + } + + return decodedOutput; } - private Vector2 Scale(Vector2 position) + protected Vector2 Scale(Vector2 position) { return position * MinimapScale + MidpointVector; } + + protected Vector2 GetOffset() + { + return _offset + (_physics != null ? _physics.LocalCenter : new Vector2()); + } +} + +public struct NavMapBlip +{ + public EntityCoordinates Coordinates; + public Texture Texture; + public Color Color; + public bool Blinks; + public bool Selectable; + + public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true) + { + Coordinates = coordinates; + Texture = texture; + Color = color; + Blinks = blinks; + Selectable = selectable; + } +} + +public struct NavMapLine +{ + public readonly Vector2 Origin; + public readonly Vector2 Terminus; + + public NavMapLine(Vector2 origin, Vector2 terminus) + { + Origin = origin; + Terminus = terminus; + } } diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs index 1fa12fa9e7..f52f536775 100644 --- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs @@ -21,5 +21,7 @@ public sealed partial class StationMapWindow : FancyWindow { Title = metadata.EntityName; } + + NavMapScreen.ForceNavMapUpdate(); } } diff --git a/Content.Server/Access/Components/PresetIdCardComponent.cs b/Content.Server/Access/Components/PresetIdCardComponent.cs index 5badbca311..94aef2e6ca 100644 --- a/Content.Server/Access/Components/PresetIdCardComponent.cs +++ b/Content.Server/Access/Components/PresetIdCardComponent.cs @@ -1,15 +1,14 @@ using Content.Shared.Roles; using Robust.Shared.Prototypes; -namespace Content.Server.Access.Components +namespace Content.Server.Access.Components; + +[RegisterComponent] +public sealed partial class PresetIdCardComponent : Component { - [RegisterComponent] - public sealed partial class PresetIdCardComponent : Component - { - [DataField("job")] - public ProtoId? JobName; + [DataField("job")] + public ProtoId? JobName; - [DataField("name")] - public string? IdName; - } + [DataField("name")] + public string? IdName; } diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index 3bf00d34c7..1c1e687417 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -7,200 +7,216 @@ using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Database; using Content.Shared.Popups; +using Content.Shared.Roles; using Content.Shared.StatusIcon; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Access.Systems +namespace Content.Server.Access.Systems; + +public sealed class IdCardSystem : SharedIdCardSystem { - public sealed class IdCardSystem : SharedIdCardSystem - { - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly MetaDataSystem _metaSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly MetaDataSystem _metaSystem = default!; - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnMicrowaved); - } + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnMicrowaved); + } - private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) - { - UpdateEntityName(uid, id); - } + private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) + { + UpdateEntityName(uid, id); + } - private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args) + private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args) + { + if (TryComp(uid, out var access)) { - if (TryComp(uid, out var access)) + float randomPick = _random.NextFloat(); + // if really unlucky, burn card + if (randomPick <= 0.15f) { - float randomPick = _random.NextFloat(); - // if really unlucky, burn card - if (randomPick <= 0.15f) + TryComp(uid, out TransformComponent? transformComponent); + if (transformComponent != null) { - TryComp(uid, out TransformComponent? transformComponent); - if (transformComponent != null) - { - _popupSystem.PopupCoordinates(Loc.GetString("id-card-component-microwave-burnt", ("id", uid)), - transformComponent.Coordinates, PopupType.Medium); - EntityManager.SpawnEntity("FoodBadRecipe", - transformComponent.Coordinates); - } - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Microwave)} burnt {ToPrettyString(uid):entity}"); - EntityManager.QueueDeleteEntity(uid); - return; + _popupSystem.PopupCoordinates(Loc.GetString("id-card-component-microwave-burnt", ("id", uid)), + transformComponent.Coordinates, PopupType.Medium); + EntityManager.SpawnEntity("FoodBadRecipe", + transformComponent.Coordinates); } - // If they're unlucky, brick their ID - if (randomPick <= 0.25f) - { - _popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-bricked", ("id", uid)), uid); - - access.Tags.Clear(); - Dirty(access); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Microwave)} cleared access on {ToPrettyString(uid):entity}"); - } - else - { - _popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-safe", ("id", uid)), uid, PopupType.Medium); - } - - // Give them a wonderful new access to compensate for everything - var random = _random.Pick(_prototypeManager.EnumeratePrototypes().ToArray()); - - access.Tags.Add(random.ID); - Dirty(access); - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}"); + $"{ToPrettyString(args.Microwave)} burnt {ToPrettyString(uid):entity}"); + EntityManager.QueueDeleteEntity(uid); + return; } - } - - /// - /// Attempts to change the job title of a card. - /// Returns true/false. - /// - /// - /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. - /// - public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null) - { - if (!Resolve(uid, ref id)) - return false; - - if (!string.IsNullOrWhiteSpace(jobTitle)) + // If they're unlucky, brick their ID + if (randomPick <= 0.25f) { - jobTitle = jobTitle.Trim(); + _popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-bricked", ("id", uid)), uid); - if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength) - jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength]; + access.Tags.Clear(); + Dirty(access); + + _adminLogger.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(args.Microwave)} cleared access on {ToPrettyString(uid):entity}"); } else { - jobTitle = null; + _popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-safe", ("id", uid)), uid, PopupType.Medium); } - if (id.JobTitle == jobTitle) - return true; - id.JobTitle = jobTitle; - Dirty(id); - UpdateEntityName(uid, id); + // Give them a wonderful new access to compensate for everything + var random = _random.Pick(_prototypeManager.EnumeratePrototypes().ToArray()); - if (player != null) - { - _adminLogger.Add(LogType.Identity, LogImpact.Low, - $"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} "); - } - return true; + access.Tags.Add(random.ID); + Dirty(access); + + _adminLogger.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}"); } + } + + /// + /// Attempts to change the job title of a card. + /// Returns true/false. + /// + /// + /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. + /// + public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null) + { + if (!Resolve(uid, ref id)) + return false; - public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) + if (!string.IsNullOrWhiteSpace(jobTitle)) { - if (!Resolve(uid, ref id)) - { - return false; - } + jobTitle = jobTitle.Trim(); - if (id.JobIcon == jobIcon.ID) - { - return true; - } + if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength) + jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength]; + } + else + { + jobTitle = null; + } - id.JobIcon = jobIcon.ID; - Dirty(uid, id); + if (id.JobTitle == jobTitle) + return true; + id.JobTitle = jobTitle; + Dirty(id); + UpdateEntityName(uid, id); - if (player != null) - { - _adminLogger.Add(LogType.Identity, LogImpact.Low, - $"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} "); - } + if (player != null) + { + _adminLogger.Add(LogType.Identity, LogImpact.Low, + $"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} "); + } + return true; + } + + public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) + { + if (!Resolve(uid, ref id)) + { + return false; + } + if (id.JobIcon == jobIcon.ID) + { return true; } - /// - /// Attempts to change the full name of a card. - /// Returns true/false. - /// - /// - /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. - /// - public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null) + id.JobIcon = jobIcon.ID; + Dirty(uid, id); + + if (player != null) { - if (!Resolve(uid, ref id)) - return false; + _adminLogger.Add(LogType.Identity, LogImpact.Low, + $"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} "); + } - if (!string.IsNullOrWhiteSpace(fullName)) - { - fullName = fullName.Trim(); - if (fullName.Length > IdCardConsoleComponent.MaxFullNameLength) - fullName = fullName[..IdCardConsoleComponent.MaxFullNameLength]; - } - else - { - fullName = null; - } + return true; + } - if (id.FullName == fullName) - return true; - id.FullName = fullName; - Dirty(id); - UpdateEntityName(uid, id); + public bool TryChangeJobDepartment(EntityUid uid, JobPrototype job, IdCardComponent? id = null) + { + if (!Resolve(uid, ref id)) + return false; - if (player != null) - { - _adminLogger.Add(LogType.Identity, LogImpact.Low, - $"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} "); - } - return true; + foreach (var department in _prototypeManager.EnumeratePrototypes()) + { + if (department.Roles.Contains(job.ID)) + id.JobDepartments.Add("department-" + department.ID); } - /// - /// Changes the name of the id's owner. - /// - /// - /// If either or is empty, it's replaced by placeholders. - /// If both are empty, the original entity's name is restored. - /// - private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) + Dirty(uid, id); + + return true; + } + + /// + /// Attempts to change the full name of a card. + /// Returns true/false. + /// + /// + /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. + /// + public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null) + { + if (!Resolve(uid, ref id)) + return false; + + if (!string.IsNullOrWhiteSpace(fullName)) { - if (!Resolve(uid, ref id)) - return; + fullName = fullName.Trim(); + if (fullName.Length > IdCardConsoleComponent.MaxFullNameLength) + fullName = fullName[..IdCardConsoleComponent.MaxFullNameLength]; + } + else + { + fullName = null; + } - var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})"; + if (id.FullName == fullName) + return true; + id.FullName = fullName; + Dirty(id); + UpdateEntityName(uid, id); - var val = string.IsNullOrWhiteSpace(id.FullName) - ? Loc.GetString("access-id-card-component-owner-name-job-title-text", - ("jobSuffix", jobSuffix)) - : Loc.GetString("access-id-card-component-owner-full-name-job-title-text", - ("fullName", id.FullName), - ("jobSuffix", jobSuffix)); - _metaSystem.SetEntityName(uid, val); + if (player != null) + { + _adminLogger.Add(LogType.Identity, LogImpact.Low, + $"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} "); } + return true; + } + + /// + /// Changes the name of the id's owner. + /// + /// + /// If either or is empty, it's replaced by placeholders. + /// If both are empty, the original entity's name is restored. + /// + private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) + { + if (!Resolve(uid, ref id)) + return; + + var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})"; + + var val = string.IsNullOrWhiteSpace(id.FullName) + ? Loc.GetString("access-id-card-component-owner-name-job-title-text", + ("jobSuffix", jobSuffix)) + : Loc.GetString("access-id-card-component-owner-full-name-job-title-text", + ("fullName", id.FullName), + ("jobSuffix", jobSuffix)); + _metaSystem.SetEntityName(uid, val); } } diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs index 96a38278b5..4073b4baa2 100644 --- a/Content.Server/Access/Systems/PresetIdCardSystem.cs +++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs @@ -7,82 +7,82 @@ using Content.Shared.Roles; using Content.Shared.StatusIcon; using Robust.Shared.Prototypes; -namespace Content.Server.Access.Systems +namespace Content.Server.Access.Systems; + +public sealed class PresetIdCardSystem : EntitySystem { - public sealed class PresetIdCardSystem : EntitySystem + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IdCardSystem _cardSystem = default!; + [Dependency] private readonly SharedAccessSystem _accessSystem = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + + public override void Initialize() { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IdCardSystem _cardSystem = default!; - [Dependency] private readonly SharedAccessSystem _accessSystem = default!; - [Dependency] private readonly StationSystem _stationSystem = default!; + SubscribeLocalEvent(OnMapInit); - public override void Initialize() - { - SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(PlayerJobsAssigned); + } - SubscribeLocalEvent(PlayerJobsAssigned); - } + private void PlayerJobsAssigned(RulePlayerJobsAssignedEvent ev) + { + // Go over all ID cards and make sure they're correctly configured for extended access. - private void PlayerJobsAssigned(RulePlayerJobsAssignedEvent ev) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var card)) { - // Go over all ID cards and make sure they're correctly configured for extended access. - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var card)) - { - var station = _stationSystem.GetOwningStation(uid); + var station = _stationSystem.GetOwningStation(uid); - // If we're not on an extended access station, the ID is already configured correctly from MapInit. - if (station == null || !Comp(station.Value).ExtendedAccess) - return; + // If we're not on an extended access station, the ID is already configured correctly from MapInit. + if (station == null || !Comp(station.Value).ExtendedAccess) + return; - SetupIdAccess(uid, card, true); - SetupIdName(uid, card); - } + SetupIdAccess(uid, card, true); + SetupIdName(uid, card); } + } - private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args) - { - // If a preset ID card is spawned on a station at setup time, - // the station may not exist, - // or may not yet know whether it is on extended access (players not spawned yet). - // PlayerJobsAssigned makes sure extended access is configured correctly in that case. + private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args) + { + // If a preset ID card is spawned on a station at setup time, + // the station may not exist, + // or may not yet know whether it is on extended access (players not spawned yet). + // PlayerJobsAssigned makes sure extended access is configured correctly in that case. + + var station = _stationSystem.GetOwningStation(uid); + var extended = false; + if (station != null) + extended = Comp(station.Value).ExtendedAccess; + + SetupIdAccess(uid, id, extended); + SetupIdName(uid, id); + } - var station = _stationSystem.GetOwningStation(uid); - var extended = false; - if (station != null) - extended = Comp(station.Value).ExtendedAccess; + private void SetupIdName(EntityUid uid, PresetIdCardComponent id) + { + if (id.IdName == null) + return; + _cardSystem.TryChangeFullName(uid, id.IdName); + } - SetupIdAccess(uid, id, extended); - SetupIdName(uid, id); - } + private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extended) + { + if (id.JobName == null) + return; - private void SetupIdName(EntityUid uid, PresetIdCardComponent id) + if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job)) { - if (id.IdName == null) - return; - _cardSystem.TryChangeFullName(uid, id.IdName); + Log.Error($"Invalid job id ({id.JobName}) for preset card"); + return; } - private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extended) - { - if (id.JobName == null) - return; - - if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job)) - { - Log.Error($"Invalid job id ({id.JobName}) for preset card"); - return; - } - - _accessSystem.SetAccessToJob(uid, job, extended); + _accessSystem.SetAccessToJob(uid, job, extended); - _cardSystem.TryChangeJobTitle(uid, job.LocalizedName); + _cardSystem.TryChangeJobTitle(uid, job.LocalizedName); + _cardSystem.TryChangeJobDepartment(uid, job); - if (_prototypeManager.TryIndex(job.Icon, out var jobIcon)) - { - _cardSystem.TryChangeJobIcon(uid, jobIcon); - } + if (_prototypeManager.TryIndex(job.Icon, out var jobIcon)) + { + _cardSystem.TryChangeJobIcon(uid, jobIcon); } } } diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs index 7aaa2ef368..c2d7124194 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs @@ -1,33 +1,19 @@ using Content.Shared.Medical.SuitSensor; -namespace Content.Server.Medical.CrewMonitoring -{ - [RegisterComponent] - [Access(typeof(CrewMonitoringConsoleSystem))] - public sealed partial class CrewMonitoringConsoleComponent : Component - { - /// - /// List of all currently connected sensors to this console. - /// - public Dictionary ConnectedSensors = new(); - - /// - /// After what time sensor consider to be lost. - /// - [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)] - public float SensorTimeout = 10f; +namespace Content.Server.Medical.CrewMonitoring; - /// - /// Whether the direction arrows in the monitor UI should snap the nearest diagonal or cardinal direction, or whether they should point exactly towards the target. - /// - [DataField("snap"), ViewVariables(VVAccess.ReadWrite)] - public bool Snap = true; +[RegisterComponent] +[Access(typeof(CrewMonitoringConsoleSystem))] +public sealed partial class CrewMonitoringConsoleComponent : Component +{ + /// + /// List of all currently connected sensors to this console. + /// + public Dictionary ConnectedSensors = new(); - /// - /// Minimum distance before the monitor direction indicator stops pointing towards the target and instead - /// shows an icon indicating that the target is "here". Does not affect the displayed coordinates. - /// - [DataField("precision"), ViewVariables(VVAccess.ReadWrite)] - public float Precision = 10f; - } + /// + /// After what time sensor consider to be lost. + /// + [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)] + public float SensorTimeout = 10f; } diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs index ed65070436..ff02b9cbdf 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs @@ -4,62 +4,71 @@ using Content.Server.DeviceNetwork.Systems; using Content.Server.PowerCell; using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.SuitSensor; +using Content.Shared.Pinpointer; using Robust.Server.GameObjects; -namespace Content.Server.Medical.CrewMonitoring +namespace Content.Server.Medical.CrewMonitoring; + +public sealed class CrewMonitoringConsoleSystem : EntitySystem { - public sealed class CrewMonitoringConsoleSystem : EntitySystem + [Dependency] private readonly PowerCellSystem _cell = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnPacketReceived); + SubscribeLocalEvent(OnUIOpened); + } + + private void OnRemove(EntityUid uid, CrewMonitoringConsoleComponent component, ComponentRemove args) + { + component.ConnectedSensors.Clear(); + } + + private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, DeviceNetworkPacketEvent args) { - [Dependency] private readonly PowerCellSystem _cell = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnRemove); - SubscribeLocalEvent(OnPacketReceived); - SubscribeLocalEvent(OnUIOpened); - } - - private void OnRemove(EntityUid uid, CrewMonitoringConsoleComponent component, ComponentRemove args) - { - component.ConnectedSensors.Clear(); - } - - private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, DeviceNetworkPacketEvent args) - { - var payload = args.Data; - // check command - if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command)) - return; - if (command != DeviceNetworkConstants.CmdUpdatedState) - return; - if (!payload.TryGetValue(SuitSensorConstants.NET_STATUS_COLLECTION, out Dictionary? sensorStatus)) - return; - - component.ConnectedSensors = sensorStatus; - UpdateUserInterface(uid, component); - } - - private void OnUIOpened(EntityUid uid, CrewMonitoringConsoleComponent component, BoundUIOpenedEvent args) - { - if (!_cell.TryUseActivatableCharge(uid)) - return; - - UpdateUserInterface(uid, component); - } - - private void UpdateUserInterface(EntityUid uid, CrewMonitoringConsoleComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (!_uiSystem.TryGetUi(uid, CrewMonitoringUIKey.Key, out var bui)) - return; - - // update all sensors info - var allSensors = component.ConnectedSensors.Values.ToList(); - _uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors, component.Snap, component.Precision)); - } + var payload = args.Data; + + // Check command + if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command)) + return; + + if (command != DeviceNetworkConstants.CmdUpdatedState) + return; + + if (!payload.TryGetValue(SuitSensorConstants.NET_STATUS_COLLECTION, out Dictionary? sensorStatus)) + return; + + component.ConnectedSensors = sensorStatus; + UpdateUserInterface(uid, component); + } + + private void OnUIOpened(EntityUid uid, CrewMonitoringConsoleComponent component, BoundUIOpenedEvent args) + { + if (!_cell.TryUseActivatableCharge(uid)) + return; + + UpdateUserInterface(uid, component); + } + + private void UpdateUserInterface(EntityUid uid, CrewMonitoringConsoleComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (!_uiSystem.TryGetUi(uid, CrewMonitoringUIKey.Key, out var bui)) + return; + + // The grid must have a NavMapComponent to visualize the map in the UI + var xform = Transform(uid); + + if (xform.GridUid != null) + EnsureComp(xform.GridUid.Value); + + // Update all sensors info + var allSensors = component.ConnectedSensors.Values.ToList(); + _uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors)); } } diff --git a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs index 294be8c7e1..32b81e2921 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs @@ -1,76 +1,75 @@ using Content.Shared.Medical.SuitSensor; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Medical.SuitSensors +namespace Content.Server.Medical.SuitSensors; + +/// +/// Tracking device, embedded in almost all uniforms and jumpsuits. +/// If enabled, will report to crew monitoring console owners position and status. +/// +[RegisterComponent] +[Access(typeof(SuitSensorSystem))] +public sealed partial class SuitSensorComponent : Component { /// - /// Tracking device, embedded in almost all uniforms and jumpsuits. - /// If enabled, will report to crew monitoring console owners position and status. + /// Choose a random sensor mode when item is spawned. /// - [RegisterComponent] - [Access(typeof(SuitSensorSystem))] - public sealed partial class SuitSensorComponent : Component - { - /// - /// Choose a random sensor mode when item is spawned. - /// - [DataField("randomMode")] - public bool RandomMode = true; + [DataField("randomMode")] + public bool RandomMode = true; - /// - /// If true user can't change suit sensor mode - /// - [DataField("controlsLocked")] - public bool ControlsLocked = false; + /// + /// If true user can't change suit sensor mode + /// + [DataField("controlsLocked")] + public bool ControlsLocked = false; - /// - /// Current sensor mode. Can be switched by user verbs. - /// - [DataField("mode")] - public SuitSensorMode Mode = SuitSensorMode.SensorOff; + /// + /// Current sensor mode. Can be switched by user verbs. + /// + [DataField("mode")] + public SuitSensorMode Mode = SuitSensorMode.SensorOff; - /// - /// Activate sensor if user wear it in this slot. - /// - [DataField("activationSlot")] - public string ActivationSlot = "jumpsuit"; + /// + /// Activate sensor if user wear it in this slot. + /// + [DataField("activationSlot")] + public string ActivationSlot = "jumpsuit"; - /// - /// Activate sensor if user has this in a sensor-compatible container. - /// - [DataField("activationContainer")] - public string? ActivationContainer; + /// + /// Activate sensor if user has this in a sensor-compatible container. + /// + [DataField("activationContainer")] + public string? ActivationContainer; - /// - /// How often does sensor update its owners status (in seconds). Limited by the system update rate. - /// - [DataField("updateRate")] - public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f); + /// + /// How often does sensor update its owners status (in seconds). Limited by the system update rate. + /// + [DataField("updateRate")] + public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f); - /// - /// Current user that wears suit sensor. Null if nobody wearing it. - /// - [ViewVariables] - public EntityUid? User = null; + /// + /// Current user that wears suit sensor. Null if nobody wearing it. + /// + [ViewVariables] + public EntityUid? User = null; - /// - /// Next time when sensor updated owners status - /// - [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate = TimeSpan.Zero; + /// + /// Next time when sensor updated owners status + /// + [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate = TimeSpan.Zero; - /// - /// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work. - /// - [DataField("station")] - public EntityUid? StationId = null; + /// + /// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work. + /// + [DataField("station")] + public EntityUid? StationId = null; - /// - /// The server the suit sensor sends it state to. - /// The suit sensor will try connecting to a new server when no server is connected. - /// It does this by calling the servers entity system for performance reasons. - /// - [DataField("server")] - public string? ConnectedServer = null; - } + /// + /// The server the suit sensor sends it state to. + /// The suit sensor will try connecting to a new server when no server is connected. + /// It does this by calling the servers entity system for performance reasons. + /// + [DataField("server")] + public string? ConnectedServer = null; } diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index f382d520ff..4629a1bf55 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -18,399 +18,409 @@ using Robust.Shared.Map; using Robust.Shared.Random; using Robust.Shared.Timing; -namespace Content.Server.Medical.SuitSensors +namespace Content.Server.Medical.SuitSensors; + +public sealed class SuitSensorSystem : EntitySystem { - public sealed class SuitSensorSystem : EntitySystem + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!; + [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; + [Dependency] private readonly IdCardSystem _idCardSystem = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + + public override void Initialize() { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!; - [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; - [Dependency] private readonly IdCardSystem _idCardSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly StationSystem _stationSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnPlayerSpawn); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnEquipped); - SubscribeLocalEvent(OnUnequipped); - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent>(OnVerb); - SubscribeLocalEvent(OnInsert); - SubscribeLocalEvent(OnRemove); - } - - private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args) - { - component.NextUpdate += args.PausedTime; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var curTime = _gameTiming.CurTime; - var sensors = EntityManager.EntityQueryEnumerator(); + base.Initialize(); + SubscribeLocalEvent(OnPlayerSpawn); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnInsert); + SubscribeLocalEvent(OnRemove); + } - while (sensors.MoveNext(out var uid, out var sensor, out var device)) - { - if (device.TransmitFrequency is null) - continue; + private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args) + { + component.NextUpdate += args.PausedTime; + } - // check if sensor is ready to update - if (curTime < sensor.NextUpdate) - continue; + public override void Update(float frameTime) + { + base.Update(frameTime); - if (!CheckSensorAssignedStation(uid, sensor)) - continue; + var curTime = _gameTiming.CurTime; + var sensors = EntityManager.EntityQueryEnumerator(); - // TODO: This would cause imprecision at different tick rates. - sensor.NextUpdate = curTime + sensor.UpdateRate; + while (sensors.MoveNext(out var uid, out var sensor, out var device)) + { + if (device.TransmitFrequency is null) + continue; - // get sensor status - var status = GetSensorState(uid, sensor); - if (status == null) - continue; + // check if sensor is ready to update + if (curTime < sensor.NextUpdate) + continue; - //Retrieve active server address if the sensor isn't connected to a server - if (sensor.ConnectedServer == null) - { - if (!_monitoringServerSystem.TryGetActiveServerAddress(sensor.StationId!.Value, out var address)) - continue; + if (!CheckSensorAssignedStation(uid, sensor)) + continue; - sensor.ConnectedServer = address; - } + // TODO: This would cause imprecision at different tick rates. + sensor.NextUpdate = curTime + sensor.UpdateRate; - // Send it to the connected server - var payload = SuitSensorToPacket(status); + // get sensor status + var status = GetSensorState(uid, sensor); + if (status == null) + continue; - // Clear the connected server if its address isn't on the network - if (!_deviceNetworkSystem.IsAddressPresent(device.DeviceNetId, sensor.ConnectedServer)) - { - sensor.ConnectedServer = null; + //Retrieve active server address if the sensor isn't connected to a server + if (sensor.ConnectedServer == null) + { + if (!_monitoringServerSystem.TryGetActiveServerAddress(sensor.StationId!.Value, out var address)) continue; - } - _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device); + sensor.ConnectedServer = address; } - } - /// - /// Checks whether the sensor is assigned to a station or not - /// and tries to assign an unassigned sensor to a station if it's currently on a grid - /// - /// True if the sensor is assigned to a station or assigning it was successful. False otherwise. - private bool CheckSensorAssignedStation(EntityUid uid, SuitSensorComponent sensor) - { - if (!sensor.StationId.HasValue && Transform(uid).GridUid == null) - return false; + // Send it to the connected server + var payload = SuitSensorToPacket(status); - sensor.StationId = _stationSystem.GetOwningStation(uid); - return sensor.StationId.HasValue; - } + // Clear the connected server if its address isn't on the network + if (!_deviceNetworkSystem.IsAddressPresent(device.DeviceNetId, sensor.ConnectedServer)) + { + sensor.ConnectedServer = null; + continue; + } - private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev) - { - // If the player spawns in arrivals then the grid underneath them may not be appropriate. - // in which case we'll just use the station spawn code told us they are attached to and set all of their - // sensors. - var sensorQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); - RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery); + _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device); } + } - private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery sensorQuery, EntityQuery xformQuery) - { - var xform = xformQuery.GetComponent(uid); - var enumerator = xform.ChildEnumerator; + /// + /// Checks whether the sensor is assigned to a station or not + /// and tries to assign an unassigned sensor to a station if it's currently on a grid + /// + /// True if the sensor is assigned to a station or assigning it was successful. False otherwise. + private bool CheckSensorAssignedStation(EntityUid uid, SuitSensorComponent sensor) + { + if (!sensor.StationId.HasValue && Transform(uid).GridUid == null) + return false; - while (enumerator.MoveNext(out var child)) - { - if (sensorQuery.TryGetComponent(child, out var sensor)) - { - sensor.StationId = stationUid; - } + sensor.StationId = _stationSystem.GetOwningStation(uid); + return sensor.StationId.HasValue; + } - RecursiveSensor(child.Value, stationUid, sensorQuery, xformQuery); - } - } + private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev) + { + // If the player spawns in arrivals then the grid underneath them may not be appropriate. + // in which case we'll just use the station spawn code told us they are attached to and set all of their + // sensors. + var sensorQuery = GetEntityQuery(); + var xformQuery = GetEntityQuery(); + RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery); + } - private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args) - { - // Fallback - component.StationId ??= _stationSystem.GetOwningStation(uid); + private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery sensorQuery, EntityQuery xformQuery) + { + var xform = xformQuery.GetComponent(uid); + var enumerator = xform.ChildEnumerator; - // generate random mode - if (component.RandomMode) + while (enumerator.MoveNext(out var child)) + { + if (sensorQuery.TryGetComponent(child, out var sensor)) { - //make the sensor mode favor higher levels, except coords. - var modesDist = new[] - { - SuitSensorMode.SensorOff, - SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary, - SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, - SuitSensorMode.SensorCords, SuitSensorMode.SensorCords - }; - component.Mode = _random.Pick(modesDist); + sensor.StationId = stationUid; } + + RecursiveSensor(child.Value, stationUid, sensorQuery, xformQuery); } + } - private void OnEquipped(EntityUid uid, SuitSensorComponent component, GotEquippedEvent args) - { - if (args.Slot != component.ActivationSlot) - return; + private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args) + { + // Fallback + component.StationId ??= _stationSystem.GetOwningStation(uid); - component.User = args.Equipee; + // generate random mode + if (component.RandomMode) + { + //make the sensor mode favor higher levels, except coords. + var modesDist = new[] + { + SuitSensorMode.SensorOff, + SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary, + SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, + SuitSensorMode.SensorCords, SuitSensorMode.SensorCords + }; + component.Mode = _random.Pick(modesDist); } + } - private void OnUnequipped(EntityUid uid, SuitSensorComponent component, GotUnequippedEvent args) - { - if (args.Slot != component.ActivationSlot) - return; + private void OnEquipped(EntityUid uid, SuitSensorComponent component, GotEquippedEvent args) + { + if (args.Slot != component.ActivationSlot) + return; - component.User = null; - } + component.User = args.Equipee; + } - private void OnExamine(EntityUid uid, SuitSensorComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; + private void OnUnequipped(EntityUid uid, SuitSensorComponent component, GotUnequippedEvent args) + { + if (args.Slot != component.ActivationSlot) + return; - string msg; - switch (component.Mode) - { - case SuitSensorMode.SensorOff: - msg = "suit-sensor-examine-off"; - break; - case SuitSensorMode.SensorBinary: - msg = "suit-sensor-examine-binary"; - break; - case SuitSensorMode.SensorVitals: - msg = "suit-sensor-examine-vitals"; - break; - case SuitSensorMode.SensorCords: - msg = "suit-sensor-examine-cords"; - break; - default: - return; - } + component.User = null; + } - args.PushMarkup(Loc.GetString(msg)); - } + private void OnExamine(EntityUid uid, SuitSensorComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; - private void OnVerb(EntityUid uid, SuitSensorComponent component, GetVerbsEvent args) + string msg; + switch (component.Mode) { - // check if user can change sensor - if (component.ControlsLocked) + case SuitSensorMode.SensorOff: + msg = "suit-sensor-examine-off"; + break; + case SuitSensorMode.SensorBinary: + msg = "suit-sensor-examine-binary"; + break; + case SuitSensorMode.SensorVitals: + msg = "suit-sensor-examine-vitals"; + break; + case SuitSensorMode.SensorCords: + msg = "suit-sensor-examine-cords"; + break; + default: return; + } - // standard interaction checks - if (!args.CanAccess || !args.CanInteract || args.Hands == null) - return; + args.PushMarkup(Loc.GetString(msg)); + } - args.Verbs.UnionWith(new[] - { - CreateVerb(uid, component, args.User, SuitSensorMode.SensorOff), - CreateVerb(uid, component, args.User, SuitSensorMode.SensorBinary), - CreateVerb(uid, component, args.User, SuitSensorMode.SensorVitals), - CreateVerb(uid, component, args.User, SuitSensorMode.SensorCords) - }); - } + private void OnVerb(EntityUid uid, SuitSensorComponent component, GetVerbsEvent args) + { + // check if user can change sensor + if (component.ControlsLocked) + return; - private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args) + // standard interaction checks + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + args.Verbs.UnionWith(new[] { - if (args.Container.ID != component.ActivationContainer) - return; + CreateVerb(uid, component, args.User, SuitSensorMode.SensorOff), + CreateVerb(uid, component, args.User, SuitSensorMode.SensorBinary), + CreateVerb(uid, component, args.User, SuitSensorMode.SensorVitals), + CreateVerb(uid, component, args.User, SuitSensorMode.SensorCords) + }); + } - component.User = args.Container.Owner; - } + private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != component.ActivationContainer) + return; - private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args) - { - if (args.Container.ID != component.ActivationContainer) - return; + component.User = args.Container.Owner; + } - component.User = null; - } + private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args) + { + if (args.Container.ID != component.ActivationContainer) + return; - private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode) - { - return new Verb() - { - Text = GetModeName(mode), - Disabled = component.Mode == mode, - Priority = -(int) mode, // sort them in descending order - Category = VerbCategory.SetSensor, - Act = () => SetSensor(uid, mode, userUid, component) - }; - } + component.User = null; + } - private string GetModeName(SuitSensorMode mode) + private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode) + { + return new Verb() { - string name; - switch (mode) - { - case SuitSensorMode.SensorOff: - name = "suit-sensor-mode-off"; - break; - case SuitSensorMode.SensorBinary: - name = "suit-sensor-mode-binary"; - break; - case SuitSensorMode.SensorVitals: - name = "suit-sensor-mode-vitals"; - break; - case SuitSensorMode.SensorCords: - name = "suit-sensor-mode-cords"; - break; - default: - return ""; - } + Text = GetModeName(mode), + Disabled = component.Mode == mode, + Priority = -(int) mode, // sort them in descending order + Category = VerbCategory.SetSensor, + Act = () => SetSensor(uid, mode, userUid, component) + }; + } - return Loc.GetString(name); + private string GetModeName(SuitSensorMode mode) + { + string name; + switch (mode) + { + case SuitSensorMode.SensorOff: + name = "suit-sensor-mode-off"; + break; + case SuitSensorMode.SensorBinary: + name = "suit-sensor-mode-binary"; + break; + case SuitSensorMode.SensorVitals: + name = "suit-sensor-mode-vitals"; + break; + case SuitSensorMode.SensorCords: + name = "suit-sensor-mode-cords"; + break; + default: + return ""; } - public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = null, - SuitSensorComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; + return Loc.GetString(name); + } - component.Mode = mode; + public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = null, + SuitSensorComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; - if (userUid != null) - { - var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode))); - _popupSystem.PopupEntity(msg, uid, userUid.Value); - } - } + component.Mode = mode; - public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null) + if (userUid != null) { - if (!Resolve(uid, ref sensor, ref transform)) - return null; + var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode))); + _popupSystem.PopupEntity(msg, uid, userUid.Value); + } + } - // check if sensor is enabled and worn by user - if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || transform.GridUid == null) - return null; + public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null) + { + if (!Resolve(uid, ref sensor, ref transform)) + return null; - // try to get mobs id from ID slot - var userName = Loc.GetString("suit-sensor-component-unknown-name"); - var userJob = Loc.GetString("suit-sensor-component-unknown-job"); - if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card)) - { - if (card.Comp.FullName != null) - userName = card.Comp.FullName; - if (card.Comp.JobTitle != null) - userJob = card.Comp.JobTitle; - } + // check if sensor is enabled and worn by user + if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || transform.GridUid == null) + return null; - // get health mob state - var isAlive = false; - if (EntityManager.TryGetComponent(sensor.User.Value, out MobStateComponent? mobState)) - isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState); + // try to get mobs id from ID slot + var userName = Loc.GetString("suit-sensor-component-unknown-name"); + var userJob = Loc.GetString("suit-sensor-component-unknown-job"); + var userJobIcon = "JobIconNoId"; + var userJobDepartments = new List(); - // get mob total damage - var totalDamage = 0; - if (TryComp(sensor.User.Value, out var damageable)) - totalDamage = damageable.TotalDamage.Int(); + if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card)) + { + if (card.Comp.FullName != null) + userName = card.Comp.FullName; + if (card.Comp.JobTitle != null) + userJob = card.Comp.JobTitle; + if (card.Comp.JobIcon != null) + userJobIcon = card.Comp.JobIcon; + + foreach (var department in card.Comp.JobDepartments) + userJobDepartments.Add(Loc.GetString(department)); + } - // finally, form suit sensor status - var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob); - switch (sensor.Mode) - { - case SuitSensorMode.SensorBinary: - status.IsAlive = isAlive; - break; - case SuitSensorMode.SensorVitals: - status.IsAlive = isAlive; - status.TotalDamage = totalDamage; - break; - case SuitSensorMode.SensorCords: - status.IsAlive = isAlive; - status.TotalDamage = totalDamage; - EntityCoordinates coordinates; - var xformQuery = GetEntityQuery(); - - if (transform.GridUid != null) - { - coordinates = new EntityCoordinates(transform.GridUid.Value, - _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery) - .Transform(_transform.GetWorldPosition(transform, xformQuery))); - } - else if (transform.MapUid != null) - { - coordinates = new EntityCoordinates(transform.MapUid.Value, - _transform.GetWorldPosition(transform, xformQuery)); - } - else - { - coordinates = EntityCoordinates.Invalid; - } - - status.Coordinates = GetNetCoordinates(coordinates); - break; - } + // get health mob state + var isAlive = false; + if (EntityManager.TryGetComponent(sensor.User.Value, out MobStateComponent? mobState)) + isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState); - return status; - } + // get mob total damage + var totalDamage = 0; + if (TryComp(sensor.User.Value, out var damageable)) + totalDamage = damageable.TotalDamage.Int(); - /// - /// Serialize create a device network package from the suit sensors status. - /// - public NetworkPayload SuitSensorToPacket(SuitSensorStatus status) + // finally, form suit sensor status + var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob, userJobIcon, userJobDepartments); + switch (sensor.Mode) { - var payload = new NetworkPayload() - { - [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState, - [SuitSensorConstants.NET_NAME] = status.Name, - [SuitSensorConstants.NET_JOB] = status.Job, - [SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive, - [SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid, - }; + case SuitSensorMode.SensorBinary: + status.IsAlive = isAlive; + break; + case SuitSensorMode.SensorVitals: + status.IsAlive = isAlive; + status.TotalDamage = totalDamage; + break; + case SuitSensorMode.SensorCords: + status.IsAlive = isAlive; + status.TotalDamage = totalDamage; + EntityCoordinates coordinates; + var xformQuery = GetEntityQuery(); + + if (transform.GridUid != null) + { + coordinates = new EntityCoordinates(transform.GridUid.Value, + _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery) + .Transform(_transform.GetWorldPosition(transform, xformQuery))); + } + else if (transform.MapUid != null) + { + coordinates = new EntityCoordinates(transform.MapUid.Value, + _transform.GetWorldPosition(transform, xformQuery)); + } + else + { + coordinates = EntityCoordinates.Invalid; + } - if (status.TotalDamage != null) - payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage); - if (status.Coordinates != null) - payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates); + status.Coordinates = GetNetCoordinates(coordinates); + break; + } + return status; + } - return payload; - } + /// + /// Serialize create a device network package from the suit sensors status. + /// + public NetworkPayload SuitSensorToPacket(SuitSensorStatus status) + { + var payload = new NetworkPayload() + { + [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState, + [SuitSensorConstants.NET_NAME] = status.Name, + [SuitSensorConstants.NET_JOB] = status.Job, + [SuitSensorConstants.NET_JOB_ICON] = status.JobIcon, + [SuitSensorConstants.NET_JOB_DEPARTMENTS] = status.JobDepartments, + [SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive, + [SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid, + }; + + if (status.TotalDamage != null) + payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage); + if (status.Coordinates != null) + payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates); + + return payload; + } - /// - /// Try to create the suit sensors status from the device network message - /// - public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload) + /// + /// Try to create the suit sensors status from the device network message + /// + public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload) + { + // check command + if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command)) + return null; + if (command != DeviceNetworkConstants.CmdUpdatedState) + return null; + + // check name, job and alive + if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null; + if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null; + if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_ICON, out string? jobIcon)) return null; + if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_DEPARTMENTS, out List? jobDepartments)) return null; + if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null; + if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null; + + // try get total damage and cords (optionals) + payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage); + payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords); + + var status = new SuitSensorStatus(suitSensorUid, name, job, jobIcon, jobDepartments) { - // check command - if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command)) - return null; - if (command != DeviceNetworkConstants.CmdUpdatedState) - return null; - - // check name, job and alive - if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null; - if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null; - if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null; - if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null; - - // try get total damage and cords (optionals) - payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage); - payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords); - - var status = new SuitSensorStatus(suitSensorUid, name, job) - { - IsAlive = isAlive.Value, - TotalDamage = totalDamage, - Coordinates = coords, - }; - return status; - } + IsAlive = isAlive.Value, + TotalDamage = totalDamage, + Coordinates = coords, + }; + return status; } } diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs index 975f0e6b51..7635716d26 100644 --- a/Content.Shared/Access/Components/IdCardComponent.cs +++ b/Content.Shared/Access/Components/IdCardComponent.cs @@ -4,29 +4,34 @@ using Content.Shared.StatusIcon; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Shared.Access.Components +namespace Content.Shared.Access.Components; + +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] +public sealed partial class IdCardComponent : Component { - [RegisterComponent, NetworkedComponent] - [AutoGenerateComponentState] - [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] - public sealed partial class IdCardComponent : Component - { - [DataField("fullName"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - // FIXME Friends - public string? FullName; + [DataField("fullName"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + // FIXME Friends + public string? FullName; - [DataField("jobTitle")] - [AutoNetworkedField] - [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite), ViewVariables(VVAccess.ReadWrite)] - public string? JobTitle; + [DataField("jobTitle")] + [AutoNetworkedField] + [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite), ViewVariables(VVAccess.ReadWrite)] + public string? JobTitle; - /// - /// The state of the job icon rsi. - /// - [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer))] - [AutoNetworkedField] - public string JobIcon = "JobIconUnknown"; + /// + /// The state of the job icon rsi. + /// + [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer))] + [AutoNetworkedField] + public string JobIcon = "JobIconUnknown"; - } + /// + /// The unlocalized names of the departments associated with the job + /// + [DataField("jobDepartments")] + [AutoNetworkedField] + public List JobDepartments = new(); } diff --git a/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs index 7e5c00558b..5b78839678 100644 --- a/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs +++ b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs @@ -1,27 +1,21 @@ using Content.Shared.Medical.SuitSensor; using Robust.Shared.Serialization; -namespace Content.Shared.Medical.CrewMonitoring +namespace Content.Shared.Medical.CrewMonitoring; + +[Serializable, NetSerializable] +public enum CrewMonitoringUIKey { - [Serializable, NetSerializable] - public enum CrewMonitoringUIKey - { - Key - } + Key +} - [Serializable, NetSerializable] - public sealed class CrewMonitoringState : BoundUserInterfaceState - { - public List Sensors; - public readonly bool Snap; - public readonly float Precision; +[Serializable, NetSerializable] +public sealed class CrewMonitoringState : BoundUserInterfaceState +{ + public List Sensors; - public CrewMonitoringState(List sensors, bool snap, float precision) - { - Sensors = sensors; - Snap = snap; - Precision = precision; - } + public CrewMonitoringState(List sensors) + { + Sensors = sensors; } - } diff --git a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs index 4e27959f89..07e0eca33b 100644 --- a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs +++ b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs @@ -1,61 +1,66 @@ using Robust.Shared.Map; using Robust.Shared.Serialization; -namespace Content.Shared.Medical.SuitSensor +namespace Content.Shared.Medical.SuitSensor; + +[Serializable, NetSerializable] +public sealed class SuitSensorStatus { - [Serializable, NetSerializable] - public sealed class SuitSensorStatus + public SuitSensorStatus(NetEntity suitSensorUid, string name, string job, string jobIcon, List jobDepartments) { - public SuitSensorStatus(NetEntity suitSensorUid, string name, string job) - { - SuitSensorUid = suitSensorUid; - Name = name; - Job = job; - } - - public TimeSpan Timestamp; - public NetEntity SuitSensorUid; - public string Name; - public string Job; - public bool IsAlive; - public int? TotalDamage; - public NetCoordinates? Coordinates; + SuitSensorUid = suitSensorUid; + Name = name; + Job = job; + JobIcon = jobIcon; + JobDepartments = jobDepartments; } - [Serializable, NetSerializable] - public enum SuitSensorMode : byte - { - /// - /// Sensor doesn't send any information about owner - /// - SensorOff = 0, - - /// - /// Sensor sends only binary status (alive/dead) - /// - SensorBinary = 1, - - /// - /// Sensor sends health vitals status - /// - SensorVitals = 2, - - /// - /// Sensor sends vitals status and GPS position - /// - SensorCords = 3 - } + public TimeSpan Timestamp; + public NetEntity SuitSensorUid; + public string Name; + public string Job; + public string JobIcon; + public List JobDepartments; + public bool IsAlive; + public int? TotalDamage; + public NetCoordinates? Coordinates; +} - public static class SuitSensorConstants - { - public const string NET_NAME = "name"; - public const string NET_JOB = "job"; - public const string NET_IS_ALIVE = "alive"; - public const string NET_TOTAL_DAMAGE = "vitals"; - public const string NET_COORDINATES = "coords"; - public const string NET_SUIT_SENSOR_UID = "uid"; - - ///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor - public const string NET_STATUS_COLLECTION = "suit-status-collection"; - } +[Serializable, NetSerializable] +public enum SuitSensorMode : byte +{ + /// + /// Sensor doesn't send any information about owner + /// + SensorOff = 0, + + /// + /// Sensor sends only binary status (alive/dead) + /// + SensorBinary = 1, + + /// + /// Sensor sends health vitals status + /// + SensorVitals = 2, + + /// + /// Sensor sends vitals status and GPS position + /// + SensorCords = 3 +} + +public static class SuitSensorConstants +{ + public const string NET_NAME = "name"; + public const string NET_JOB = "job"; + public const string NET_JOB_ICON = "jobIcon"; + public const string NET_JOB_DEPARTMENTS = "jobDepartments"; + public const string NET_IS_ALIVE = "alive"; + public const string NET_TOTAL_DAMAGE = "vitals"; + public const string NET_COORDINATES = "coords"; + public const string NET_SUIT_SENSOR_UID = "uid"; + + ///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor + public const string NET_STATUS_COLLECTION = "suit-status-collection"; } diff --git a/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl index e089b09234..f77f334c7e 100644 --- a/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl +++ b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl @@ -1,6 +1,6 @@ ## UI -crew-monitoring-user-interface-title = Crew Monitoring +crew-monitoring-user-interface-title = Crew Monitoring Console crew-monitoring-user-interface-name = Name crew-monitoring-user-interface-job = Job @@ -12,3 +12,8 @@ crew-monitoring-user-interface-dead = Dead crew-monitoring-user-interface-no-info = N/A crew-monitoring-user-interface-no-server = Server not found + +crew-monitoring-user-interface-no-department = Unknown + +crew-monitoring-user-interface-flavor-left = In case of an emergancy, contact station medical staff immediately +crew-monitoring-user-interface-flavor-right = v1.7 \ No newline at end of file diff --git a/Resources/Locale/en-US/ui/navmap.ftl b/Resources/Locale/en-US/ui/navmap.ftl new file mode 100644 index 0000000000..e600e7a1f2 --- /dev/null +++ b/Resources/Locale/en-US/ui/navmap.ftl @@ -0,0 +1,3 @@ +navmap-zoom = Zoom: {$value}% +navmap-recenter = Recenter +navmap-toggle-beacons = Show departments \ No newline at end of file diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/alive.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/alive.png new file mode 100644 index 0000000000000000000000000000000000000000..69351e41b8cc2c4ad37b1ce6a13b96781f8e13d8 GIT binary patch literal 615 zcmV-t0+{`YP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD78@IBri4cT003i2L_t(2&tqgj1F4)n{~0I* zv%i-xVwFhc?D@Zf@74bme6Rk)0E~?+j*Et|ksZl|&7$y+q6{nfUz6-|YP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD79S!~&&f*w0056kL_t(I%hi;@4#Xe`L?3=R ze?EI?Z77Qy+q#ENLJ&y68Hq-QOFV^}jY@JJ_cv$I-zC^gI-heK^RWifNCY{Ob|alB zY7{l0C_*a-3CuQsuugH$ML#bgpxqxsQ`~dW7id4{*f*)AP(kV<+O%lJ3oIvNiwXSo t!rZ&?6oIbx)s>khteY!iGpUz-qaVxXimI3o`QQKm002ovPDHLkV1m_}7773W literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/dead.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/dead.png new file mode 100644 index 0000000000000000000000000000000000000000..c3dfe488d6c9beaf2e2fe6c6ec7f9d359a7306f5 GIT binary patch literal 615 zcmV-t0+{`YP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD78?hBr`QMp003i2L_t(2&$W`#4Zt7>L=Q)l z9o)(8U{}0;kys)rwdwofa^Ml7z@b`+4~%h2XsT9rfJO&G9eIAi5``TiXyi=;KqHsl ztWyD==z%B7&d{5%bby`6*V_Sh>&(`^cSipHrWX{AP@5sLvGD)^002ovPDHLkV1nU? B1?T_( literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health0.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health0.png new file mode 100644 index 0000000000000000000000000000000000000000..755c74eba5755f1b90572c005ac99f574e47d2ce GIT binary patch literal 614 zcmV-s0-61ZP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD791xDh;9b}003f1L_t(2&$W`l4S+BV1J4Z8 z6|x#5bhKs#mI*JUQX)Wt-Y@dQaU3yVwFDpWbyF%>EkPq_#KD_+)cPft%qT373%O47 zsQ1W4*fspZWNrEX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD791K;;35_P003)AL_t(2&tqgj0zZEIU`R-{ z{?9-mSh4slBUXusSmXZ%3mE))?+cCTte<$Ny(2T*!#ig@ojh z0TTm_0nrwqyShLAKXolAT*!!GD6(ZRIc&kkM2&F92N-FcOaXfDT2*rGbpHSV002ov JPDHLkV1j?P0{Q>| literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health2.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health2.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe54411275e856872b8f772340f079087170af7 GIT binary patch literal 611 zcmV-p0-XJcP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD790iK=r7{{003V}L_t(2&tqgj0~HYr{~0I* z9V-|ZVQfZp!!;Bb7~vQJV02&gv%Ak8spplV_1gFM^}#wm>6gb xa9V)PmG}TI^>~Ab6hLth5fcyC03)rFDFCtoGbvgAgAxD$002ovPDHLkV1m&h`Pcve literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health3.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health3.png new file mode 100644 index 0000000000000000000000000000000000000000..b82f46e453c1927041be85502333822d15d914aa GIT binary patch literal 617 zcmV-v0+#)WP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD78^HPe(0nC003o4L_t(2&tqgj13fyN{~0I* zC0D*P!q|-HhMNd5!}<1yKQk~eFfbgn<;5z7O`d^)f#J-Dzi>;K2)PE?Ap66gF$_bN z#|8*mfXz@+07*VT4kD^qfXzaxT0kg>2+1?jI++3h%eXKOcu79j00000NkvXXu0mjf D_qG3@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health4.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health4.png new file mode 100644 index 0000000000000000000000000000000000000000..d371bb2a14b65963734b65d9dc1ba87a5ae42748 GIT binary patch literal 617 zcmV-v0+#)WP)EX>4Tx04R}tkv&MmKpe$iTSX}q2P23mWT;LSL`8IyDi*;)X)CnqU~=gfG-*gu zTpR`0f`cE6RR+@w$x=zOv5j}f427iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(!xhT?>2C8-O}Ve;Bp5Tc+w?9a-;xFe?AYqpV2pEfxcTHxaRiO+{ftykfyGdZ-9eC zV6;Hl>mKj!Ztv~iGtK^f0O$a6){{2HBLDyZ24YJ`L;zC&Qvg#MXr99W000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j>bD78@&1f3)iW003o4L_t(2&$W`#3BVu>1HKLlh>+~qDnLa zAevEBr+&sVVD#uNuP3O1p9YKqciqv7T0fgE{QXTI14B{4XT$3200000NkvXXu0mjf D@-qdQ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/meta.json b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/meta.json new file mode 100644 index 0000000000..e95420f02c --- /dev/null +++ b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/meta.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by chromiumboy, derived from https://github.com/tgstation/tgstation/commits/50689f89a40e5e7a2732a0c5fb38c787b69f7d28/icons/hud/screen_gen.dmi, ", + "size": { + "x": 24, + "y": 8 + }, + "states": [ + { + "name": "alive" + }, + { + "name": "dead" + }, + { + "name": "health0" + }, + { + "name": "health1" + }, + { + "name": "health2" + }, + { + "name": "health3" + }, + { + "name": "health4" + }, + { + "name": "critical", + "delays": [ + [ + 0.35, + 0.35 + ] + ] + } + ] +} diff --git a/Resources/Textures/Interface/NavMap/beveled_circle.png b/Resources/Textures/Interface/NavMap/beveled_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..a54a9d6221296ceee4f540a231ae712e4481d5b1 GIT binary patch literal 1365 zcmV-b1*-aqP)EX>4Tx04R}tkv&MmKpe$i(@Iq;3U(0bkfAzRC@SJ8RV;#q(pG5I!Q|2}Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;tBaGOirkx9)H2Z_aE2g@DIN`^{2O&nHKjq-)8 z%L?Z$&T6H`TKD8H4CJ+yG}mc{5XTY{NJ4~+8p^1^LWEY06cZ`hk9F~nI{qZNWO9|k z$gzMbR7j2={11M2YZj&^-K0Pa=y|d2k1@c%3pDGt{e5iP%@e@;3|wh#f3*S3ev)2q zYvChca2vR|Zfo)$aJd7FJn51lIg*#AP$&TJXY@@uVE7j3UvqnF?c?+T$WT|yH^9Lm zFkYnW^)B!3?dXiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>YK1Qa(xP|->N000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0009lNkl^8 zC6{N5lxzbMX()0;g|ZD#fM{sp1)!ikAf%H}I0=r8m$bNMAW3OQ&TCm{vPudeX+7EYx%yvJ_|sT=A^-3(3~aabJK7* zq^GB+iJaiYYzI)ESG^MvIOnKTD)qa&yNzD}09XLF=h<{P=g>55yId|?698{bW`7Z# zk8=(o#QxRQl_;LT9)Qh-v|h%*7~3=qW3LFXyFhbr&QYyacZ&c!i!x`7K?ng|*LR8l zTMLXogb*O2ts=nMqC&tq2N5AjlC|kFT!`{KN0w!X<9MPEIEyq-k_2%aJ4JxUd7Bdv zlu}5N1YsB=j^oE7!0jRcvMfUsMF@fbVHn;P0j}q5E~P}0B#5F2!{HE~=Uo>8F2gWv z=XpN2*r#cVAPC_5K78MA_xt_JF#uysMp1O0rYVRBL^LKJJ%83b48zgR;d$Qq;o+g2 zC_^E{Sr7yd!{HD~k|0e}D5X9HQ8f2F4}-w~j^jMowte=?b-c2&(g}j##C6?KmLkhC zNGYL|!h6H>cjobzn0itQz_+pthmQs!rZnX1W zuLs+<|7^G0M>9=EH#axy0RE`eYTqiA3PvE)CnAIZV+?toLn(zcO%ccOINJw<0bX8S zVB7ZhPN#Epe0==!uF>q`;=)>8U9DHE)$LNLgi@(Ah8O`c#-Nl!9LEU55K$Dt_kFmo z3&(LDdcEFBywKkTQm#w9xrG3LNHb)?(QVE)-K`Ax9T#9w>y6$R;%q+tJNLO`IeM&Ez2^bY3gKI_877 literal 0 HcmV?d00001 -- 2.51.2