]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Crew monitor revisit (#22240)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Sun, 10 Dec 2023 05:38:50 +0000 (23:38 -0600)
committerGitHub <noreply@github.com>
Sun, 10 Dec 2023 05:38:50 +0000 (16:38 +1100)
28 files changed:
Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs
Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs [new file with mode: 0644]
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs
Content.Server/Access/Components/PresetIdCardComponent.cs
Content.Server/Access/Systems/IdCardSystem.cs
Content.Server/Access/Systems/PresetIdCardSystem.cs
Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs
Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
Content.Server/Medical/SuitSensors/SuitSensorComponent.cs
Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
Content.Shared/Access/Components/IdCardComponent.cs
Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs
Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs
Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl
Resources/Locale/en-US/ui/navmap.ftl [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/alive.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/critical.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/dead.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health0.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health1.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health2.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health3.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health4.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_circle.png [new file with mode: 0644]

index fc632575c735287e456fb189477940d9ed045099..397888098716b8bb4655a8f0fee6dd8111f3e936 100644 (file)
@@ -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<TransformComponent>(Owner, out var xform))
         {
-            EntityUid? gridUid = null;
+            gridUid = xform.GridUid;
 
-            if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
+            if (EntMan.TryGetComponent<MetaDataComponent>(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<TransformComponent>(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<TransformComponent>(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 (file)
index 0000000..f4ae829
--- /dev/null
@@ -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<NetEntity, string> 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;
+    }
+}
index 559a12d63bc4a8188a42b080e18077b45a1ab3ba..80bf5a3f8b91f6605614e1fa752e92ddbd5a1c7b 100644 (file)
@@ -1,39 +1,49 @@
 <controls:FancyWindow xmlns="https://spacestation14.io"
-               xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
+               xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
                xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
                Title="{Loc 'crew-monitoring-user-interface-title'}"
-               SetSize="1130 700"
-               MinSize="1130 700">
-    <BoxContainer Orientation="Horizontal">
-        <ScrollContainer HorizontalExpand="True"
-                         VerticalExpand="True"
-                         Margin="8, 8, 8, 8">
-            <GridContainer Name="SensorsTable"
-                           HorizontalExpand="True"
-                           VerticalExpand="True"
-                           HSeparationOverride="5"
-                           VSeparationOverride="20"
-                           Columns="4">
-                <!-- Table header -->
-                <Label Text="{Loc 'crew-monitoring-user-interface-name'}"
-                       StyleClasses="LabelHeading"/>
-                <Label Text="{Loc 'crew-monitoring-user-interface-job'}"
-                       StyleClasses="LabelHeading"/>
-                <Label Text="{Loc 'crew-monitoring-user-interface-status'}"
-                       StyleClasses="LabelHeading"/>
-                <Label Text="{Loc 'crew-monitoring-user-interface-location'}"
-                       StyleClasses="LabelHeading"/>
+               SetSize="1200 700"
+               MinSize="1200 700">
+    <BoxContainer Orientation="Vertical">
+        <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
+            <ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
+            <BoxContainer Orientation="Vertical">
+                <controls:StripeBack>
+                    <PanelContainer>
+                        <Label Name="StationName" Text="Unknown station" Align="Center" />
+                    </PanelContainer>
+                </controls:StripeBack>
+                
+                <ScrollContainer Name="SensorScroller"
+                                 VerticalExpand="True"
+                                 SetWidth="520"
+                                 Margin="8, 8, 8, 8">
+                    <BoxContainer Name="SensorsTable"
+                                   Orientation="Vertical"
+                                   HorizontalExpand="True"
+                                   Margin="0 0 10 0">
+                        <!-- Table rows are filled by code -->
+                    </BoxContainer>
+                    <Label  Name="NoServerLabel"
+                            Text="{Loc 'crew-monitoring-user-interface-no-server'}"
+                            StyleClasses="LabelHeading"
+                            FontColorOverride="Red"
+                            HorizontalAlignment="Center"
+                            Visible="false"/>
+                </ScrollContainer>
+            </BoxContainer>
+        </BoxContainer>
 
-                <!-- Table rows are filled by code -->
-            </GridContainer>
-            <Label  Name="NoServerLabel"
-                    Text="{Loc 'crew-monitoring-user-interface-no-server'}"
-                    StyleClasses="LabelHeading"
-                    FontColorOverride="Red"
-                    HorizontalAlignment="Center"
-                    Visible="false"/>
-        </ScrollContainer>
-        <ui:NavMapControl Name="NavMap"
-                          Margin="5 5"/>
+        <!-- Footer -->
+        <BoxContainer Orientation="Vertical">
+            <PanelContainer StyleClasses="LowDivider" />
+            <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+                <Label Text="{Loc 'crew-monitoring-user-interface-flavor-left'}" StyleClasses="WindowFooterText" />
+                <Label Text="{Loc 'crew-monitoring-user-interface-flavor-right'}" StyleClasses="WindowFooterText"
+                        HorizontalAlignment="Right" HorizontalExpand="True"  Margin="0 0 5 0" />
+                <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+                        VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+            </BoxContainer>
+        </BoxContainer>
     </BoxContainer>
 </controls:FancyWindow>
index ff08af6bb6b83f5da52d3dd0f4d805749738210c..d8c87899db419b99e6fbfe11ceae4d2fe5ad1e30 100644 (file)
+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<Control> _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<IEntityManager>();
+        _prototypeManager = IoCManager.Resolve<IPrototypeManager>();
+        _spriteSystem = _entManager.System<SpriteSystem>();
+
+        _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+
+        if (_entManager.TryGetComponent<TransformComponent>(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<Control> _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<SuitSensorStatus> sensors, EntityUid monitor, EntityCoordinates? monitorCoords)
+    {
+        ClearOutDatedData();
+
+        // No server label
+        if (sensors.Count == 0)
         {
-            RobustXamlLoader.Load(this);
-            _eye = IoCManager.Resolve<IEyeManager>();
-            _entManager = IoCManager.Resolve<IEntityManager>();
-            _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<SuitSensorStatus>();
+        var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n);
 
-            if (_entManager.TryGetComponent<TransformComponent>(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<SuitSensorStatus> 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<SuitSensorStatus> 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<TransformComponent>(_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<StatusIconPrototype>(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;
+}
index 04d8cc76f9b2331948a7057a659680b2569c1847..cae5e15037d559f4d42b8e246a40b3c9832f8714 100644 (file)
@@ -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;
 
 /// <summary>
 /// Displays the nav map data of the specified grid.
 /// </summary>
-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<NetEntity?>? TrackedEntitySelectedAction;
+
+    // Tracked data
     public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
+    public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
+    public Dictionary<Vector2i, List<NavMapLine>> 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<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
+    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<SharedTransformSystem>();
         var cache = IoCManager.Resolve<IResourceCache>();
-        _font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16);
+
+        _transformSystem = _entManager.System<SharedTransformSystem>();
+        _font = new VectorFont(cache.GetResource<FontResource>("/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<PhysicsComponent>(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<NavMapComponent>(MapUid, out var navMap) ||
-            !_entManager.TryGetComponent<TransformComponent>(MapUid, out var xform) ||
-            !_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
-        {
+        if (_navMap == null || _xform == null)
             return;
-        }
 
         var offset = _offset;
-        var lineColor = new Color(102, 217, 102);
 
-        if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics))
-        {
-            offset += physics.LocalCenter;
-        }
+        if (_physics != null)
+            offset += _physics.LocalCenter;
 
         // Draw tiles
-        if (_entManager.TryGetComponent<FixturesComponent>(MapUid, out var manager))
+        if (_fixtures != null)
         {
             Span<Vector2> 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<Vector2>();
+
+            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<DrawVertexUV2D>>();
 
-        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<Vector2i, List<NavMapLine>> GetDecodedWallChunks
+        (Dictionary<Vector2i, NavMapChunk> chunks,
+        MapGridComponent grid)
+    {
+        var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
+
+        foreach ((var chunkOrigin, var chunk) in chunks)
+        {
+            var list = new List<NavMapLine>();
+
+            // 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;
+    }
 }
index 1fa12fa9e7c8f240eb04163c431eae198bc84c21..f52f53677530899fb9889cad81359dc0d86767c8 100644 (file)
@@ -21,5 +21,7 @@ public sealed partial class StationMapWindow : FancyWindow
         {
             Title = metadata.EntityName;
         }
+
+        NavMapScreen.ForceNavMapUpdate();
     }
 }
index 5badbca311d9d470198a7cd0fb5f6a85c873fc19..94aef2e6ca0aa2f1ed977a0a945cb3c817d7b856 100644 (file)
@@ -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<JobPrototype>? JobName;
+    [DataField("job")]
+    public ProtoId<JobPrototype>? JobName;
 
-        [DataField("name")]
-        public string? IdName;
-    }
+    [DataField("name")]
+    public string? IdName;
 }
index 3bf00d34c73d16dfa08a1bccd319589d932d8512..1c1e68741720375304c240e67a8a104b70b35780 100644 (file)
@@ -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<IdCardComponent, MapInitEvent>(OnMapInit);
-            SubscribeLocalEvent<IdCardComponent, BeingMicrowavedEvent>(OnMicrowaved);
-        }
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<IdCardComponent, BeingMicrowavedEvent>(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<AccessComponent>(uid, out var access))
         {
-            if (TryComp<AccessComponent>(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<AccessLevelPrototype>().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;
             }
-        }
-
-        /// <summary>
-        /// Attempts to change the job title of a card.
-        /// Returns true/false.
-        /// </summary>
-        /// <remarks>
-        /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
-        /// </remarks>
-        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<AccessLevelPrototype>().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}");
         }
+    }
+
+    /// <summary>
+    /// Attempts to change the job title of a card.
+    /// Returns true/false.
+    /// </summary>
+    /// <remarks>
+    /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
+    /// </remarks>
+    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;
         }
 
-        /// <summary>
-        /// Attempts to change the full name of a card.
-        /// Returns true/false.
-        /// </summary>
-        /// <remarks>
-        /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
-        /// </remarks>
-        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<DepartmentPrototype>())
+        {
+            if (department.Roles.Contains(job.ID))
+                id.JobDepartments.Add("department-" + department.ID);
         }
 
