Admin Overlay stacking and ghost hiding (#35622)

* 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>
This commit is contained in:
Errant
2025-03-25 21:15:22 +01:00
committed by GitHub
parent 098e594161
commit c82d531a8f
5 changed files with 156 additions and 31 deletions

View File

@@ -2,7 +2,9 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.Administration.Systems; using Content.Client.Administration.Systems;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind; using Content.Shared.Mind;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
@@ -26,12 +28,15 @@ internal sealed class AdminNameOverlay : Overlay
private bool _overlaySymbols; private bool _overlaySymbols;
private bool _overlayPlaytime; private bool _overlayPlaytime;
private bool _overlayStartingJob; private bool _overlayStartingJob;
private float _ghostFadeDistance;
private float _ghostHideDistance;
private int _overlayStackMax;
private float _overlayMergeDistance;
//TODO make this adjustable via GUI //TODO make this adjustable via GUI
private readonly ProtoId<RoleTypePrototype>[] _filter = private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"]; ["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic"); private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
private readonly Color _antagColorClassic = Color.OrangeRed;
public AdminNameOverlay( public AdminNameOverlay(
AdminSystem system, AdminSystem system,
@@ -48,7 +53,7 @@ internal sealed class AdminNameOverlay : Overlay
_entityLookup = entityLookup; _entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager; _userInterfaceManager = userInterfaceManager;
ZIndex = 200; 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(); _font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold"); _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.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true); config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = 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; public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -63,58 +72,114 @@ internal sealed class AdminNameOverlay : Overlay
protected override void Draw(in OverlayDrawArgs args) protected override void Draw(in OverlayDrawArgs args)
{ {
var viewport = args.WorldAABB; var viewport = args.WorldAABB;
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
foreach (var playerInfo in _system.PlayerList) // 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 does not exist or is on a different map, skip
if (entity == null || !_entityManager.EntityExists(entity)) if (entity == null
{ || !_entityManager.EntityExists(entity)
|| _entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
continue; continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value); var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, skip
// if not on screen, continue
if (!aabb.Intersects(in viewport)) if (!aabb.Intersects(in viewport))
{
continue; continue;
}
var uiScale = _userInterfaceManager.RootControl.UIScale; // Get on-screen coordinates of player
var lineoffset = new Vector2(0f, 14f) * uiScale; var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center);
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
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; 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 // 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; currentOffset += lineoffset;
// Username // 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; currentOffset += lineoffset;
// Playtime // Playtime
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime) 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; currentOffset += lineoffset;
} }
// Job // Job
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && _overlayStartingJob) 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; currentOffset += lineoffset;
} }
@@ -127,7 +192,9 @@ internal sealed class AdminNameOverlay : Overlay
("symbol", symbol), ("symbol", symbol),
("name", _antagLabelClassic)) ("name", _antagLabelClassic))
: _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; currentOffset += lineoffset;
} }
// Role Type // Role Type
@@ -138,11 +205,14 @@ internal sealed class AdminNameOverlay : Overlay
var label = _overlaySymbols var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role)) ? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role))
: role; : role;
var color = playerInfo.RoleProto.Color; color = playerInfo.RoleProto.Color;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color); args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset; currentOffset += lineoffset;
} }
//Save the coordinates and size of the text block, for stack merge check
drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
} }
} }
} }

View File

@@ -3,7 +3,6 @@
xmlns:ui="clr-namespace:Content.Client.Options.UI"> xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False"> <ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8"> <BoxContainer Orientation="Vertical" Margin="8">
<Label Text="{Loc 'ui-options-admin-player-panel'}" <Label Text="{Loc 'ui-options-admin-player-panel'}"
StyleClasses="LabelKeyText"/> StyleClasses="LabelKeyText"/>
@@ -16,6 +15,9 @@
<CheckBox Name="EnableOverlaySymbolsCheckBox" Text="{Loc 'ui-options-enable-overlay-symbols'}" /> <CheckBox Name="EnableOverlaySymbolsCheckBox" Text="{Loc 'ui-options-enable-overlay-symbols'}" />
<CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-enable-overlay-playtime'}" /> <CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-enable-overlay-playtime'}" />
<CheckBox Name="EnableOverlayStartingJobCheckBox" Text="{Loc 'ui-options-enable-overlay-starting-job'}" /> <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> </BoxContainer>
</ScrollContainer> </ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" /> <ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -8,6 +8,13 @@ namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class AdminOptionsTab : Control 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() public AdminOptionsTab()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
@@ -22,6 +29,24 @@ public sealed partial class AdminOptionsTab : Control
Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox); Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox);
Control.Initialize(); Control.Initialize();
Control.AddOptionPercentSlider(
CCVars.AdminOverlayMergeDistance,
OverlayMergeDistanceSlider,
OverlayMergeMin,
OverlayMergeMax);
Control.AddOptionSlider(
CCVars.AdminOverlayGhostFadeDistance,
OverlayGhostFadeSlider,
OverlayGhostFadeMin,
OverlayGhostFadeMax);
Control.AddOptionSlider(
CCVars.AdminOverlayGhostHideDistance,
OverlayGhostHideSlider,
OverlayGhostHideMin,
OverlayGhostHideMax);
} }
} }

View File

@@ -78,4 +78,29 @@ public sealed partial class CCVars
/// </summary> /// </summary>
public static readonly CVarDef<bool> AdminOverlaySymbols = public static readonly CVarDef<bool> AdminOverlaySymbols =
CVarDef.Create("ui.admin_overlay_symbols", true, CVar.CLIENTONLY | CVar.ARCHIVE); 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);
} }

View 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-symbols = Add antag symbol to text
ui-options-enable-overlay-playtime = Show playtime ui-options-enable-overlay-playtime = Show playtime
ui-options-enable-overlay-starting-job = Show starting job 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