]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Admin Overlay stacking and ghost hiding (#35622)
authorErrant <35878406+Errant-4@users.noreply.github.com>
Tue, 25 Mar 2025 20:15:22 +0000 (21:15 +0100)
committerGitHub <noreply@github.com>
Tue, 25 Mar 2025 20:15:22 +0000 (21:15 +0100)
* ghostbuster mouse and overlay stacks

* variable adjustment

* use map coords for distance check

* vertical stack ordering, and cvars

* skreee

* fix stack merge 'sidedness' issue

* overlays no longer try to stack to overlays at the wrong coordinates

* options slider for stack merge distance

* admin option sliders for ghost fade/hide

* Update AdminOptionsTab.xaml.cs

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Content.Client/Administration/AdminNameOverlay.cs
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
Content.Shared/CCVar/CCVars.Interface.cs
Resources/Locale/en-US/escape-menu/ui/options-menu.ftl

index 205e9757e642aa974b12a789ad18678044abcb89..9fb93e926ca478b7059e0cd352dcdc13a7ed88ba 100644 (file)
@@ -2,7 +2,9 @@ using System.Linq;
 using System.Numerics;
 using Content.Client.Administration.Systems;
 using Content.Client.Stylesheets;
+using Content.Shared.Administration;
 using Content.Shared.CCVar;
+using Content.Shared.Ghost;
 using Content.Shared.Mind;
 using Robust.Client.Graphics;
 using Robust.Client.ResourceManagement;
@@ -26,12 +28,15 @@ internal sealed class AdminNameOverlay : Overlay
     private bool _overlaySymbols;
     private bool _overlayPlaytime;
     private bool _overlayStartingJob;
+    private float _ghostFadeDistance;
+    private float _ghostHideDistance;
+    private int _overlayStackMax;
+    private float _overlayMergeDistance;
 
     //TODO make this adjustable via GUI
     private readonly ProtoId<RoleTypePrototype>[] _filter =
         ["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
     private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
-    private readonly Color _antagColorClassic = Color.OrangeRed;
 
     public AdminNameOverlay(
         AdminSystem system,
@@ -48,7 +53,7 @@ internal sealed class AdminNameOverlay : Overlay
         _entityLookup = entityLookup;
         _userInterfaceManager = userInterfaceManager;
         ZIndex = 200;
-        // Setting this to a specific font would break the antag symbols
+        // Setting these to a specific ttf would break the antag symbols
         _font = resourceCache.NotoStack();
         _fontBold = resourceCache.NotoStack(variation: "Bold");
 
@@ -56,6 +61,10 @@ internal sealed class AdminNameOverlay : Overlay
         config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true);
         config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
         config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true);
+        config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true);
+        config.OnValueChanged(CCVars.AdminOverlayGhostFadeDistance, (f) => { _ghostFadeDistance = f; }, true);
+        config.OnValueChanged(CCVars.AdminOverlayStackMax, (i) => { _overlayStackMax = i; }, true);
+        config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true);
     }
 
     public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -63,58 +72,114 @@ internal sealed class AdminNameOverlay : Overlay
     protected override void Draw(in OverlayDrawArgs args)
     {
         var viewport = args.WorldAABB;
-
-        foreach (var playerInfo in _system.PlayerList)
+        var colorDisconnected = Color.White;
+        var uiScale = _userInterfaceManager.RootControl.UIScale;
+        var lineoffset = new Vector2(0f, 14f) * uiScale;
+        var drawnOverlays = new List<(Vector2,Vector2)>() ; // A saved list of the overlays already drawn
+
+        // Get all player positions before drawing overlays, so they can be sorted before iteration
+        var sortable = new List<(PlayerInfo, Box2, EntityUid, Vector2)>();
+        foreach (var info in _system.PlayerList)
         {
-            var entity = _entityManager.GetEntity(playerInfo.NetEntity);
+            var entity = _entityManager.GetEntity(info.NetEntity);
 
-            // Otherwise the entity can not exist yet
-            if (entity == null || !_entityManager.EntityExists(entity))
-            {
+            // If entity does not exist or is on a different map, skip
+            if (entity == null
+                || !_entityManager.EntityExists(entity)
+                || _entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
                 continue;
-            }
-
-            // if not on the same map, continue
-            if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
-            {
-                continue;
-            }
 
             var aabb = _entityLookup.GetWorldAABB(entity.Value);
-
-            // if not on screen, continue
+            // if not on screen, skip
             if (!aabb.Intersects(in viewport))
-            {
                 continue;
-            }
 
-            var uiScale = _userInterfaceManager.RootControl.UIScale;
-            var lineoffset = new Vector2(0f, 14f) * uiScale;
-            var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
-                                                              new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
-                                                                  aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
+            // Get on-screen coordinates of player
+            var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center);
+
+            sortable.Add((info, aabb, entity.Value, screenCoordinates));
+        }
 
+        // Draw overlays for visible players, starting from the top of the screen
+        foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList())
+        {
+            var playerInfo = info.Item1;
+            var aabb = info.Item2;
+            var entity = info.Item3;
+            var screenCoordinatesCenter = info.Item4;
+            //the center position is kept separately, for simpler position comparison later
+            var centerOffset = new Vector2(28f, -18f) * uiScale;
+            var screenCoordinates = screenCoordinatesCenter + centerOffset;
+            var alpha = 1f;
+
+            //TODO make a smarter system where the starting offset can be modified by the predicted position and size of already-drawn overlays/stacks?
             var currentOffset = Vector2.Zero;
 
+            //  Ghosts near the cursor are made transparent/invisible
+            //  TODO would be "cheaper" if playerinfo already contained a ghost bool, this gets called every frame for every onscreen player!
+            if (_entityManager.HasComponent<GhostComponent>(entity))
+            {
+                // We want the map positions here, so we don't have to worry about resolution and such shenanigans
+                var mobPosition = aabb.Center;
+                var mousePosition = _eyeManager
+                    .ScreenToMap(_userInterfaceManager.MousePositionScaled.Position * uiScale)
+                    .Position;
+                var dist = Vector2.Distance(mobPosition, mousePosition);
+                if (dist < _ghostHideDistance)
+                    continue;
+
+                alpha = Math.Clamp((dist - _ghostHideDistance) / (_ghostFadeDistance - _ghostHideDistance), 0f, 1f);
+                colorDisconnected.A = alpha;
+            }
+
+            // If the new overlay text block is within merge distance of any previous ones
+            // merge them into a stack so they don't hide each other
+            var stack = drawnOverlays.FindAll(x =>
+                Vector2.Distance(_eyeManager.ScreenToMap(x.Item1).Position, aabb.Center) <= _overlayMergeDistance);
+            if (stack.Count > 0)
+            {
+                screenCoordinates = stack.First().Item1 + centerOffset;
+                // Replacing this overlay's coordinates for the later save with the stack root's coordinates
+                // so that other overlays don't try to stack to these coordinates
+                screenCoordinatesCenter = stack.First().Item1;
+
+                var i = 1;
+                foreach (var s in stack)
+                {
+                    // additional entries after maximum stack size is reached will be drawn over the last entry
+                    if (i <= _overlayStackMax - 1)
+                        currentOffset = lineoffset + s.Item2 ;
+                    i++;
+                }
+            }
+
             // Character name
-            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
+            var color = Color.Aquamarine;
+            color.A = alpha;
+            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected);
             currentOffset += lineoffset;
 
             // Username