-        /// <summary>
-        /// Changes the name of the id's owner.
-        /// </summary>
-        /// <remarks>
-        /// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
-        /// If both are empty, the original entity's name is restored.
-        /// </remarks>
-        private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null)
+        Dirty(uid, id);
+
+        return true;
+    }
+
+    /// <summary>
+    /// Attempts to change the full name of a card.
+    /// Returns true/false.
+    /// </summary>
+    /// <remarks>
+    /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
+    /// </remarks>
+    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;
+    }
+
+    /// <summary>
+    /// Changes the name of the id's owner.
+    /// </summary>
+    /// <remarks>
+    /// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
+    /// If both are empty, the original entity's name is restored.
+    /// </remarks>
+    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);
     }
 }
index 96a38278b5babf83779b08e4b1ce23c6109fc163..4073b4baa2899f1d84fd8797381d7bdbc87a116e 100644 (file)
@@ -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<PresetIdCardComponent, MapInitEvent>(OnMapInit);
 
-        public override void Initialize()
-        {
-            SubscribeLocalEvent<PresetIdCardComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(PlayerJobsAssigned);
+    }
 
-            SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(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<PresetIdCardComponent>();
+        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<PresetIdCardComponent>();
-            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<StationJobsComponent>(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<StationJobsComponent>(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<StationJobsComponent>(station.Value).ExtendedAccess;
+
+        SetupIdAccess(uid, id, extended);
+        SetupIdName(uid, id);
+    }
 
-            var station = _stationSystem.GetOwningStation(uid);
-            var extended = false;
-            if (station != null)
-                extended = Comp<StationJobsComponent>(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<StatusIconPrototype>(job.Icon, out var jobIcon))
-            {
-                _cardSystem.TryChangeJobIcon(uid, jobIcon);
-            }
+        if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
+        {
+            _cardSystem.TryChangeJobIcon(uid, jobIcon);
         }
     }
 }
