]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Implement a playerpanel (#30238)
authornikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Fri, 9 Aug 2024 05:33:25 +0000 (08:33 +0300)
committerGitHub <noreply@github.com>
Fri, 9 Aug 2024 05:33:25 +0000 (15:33 +1000)
* Basic structure for the player panel ui

* Ensure basic functionality

Player panel now receives and displays basic info

* Make whitelistcommands accept user ids

* Make PlayerPanel use GUIDs where possible

* Add functionality to most playerpanel buttons

* Implement remaining playerpanel features

* Localize everything

* Finish up

* Put command arguments in quotes

I am not sure if it's even possible to have something like a space in
them considering they are guids and usernames but sure why not

* Make playerpanel a verb

* Add Logs button to player panel

* Change Notesbutton text and make whitelistbutton a confirmtion button

* Add freeze button that does not mute the player

* Add sharedconnections counter to playerpanel

* Make the playetime format clearer

* Allow for copying of the a player's username

* Do minor cleanup

* Rearrange buttons

* Fix unfreeze button not updating

* Fix wrong localisation text

* "Fix" the same role ban counting multiple times

The way rolebans are stored is horrible.
As such if you ban someone from a departmenrt or something
role bans are individually placed for every role.
The only way I found to distinguish them is the bantime.
This is horrible but I do not want to rewrite how all the bans are
stored right now.

* Add Delete and Rejuvenate buttons to player panel

By popular demand

* Marginally improve ui

* Add logs

* review update

* Fix verb

* Fix double notes

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
12 files changed:
Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml [new file with mode: 0644]
Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs [new file with mode: 0644]
Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs [new file with mode: 0644]
Content.Server/Administration/Commands/PlayerPanelCommand.cs [new file with mode: 0644]
Content.Server/Administration/PlayerPanelEui.cs [new file with mode: 0644]
Content.Server/Administration/Systems/AdminVerbSystem.cs
Content.Server/Whitelist/WhitelistCommands.cs
Content.Shared/Administration/PlayerPanelEuiState.cs [new file with mode: 0644]
Resources/Locale/en-US/administration/ui/actions.ftl
Resources/Locale/en-US/administration/ui/player-panel.ftl [new file with mode: 0644]
Resources/Locale/en-US/connection-messages.ftl
Resources/Locale/en-US/info/playerpanel.ftl [new file with mode: 0644]

diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml
new file mode 100644 (file)
index 0000000..8feec27
--- /dev/null
@@ -0,0 +1,36 @@
+<ui:FancyWindow
+    xmlns="https://spacestation14.io"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+    xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+    Title="{Loc ban-panel-title}" MinSize="300 300">
+    <BoxContainer Orientation="Vertical">
+        <BoxContainer Orientation="Horizontal">
+        <Label Name="PlayerName"/>
+        <Button Name="UsernameCopyButton" Text="{Loc player-panel-copy-username}"/>
+        </BoxContainer>
+       <BoxContainer Orientation="Horizontal">
+           <Label Name="Whitelisted"/>
+           <controls:ConfirmButton Name="WhitelistToggle" Text="{Loc 'player-panel-false'}" Visible="False"></controls:ConfirmButton>
+       </BoxContainer>
+       <Label Name="Playtime"/>
+       <Label Name="Notes"/>
+       <Label Name="Bans"/>
+       <Label Name="RoleBans"/>
+       <Label Name="SharedConnections"/>
+
+       <BoxContainer Align="Center">
+           <GridContainer Rows="5">
+            <Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/>
+            <Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/>
+            <Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/>
+            <controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/>
+            <controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/>
+            <Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/>
+            <Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/>
+            <Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>
+            <Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
+            <controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
+           </GridContainer>
+       </BoxContainer>
+    </BoxContainer>
+</ui:FancyWindow>
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
new file mode 100644 (file)
index 0000000..824d9eb
--- /dev/null
@@ -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<string>? OnUsernameCopy;
+    public event Action<NetUserId?>? OnOpenNotes;
+    public event Action<NetUserId?>? OnOpenBans;
+    public event Action<NetUserId?>? OnAhelp;
+    public event Action<string?>? OnKick;
+    public event Action<NetUserId?>? OnOpenBanPanel;
+    public event Action<NetUserId?, bool>? 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.ToString();
+            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("addwhitelist") && _adminManager.CanCommand("removewhitelist"));
+        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 (file)
index 0000000..87ce756
--- /dev/null
@@ -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.Server/Administration/Commands/PlayerPanelCommand.cs b/Content.Server/Administration/Commands/PlayerPanelCommand.cs
new file mode 100644 (file)
index 0000000..4a065bd
--- /dev/null
@@ -0,0 +1,56 @@
+using System.Linq;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+
+namespace Content.Server.Administration.Commands;
+
+[AdminCommand(AdminFlags.Admin)]
+public sealed class PlayerPanelCommand : LocalizedCommands
+{
+    [Dependency] private readonly IPlayerLocator _locator = default!;
+    [Dependency] private readonly EuiManager _euis = default!;
+    [Dependency] private readonly IPlayerManager _players = default!;
+
+    public override string Command => "playerpanel";
+
+    public override async void Execute(IConsoleShell shell, string argStr, string[] args)
+    {
+        if (shell.Player is not { } admin)
+        {
+            shell.WriteError(Loc.GetString("cmd-playerpanel-server"));
+            return;
+        }
+
+        if (args.Length != 1)
+        {
+            shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-arguments"));
+            return;
+        }
+
+        var queriedPlayer = await _locator.LookupIdByNameOrIdAsync(args[0]);
+
+        if (queriedPlayer == null)
+        {
+            shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-player"));
+            return;
+        }
+
+        var ui = new PlayerPanelEui(queriedPlayer);
+        _euis.OpenEui(ui, admin);
+        ui.SetPlayerState();
+    }
+
+    public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
+
+            return CompletionResult.FromHintOptions(options, LocalizationManager.GetString("cmd-playerpanel-completion"));
+        }
+
+        return CompletionResult.Empty;
+    }
+}
diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs
new file mode 100644 (file)
index 0000000..4c0df80
--- /dev/null
@@ -0,0 +1,210 @@
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.Administration.Managers;
+using Content.Server.Administration.Notes;
+using Content.Server.Administration.Systems;
+using Content.Server.Database;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Content.Shared.Database;
+using Content.Shared.Eui;
+using Robust.Server.Player;
+using Robust.Shared.Player;
+
+namespace Content.Server.Administration;
+
+public sealed class PlayerPanelEui : BaseEui
+{
+    [Dependency] private readonly IAdminManager _admins = default!;
+    [Dependency] private readonly IServerDbManager _db = default!;
+    [Dependency] private readonly IAdminNotesManager _notesMan = default!;
+    [Dependency] private readonly IEntityManager _entity = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+    [Dependency] private readonly EuiManager _eui = default!;
+    [Dependency] private readonly IAdminLogManager _adminLog = default!;
+
+    private readonly LocatedPlayerData _targetPlayer;
+    private int? _notes;
+    private int? _bans;
+    private int? _roleBans;
+    private int _sharedConnections;
+    private bool? _whitelisted;
+    private TimeSpan _playtime;
+    private bool _frozen;
+    private bool _canFreeze;
+    private bool _canAhelp;
+
+    public PlayerPanelEui(LocatedPlayerData player)
+    {
+        IoCManager.InjectDependencies(this);
+        _targetPlayer = player;
+    }
+
+    public override void Opened()
+    {
+        base.Opened();
+        _admins.OnPermsChanged += OnPermsChanged;
+    }
+
+    public override void Closed()
+    {
+        base.Closed();
+        _admins.OnPermsChanged -= OnPermsChanged;
+    }
+
+    public override EuiStateBase GetNewState()
+    {
+        return new PlayerPanelEuiState(_targetPlayer.UserId,
+            _targetPlayer.Username,
+            _playtime,
+            _notes,
+            _bans,
+            _roleBans,
+            _sharedConnections,
+            _whitelisted,
+            _canFreeze,
+            _frozen,
+            _canAhelp);
+    }
+
+    private void OnPermsChanged(AdminPermsChangedEventArgs args)
+    {
+        if (args.Player != Player)
+            return;
+
+        SetPlayerState();
+    }
+
+    public override void HandleMessage(EuiMessageBase msg)
+    {
+        base.HandleMessage(msg);
+
+        ICommonSession? session;
+
+        switch (msg)
+        {
+            case PlayerPanelFreezeMessage freezeMsg:
+                if (!_admins.IsAdmin(Player) ||
+                    !_entity.TrySystem<AdminFrozenSystem>(out var frozenSystem) ||
+                    !_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
+                    session.AttachedEntity == null)
+                    return;
+
+                if (_entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity))
+                {
+                    _adminLog.Add(LogType.Action,$"{Player:actor} unfroze {_entity.ToPrettyString(session.AttachedEntity):subject}");
+                    _entity.RemoveComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
+                    SetPlayerState();
+                    return;
+                }
+
+                if (freezeMsg.Mute)
+                {
+                    _adminLog.Add(LogType.Action,$"{Player:actor} froze and muted {_entity.ToPrettyString(session.AttachedEntity):subject}");
+                    frozenSystem.FreezeAndMute(session.AttachedEntity.Value);
+                }
+                else
+                {
+                    _adminLog.Add(LogType.Action,$"{Player:actor} froze {_entity.ToPrettyString(session.AttachedEntity):subject}");
+                    _entity.EnsureComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
+                }
+                SetPlayerState();
+                break;
+
+            case PlayerPanelLogsMessage:
+                if (!_admins.HasAdminFlag(Player, AdminFlags.Logs))
+                    return;
+
+                _adminLog.Add(LogType.Action, $"{Player:actor} opened logs on {_targetPlayer.Username:subject}");
+                var ui = new AdminLogsEui();
+                _eui.OpenEui(ui, Player);
+                ui.SetLogFilter(search: _targetPlayer.Username);
+                break;
+            case PlayerPanelDeleteMessage:
+            case PlayerPanelRejuvenationMessage:
+                if (!_admins.HasAdminFlag(Player, AdminFlags.Debug) ||
+                    !_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
+                    session.AttachedEntity == null)
+                    return;
+
+                if (msg is PlayerPanelRejuvenationMessage)
+                {
+                    _adminLog.Add(LogType.Action,$"{Player:actor} rejuvenated {_entity.ToPrettyString(session.AttachedEntity):subject}");
+                    if (!_entity.TrySystem<RejuvenateSystem>(out var rejuvenate))
+                        return;
+
+                    rejuvenate.PerformRejuvenate(session.AttachedEntity.Value);
+                }
+                else
+                {
+                    _adminLog.Add(LogType.Action,$"{Player:actor} deleted {_entity.ToPrettyString(session.AttachedEntity):subject}");
+                    _entity.DeleteEntity(session.AttachedEntity);
+                }
+                break;
+        }
+    }
+
+    public async void SetPlayerState()
+    {
+        if (!_admins.IsAdmin(Player))
+        {
+            Close();
+            return;
+        }
+
+        _playtime = (await _db.GetPlayTimes(_targetPlayer.UserId))
+            .Where(p => p.Tracker == "Overall")
+            .Select(p => p.TimeSpent)
+            .FirstOrDefault();
+
+        if (_notesMan.CanView(Player))
+        {
+            _notes = (await _notesMan.GetAllAdminRemarks(_targetPlayer.UserId)).Count;
+        }
+        else
+        {
+            _notes = null;
+        }
+
+        _sharedConnections = _player.Sessions.Count(s => s.Channel.RemoteEndPoint.Address.Equals(_targetPlayer.LastAddress) && s.UserId != _targetPlayer.UserId);
+
+    // Apparently the Bans flag is also used for whitelists
+    if (_admins.HasAdminFlag(Player, AdminFlags.Ban))
+        {
+            _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
+            // This won't get associated ip or hwid bans but they were not placed on this account anyways
+            _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
+            // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
+            // The only way to distinguish whether a role ban is the same is to compare the ban time.
+            // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
+            _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
+        }
+        else
+        {
+            _whitelisted = null;
+            _bans = null;
+            _roleBans = null;
+        }
+
+        if (_player.TryGetSessionById(_targetPlayer.UserId, out var session))
+        {
+            _canFreeze = session.AttachedEntity != null;
+            _frozen = _entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity);
+        }
+        else
+        {
+            _canFreeze = false;
+        }
+
+        if (_admins.HasAdminFlag(Player, AdminFlags.Adminhelp))
+        {
+            _canAhelp = true;
+        }
+        else
+        {
+            _canAhelp = false;
+        }
+
+        StateDirty();
+    }
+}
index 3c8f7cd553c5fa3191467ef7ccdca7b0c3a2be6d..6198611ac8f244b16bcb9cf5c5fa2670f95a531c 100644 (file)
@@ -208,6 +208,15 @@ namespace Content.Server.Administration.Systems
                         ConfirmationPopup = true,
                         Impact = LogImpact.High,
                     });
