BwoinkControl.xaml.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. using System.Linq;
  2. using System.Text;
  3. using Content.Client.Administration.Managers;
  4. using Content.Client.Administration.UI.CustomControls;
  5. using Content.Client.UserInterface.Systems.Bwoink;
  6. using Content.Shared.Administration;
  7. using Content.Shared.CCVar;
  8. using Robust.Client.AutoGenerated;
  9. using Robust.Client.Console;
  10. using Robust.Client.UserInterface;
  11. using Robust.Client.UserInterface.Controls;
  12. using Robust.Client.UserInterface.XAML;
  13. using Robust.Shared.Network;
  14. using Robust.Shared.Configuration;
  15. using Robust.Shared.Utility;
  16. namespace Content.Client.Administration.UI.Bwoink
  17. {
  18. /// <summary>
  19. /// This window connects to a BwoinkSystem channel. BwoinkSystem manages the rest.
  20. /// </summary>
  21. [GenerateTypedNameReferences]
  22. public sealed partial class BwoinkControl : Control
  23. {
  24. [Dependency] private readonly IClientAdminManager _adminManager = default!;
  25. [Dependency] private readonly IClientConsoleHost _console = default!;
  26. [Dependency] private readonly IUserInterfaceManager _ui = default!;
  27. [Dependency] private readonly IConfigurationManager _cfg = default!;
  28. public AdminAHelpUIHandler AHelpHelper = default!;
  29. private PlayerInfo? _currentPlayer;
  30. private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
  31. public BwoinkControl()
  32. {
  33. RobustXamlLoader.Load(this);
  34. IoCManager.InjectDependencies(this);
  35. var newPlayerThreshold = 0;
  36. _cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
  37. var uiController = _ui.GetUIController<AHelpUIController>();
  38. if (uiController.UIHelper is not AdminAHelpUIHandler helper)
  39. return;
  40. AHelpHelper = helper;
  41. _adminManager.AdminStatusUpdated += UpdateButtons;
  42. UpdateButtons();
  43. AdminOnly.OnToggled += args => PlaySound.Disabled = args.Pressed;
  44. ChannelSelector.OnSelectionChanged += sel =>
  45. {
  46. _currentPlayer = sel;
  47. SwitchToChannel(sel?.SessionId);
  48. ChannelSelector.PlayerListContainer.DirtyList();
  49. };
  50. ChannelSelector.OverrideText += (info, text) =>
  51. {
  52. var sb = new StringBuilder();
  53. if (info.Connected)
  54. sb.Append(info.ActiveThisRound ? '⚫' : '◐');
  55. else
  56. sb.Append(info.ActiveThisRound ? '⭘' : '·');
  57. sb.Append(' ');
  58. if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
  59. {
  60. if (panel.Unread < 11)
  61. sb.Append(new Rune('➀' + (panel.Unread-1)));
  62. else
  63. sb.Append(new Rune(0x2639)); // ☹
  64. sb.Append(' ');
  65. }
  66. // Mark antagonists with symbol
  67. if (info.Antag && info.ActiveThisRound)
  68. sb.Append(new Rune(0x1F5E1)); // 🗡
  69. // Mark new players with symbol
  70. if (IsNewPlayer(info))
  71. sb.Append(new Rune(0x23F2)); // ⏲
  72. sb.AppendFormat("\"{0}\"", text);
  73. return sb.ToString();
  74. };
  75. // <summary>
  76. // Returns true if the player's overall playtime is under the set threshold
  77. // </summary>
  78. bool IsNewPlayer(PlayerInfo info)
  79. {
  80. // Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
  81. if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
  82. return false;
  83. return (info.OverallPlaytime is null
  84. || info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
  85. }
  86. ChannelSelector.Comparison = (a, b) =>
  87. {
  88. var ach = AHelpHelper.EnsurePanel(a.SessionId);
  89. var bch = AHelpHelper.EnsurePanel(b.SessionId);
  90. // Pinned players first
  91. if (a.IsPinned != b.IsPinned)
  92. return a.IsPinned ? -1 : 1;
  93. // Then, any chat with unread messages.
  94. var aUnread = ach.Unread > 0;
  95. var bUnread = bch.Unread > 0;
  96. if (aUnread != bUnread)
  97. return aUnread ? -1 : 1;
  98. // Then, any chat with recent messages from the current round
  99. var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
  100. var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
  101. if (aRecent != bRecent)
  102. return aRecent ? -1 : 1;
  103. // Sort by connection status. Disconnected players will be last.
  104. if (a.Connected != b.Connected)
  105. return a.Connected ? -1 : 1;
  106. // Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
  107. if (a.Connected && b.Connected)
  108. {
  109. var aNewPlayer = IsNewPlayer(a);
  110. var bNewPlayer = IsNewPlayer(b);
  111. // Players who have joined the round will be listed before players in the lobby
  112. if (a.ActiveThisRound != b.ActiveThisRound)
  113. return a.ActiveThisRound ? -1 : 1;
  114. // Within both the joined group and lobby group, new players will be grouped and listed first
  115. if (aNewPlayer != bNewPlayer)
  116. return aNewPlayer ? -1 : 1;
  117. // Within all four previous groups, antagonists will be listed first.
  118. if (a.Antag != b.Antag)
  119. return a.Antag ? -1 : 1;
  120. }
  121. // Sort disconnected players by participation in the round
  122. if (!a.Connected && !b.Connected)
  123. {
  124. if (a.ActiveThisRound != b.ActiveThisRound)
  125. return a.ActiveThisRound ? -1 : 1;
  126. }
  127. // Finally, sort by the most recent message.
  128. return bch.LastMessage.CompareTo(ach.LastMessage);
  129. };
  130. Bans.OnPressed += _ =>
  131. {
  132. if (_currentPlayer is not null)
  133. _console.ExecuteCommand($"banlist \"{_currentPlayer.SessionId}\"");
  134. };
  135. Notes.OnPressed += _ =>
  136. {
  137. if (_currentPlayer is not null)
  138. _console.ExecuteCommand($"adminnotes \"{_currentPlayer.SessionId}\"");
  139. };
  140. Ban.OnPressed += _ =>
  141. {
  142. if (_currentPlayer is not null)
  143. _console.ExecuteCommand($"banpanel \"{_currentPlayer.SessionId}\"");
  144. };
  145. Kick.OnPressed += _ =>
  146. {
  147. if (!AdminUIHelpers.TryConfirm(Kick, _confirmations))
  148. {
  149. return;
  150. }
  151. // TODO: Reason field
  152. if (_currentPlayer is not null)
  153. _console.ExecuteCommand($"kick \"{_currentPlayer.Username}\"");
  154. };
  155. Follow.OnPressed += _ =>
  156. {
  157. if (_currentPlayer is not null)
  158. _console.ExecuteCommand($"follow \"{_currentPlayer.NetEntity}\"");
  159. };
  160. Respawn.OnPressed += _ =>
  161. {
  162. if (!AdminUIHelpers.TryConfirm(Respawn, _confirmations))
  163. {
  164. return;
  165. }
  166. if (_currentPlayer is not null)
  167. _console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\"");
  168. };
  169. PopOut.OnPressed += _ =>
  170. {
  171. uiController.PopOut();
  172. };
  173. }
  174. public void OnBwoink(NetUserId channel)
  175. {
  176. ChannelSelector.PopulateList();
  177. }
  178. public void SelectChannel(NetUserId channel)
  179. {
  180. if (!ChannelSelector.PlayerInfo.TryFirstOrDefault(
  181. i => i.SessionId == channel, out var info))
  182. return;
  183. // clear filter if we're trying to select a channel for a player that isn't currently filtered
  184. // i.e. through the message verb.
  185. var data = new PlayerListData(info);
  186. if (!ChannelSelector.PlayerListContainer.Data.Contains(data))
  187. {
  188. ChannelSelector.StopFiltering();
  189. }
  190. ChannelSelector.PopulateList();
  191. ChannelSelector.PlayerListContainer.Select(data);
  192. }
  193. public void UpdateButtons()
  194. {
  195. var disabled = _currentPlayer == null;
  196. Bans.Visible = _adminManager.HasFlag(AdminFlags.Ban);
  197. Bans.Disabled = !Bans.Visible || disabled;
  198. Notes.Visible = _adminManager.HasFlag(AdminFlags.ViewNotes);
  199. Notes.Disabled = !Notes.Visible || disabled;
  200. Ban.Visible = _adminManager.HasFlag(AdminFlags.Ban);
  201. Ban.Disabled = !Ban.Visible || disabled;
  202. Kick.Visible = _adminManager.CanCommand("kick");
  203. Kick.Disabled = !Kick.Visible || disabled;
  204. Respawn.Visible = _adminManager.CanCommand("respawn");
  205. Respawn.Disabled = !Respawn.Visible || disabled;
  206. Follow.Visible = _adminManager.CanCommand("follow");
  207. Follow.Disabled = !Follow.Visible || disabled;
  208. }
  209. private string FormatTabTitle(ItemList.Item li, PlayerInfo? pl = default)
  210. {
  211. pl ??= (PlayerInfo) li.Metadata!;
  212. var sb = new StringBuilder();
  213. sb.Append(pl.Connected ? '●' : '○');
  214. sb.Append(' ');
  215. if (AHelpHelper.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0)
  216. {
  217. if (panel.Unread < 11)
  218. sb.Append(new Rune('➀' + (panel.Unread-1)));
  219. else
  220. sb.Append(new Rune(0x2639)); // ☹
  221. sb.Append(' ');
  222. }
  223. if (pl.Antag)
  224. sb.Append(new Rune(0x1F5E1)); // 🗡
  225. if (pl.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
  226. sb.Append(new Rune(0x23F2)); // ⏲
  227. sb.AppendFormat("\"{0}\"", pl.CharacterName);
  228. if (pl.IdentityName != pl.CharacterName && pl.IdentityName != string.Empty)
  229. sb.Append(' ').AppendFormat("[{0}]", pl.IdentityName);
  230. sb.Append(' ').Append(pl.Username);
  231. return sb.ToString();
  232. }
  233. private void SwitchToChannel(NetUserId? ch)
  234. {
  235. UpdateButtons();
  236. AHelpHelper.HideAllPanels();
  237. if (ch != null)
  238. {
  239. var panel = AHelpHelper.EnsurePanel(ch.Value);
  240. panel.Visible = true;
  241. }
  242. }
  243. public void PopulateList()
  244. {
  245. // Maintain existing pin statuses
  246. var pinnedPlayers = ChannelSelector.PlayerInfo.Where(p => p.IsPinned).ToDictionary(p => p.SessionId);
  247. ChannelSelector.PopulateList();
  248. // Restore pin statuses
  249. foreach (var player in ChannelSelector.PlayerInfo)
  250. {
  251. if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer))
  252. {
  253. player.IsPinned = pinnedPlayer.IsPinned;
  254. }
  255. }
  256. UpdateButtons();
  257. }
  258. }
  259. }