-            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
+            color = Color.Yellow;
+            color.A = alpha;
+            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected);
             currentOffset += lineoffset;
 
             // Playtime
             if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime)
             {
-                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
+                color = Color.Orange;
+                color.A = alpha;
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected);
                 currentOffset += lineoffset;
             }
 
             // Job
             if (!string.IsNullOrEmpty(playerInfo.StartingJob) && _overlayStartingJob)
             {
-                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
+                color = Color.GreenYellow;
+                color.A = alpha;
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
                 currentOffset += lineoffset;
             }
 
@@ -127,7 +192,9 @@ internal sealed class AdminNameOverlay : Overlay
                         ("symbol", symbol),
                         ("name", _antagLabelClassic))
                     : _antagLabelClassic;
-                args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, _antagColorClassic);
+                color = Color.OrangeRed;
+                color.A = alpha;
+                args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
                 currentOffset += lineoffset;
             }
             // Role Type
@@ -138,11 +205,14 @@ internal sealed class AdminNameOverlay : Overlay
                 var label = _overlaySymbols
                 ? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role))
                 : role;
-                var color = playerInfo.RoleProto.Color;
-
+                color =  playerInfo.RoleProto.Color;
+                color.A = alpha;
                 args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
                 currentOffset += lineoffset;
             }
+
+            //Save the coordinates and size of the text block, for stack merge check
+            drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
         }
     }
 }
index 88bfdbd19e4a32b61f5faf35c5f0a0edb277be13..449c16ad9131961c049138164369cf450707e748 100644 (file)
@@ -3,7 +3,6 @@
          xmlns:ui="clr-namespace:Content.Client.Options.UI">
     <BoxContainer Orientation="Vertical">
         <ScrollContainer VerticalExpand="True" HScrollEnabled="False">
-
             <BoxContainer Orientation="Vertical" Margin="8">
                 <Label Text="{Loc 'ui-options-admin-player-panel'}"
                        StyleClasses="LabelKeyText"/>