+
+                    // PlayerPanel
+                    args.Verbs.Add(new Verb
+                    {
+                        Text = Loc.GetString("admin-player-actions-player-panel"),
+                        Category = VerbCategory.Admin,
+                        Act = () => _console.ExecuteCommand(player, $"playerpanel \"{targetActor.PlayerSession.UserId}\""),
+                        Impact = LogImpact.Low
+                    });
                 }
 
                 // Freeze
index ab0613062159a538581d86a4094bf138eb49896a..09df1d1419ca6e8a5927eb5a024c3e789bd120aa 100644 (file)
@@ -27,7 +27,7 @@ public sealed class AddWhitelistCommand : LocalizedCommands
         var loc = IoCManager.Resolve<IPlayerLocator>();
 
         var name = string.Join(' ', args).Trim();
-        var data = await loc.LookupIdByNameAsync(name);
+        var data = await loc.LookupIdByNameOrIdAsync(name);
 
         if (data != null)
         {
@@ -76,7 +76,7 @@ public sealed class RemoveWhitelistCommand : LocalizedCommands
         var loc = IoCManager.Resolve<IPlayerLocator>();
 
         var name = string.Join(' ', args).Trim();
-        var data = await loc.LookupIdByNameAsync(name);
+        var data = await loc.LookupIdByNameOrIdAsync(name);
 
         if (data != null)
         {
diff --git a/Content.Shared/Administration/PlayerPanelEuiState.cs b/Content.Shared/Administration/PlayerPanelEuiState.cs
new file mode 100644 (file)
index 0000000..186b992
--- /dev/null
@@ -0,0 +1,54 @@
+using Content.Shared.Eui;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using YamlDotNet.Serialization.Callbacks;
+
+namespace Content.Shared.Administration;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelEuiState(NetUserId guid,
+    string username,
+    TimeSpan playtime,
+    int? totalNotes,
+    int? totalBans,
+    int? totalRoleBans,
+    int sharedConnections,
+    bool? whitelisted,
+    bool canFreeze,
+    bool frozen,
+    bool canAhelp)
+    : EuiStateBase
+{
+    public readonly NetUserId Guid = guid;
+    public readonly string Username = username;
+    public readonly TimeSpan Playtime = playtime;
+    public readonly int? TotalNotes = totalNotes;
+    public readonly int? TotalBans = totalBans;
+    public readonly int? TotalRoleBans = totalRoleBans;
+    public readonly int SharedConnections = sharedConnections;
+    public readonly bool? Whitelisted = whitelisted;
+    public readonly bool CanFreeze = canFreeze;
+    public readonly bool Frozen = frozen;
+    public readonly bool CanAhelp = canAhelp;
+}
+
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelFreezeMessage : EuiMessageBase
+{
+    public readonly bool Mute;
+
+    public PlayerPanelFreezeMessage(bool mute = false)
+    {
+        Mute = mute;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelLogsMessage : EuiMessageBase;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelDeleteMessage : EuiMessageBase;
+
+[Serializable, NetSerializable]
+public sealed class PlayerPanelRejuvenationMessage: EuiMessageBase;
index 3c13d56c948fa3cb24cd59ed49e2c211350ef10c..9893eb59afc1c6508a7aca80207e054e6ba26183 100644 (file)
@@ -7,6 +7,7 @@ admin-player-actions-ahelp = AHelp
 admin-player-actions-respawn = Respawn
 admin-player-actions-spawn = Spawn here
 admin-player-spawn-failed = Failed to find valid coordinates
+admin-player-actions-player-panel = Open Player Panel
 
 admin-player-actions-clone = Clone
 admin-player-actions-follow = Follow
diff --git a/Resources/Locale/en-US/administration/ui/player-panel.ftl b/Resources/Locale/en-US/administration/ui/player-panel.ftl
new file mode 100644 (file)
index 0000000..ed63dd6
--- /dev/null
@@ -0,0 +1,22 @@
+player-panel-title = information for {$player}
+player-panel-username = Username: {$player}
+player-panel-whitelisted = Whitelisted:
+player-panel-bans = Total Bans: {$totalBans}
+player-panel-rolebans = Total Role Bans: {$totalRoleBans}
+player-panel-notes = Total Notes: {$totalNotes}
+player-panel-playtime = Total Playtime: {$days}d:{$hours}h:{$minutes}m
+player-panel-shared-connections = Shared Connections: {$sharedConnections}
+
+player-panel-copy-username = Copy
+player-panel-show-notes = Notes
+player-panel-show-bans = Show Bans
+player-panel-help = Ahelp
+player-panel-freeze-and-mute = Freeze & Mute
+player-panel-freeze = Freeze
+player-panel-unfreeze = Unfreeze
+player-panel-kick = Kick
+player-panel-ban = Ban
+player-panel-logs = Logs
+player-panel-delete = Delete
+player-panel-rejuvenate = Rejuvenate
+player-panel-false = False
index f1596d90152bf465853426cf472190fa4c3b901d..309e8fc2f83933095b082adfdf50375417832a60 100644 (file)
@@ -11,14 +11,14 @@ whitelist-playercount-invalid = {$min ->
 whitelist-not-whitelisted-rp = You are not whitelisted. To become whitelisted, visit our Discord (which can be found at https://spacestation14.io) and check the #rp-whitelist channel.
 
 cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
-cmd-whitelistadd-help = Usage: whitelistadd <username>
+cmd-whitelistadd-help = Usage: whitelistadd <username or User ID>
 cmd-whitelistadd-existing = {$username} is already on the whitelist!
 cmd-whitelistadd-added = {$username} added to the whitelist
 cmd-whitelistadd-not-found = Unable to find '{$username}'
 cmd-whitelistadd-arg-player = [player]
 
 cmd-whitelistremove-desc = Removes the player with the given username from the server whitelist.
-cmd-whitelistremove-help = Usage: whitelistremove <username>
+cmd-whitelistremove-help = Usage: whitelistremove <username or User ID>
 cmd-whitelistremove-existing = {$username} is not on the whitelist!
 cmd-whitelistremove-removed = {$username} removed from the whitelist
 cmd-whitelistremove-not-found = Unable to find '{$username}'
diff --git a/Resources/Locale/en-US/info/playerpanel.ftl b/Resources/Locale/en-US/info/playerpanel.ftl
new file mode 100644 (file)
index 0000000..138939c
--- /dev/null
@@ -0,0 +1,7 @@
+cmd-playerpanel-desc = Displays general information and actions for a player
+cmd-playerpanel-help = Usage: playerpanel <name or user ID>
+
+cmd-playerpanel-server = This command cannot be run from the server
+cmd-playerpanel-invalid-arguments = Invalid amount of arguments
+cmd-playerpanel-invalid-player = Player not found
+cmd-playerpanel-completion = <PlayerIndex>