index 7aaa2ef368fecf465b90baf20a00dde04b443591..c2d7124194f7138686a2a95981955258cd5bb838 100644 (file)
@@ -1,33 +1,19 @@
 using Content.Shared.Medical.SuitSensor;
 
-namespace Content.Server.Medical.CrewMonitoring
-{
-    [RegisterComponent]
-    [Access(typeof(CrewMonitoringConsoleSystem))]
-    public sealed partial class CrewMonitoringConsoleComponent : Component
-    {
-        /// <summary>
-        ///     List of all currently connected sensors to this console.
-        /// </summary>
-        public Dictionary<string, SuitSensorStatus> ConnectedSensors = new();
-
-        /// <summary>
-        ///     After what time sensor consider to be lost.
-        /// </summary>
-        [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)]
-        public float SensorTimeout = 10f;
+namespace Content.Server.Medical.CrewMonitoring;
 
-        /// <summary>
-        ///     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.
-        /// </summary>
-        [DataField("snap"), ViewVariables(VVAccess.ReadWrite)]
-        public bool Snap = true;
+[RegisterComponent]
+[Access(typeof(CrewMonitoringConsoleSystem))]
+public sealed partial class CrewMonitoringConsoleComponent : Component
+{
+    /// <summary>
+    ///     List of all currently connected sensors to this console.
+    /// </summary>
+    public Dictionary<string, SuitSensorStatus> ConnectedSensors = new();
 
-        /// <summary>
-        ///     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.
-        /// </summary>
-        [DataField("precision"), ViewVariables(VVAccess.ReadWrite)]
-        public float Precision = 10f;
-    }
+    /// <summary>
+    ///     After what time sensor consider to be lost.
+    /// </summary>
+    [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)]
+    public float SensorTimeout = 10f;
 }
