diff --git a/.editorconfig b/.editorconfig
index a5dfab07a50..1583c600aa5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -129,7 +129,7 @@ csharp_indent_braces = false
csharp_indent_switch_labels = true
# Space preferences
-csharp_space_after_cast = true
+csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4dd6ccfe411..78544b608cc 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,10 +18,17 @@ Small fixes/refactors are exempt.
Any media may be used in SS14 progress reports, with clear credit given.
If you're unsure whether your PR will require media, ask a maintainer.
-
-Check the box below to confirm that you have in fact seen this (put an X in the brackets, like [X]):
-->
+## Requirements
+
+- [ ] I have read and I am following the [Pull Request Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). I understand that not doing so may get my pr closed at maintainer’s discretion
- [ ] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase
## Breaking changes
@@ -35,7 +42,7 @@ Make players aware of new features and changes that could affect how they play t
-->
-
-
+
+
+
+
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index 6d0b2a184f4..320bb88a67e 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -8,6 +8,7 @@
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
+using System.Linq;
namespace Content.Client.Access.UI
{
@@ -17,19 +18,19 @@ public sealed partial class AgentIDCardWindow : DefaultWindow
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
private readonly SpriteSystem _spriteSystem;
- private readonly AgentIDCardBoundUserInterface _bui;
private const int JobIconColumnCount = 10;
public event Action? OnNameChanged;
public event Action? OnJobChanged;
- public AgentIDCardWindow(AgentIDCardBoundUserInterface bui)
+ public event Action>? OnJobIconChanged;
+
+ public AgentIDCardWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entitySystem.GetEntitySystem();
- _bui = bui;
NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
@@ -38,17 +39,16 @@ public AgentIDCardWindow(AgentIDCardBoundUserInterface bui)
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
}
- public void SetAllowedIcons(HashSet> icons, string currentJobIconId)
+ public void SetAllowedIcons(string currentJobIconId)
{
IconGrid.DisposeAllChildren();
- var jobIconGroup = new ButtonGroup();
+ var jobIconButtonGroup = new ButtonGroup();
var i = 0;
- foreach (var jobIconId in icons)
+ var icons = _prototypeManager.EnumeratePrototypes().Where(icon => icon.AllowSelection).ToList();
+ icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
+ foreach (var jobIcon in icons)
{
- if (!_prototypeManager.TryIndex(jobIconId, out var jobIcon))
- continue;
-
String styleBase = StyleBase.ButtonOpenBoth;
var modulo = i % JobIconColumnCount;
if (modulo == 0)
@@ -62,12 +62,13 @@ public void SetAllowedIcons(HashSet> icons, string
Access = AccessLevel.Public,
StyleClasses = { styleBase },
MaxSize = new Vector2(42, 28),
- Group = jobIconGroup,
- Pressed = i == 0,
+ Group = jobIconButtonGroup,
+ Pressed = currentJobIconId == jobIcon.ID,
+ ToolTip = jobIcon.LocalizedJobName
};
// Generate buttons textures
- TextureRect jobIconTexture = new TextureRect
+ var jobIconTexture = new TextureRect
{
Texture = _spriteSystem.Frame0(jobIcon.Icon),
TextureScale = new Vector2(2.5f, 2.5f),
@@ -75,12 +76,9 @@ public void SetAllowedIcons(HashSet> icons, string
};
jobIconButton.AddChild(jobIconTexture);
- jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIconId);
+ jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID);
IconGrid.AddChild(jobIconButton);
- if (jobIconId.Equals(currentJobIconId))
- jobIconButton.Pressed = true;
-
i++;
}
}
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index aff6c1ff7be..f05e4455880 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -48,6 +48,7 @@ public override void Initialize()
SubscribeLocalEvent(OnInstantHandleState);
SubscribeLocalEvent(OnEntityTargetHandleState);
SubscribeLocalEvent(OnWorldTargetHandleState);
+ SubscribeLocalEvent(OnEntityWorldTargetHandleState);
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
@@ -76,6 +77,18 @@ private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent
BaseHandleState(uid, component, state);
}
+ private void OnEntityWorldTargetHandleState(EntityUid uid,
+ EntityWorldTargetActionComponent component,
+ ref ComponentHandleState args)
+ {
+ if (args.Current is not EntityWorldTargetActionComponentState state)
+ return;
+
+ component.Whitelist = state.Whitelist;
+ component.CanTargetSelf = state.CanTargetSelf;
+ BaseHandleState(uid, component, state);
+ }
+
private void BaseHandleState(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
{
// TODO ACTIONS use auto comp states
@@ -107,7 +120,7 @@ private void BaseHandleState(EntityUid uid, BaseActionComponent component, Ba
UpdateAction(uid, component);
}
- protected override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
+ public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
{
if (!ResolveActionData(actionId, ref action))
return;
@@ -293,7 +306,7 @@ public void LoadActionAssignments(string path, bool userData)
continue;
var action = _serialization.Read(actionNode, notNullableOverride: true);
- var actionId = Spawn(null);
+ var actionId = Spawn();
AddComp(actionId, action);
AddActionDirect(user, actionId);
diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs
index c21ba2e32ca..6a1881a2276 100644
--- a/Content.Client/Administration/AdminNameOverlay.cs
+++ b/Content.Client/Administration/AdminNameOverlay.cs
@@ -7,67 +7,66 @@
using Robust.Shared.IoC;
using Robust.Shared.Maths;
-namespace Content.Client.Administration
+namespace Content.Client.Administration;
+
+internal sealed class AdminNameOverlay : Overlay
{
- internal sealed class AdminNameOverlay : Overlay
+ private readonly AdminSystem _system;
+ private readonly IEntityManager _entityManager;
+ private readonly IEyeManager _eyeManager;
+ private readonly EntityLookupSystem _entityLookup;
+ private readonly Font _font;
+
+ public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
{
- private readonly AdminSystem _system;
- private readonly IEntityManager _entityManager;
- private readonly IEyeManager _eyeManager;
- private readonly EntityLookupSystem _entityLookup;
- private readonly Font _font;
+ _system = system;
+ _entityManager = entityManager;
+ _eyeManager = eyeManager;
+ _entityLookup = entityLookup;
+ ZIndex = 200;
+ _font = new VectorFont(resourceCache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+ }
- public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
- {
- _system = system;
- _entityManager = entityManager;
- _eyeManager = eyeManager;
- _entityLookup = entityLookup;
- ZIndex = 200;
- _font = new VectorFont(resourceCache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
- }
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
- public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var viewport = args.WorldAABB;
- protected override void Draw(in OverlayDrawArgs args)
+ foreach (var playerInfo in _system.PlayerList)
{
- var viewport = args.WorldAABB;
+ var entity = _entityManager.GetEntity(playerInfo.NetEntity);
- foreach (var playerInfo in _system.PlayerList)
+ // Otherwise the entity can not exist yet
+ if (entity == null || !_entityManager.EntityExists(entity))
{
- var entity = _entityManager.GetEntity(playerInfo.NetEntity);
-
- // Otherwise the entity can not exist yet
- if (entity == null || !_entityManager.EntityExists(entity))
- {
- continue;
- }
+ continue;
+ }
- // if not on the same map, continue
- if (_entityManager.GetComponent(entity.Value).MapID != _eyeManager.CurrentMap)
- {
- continue;
- }
+ // if not on the same map, continue
+ if (_entityManager.GetComponent(entity.Value).MapID != args.MapId)
+ {
+ continue;
+ }
- var aabb = _entityLookup.GetWorldAABB(entity.Value);
+ var aabb = _entityLookup.GetWorldAABB(entity.Value);
- // if not on screen, continue
- if (!aabb.Intersects(in viewport))
- {
- continue;
- }
+ // if not on screen, continue
+ if (!aabb.Intersects(in viewport))
+ {
+ continue;
+ }
- var lineoffset = new Vector2(0f, 11f);
- var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
- new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
- aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
- if (playerInfo.Antag)
- {
- args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
- }
- args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
- args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
+ var lineoffset = new Vector2(0f, 11f);
+ var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
+ new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
+ aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
+ if (playerInfo.Antag)
+ {
+ args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
}
+ args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
+ args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
}
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
index ddd66623bd4..dd8e3e22121 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
@@ -88,26 +88,51 @@ public BwoinkControl()
var ach = AHelpHelper.EnsurePanel(a.SessionId);
var bch = AHelpHelper.EnsurePanel(b.SessionId);
- // First, sort by unread. Any chat with unread messages appears first. We just sort based on unread
- // status, not number of unread messages, so that more recent unread messages take priority.
+ // Pinned players first
+ if (a.IsPinned != b.IsPinned)
+ return a.IsPinned ? -1 : 1;
+
+ // First, sort by unread. Any chat with unread messages appears first.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
+ // Sort by recent messages during the current round.
+ var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
+ var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
+ if (aRecent != bRecent)
+ return aRecent ? -1 : 1;
+
// Next, sort by connection status. Any disconnected players are grouped towards the end.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
- // Next, group by whether or not the players have participated in this round.
- // The ahelp window shows all players that have connected since server restart, this groups them all towards the bottom.
- if (a.ActiveThisRound != b.ActiveThisRound)
- return a.ActiveThisRound ? -1 : 1;
+ // Sort connected players by New Player status, then by Antag status
+ if (a.Connected && b.Connected)
+ {
+ var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
+ var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
+
+ if (aNewPlayer != bNewPlayer)
+ return aNewPlayer ? -1 : 1;
+
+ if (a.Antag != b.Antag)
+ return a.Antag ? -1 : 1;
+ }
+
+ // Sort disconnected players by participation in the round
+ if (!a.Connected && !b.Connected)
+ {
+ if (a.ActiveThisRound != b.ActiveThisRound)
+ return a.ActiveThisRound ? -1 : 1;
+ }
// Finally, sort by the most recent message.
return bch.LastMessage.CompareTo(ach.LastMessage);
};
+
Bans.OnPressed += _ =>
{
if (_currentPlayer is not null)
@@ -253,7 +278,20 @@ private void SwitchToChannel(NetUserId? ch)
public void PopulateList()
{
+ // Maintain existing pin statuses
+ var pinnedPlayers = ChannelSelector.PlayerInfo.Where(p => p.IsPinned).ToDictionary(p => p.SessionId);
+
ChannelSelector.PopulateList();
+
+ // Restore pin statuses
+ foreach (var player in ChannelSelector.PlayerInfo)
+ {
+ if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer))
+ {
+ player.IsPinned = pinnedPlayer.IsPinned;
+ }
+ }
+
UpdateButtons();
}
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
index 30f9d24df1d..e8653843c74 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
@@ -30,7 +30,11 @@ public BwoinkWindow()
}
};
- OnOpen += () => Bwoink.PopulateList();
+ OnOpen += () =>
+ {
+ Bwoink.ChannelSelector.StopFiltering();
+ Bwoink.PopulateList();
+ };
}
}
}
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
index dffde2d2e99..c7fbf6c2dc0 100644
--- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
@@ -4,154 +4,166 @@
using Content.Client.Verbs.UI;
using Content.Shared.Administration;
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.Input;
+using Robust.Shared.Utility;
-namespace Content.Client.Administration.UI.CustomControls
+namespace Content.Client.Administration.UI.CustomControls;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlayerListControl : BoxContainer
{
- [GenerateTypedNameReferences]
- public sealed partial class PlayerListControl : BoxContainer
- {
- private readonly AdminSystem _adminSystem;
+ private readonly AdminSystem _adminSystem;
- private List _playerList = new();
- private readonly List _sortedPlayerList = new();
+ private readonly IEntityManager _entManager;
+ private readonly IUserInterfaceManager _uiManager;
- public event Action? OnSelectionChanged;
- public IReadOnlyList PlayerInfo => _playerList;
+ private PlayerInfo? _selectedPlayer;
- public Func? OverrideText;
- public Comparison? Comparison;
+ private List _playerList = new();
+ private List _sortedPlayerList = new();
- private IEntityManager _entManager;
- private IUserInterfaceManager _uiManager;
+ public Comparison? Comparison;
+ public Func? OverrideText;
- private PlayerInfo? _selectedPlayer;
+ public PlayerListControl()
+ {
+ _entManager = IoCManager.Resolve();
+ _uiManager = IoCManager.Resolve();
+ _adminSystem = _entManager.System();
+ RobustXamlLoader.Load(this);
+ // Fill the Option data
+ PlayerListContainer.ItemPressed += PlayerListItemPressed;
+ PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
+ PlayerListContainer.GenerateItem += GenerateButton;
+ PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
+ PopulateList(_adminSystem.PlayerList);
+ FilterLineEdit.OnTextChanged += _ => FilterList();
+ _adminSystem.PlayerListChanged += PopulateList;
+ BackgroundPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 40) };
+ }
- public PlayerListControl()
- {
- _entManager = IoCManager.Resolve();
- _uiManager = IoCManager.Resolve();
- _adminSystem = _entManager.System();
- RobustXamlLoader.Load(this);
- // Fill the Option data
- PlayerListContainer.ItemPressed += PlayerListItemPressed;
- PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
- PlayerListContainer.GenerateItem += GenerateButton;
- PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
- PopulateList(_adminSystem.PlayerList);
- FilterLineEdit.OnTextChanged += _ => FilterList();
- _adminSystem.PlayerListChanged += PopulateList;
- BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 38, 32)};
- }
+ public IReadOnlyList PlayerInfo => _playerList;
- private void PlayerListNoItemSelected()
- {
- _selectedPlayer = null;
- OnSelectionChanged?.Invoke(null);
- }
+ public event Action? OnSelectionChanged;
- private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
- {
- if (args == null || data is not PlayerListData {Info: var selectedPlayer})
- return;
+ private void PlayerListNoItemSelected()
+ {
+ _selectedPlayer = null;
+ OnSelectionChanged?.Invoke(null);
+ }
- if (selectedPlayer == _selectedPlayer)
- return;
+ private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
+ {
+ if (args == null || data is not PlayerListData { Info: var selectedPlayer })
+ return;
- if (args.Event.Function != EngineKeyFunctions.UIClick)
- return;
+ if (selectedPlayer == _selectedPlayer)
+ return;
- OnSelectionChanged?.Invoke(selectedPlayer);
- _selectedPlayer = selectedPlayer;
+ if (args.Event.Function != EngineKeyFunctions.UIClick)
+ return;
- // update label text. Only required if there is some override (e.g. unread bwoink count).
- if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label)
- label.Text = GetText(selectedPlayer);
- }
+ OnSelectionChanged?.Invoke(selectedPlayer);
+ _selectedPlayer = selectedPlayer;
- private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
- {
- if (args == null || data is not PlayerListData { Info: var selectedPlayer })
- return;
+ // update label text. Only required if there is some override (e.g. unread bwoink count).
+ if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label)
+ label.Text = GetText(selectedPlayer);
+ }
- if (args.Function != EngineKeyFunctions.UIRightClick || selectedPlayer.NetEntity == null)
- return;
+ private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
+ {
+ if (args == null || data is not PlayerListData { Info: var selectedPlayer })
+ return;
- _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
- args.Handle();
- }
+ if (args.Function != EngineKeyFunctions.UIRightClick || selectedPlayer.NetEntity == null)
+ return;
+
+ _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
+ args.Handle();
+ }
+
+ public void StopFiltering()
+ {
+ FilterLineEdit.Text = string.Empty;
+ }
- public void StopFiltering()
+ private void FilterList()
+ {
+ _sortedPlayerList.Clear();
+ foreach (var info in _playerList)
{
- FilterLineEdit.Text = string.Empty;
+ var displayName = $"{info.CharacterName} ({info.Username})";
+ if (info.IdentityName != info.CharacterName)
+ displayName += $" [{info.IdentityName}]";
+ if (!string.IsNullOrEmpty(FilterLineEdit.Text)
+ && !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
+ continue;
+ _sortedPlayerList.Add(info);
}
- private void FilterList()
- {
- _sortedPlayerList.Clear();
- foreach (var info in _playerList)
- {
- var displayName = $"{info.CharacterName} ({info.Username})";
- if (info.IdentityName != info.CharacterName)
- displayName += $" [{info.IdentityName}]";
- if (!string.IsNullOrEmpty(FilterLineEdit.Text)
- && !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
- continue;
- _sortedPlayerList.Add(info);
- }
+ if (Comparison != null)
+ _sortedPlayerList.Sort((a, b) => Comparison(a, b));
- if (Comparison != null)
- _sortedPlayerList.Sort((a, b) => Comparison(a, b));
+ PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList());
+ if (_selectedPlayer != null)
+ PlayerListContainer.Select(new PlayerListData(_selectedPlayer));
+ }
- PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList());
- if (_selectedPlayer != null)
- PlayerListContainer.Select(new PlayerListData(_selectedPlayer));
- }
- public void PopulateList(IReadOnlyList? players = null)
- {
- players ??= _adminSystem.PlayerList;
+ public void PopulateList(IReadOnlyList? players = null)
+ {
+ // Maintain existing pin statuses
+ var pinnedPlayers = _playerList.Where(p => p.IsPinned).ToDictionary(p => p.SessionId);
- _playerList = players.ToList();
- if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer))
- _selectedPlayer = null;
+ players ??= _adminSystem.PlayerList;
- FilterList();
- }
+ _playerList = players.ToList();
- private string GetText(PlayerInfo info)
+ // Restore pin statuses
+ foreach (var player in _playerList)
{
- var text = $"{info.CharacterName} ({info.Username})";
- if (OverrideText != null)
- text = OverrideText.Invoke(info, text);
- return text;
+ if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer))
+ {
+ player.IsPinned = pinnedPlayer.IsPinned;
+ }
}
- private void GenerateButton(ListData data, ListContainerButton button)
- {
- if (data is not PlayerListData { Info: var info })
- return;
+ if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer))
+ _selectedPlayer = null;
- button.AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- Children =
- {
- new Label
- {
- ClipText = true,
- Text = GetText(info)
- }
- }
- });
-
- button.AddStyleClass(ListContainer.StyleClassListContainerButton);
- }
+ FilterList();
+ }
+
+
+ private string GetText(PlayerInfo info)
+ {
+ var text = $"{info.CharacterName} ({info.Username})";
+ if (OverrideText != null)
+ text = OverrideText.Invoke(info, text);
+ return text;
}
- public record PlayerListData(PlayerInfo Info) : ListData;
+ private void GenerateButton(ListData data, ListContainerButton button)
+ {
+ if (data is not PlayerListData { Info: var info })
+ return;
+
+ var entry = new PlayerListEntry();
+ entry.Setup(info, OverrideText);
+ entry.OnPinStatusChanged += _ =>
+ {
+ FilterList();
+ };
+
+ button.AddChild(entry);
+ button.AddStyleClass(ListContainer.StyleClassListContainerButton);
+ }
}
+
+public record PlayerListData(PlayerInfo Info) : ListData;
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml
new file mode 100644
index 00000000000..af13ccc0e09
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs
new file mode 100644
index 00000000000..cd6a56ea71e
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs
@@ -0,0 +1,58 @@
+using Content.Client.Stylesheets;
+using Content.Shared.Administration;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlayerListEntry : BoxContainer
+{
+ public PlayerListEntry()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public event Action? OnPinStatusChanged;
+
+ public void Setup(PlayerInfo info, Func? overrideText)
+ {
+ Update(info, overrideText);
+ PlayerEntryPinButton.OnPressed += HandlePinButtonPressed(info);
+ }
+
+ private Action HandlePinButtonPressed(PlayerInfo info)
+ {
+ return args =>
+ {
+ info.IsPinned = !info.IsPinned;
+ UpdatePinButtonTexture(info.IsPinned);
+ OnPinStatusChanged?.Invoke(info);
+ };
+ }
+
+ private void Update(PlayerInfo info, Func? overrideText)
+ {
+ PlayerEntryLabel.Text = overrideText?.Invoke(info, $"{info.CharacterName} ({info.Username})") ??
+ $"{info.CharacterName} ({info.Username})";
+
+ UpdatePinButtonTexture(info.IsPinned);
+ }
+
+ private void UpdatePinButtonTexture(bool isPinned)
+ {
+ if (isPinned)
+ {
+ PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonUnpinned);
+ PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonPinned);
+ }
+ else
+ {
+ PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonPinned);
+ PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonUnpinned);
+ }
+ }
+}
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml
new file mode 100644
index 00000000000..8feec273b47
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
new file mode 100644
index 00000000000..53cc8faa10c
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
@@ -0,0 +1,132 @@
+using Content.Client.Administration.Managers;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Administration;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Network;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.PlayerPanel;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlayerPanel : FancyWindow
+{
+ private readonly IClientAdminManager _adminManager;
+
+ public event Action? OnUsernameCopy;
+ public event Action? OnOpenNotes;
+ public event Action? OnOpenBans;
+ public event Action? OnAhelp;
+ public event Action? OnKick;
+ public event Action? OnOpenBanPanel;
+ public event Action? OnWhitelistToggle;
+ public event Action? OnFreezeAndMuteToggle;
+ public event Action? OnFreeze;
+ public event Action? OnLogs;
+ public event Action? OnDelete;
+ public event Action? OnRejuvenate;
+
+ public NetUserId? TargetPlayer;
+ public string? TargetUsername;
+ private bool _isWhitelisted;
+
+ public PlayerPanel(IClientAdminManager adminManager)
+ {
+ RobustXamlLoader.Load(this);
+ _adminManager = adminManager;
+
+ UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
+ BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
+ KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
+ NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
+ ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
+ AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
+ WhitelistToggle.OnPressed += _ =>
+ {
+ OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
+ SetWhitelisted(!_isWhitelisted);
+ };
+ FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
+ FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
+ LogsButton.OnPressed += _ => OnLogs?.Invoke();
+ DeleteButton.OnPressed += _ => OnDelete?.Invoke();
+ RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
+ }
+
+ public void SetUsername(string player)
+ {
+ Title = Loc.GetString("player-panel-title", ("player", player));
+ PlayerName.Text = Loc.GetString("player-panel-username", ("player", player));
+ }
+
+ public void SetWhitelisted(bool? whitelisted)
+ {
+ if (whitelisted == null)
+ {
+ Whitelisted.Text = null;
+ WhitelistToggle.Visible = false;
+ }
+ else
+ {
+ Whitelisted.Text = Loc.GetString("player-panel-whitelisted");
+ WhitelistToggle.Text = whitelisted.Value ? Loc.GetString("player-panel-true") : Loc.GetString("player-panel-false");
+ WhitelistToggle.Visible = true;
+ _isWhitelisted = whitelisted.Value;
+ }
+ }
+
+ public void SetBans(int? totalBans, int? totalRoleBans)
+ {
+ // If one value exists then so should the other.
+ DebugTools.Assert(totalBans.HasValue && totalRoleBans.HasValue || totalBans == null && totalRoleBans == null);
+
+ Bans.Text = totalBans != null ? Loc.GetString("player-panel-bans", ("totalBans", totalBans)) : null;
+
+ RoleBans.Text = totalRoleBans != null ? Loc.GetString("player-panel-rolebans", ("totalRoleBans", totalRoleBans)) : null;
+ }
+
+ public void SetNotes(int? totalNotes)
+ {
+ Notes.Text = totalNotes != null ? Loc.GetString("player-panel-notes", ("totalNotes", totalNotes)) : null;
+ }
+
+ public void SetSharedConnections(int sharedConnections)
+ {
+ SharedConnections.Text = Loc.GetString("player-panel-shared-connections", ("sharedConnections", sharedConnections));
+ }
+
+ public void SetPlaytime(TimeSpan playtime)
+ {
+ Playtime.Text = Loc.GetString("player-panel-playtime",
+ ("days", playtime.Days),
+ ("hours", playtime.Hours % 24),
+ ("minutes", playtime.Minutes % (24 * 60)));
+ }
+
+ public void SetFrozen(bool canFreeze, bool frozen)
+ {
+ FreezeAndMuteToggleButton.Disabled = !canFreeze;
+ FreezeButton.Disabled = !canFreeze || frozen;
+
+ FreezeAndMuteToggleButton.Text = Loc.GetString(!frozen ? "player-panel-freeze-and-mute" : "player-panel-unfreeze");
+ }
+
+ public void SetAhelp(bool canAhelp)
+ {
+ AhelpButton.Disabled = !canAhelp;
+ }
+
+ public void SetButtons()
+ {
+ BanButton.Disabled = !_adminManager.CanCommand("banpanel");
+ KickButton.Disabled = !_adminManager.CanCommand("kick");
+ NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
+ ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
+ WhitelistToggle.Disabled =
+ !(_adminManager.CanCommand("whitelistadd") && _adminManager.CanCommand("whitelistremove"));
+ LogsButton.Disabled = !_adminManager.CanCommand("adminlogs");
+ RejuvenateButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
+ DeleteButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
+ }
+}
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
new file mode 100644
index 00000000000..87ce7560463
--- /dev/null
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
@@ -0,0 +1,72 @@
+using Content.Client.Administration.Managers;
+using Content.Client.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using Robust.Client.Console;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Administration.UI.PlayerPanel;
+
+[UsedImplicitly]
+public sealed class PlayerPanelEui : BaseEui
+{
+ [Dependency] private readonly IClientConsoleHost _console = default!;
+ [Dependency] private readonly IClientAdminManager _admin = default!;
+ [Dependency] private readonly IClipboardManager _clipboard = default!;
+
+ private PlayerPanel PlayerPanel { get; }
+
+ public PlayerPanelEui()
+ {
+ PlayerPanel = new PlayerPanel(_admin);
+
+ PlayerPanel.OnUsernameCopy += username => _clipboard.SetText(username);
+ PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
+ // Kick command does not support GUIDs
+ PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
+ PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
+ PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
+ PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
+ PlayerPanel.OnWhitelistToggle += (id, whitelisted) =>
+ {
+ _console.ExecuteCommand(whitelisted ? $"whitelistremove \"{id}\"" : $"whitelistadd \"{id}\"");
+ };
+
+ PlayerPanel.OnFreezeAndMuteToggle += () => SendMessage(new PlayerPanelFreezeMessage(true));
+ PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
+ PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
+ PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
+ PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
+
+ PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
+ }
+
+ public override void Opened()
+ {
+ PlayerPanel.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ PlayerPanel.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not PlayerPanelEuiState s)
+ return;
+
+ PlayerPanel.TargetPlayer = s.Guid;
+ PlayerPanel.TargetUsername = s.Username;
+ PlayerPanel.SetUsername(s.Username);
+ PlayerPanel.SetPlaytime(s.Playtime);
+ PlayerPanel.SetBans(s.TotalBans, s.TotalRoleBans);
+ PlayerPanel.SetNotes(s.TotalNotes);
+ PlayerPanel.SetWhitelisted(s.Whitelisted);
+ PlayerPanel.SetSharedConnections(s.SharedConnections);
+ PlayerPanel.SetFrozen(s.CanFreeze, s.Frozen);
+ PlayerPanel.SetAhelp(s.CanAhelp);
+ PlayerPanel.SetButtons();
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
index 78eefa34628..4b50771b0fe 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
@@ -1,20 +1,21 @@
+using Content.Client.Administration.Managers;
using Content.Client.Station;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
+using Robust.Client.Console;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map.Components;
-using Robust.Shared.Timing;
namespace Content.Client.Administration.UI.Tabs.ObjectsTab;
[GenerateTypedNameReferences]
public sealed partial class ObjectsTab : Control
{
+ [Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IClientConsoleHost _console = default!;
private readonly Color _altColor = Color.FromHex("#292B38");
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
@@ -50,10 +51,20 @@ public ObjectsTab()
RefreshListButton.OnPressed += _ => RefreshObjectList();
var defaultSelection = ObjectsTabSelection.Grids;
- ObjectTypeOptions.SelectId((int) defaultSelection);
+ ObjectTypeOptions.SelectId((int)defaultSelection);
RefreshObjectList(defaultSelection);
}
+ private void TeleportTo(NetEntity nent)
+ {
+ _console.ExecuteCommand($"tpto {nent}");
+ }
+
+ private void Delete(NetEntity nent)
+ {
+ _console.ExecuteCommand($"delete {nent}");
+ }
+
public void RefreshObjectList()
{
RefreshObjectList(_selections[ObjectTypeOptions.SelectedId]);
@@ -117,9 +128,9 @@ private void GenerateButton(ListData data, ListContainerButton button)
if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor })
return;
- var entry = new ObjectsTabEntry(info.Name,
- info.Entity,
- new StyleBoxFlat { BackgroundColor = backgroundColor });
+ var entry = new ObjectsTabEntry(_admin, info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor });
+ entry.OnTeleport += TeleportTo;
+ entry.OnDelete += Delete;
button.ToolTip = $"{info.Name}, {info.Entity}";
button.AddChild(entry);
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
index 83c4cc5697f..c561125a30c 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
@@ -5,13 +5,25 @@
HorizontalExpand="True"
SeparationOverride="4">
+
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs
index aab06c6ccd0..ee5d3701f58 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs
@@ -1,4 +1,5 @@
-using Robust.Client.AutoGenerated;
+using Content.Client.Administration.Managers;
+using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -10,12 +11,30 @@ public sealed partial class ObjectsTabEntry : PanelContainer
{
public NetEntity AssocEntity;
- public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox)
+ public Action? OnTeleport;
+ public Action? OnDelete;
+ private readonly Dictionary