diff --git a/.github/labeler.yml b/.github/labeler.yml index e4f7b604184..356ec09747f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -13,9 +13,13 @@ - changed-files: - any-glob-to-any-file: '**/*.xaml*' +"Changes: Shaders": +- changed-files: + - any-glob-to-any-file: '**/*.swsl' + "Changes: Localization": - changed-files: - - any-glob-to-any-file: 'Resources/Locale/**/*.ftl' + - any-glob-to-any-file: '**/*.ftl' "No C#": - changed-files: diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs index ddd66623bd4..18aa02e9d67 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs @@ -88,6 +88,10 @@ public BwoinkControl() var ach = AHelpHelper.EnsurePanel(a.SessionId); var bch = AHelpHelper.EnsurePanel(b.SessionId); + // Pinned players first + if (a.IsPinned != b.IsPinned) + return a.IsPinned ? -1 : 1; + // 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. var aUnread = ach.Unread > 0; @@ -99,15 +103,31 @@ public BwoinkControl() 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 +273,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 12522d552d7..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, 32, 40)}; - } + 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/Arcade/BlockGameMenu.cs b/Content.Client/Arcade/BlockGameMenu.cs index 4a579fc4bf4..ad360c54394 100644 --- a/Content.Client/Arcade/BlockGameMenu.cs +++ b/Content.Client/Arcade/BlockGameMenu.cs @@ -380,7 +380,7 @@ private Control SetupGameGrid(Texture panelTex) { PanelOverride = back, HorizontalExpand = true, - SizeFlagsStretchRatio = 60 + SizeFlagsStretchRatio = 34.25f }; var backgroundPanel = new PanelContainer { diff --git a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs index 8fa8035afd6..4f08e6bd0ac 100644 --- a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs @@ -17,6 +17,7 @@ protected override void Open() base.Open(); _menu = this.CreateWindow(); + _menu.OnAction += SendAction; } protected override void ReceiveMessage(BoundUserInterfaceMessage message) diff --git a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs index c0704530de2..8fff406e86c 100644 --- a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs @@ -25,6 +25,7 @@ protected override void Open() base.Open(); _menu = this.CreateWindow(); + _menu.OnPlayerAction += SendAction; } protected override void ReceiveMessage(BoundUserInterfaceMessage message) diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index f1cb27a62a4..1bc1c0dba9a 100644 --- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -9,11 +10,15 @@ namespace Content.Client.Chemistry.UI [UsedImplicitly] public sealed class TransferAmountBoundUserInterface : BoundUserInterface { + private IEntityManager _entManager; + private EntityUid _owner; [ViewVariables] private TransferAmountWindow? _window; public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { + _owner = owner; + _entManager = IoCManager.Resolve(); } protected override void Open() @@ -21,6 +26,9 @@ protected override void Open() base.Open(); _window = this.CreateWindow(); + if (_entManager.TryGetComponent(_owner, out var comp)) + _window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int()); + _window.ApplyButton.OnPressed += _ => { if (int.TryParse(_window.AmountLineEdit.Text, out var i)) diff --git a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml index 3d787c69c10..c73d86b10f1 100644 --- a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml +++ b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml @@ -6,6 +6,10 @@ + +