index ed650704361a7c7040842b0fcad22466706432a5..ff02b9cbdfe926583495e98a070e72503f4b2873 100644 (file)
@@ -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<CrewMonitoringConsoleComponent, ComponentRemove>(OnRemove);
+        SubscribeLocalEvent<CrewMonitoringConsoleComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
+        SubscribeLocalEvent<CrewMonitoringConsoleComponent, BoundUIOpenedEvent>(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<CrewMonitoringConsoleComponent, ComponentRemove>(OnRemove);
-            SubscribeLocalEvent<CrewMonitoringConsoleComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
-            SubscribeLocalEvent<CrewMonitoringConsoleComponent, BoundUIOpenedEvent>(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<string, SuitSensorStatus>? 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<string, SuitSensorStatus>? 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<NavMapComponent>(xform.GridUid.Value);
+
+        // Update all sensors info
+        var allSensors = component.ConnectedSensors.Values.ToList();
+        _uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors));
     }
 }
index 294be8c7e1d5ac532cbd7a0125fd72f7c5bf6cde..32b81e2921a322ca941fccf2b6e2d3bf23b739f7 100644 (file)
@@ -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;
+
+/// <summary>
+///     Tracking device, embedded in almost all uniforms and jumpsuits.
+///     If enabled, will report to crew monitoring console owners position and status.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(SuitSensorSystem))]
+public sealed partial class SuitSensorComponent : Component
 {
     /// <summary>
-    ///     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.
     /// </summary>
-    [RegisterComponent]
-    [Access(typeof(SuitSensorSystem))]
-    public sealed partial class SuitSensorComponent : Component
-    {
-        /// <summary>
-        ///     Choose a random sensor mode when item is spawned.
-        /// </summary>
-        [DataField("randomMode")]
-        public bool RandomMode = true;
+    [DataField("randomMode")]
+    public bool RandomMode = true;
 
-        /// <summary>
-        ///     If true user can't change suit sensor mode
-        /// </summary>
-        [DataField("controlsLocked")]
-        public bool ControlsLocked = false;
+    /// <summary>
+    ///     If true user can't change suit sensor mode
+    /// </summary>
+    [DataField("controlsLocked")]
+    public bool ControlsLocked = false;
 
-        /// <summary>
-        ///     Current sensor mode. Can be switched by user verbs.
-        /// </summary>
-        [DataField("mode")]
-        public SuitSensorMode Mode = SuitSensorMode.SensorOff;
+    /// <summary>
+    ///     Current sensor mode. Can be switched by user verbs.
+    /// </summary>
+    [DataField("mode")]
+    public SuitSensorMode Mode = SuitSensorMode.SensorOff;
 
-        /// <summary>
-        ///     Activate sensor if user wear it in this slot.
-        /// </summary>
-        [DataField("activationSlot")]
-        public string ActivationSlot = "jumpsuit";
+    /// <summary>
+    ///     Activate sensor if user wear it in this slot.
+    /// </summary>
+    [DataField("activationSlot")]
+    public string ActivationSlot = "jumpsuit";
 
-        /// <summary>
-        /// Activate sensor if user has this in a sensor-compatible container.
-        /// </summary>
-        [DataField("activationContainer")]
-        public string? ActivationContainer;
+    /// <summary>
+    /// Activate sensor if user has this in a sensor-compatible container.
+    /// </summary>
+    [DataField("activationContainer")]
+    public string? ActivationContainer;
 
-        /// <summary>
-        ///     How often does sensor update its owners status (in seconds). Limited by the system update rate.
-        /// </summary>
-        [DataField("updateRate")]
-        public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f);
+    /// <summary>
+    ///     How often does sensor update its owners status (in seconds). Limited by the system update rate.
+    /// </summary>
+    [DataField("updateRate")]
+    public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f);
 
