diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 7f261f5df2..30f657a2b5 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -293,7 +293,7 @@ namespace Content.Client.Actions 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 c21ba2e32c..27b2a5dedb 100644 --- a/Content.Client/Administration/AdminNameOverlay.cs +++ b/Content.Client/Administration/AdminNameOverlay.cs @@ -44,7 +44,7 @@ namespace Content.Client.Administration } // if not on the same map, continue - if (_entityManager.GetComponent(entity.Value).MapID != _eyeManager.CurrentMap) + if (_entityManager.GetComponent(entity.Value).MapID != args.MapId) { continue; } diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs index ddd66623bd..dd8e3e2212 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs @@ -88,26 +88,51 @@ namespace Content.Client.Administration.UI.Bwoink 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 @@ namespace Content.Client.Administration.UI.Bwoink 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 30f9d24df1..e8653843c7 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs @@ -30,7 +30,11 @@ namespace Content.Client.Administration.UI.Bwoink } }; - 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 12522d552d..c7fbf6c2dc 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.UserInterface.Controls; 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 IEntityManager _entManager; + private readonly IUserInterfaceManager _uiManager; + + private PlayerInfo? _selectedPlayer; + + private List _playerList = new(); + private List _sortedPlayerList = new(); + + public Comparison? Comparison; + public Func? OverrideText; + + public PlayerListControl() { - private readonly AdminSystem _adminSystem; - - private List _playerList = new(); - private readonly List _sortedPlayerList = new(); - - public event Action? OnSelectionChanged; - public IReadOnlyList PlayerInfo => _playerList; - - public Func? OverrideText; - public Comparison? Comparison; - - private IEntityManager _entManager; - private IUserInterfaceManager _uiManager; - - 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)}; - } - - private void PlayerListNoItemSelected() - { - _selectedPlayer = null; - OnSelectionChanged?.Invoke(null); - } - - private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data) - { - if (args == null || data is not PlayerListData {Info: var selectedPlayer}) - return; - - if (selectedPlayer == _selectedPlayer) - return; - - if (args.Event.Function != EngineKeyFunctions.UIClick) - return; - - OnSelectionChanged?.Invoke(selectedPlayer); - _selectedPlayer = selectedPlayer; - - // 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); - } - - private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data) - { - if (args == null || data is not PlayerListData { Info: var selectedPlayer }) - return; - - 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; - } - - 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)); - - 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; - - _playerList = players.ToList(); - if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) - _selectedPlayer = null; - - FilterList(); - } - - 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; - - button.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - new Label - { - ClipText = true, - Text = GetText(info) - } - } - }); - - button.AddStyleClass(ListContainer.StyleClassListContainerButton); - } + _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 record PlayerListData(PlayerInfo Info) : ListData; + public IReadOnlyList PlayerInfo => _playerList; + + public event Action? OnSelectionChanged; + + private void PlayerListNoItemSelected() + { + _selectedPlayer = null; + OnSelectionChanged?.Invoke(null); + } + + private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data) + { + if (args == null || data is not PlayerListData { Info: var selectedPlayer }) + return; + + if (selectedPlayer == _selectedPlayer) + return; + + if (args.Event.Function != EngineKeyFunctions.UIClick) + return; + + OnSelectionChanged?.Invoke(selectedPlayer); + _selectedPlayer = selectedPlayer; + + // 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); + } + + private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data) + { + if (args == null || data is not PlayerListData { Info: var selectedPlayer }) + return; + + 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; + } + + 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)); + + PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList()); + if (_selectedPlayer != null) + PlayerListContainer.Select(new PlayerListData(_selectedPlayer)); + } + + + public void PopulateList(IReadOnlyList? players = null) + { + // Maintain existing pin statuses + var pinnedPlayers = _playerList.Where(p => p.IsPinned).ToDictionary(p => p.SessionId); + + players ??= _adminSystem.PlayerList; + + _playerList = players.ToList(); + + // Restore pin statuses + foreach (var player in _playerList) + { + if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer)) + { + player.IsPinned = pinnedPlayer.IsPinned; + } + } + + if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) + _selectedPlayer = null; + + FilterList(); + } + + + 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/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index f1cb27a62a..1bc1c0dba9 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 @@ namespace Content.Client.Chemistry.UI 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 3d787c69c1..c73d86b10f 100644 --- a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml +++ b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml @@ -6,6 +6,10 @@ + +