AHelpUIController.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Client.Administration.Managers;
  5. using Content.Client.Administration.Systems;
  6. using Content.Client.Administration.UI.Bwoink;
  7. using Content.Client.Gameplay;
  8. using Content.Client.Lobby;
  9. using Content.Client.Lobby.UI;
  10. using Content.Client.Stylesheets;
  11. using Content.Client.UserInterface.Controls;
  12. using Content.Client.UserInterface.Systems.MenuBar.Widgets;
  13. using Content.Shared.Administration;
  14. using Content.Shared.CCVar;
  15. using Content.Shared.Input;
  16. using JetBrains.Annotations;
  17. using Robust.Client.Audio;
  18. using Robust.Client.Graphics;
  19. using Robust.Client.Player;
  20. using Robust.Client.UserInterface;
  21. using Robust.Client.UserInterface.Controllers;
  22. using Robust.Client.UserInterface.Controls;
  23. using Robust.Client.UserInterface.CustomControls;
  24. using Robust.Shared.Configuration;
  25. using Robust.Shared.Input.Binding;
  26. using Robust.Shared.Network;
  27. using Robust.Shared.Player;
  28. using Robust.Shared.Utility;
  29. namespace Content.Client.UserInterface.Systems.Bwoink;
  30. [UsedImplicitly]
  31. public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSystem>, IOnStateChanged<GameplayState>, IOnStateChanged<LobbyState>
  32. {
  33. [Dependency] private readonly IClientAdminManager _adminManager = default!;
  34. [Dependency] private readonly IConfigurationManager _config = default!;
  35. [Dependency] private readonly IPlayerManager _playerManager = default!;
  36. [Dependency] private readonly IClyde _clyde = default!;
  37. [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
  38. [UISystemDependency] private readonly AudioSystem _audio = default!;
  39. private BwoinkSystem? _bwoinkSystem;
  40. private MenuButton? GameAHelpButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.AHelpButton;
  41. private Button? LobbyAHelpButton => (UIManager.ActiveScreen as LobbyGui)?.AHelpButton;
  42. public IAHelpUIHandler? UIHelper;
  43. private bool _discordRelayActive;
  44. private bool _hasUnreadAHelp;
  45. private bool _bwoinkSoundEnabled;
  46. private string? _aHelpSound;
  47. public override void Initialize()
  48. {
  49. base.Initialize();
  50. SubscribeNetworkEvent<BwoinkDiscordRelayUpdated>(DiscordRelayUpdated);
  51. SubscribeNetworkEvent<BwoinkPlayerTypingUpdated>(PeopleTypingUpdated);
  52. _adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
  53. _config.OnValueChanged(CCVars.AHelpSound, v => _aHelpSound = v, true);
  54. _config.OnValueChanged(CCVars.BwoinkSoundEnabled, v => _bwoinkSoundEnabled = v, true);
  55. }
  56. public void UnloadButton()
  57. {
  58. if (GameAHelpButton != null)
  59. GameAHelpButton.OnPressed -= AHelpButtonPressed;
  60. if (LobbyAHelpButton != null)
  61. LobbyAHelpButton.OnPressed -= AHelpButtonPressed;
  62. }
  63. public void LoadButton()
  64. {
  65. if (GameAHelpButton != null)
  66. GameAHelpButton.OnPressed += AHelpButtonPressed;
  67. if (LobbyAHelpButton != null)
  68. LobbyAHelpButton.OnPressed += AHelpButtonPressed;
  69. }
  70. private void OnAdminStatusUpdated()
  71. {
  72. if (UIHelper is not { IsOpen: true })
  73. return;
  74. EnsureUIHelper();
  75. }
  76. private void AHelpButtonPressed(BaseButton.ButtonEventArgs obj)
  77. {
  78. EnsureUIHelper();
  79. UIHelper!.ToggleWindow();
  80. }
  81. public void OnSystemLoaded(BwoinkSystem system)
  82. {
  83. _bwoinkSystem = system;
  84. _bwoinkSystem.OnBwoinkTextMessageRecieved += ReceivedBwoink;
  85. CommandBinds.Builder
  86. .Bind(ContentKeyFunctions.OpenAHelp,
  87. InputCmdHandler.FromDelegate(_ => ToggleWindow()))
  88. .Register<AHelpUIController>();
  89. }
  90. public void OnSystemUnloaded(BwoinkSystem system)
  91. {
  92. CommandBinds.Unregister<AHelpUIController>();
  93. DebugTools.Assert(_bwoinkSystem != null);
  94. _bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;
  95. _bwoinkSystem = null;
  96. }
  97. private void SetAHelpPressed(bool pressed)
  98. {
  99. if (GameAHelpButton != null)
  100. {
  101. GameAHelpButton.Pressed = pressed;
  102. }
  103. if (LobbyAHelpButton != null)
  104. {
  105. LobbyAHelpButton.Pressed = pressed;
  106. }
  107. UIManager.ClickSound();
  108. UnreadAHelpRead();
  109. }
  110. private void ReceivedBwoink(object? sender, SharedBwoinkSystem.BwoinkTextMessage message)
  111. {
  112. Logger.InfoS("c.s.go.es.bwoink", $"@{message.UserId}: {message.Text}");
  113. var localPlayer = _playerManager.LocalSession;
  114. if (localPlayer == null)
  115. {
  116. return;
  117. }
  118. if (message.PlaySound && localPlayer.UserId != message.TrueSender)
  119. {
  120. if (_aHelpSound != null && (_bwoinkSoundEnabled || !_adminManager.IsActive()))
  121. _audio.PlayGlobal(_aHelpSound, Filter.Local(), false);
  122. _clyde.RequestWindowAttention();
  123. }
  124. EnsureUIHelper();
  125. if (!UIHelper!.IsOpen)
  126. {
  127. UnreadAHelpReceived();
  128. }
  129. UIHelper!.Receive(message);
  130. }
  131. private void DiscordRelayUpdated(BwoinkDiscordRelayUpdated args, EntitySessionEventArgs session)
  132. {
  133. _discordRelayActive = args.DiscordRelayEnabled;
  134. UIHelper?.DiscordRelayChanged(_discordRelayActive);
  135. }
  136. private void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args, EntitySessionEventArgs session)
  137. {
  138. UIHelper?.PeopleTypingUpdated(args);
  139. }
  140. public void EnsureUIHelper()
  141. {
  142. var isAdmin = _adminManager.HasFlag(AdminFlags.Adminhelp);
  143. if (UIHelper != null && UIHelper.IsAdmin == isAdmin)
  144. return;
  145. UIHelper?.Dispose();
  146. var ownerUserId = _playerManager.LocalUser!.Value;
  147. UIHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
  148. UIHelper.DiscordRelayChanged(_discordRelayActive);
  149. UIHelper.SendMessageAction = (userId, textMessage, playSound, adminOnly) => _bwoinkSystem?.Send(userId, textMessage, playSound, adminOnly);
  150. UIHelper.InputTextChanged += (channel, text) => _bwoinkSystem?.SendInputTextUpdated(channel, text.Length > 0);
  151. UIHelper.OnClose += () => { SetAHelpPressed(false); };
  152. UIHelper.OnOpen += () => { SetAHelpPressed(true); };
  153. SetAHelpPressed(UIHelper.IsOpen);
  154. }
  155. public void Open()
  156. {
  157. var localUser = _playerManager.LocalUser;
  158. if (localUser == null)
  159. {
  160. return;
  161. }
  162. EnsureUIHelper();
  163. if (UIHelper!.IsOpen)
  164. return;
  165. UIHelper!.Open(localUser.Value, _discordRelayActive);
  166. }
  167. public void Open(NetUserId userId)
  168. {
  169. EnsureUIHelper();
  170. if (!UIHelper!.IsAdmin)
  171. return;
  172. UIHelper?.Open(userId, _discordRelayActive);
  173. }
  174. public void ToggleWindow()
  175. {
  176. EnsureUIHelper();
  177. UIHelper?.ToggleWindow();
  178. }
  179. public void PopOut()
  180. {
  181. EnsureUIHelper();
  182. if (UIHelper is not AdminAHelpUIHandler helper)
  183. return;
  184. if (helper.Window == null || helper.Control == null)
  185. {
  186. return;
  187. }
  188. helper.Control.Orphan();
  189. helper.Window.Dispose();
  190. helper.Window = null;
  191. helper.EverOpened = false;
  192. var monitor = _clyde.EnumerateMonitors().First();
  193. helper.ClydeWindow = _clyde.CreateWindow(new WindowCreateParameters
  194. {
  195. Maximized = false,
  196. Title = "Admin Help",
  197. Monitor = monitor,
  198. Width = 900,
  199. Height = 500
  200. });
  201. helper.ClydeWindow.RequestClosed += helper.OnRequestClosed;
  202. helper.ClydeWindow.DisposeOnClose = true;
  203. helper.WindowRoot = _uiManager.CreateWindowRoot(helper.ClydeWindow);
  204. helper.WindowRoot.AddChild(helper.Control);
  205. helper.Control.PopOut.Disabled = true;
  206. helper.Control.PopOut.Visible = false;
  207. }
  208. private void UnreadAHelpReceived()
  209. {
  210. GameAHelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
  211. LobbyAHelpButton?.StyleClasses.Add(StyleNano.StyleClassButtonColorRed);
  212. _hasUnreadAHelp = true;
  213. }
  214. private void UnreadAHelpRead()
  215. {
  216. GameAHelpButton?.StyleClasses.Remove(MenuButton.StyleClassRedTopButton);
  217. LobbyAHelpButton?.StyleClasses.Remove(StyleNano.StyleClassButtonColorRed);
  218. _hasUnreadAHelp = false;
  219. }
  220. public void OnStateEntered(GameplayState state)
  221. {
  222. if (GameAHelpButton != null)
  223. {
  224. GameAHelpButton.OnPressed -= AHelpButtonPressed;
  225. GameAHelpButton.OnPressed += AHelpButtonPressed;
  226. GameAHelpButton.Pressed = UIHelper?.IsOpen ?? false;
  227. if (_hasUnreadAHelp)
  228. {
  229. UnreadAHelpReceived();
  230. }
  231. else
  232. {
  233. UnreadAHelpRead();
  234. }
  235. }
  236. }
  237. public void OnStateExited(GameplayState state)
  238. {
  239. if (GameAHelpButton != null)
  240. GameAHelpButton.OnPressed -= AHelpButtonPressed;
  241. }
  242. public void OnStateEntered(LobbyState state)
  243. {
  244. if (LobbyAHelpButton != null)
  245. {
  246. LobbyAHelpButton.OnPressed -= AHelpButtonPressed;
  247. LobbyAHelpButton.OnPressed += AHelpButtonPressed;
  248. LobbyAHelpButton.Pressed = UIHelper?.IsOpen ?? false;
  249. if (_hasUnreadAHelp)
  250. {
  251. UnreadAHelpReceived();
  252. }
  253. else
  254. {
  255. UnreadAHelpRead();
  256. }
  257. }
  258. }
  259. public void OnStateExited(LobbyState state)
  260. {
  261. if (LobbyAHelpButton != null)
  262. LobbyAHelpButton.OnPressed -= AHelpButtonPressed;
  263. }
  264. }
  265. // please kill all this indirection
  266. public interface IAHelpUIHandler : IDisposable
  267. {
  268. public bool IsAdmin { get; }
  269. public bool IsOpen { get; }
  270. public void Receive(SharedBwoinkSystem.BwoinkTextMessage message);
  271. public void Close();
  272. public void Open(NetUserId netUserId, bool relayActive);
  273. public void ToggleWindow();
  274. public void DiscordRelayChanged(bool active);
  275. public void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args);
  276. public event Action OnClose;
  277. public event Action OnOpen;
  278. public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
  279. public event Action<NetUserId, string>? InputTextChanged;
  280. }
  281. public sealed class AdminAHelpUIHandler : IAHelpUIHandler
  282. {
  283. private readonly NetUserId _ownerId;
  284. public AdminAHelpUIHandler(NetUserId owner)
  285. {
  286. _ownerId = owner;
  287. }
  288. private readonly Dictionary<NetUserId, BwoinkPanel> _activePanelMap = new();
  289. public bool IsAdmin => true;
  290. public bool IsOpen => Window is { Disposed: false, IsOpen: true } || ClydeWindow is { IsDisposed: false };
  291. public bool EverOpened;
  292. public BwoinkWindow? Window;
  293. public WindowRoot? WindowRoot;
  294. public IClydeWindow? ClydeWindow;
  295. public BwoinkControl? Control;
  296. public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
  297. {
  298. var panel = EnsurePanel(message.UserId);
  299. panel.ReceiveLine(message);
  300. Control?.OnBwoink(message.UserId);
  301. }
  302. private void OpenWindow()
  303. {
  304. if (Window == null)
  305. return;
  306. if (EverOpened)
  307. Window.Open();
  308. else
  309. Window.OpenCentered();
  310. }
  311. public void Close()
  312. {
  313. Window?.Close();
  314. // popped-out window is being closed
  315. if (ClydeWindow != null)
  316. {
  317. ClydeWindow.RequestClosed -= OnRequestClosed;
  318. ClydeWindow.Dispose();
  319. // need to dispose control cause we cant reattach it directly back to the window
  320. // but orphan panels first so -they- can get readded when the window is opened again
  321. if (Control != null)
  322. {
  323. foreach (var (_, panel) in _activePanelMap)
  324. {
  325. panel.Orphan();
  326. }
  327. Control?.Dispose();
  328. }
  329. // window wont be closed here so we will invoke ourselves
  330. OnClose?.Invoke();
  331. }
  332. }
  333. public void ToggleWindow()
  334. {
  335. EnsurePanel(_ownerId);
  336. if (IsOpen)
  337. Close();
  338. else
  339. OpenWindow();
  340. }
  341. public void DiscordRelayChanged(bool active)
  342. {
  343. }
  344. public void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args)
  345. {
  346. if (_activePanelMap.TryGetValue(args.Channel, out var panel))
  347. panel.UpdatePlayerTyping(args.PlayerName, args.Typing);
  348. }
  349. public event Action? OnClose;
  350. public event Action? OnOpen;
  351. public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
  352. public event Action<NetUserId, string>? InputTextChanged;
  353. public void Open(NetUserId channelId, bool relayActive)
  354. {
  355. SelectChannel(channelId);
  356. OpenWindow();
  357. }
  358. public void OnRequestClosed(WindowRequestClosedEventArgs args)
  359. {
  360. Close();
  361. }
  362. private void EnsureControl()
  363. {
  364. if (Control is { Disposed: false })
  365. return;
  366. Window = new BwoinkWindow();
  367. Control = Window.Bwoink;
  368. Window.OnClose += () => { OnClose?.Invoke(); };
  369. Window.OnOpen += () =>
  370. {
  371. OnOpen?.Invoke();
  372. EverOpened = true;
  373. };
  374. // need to readd any unattached panels..
  375. foreach (var (_, panel) in _activePanelMap)
  376. {
  377. if (!Control!.BwoinkArea.Children.Contains(panel))
  378. {
  379. Control!.BwoinkArea.AddChild(panel);
  380. }
  381. panel.Visible = false;
  382. }
  383. }
  384. public void HideAllPanels()
  385. {
  386. foreach (var panel in _activePanelMap.Values)
  387. {
  388. panel.Visible = false;
  389. }
  390. }
  391. public BwoinkPanel EnsurePanel(NetUserId channelId)
  392. {
  393. EnsureControl();
  394. if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
  395. return existingPanel;
  396. _activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text, Window?.Bwoink.PlaySound.Pressed ?? true, Window?.Bwoink.AdminOnly.Pressed ?? false));
  397. existingPanel.InputTextChanged += text => InputTextChanged?.Invoke(channelId, text);
  398. existingPanel.Visible = false;
  399. if (!Control!.BwoinkArea.Children.Contains(existingPanel))
  400. Control.BwoinkArea.AddChild(existingPanel);
  401. return existingPanel;
  402. }
  403. public bool TryGetChannel(NetUserId ch, [NotNullWhen(true)] out BwoinkPanel? bp) => _activePanelMap.TryGetValue(ch, out bp);
  404. private void SelectChannel(NetUserId uid)
  405. {
  406. EnsurePanel(uid);
  407. Control!.SelectChannel(uid);
  408. }
  409. public void Dispose()
  410. {
  411. Window?.Dispose();
  412. Window = null;
  413. Control = null;
  414. _activePanelMap.Clear();
  415. EverOpened = false;
  416. }
  417. }
  418. public sealed class UserAHelpUIHandler : IAHelpUIHandler
  419. {
  420. private readonly NetUserId _ownerId;
  421. public UserAHelpUIHandler(NetUserId owner)
  422. {
  423. _ownerId = owner;
  424. }
  425. public bool IsAdmin => false;
  426. public bool IsOpen => _window is { Disposed: false, IsOpen: true };
  427. private DefaultWindow? _window;
  428. private BwoinkPanel? _chatPanel;
  429. private bool _discordRelayActive;
  430. public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
  431. {
  432. DebugTools.Assert(message.UserId == _ownerId);
  433. EnsureInit(_discordRelayActive);
  434. _chatPanel!.ReceiveLine(message);
  435. _window!.OpenCentered();
  436. }
  437. public void Close()
  438. {
  439. _window?.Close();
  440. }
  441. public void ToggleWindow()
  442. {
  443. EnsureInit(_discordRelayActive);
  444. if (_window!.IsOpen)
  445. {
  446. _window.Close();
  447. }
  448. else
  449. {
  450. _window.OpenCentered();
  451. }
  452. }
  453. // user can't pop out their window.
  454. public void PopOut()
  455. {
  456. }
  457. public void DiscordRelayChanged(bool active)
  458. {
  459. _discordRelayActive = active;
  460. if (_chatPanel != null)
  461. {
  462. _chatPanel.RelayedToDiscordLabel.Visible = active;
  463. }
  464. }
  465. public void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args)
  466. {
  467. }
  468. public event Action? OnClose;
  469. public event Action? OnOpen;
  470. public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
  471. public event Action<NetUserId, string>? InputTextChanged;
  472. public void Open(NetUserId channelId, bool relayActive)
  473. {
  474. EnsureInit(relayActive);
  475. _window!.OpenCentered();
  476. }
  477. private void EnsureInit(bool relayActive)
  478. {
  479. if (_window is { Disposed: false })
  480. return;
  481. _chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true, false));
  482. _chatPanel.InputTextChanged += text => InputTextChanged?.Invoke(_ownerId, text);
  483. _chatPanel.RelayedToDiscordLabel.Visible = relayActive;
  484. _window = new DefaultWindow()
  485. {
  486. TitleClass="windowTitleAlert",
  487. HeaderClass="windowHeaderAlert",
  488. Title=Loc.GetString("bwoink-user-title"),
  489. MinSize = new Vector2(500, 300),
  490. };
  491. _window.OnClose += () => { OnClose?.Invoke(); };
  492. _window.OnOpen += () => { OnOpen?.Invoke(); };
  493. _window.Contents.AddChild(_chatPanel);
  494. var introText = Loc.GetString("bwoink-system-introductory-message");
  495. var introMessage = new SharedBwoinkSystem.BwoinkTextMessage( _ownerId, SharedBwoinkSystem.SystemUserId, introText);
  496. Receive(introMessage);
  497. }
  498. public void Dispose()
  499. {
  500. _window?.Dispose();
  501. _window = null;
  502. _chatPanel = null;
  503. }
  504. }