-        /// <summary>
-        ///     Current user that wears suit sensor. Null if nobody wearing it.
-        /// </summary>
-        [ViewVariables]
-        public EntityUid? User = null;
+    /// <summary>
+    ///     Current user that wears suit sensor. Null if nobody wearing it.
+    /// </summary>
+    [ViewVariables]
+    public EntityUid? User = null;
 
-        /// <summary>
-        ///     Next time when sensor updated owners status
-        /// </summary>
-        [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
-        public TimeSpan NextUpdate = TimeSpan.Zero;
+    /// <summary>
+    ///     Next time when sensor updated owners status
+    /// </summary>
+    [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
+    public TimeSpan NextUpdate = TimeSpan.Zero;
 
-        /// <summary>
-        ///     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.
-        /// </summary>
-        [DataField("station")]
-        public EntityUid? StationId = null;
+    /// <summary>
+    ///     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.
+    /// </summary>
+    [DataField("station")]
+    public EntityUid? StationId = null;
 
-        /// <summary>
-        ///     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.
-        /// </summary>
-        [DataField("server")]
-        public string? ConnectedServer = null;
-    }
+    /// <summary>
+    ///     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.
+    /// </summary>
+    [DataField("server")]
+    public string? ConnectedServer = null;
 }
index f382d520ffe248a1740a198fd92b5a2d2ff5a89d..4629a1bf551c96fb909ae50cc1594a1d94dbee55 100644 (file)
@@ -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<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
-            SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
-            SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
-            SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
-            SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
-            SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
-            SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
-            SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
-            SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(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<SuitSensorComponent, DeviceNetworkComponent>();
+        base.Initialize();
+        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
+        SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
+        SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
+        SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
+        SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
+        SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
+        SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(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<SuitSensorComponent, DeviceNetworkComponent>();
 
-                // 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;
             }
-        }
 
-        /// <summary>
-        /// 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
-        /// </summary>
-        /// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
-        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<SuitSensorComponent>();
-            var xformQuery = GetEntityQuery<TransformComponent>();
-            RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery);
+            _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device);
         }
+    }
 
-        private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery<SuitSensorComponent> sensorQuery, EntityQuery<TransformComponent> xformQuery)
-        {
-            var xform = xformQuery.GetComponent(uid);
-            var enumerator = xform.ChildEnumerator;
+    /// <summary>
+    /// 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
+    /// </summary>
+    /// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
+    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<SuitSensorComponent>();
+        var xformQuery = GetEntityQuery<TransformComponent>();
+        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<SuitSensorComponent> sensorQuery, EntityQuery<TransformComponent> 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<Verb> 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<Verb> 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<string>();
 
-            // get mob total damage
-            var totalDamage = 0;
-            if (TryComp<DamageableComponent>(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<TransformComponent>();
-
-                    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<DamageableComponent>(sensor.User.Value, out var damageable))
+            totalDamage = damageable.TotalDamage.Int();
 
-        /// <summary>
-        ///     Serialize create a device network package from the suit sensors status.
-        /// </summary>
-        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<TransformComponent>();
+
+                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;
-        }
+    /// <summary>
+    ///     Serialize create a device network package from the suit sensors status.
+    /// </summary>
+    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;
+    }
 