@@ -16,6 +15,9 @@
                 <CheckBox Name="EnableOverlaySymbolsCheckBox" Text="{Loc 'ui-options-enable-overlay-symbols'}" />
                 <CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-enable-overlay-playtime'}" />
                 <CheckBox Name="EnableOverlayStartingJobCheckBox" Text="{Loc 'ui-options-enable-overlay-starting-job'}" />
+                <ui:OptionSlider Name="OverlayMergeDistanceSlider"  Title="{Loc 'ui-options-overlay-merge-distance'}"/>
+                <ui:OptionSlider Name="OverlayGhostFadeSlider"  Title="{Loc 'ui-options-overlay-ghost-fade-distance'}"/>
+                <ui:OptionSlider Name="OverlayGhostHideSlider"  Title="{Loc 'ui-options-overlay-ghost-hide-distance'}"/>
             </BoxContainer>
         </ScrollContainer>
         <ui:OptionsTabControlRow Name="Control" Access="Public" />
index 9bd41e35504bf2320dd9565bda65fc4dd36ccbb8..d2e56d80ed38fd67cd43476b49bb4586cb9f47b3 100644 (file)
@@ -8,6 +8,13 @@ namespace Content.Client.Options.UI.Tabs;
 [GenerateTypedNameReferences]
 public sealed partial class AdminOptionsTab : Control
 {
+    private const float OverlayMergeMin = 0.05f;
+    private const float OverlayMergeMax = 0.95f;
+    private const int OverlayGhostFadeMin = 0;
+    private const int OverlayGhostFadeMax = 10;
+    private const int OverlayGhostHideMin = 0;
+    private const int OverlayGhostHideMax = 5;
+
     public AdminOptionsTab()
     {
         RobustXamlLoader.Load(this);
@@ -22,6 +29,24 @@ public sealed partial class AdminOptionsTab : Control
         Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox);
 
         Control.Initialize();
+
+        Control.AddOptionPercentSlider(
+            CCVars.AdminOverlayMergeDistance,
+            OverlayMergeDistanceSlider,
+            OverlayMergeMin,
+            OverlayMergeMax);
+
+        Control.AddOptionSlider(
+            CCVars.AdminOverlayGhostFadeDistance,
+            OverlayGhostFadeSlider,
+            OverlayGhostFadeMin,
+            OverlayGhostFadeMax);
+
+        Control.AddOptionSlider(
+            CCVars.AdminOverlayGhostHideDistance,
+            OverlayGhostHideSlider,
+            OverlayGhostHideMin,
+            OverlayGhostHideMax);
     }
 }
 
index 168d2319bfa4de9be563348d4e62f01bb245053f..85e06def615589ee5292e4b43f22900849861cca 100644 (file)
@@ -78,4 +78,29 @@ public sealed partial class CCVars
     /// </summary>
     public static readonly CVarDef<bool> AdminOverlaySymbols =
         CVarDef.Create("ui.admin_overlay_symbols", true, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+    /// <summary>
+    /// The range (in tiles) around the cursor within which the admin overlays of ghosts start to fade out
+    /// </summary>
+    public static readonly CVarDef<int> AdminOverlayGhostFadeDistance =
+        CVarDef.Create("ui.admin_overlay_ghost_fade_distance", 6, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+    /// <summary>
+    /// The range (in tiles) around the cursor within which the admin overlays of ghosts disappear
+    /// </summary>
+    public static readonly CVarDef<int> AdminOverlayGhostHideDistance =
+        CVarDef.Create("ui.admin_overlay_ghost_hide_distance", 2, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+    /// <summary>
+    /// The maximum range (in tiles) at which admin overlay entries still merge to form a stack
+    /// Recommended to keep under 1, otherwise the overlays of people sitting next to each other will stack
+    /// </summary>
+    public static readonly CVarDef<float> AdminOverlayMergeDistance =
+        CVarDef.Create("ui.admin_overlay_merge_distance", 0.33f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+    /// <summary>
+    /// The maximum size that an overlay stack can reach. Additional overlays will be superimposed over the last one.
+    /// </summary>
+    public static readonly CVarDef<int> AdminOverlayStackMax =
+        CVarDef.Create("ui.admin_overlay_stack_max", 3, CVar.CLIENTONLY | CVar.ARCHIVE);
 }
index 413ae2f695f544b60ee7b9ec13005715da9dae99..35ae20ec3ca908ba670a007571685a93c05d21cc 100644 (file)
@@ -348,3 +348,6 @@ ui-options-enable-classic-overlay = Revert overlay to classic mode
 ui-options-enable-overlay-symbols = Add antag symbol to text
 ui-options-enable-overlay-playtime = Show playtime
 ui-options-enable-overlay-starting-job = Show starting job
+ui-options-overlay-merge-distance = Stack merge distance
+ui-options-overlay-ghost-fade-distance = Ghost overlay fade range from mouse
+ui-options-overlay-ghost-hide-distance = Ghost overlay hide range from mouse