From b8e75dae0e25301da4ffdd1f6ca5b4aebdf5a2b8 Mon Sep 17 00:00:00 2001 From: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:07:06 -0400 Subject: [PATCH] Admin Tooling Cherry Picks (#1290) # Description Link to every PR I cherry-picked: 1. https://github.com/new-frontiers-14/frontier-station-14/pull/2283 2. https://github.com/space-wizards/space-station-14/pull/29219 3. https://github.com/space-wizards/space-station-14/pull/30075 4. https://github.com/space-wizards/space-station-14/pull/28639 5. https://github.com/space-wizards/space-station-14/pull/32527 6. https://github.com/space-wizards/space-station-14/pull/28030 7. https://github.com/space-wizards/space-station-14/pull/28178 The main purpose of this PR is the first cherry-picked PR. It adds the ability for admins to reply to ahelps via discord using an HTTP POST request. See all relevant details in the initial PR by Myzumi. --------- Co-authored-by: Myzumi <34660019+Myzumi@users.noreply.github.com> Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com> Co-authored-by: Whatstone Co-authored-by: Pieter-Jan Briers Co-authored-by: to4no_fix <156101927+chavonadelal@users.noreply.github.com> Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com> Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> Co-authored-by: metalgearsloth Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> --- .../UI/Bwoink/BwoinkWindow.xaml.cs | 21 +- .../CustomControls/PlayerListControl.xaml.cs | 230 +++--- .../UI/CustomControls/PlayerListEntry.xaml | 6 + .../UI/CustomControls/PlayerListEntry.xaml.cs | 58 ++ .../UI/Tabs/PlayerTab/PlayerTab.xaml | 26 +- .../UI/Tabs/PlayerTab/PlayerTab.xaml.cs | 104 ++- .../UI/Tabs/PlayerTab/PlayerTabEntry.xaml | 8 +- .../UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs | 26 +- .../UI/Tabs/PlayerTab/PlayerTabHeader.xaml | 3 +- Content.Client/Chat/Managers/ChatManager.cs | 10 + Content.Client/Chat/Managers/IChatManager.cs | 4 +- Content.Client/IoC/ClientContentIoC.cs | 9 +- .../RateLimiting/PlayerRateLimitManager.cs | 23 + Content.Client/Stylesheets/StyleNano.cs | 20 + .../UserInterface/Controls/ListContainer.cs | 39 +- .../Controls/SearchListContainer.cs | 68 ++ .../Systems/Admin/AdminUIController.cs | 9 +- Content.IntegrationTests/PoolManager.Cvars.cs | 4 +- .../Administration/Managers/AdminManager.cs | 2 +- Content.Server/Administration/ServerApi.cs | 51 +- .../Administration/Systems/BwoinkSystem.cs | 660 +++++++++++++++--- .../Chat/Managers/ChatManager.RateLimit.cs | 84 +-- Content.Server/Chat/Managers/ChatManager.cs | 19 +- Content.Server/Chat/Managers/IChatManager.cs | 10 +- Content.Server/Chat/Systems/ChatSystem.cs | 6 +- Content.Server/Discord/WebhookPayload.cs | 2 + Content.Server/Entry/EntryPoint.cs | 4 + .../GameTicking/GameTicker.GameRule.cs | 73 +- .../GameTicking/GameTicker.Player.cs | 11 + .../GameTicking/Rules/SecretRuleSystem.cs | 1 - Content.Server/IoC/ServerContentIoC.cs | 10 +- .../EntitySystems/FoodGuideDataSystem.cs | 2 +- .../RateLimiting/PlayerRateLimitManager.cs | 150 ++++ Content.Shared.Database/LogType.cs | 9 +- Content.Shared/Administration/PlayerInfo.cs | 2 + Content.Shared/CCVar/CCVars.cs | 152 +++- Content.Shared/Chat/ISharedChatManager.cs | 8 + .../Interaction/SharedInteractionSystem.cs | 31 +- .../RateLimiting/RateLimitRegistration.cs | 76 ++ .../SharedPlayerRateLimitManager.cs | 55 ++ .../Locale/en-US/administration/bwoink.ftl | 6 + .../administration/ui/tabs/player-tab.ftl | 3 + .../game-rules/gamerule-admin.ftl | 6 + .../game-ticking/game-rules/rule-secret.ftl | 2 - .../en-US/interaction/interaction-system.ftl | 3 +- .../Textures/Interface/Bwoink/pinned.png | Bin 0 -> 2267 bytes .../Textures/Interface/Bwoink/pinned2.png | Bin 0 -> 2028 bytes .../Textures/Interface/Bwoink/un_pinned.png | Bin 0 -> 2151 bytes 48 files changed, 1715 insertions(+), 391 deletions(-) create mode 100644 Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml create mode 100644 Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs create mode 100644 Content.Client/Players/RateLimiting/PlayerRateLimitManager.cs create mode 100644 Content.Client/UserInterface/Controls/SearchListContainer.cs create mode 100644 Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs create mode 100644 Content.Shared/Chat/ISharedChatManager.cs create mode 100644 Content.Shared/Players/RateLimiting/RateLimitRegistration.cs create mode 100644 Content.Shared/Players/RateLimiting/SharedPlayerRateLimitManager.cs create mode 100644 Resources/Locale/en-US/game-ticking/game-rules/gamerule-admin.ftl delete mode 100644 Resources/Locale/en-US/game-ticking/game-rules/rule-secret.ftl create mode 100644 Resources/Textures/Interface/Bwoink/pinned.png create mode 100644 Resources/Textures/Interface/Bwoink/pinned2.png create mode 100644 Resources/Textures/Interface/Bwoink/un_pinned.png diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs index f8d06f758f..6f6c1c8f6e 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs @@ -16,18 +16,25 @@ public BwoinkWindow() Bwoink.ChannelSelector.OnSelectionChanged += sel => { - if (sel is not null) + if (sel is null) { - Title = $"{sel.CharacterName} / {sel.Username}"; + Title = Loc.GetString("bwoink-none-selected"); + return; + } + + Title = $"{sel.CharacterName} / {sel.Username}"; - if (sel.OverallPlaytime != null) - { - Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}"; - } + if (sel.OverallPlaytime != null) + { + Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}"; } }; - 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 fdf935d7c0..b09cd727ef 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -4,147 +4,155 @@ 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; + + private PlayerInfo? _selectedPlayer; - public event Action? OnSelectionChanged; - public IReadOnlyList PlayerInfo => _playerList; + private List _playerList = new(); + private readonly List _sortedPlayerList = new(); - public Func? OverrideText; - public Comparison? Comparison; + public Comparison? Comparison; + public Func? OverrideText; - private IEntityManager _entManager; - private IUserInterfaceManager _uiManager; + 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) }; + } - private PlayerInfo? _selectedPlayer; + public IReadOnlyList PlayerInfo => _playerList; - 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; - PopulateList(_adminSystem.PlayerList); - FilterLineEdit.OnTextChanged += _ => FilterList(); - _adminSystem.PlayerListChanged += PopulateList; - BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)}; - } + 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; - public void StopFiltering() - { - FilterLineEdit.Text = string.Empty; - } + _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true); + args.Handle(); + } - private void FilterList() + public void StopFiltering() + { + FilterLineEdit.Text = string.Empty; + } + + private void FilterList() + { + _sortedPlayerList.Clear(); + foreach (var info in _playerList) { - _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)); - - PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList()); - if (_selectedPlayer != null) - PlayerListContainer.Select(new PlayerListData(_selectedPlayer)); + 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); } - public void PopulateList(IReadOnlyList? players = null) - { - players ??= _adminSystem.PlayerList; + if (Comparison != null) + _sortedPlayerList.Sort((a, b) => Comparison(a, b)); - _playerList = players.ToList(); - if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) - _selectedPlayer = null; + // Ensure pinned players are always at the top + _sortedPlayerList.Sort((a, b) => a.IsPinned != b.IsPinned && a.IsPinned ? -1 : 1); - FilterList(); - } + PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList()); + if (_selectedPlayer != null) + PlayerListContainer.Select(new PlayerListData(_selectedPlayer)); + } - private string GetText(PlayerInfo info) - { - var text = $"{info.CharacterName} ({info.Username})"; - if (OverrideText != null) - text = OverrideText.Invoke(info, text); - return text; - } + public void PopulateList(IReadOnlyList? players = null) + { + players ??= _adminSystem.PlayerList; - private void GenerateButton(ListData data, ListContainerButton button) - { - if (data is not PlayerListData { Info: var info }) - return; - - button.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - new Label - { - ClipText = true, - Text = GetText(info) - } - } - }); - - button.AddStyleClass(ListContainer.StyleClassListContainerButton); - } + _playerList = players.ToList(); + if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) + _selectedPlayer = null; + + FilterList(); } - public record PlayerListData(PlayerInfo Info) : ListData; + + private string GetText(PlayerInfo info) + { + var text = $"{info.CharacterName} ({info.Username})"; + if (OverrideText != null) + text = OverrideText.Invoke(info, text); + return text; + } + + 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 0000000000..af13ccc0e0 --- /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 0000000000..cd6a56ea71 --- /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/Tabs/PlayerTab/PlayerTab.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml index 3071bf8358..25a96df1d3 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml @@ -1,21 +1,19 @@  + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"> -