-        /// <summary>
-        ///     Try to create the suit sensors status from the device network message
-        /// </summary>
-        public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload)
+    /// <summary>
+    ///     Try to create the suit sensors status from the device network message
+    /// </summary>
+    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<string>? 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;
     }
 }
index 975f0e6b51e528bc8b9ec50c18caca0b2c6c8d91..7635716d26a6b52e1fb430ff864a1cf982f62468 100644 (file)
@@ -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;
 
-        /// <summary>
-        /// The state of the job icon rsi.
-        /// </summary>
-        [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
-        [AutoNetworkedField]
-        public string JobIcon = "JobIconUnknown";
+    /// <summary>
+    /// The state of the job icon rsi.
+    /// </summary>
+    [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
+    [AutoNetworkedField]
+    public string JobIcon = "JobIconUnknown";
 
-    }
+    /// <summary>
+    /// The unlocalized names of the departments associated with the job
+    /// </summary>
+    [DataField("jobDepartments")]
+    [AutoNetworkedField]
+    public List<LocId> JobDepartments = new();
 }
index 7e5c00558b015bcb344baac9d48e95596d8d8be5..5b788396787aa42097fee35d9ad3ea0213c3bf71 100644 (file)
@@ -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<SuitSensorStatus> Sensors;
-        public readonly bool Snap;
-        public readonly float Precision;
+[Serializable, NetSerializable]
+public sealed class CrewMonitoringState : BoundUserInterfaceState
+{
+    public List<SuitSensorStatus> Sensors;
 
-        public CrewMonitoringState(List<SuitSensorStatus> sensors, bool snap, float precision)
-        {
-            Sensors = sensors;
-            Snap = snap;
-            Precision = precision;
-        }
+    public CrewMonitoringState(List<SuitSensorStatus> sensors)
+    {
+        Sensors = sensors;
     }
-
 }
index 4e27959f89714464b3cb8b7f9cc8c37bb1739ddb..07e0eca33bdff254f2c515c3a2e39c83fb9983a5 100644 (file)
@@ -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<string> 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
-    {
-        /// <summary>
-        /// Sensor doesn't send any information about owner
-        /// </summary>
-        SensorOff = 0,
-
-        /// <summary>
-        /// Sensor sends only binary status (alive/dead)
-        /// </summary>
-        SensorBinary = 1,
-
-        /// <summary>
-        /// Sensor sends health vitals status
-        /// </summary>
-        SensorVitals = 2,
-
-        /// <summary>
-        /// Sensor sends vitals status and GPS position
-        /// </summary>
-        SensorCords = 3
-    }
+    public TimeSpan Timestamp;
+    public NetEntity SuitSensorUid;
+    public string Name;
+    public string Job;
+    public string JobIcon;
+    public List<string> 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
+{
+    /// <summary>
+    /// Sensor doesn't send any information about owner
+    /// </summary>
+    SensorOff = 0,
+
+    /// <summary>
+    /// Sensor sends only binary status (alive/dead)
+    /// </summary>
+    SensorBinary = 1,
+
+    /// <summary>
+    /// Sensor sends health vitals status
+    /// </summary>
+    SensorVitals = 2,
+
+    /// <summary>
+    /// Sensor sends vitals status and GPS position
+    /// </summary>
+    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";
 }
index e089b092346916191bd185cfae6ca3bffdd6a6b5..f77f334c7e2e73598b3aed03ba8823ef024d1eab 100644 (file)
@@ -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 (file)
index 0000000..e600e7a
--- /dev/null
@@ -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 (file)
index 0000000..69351e4
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/alive.png differ
diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/critical.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/critical.png
new file mode 100644 (file)
index 0000000..f2a58e1
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/critical.png differ
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 (file)
index 0000000..c3dfe48
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/dead.png differ
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 (file)
index 0000000..755c74e
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health0.png differ
diff --git a/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health1.png b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health1.png
new file mode 100644 (file)
index 0000000..41ff5b2
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health1.png differ
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 (file)
index 0000000..cfe5441
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health2.png differ
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 (file)
index 0000000..b82f46e
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health3.png differ
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 (file)
index 0000000..d371bb2
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/human_crew_monitoring.rsi/health4.png differ
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 (file)
index 0000000..e95420f
--- /dev/null
@@ -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 (file)
index 0000000..a54a9d6
Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_circle.png differ