fish-station/Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs
2025-12-26 17:44:36 +03:00

666 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Content.Client.Administration.Managers;
using Content.Client.Gameplay;
using Content.Client.Lobby;
using Content.Client.Lobby.UI;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared._Sunrise.MentorHelp;
using Content.Shared._Sunrise.SunriseCCVars;
using Content.Shared.Administration;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Input.Binding;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Audio;
namespace Content.Client._Sunrise.MentorHelp;
/// <summary>
/// UI controller for mentor help system
/// </summary>
[UsedImplicitly]
public sealed class MentorHelpUIController : UIController, IOnSystemChanged<MentorHelpSystem>, IOnStateChanged<GameplayState>, IOnStateChanged<LobbyState>
{
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
[UISystemDependency] private readonly AudioSystem _audio = default!;
private MentorHelpSystem? _mentorHelpSystem;
public IMentorHelpUIHandler? UIHelper;
private readonly HashSet<int> _unreadTicketIds = new();
private readonly Dictionary<int, int> _lastMessageIdByTicket = new();
// Последнее состояние тикетов. Нужны для фильтрации "моих" тикетов и авто-открытия.
private readonly Dictionary<int, MentorHelpTicketData> _ticketDataById = new();
private bool _mentorHelpSoundEnabled;
private static readonly SoundSpecifier? MentorHelpSound =
new SoundPathSpecifier("/Audio/_Sunrise/Effects/adminticketopen.ogg", AudioParams.Default.WithVolume(-3f));
private Button? LobbyMHelpButton => (UIManager.ActiveScreen as LobbyGui)?.MHelpButton;
private MenuButton? GameMHelpButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.MHelpButton;
protected override string SawmillName => "c.s.go.es.mhelp";
public override void Initialize()
{
base.Initialize();
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
_config.OnValueChanged(SunriseCCVars.MentorHelpSoundEnabled, v => _mentorHelpSoundEnabled = v, true);
}
public void OnSystemLoaded(MentorHelpSystem system)
{
_mentorHelpSystem = system;
_mentorHelpSystem.OnTicketUpdated += OnTicketUpdated;
_mentorHelpSystem.OnTicketsListReceived += OnTicketsListReceived;
_mentorHelpSystem.OnTicketMessagesReceived += OnTicketMessagesReceived;
_mentorHelpSystem.OnOpenTicketReceived += OnOpenTicketReceived;
_mentorHelpSystem.OnPlayerTypingUpdated += OnPlayerTypingUpdated;
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenMentorHelp,
InputCmdHandler.FromDelegate(_ => Open()))
.Register<MentorHelpUIController>();
}
private void OnOpenTicketReceived(object? sender, MentorHelpOpenTicketMessage message)
{
EnsureUIHelper();
if (UIHelper == null)
return;
if (!UIHelper.IsOpen)
{
UIHelper.OpenWindow();
UIHelper.OpenTicket(message.TicketId);
return;
}
if (UIHelper.CurrentTicketId == message.TicketId)
{
_mentorHelpSystem?.RequestTicketMessages(message.TicketId);
return;
}
UIHelper.OpenTicket(message.TicketId);
}
public void OnSystemUnloaded(MentorHelpSystem system)
{
CommandBinds.Unregister<MentorHelpUIController>();
if (_mentorHelpSystem != null)
{
_mentorHelpSystem.OnTicketUpdated -= OnTicketUpdated;
_mentorHelpSystem.OnTicketsListReceived -= OnTicketsListReceived;
_mentorHelpSystem.OnTicketMessagesReceived -= OnTicketMessagesReceived;
_mentorHelpSystem.OnOpenTicketReceived -= OnOpenTicketReceived;
_mentorHelpSystem.OnPlayerTypingUpdated -= OnPlayerTypingUpdated;
_mentorHelpSystem = null;
}
if (GameMHelpButton != null)
GameMHelpButton.OnPressed -= MHelpButtonPressed;
if (LobbyMHelpButton != null)
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
}
public void OnStateEntered(GameplayState state)
{
if (GameMHelpButton != null)
{
// Защита от повторной подписки, OnStateEntered может вызываться несколько раз
// Аналогично в Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs (метод OnStateEntered)
GameMHelpButton.OnPressed -= MHelpButtonPressed;
GameMHelpButton.OnPressed += MHelpButtonPressed;
GameMHelpButton.Pressed = UIHelper?.IsOpen ?? false;
UpdateButtonStyling();
}
}
public void OnStateExited(GameplayState state)
{
if (GameMHelpButton != null)
GameMHelpButton.OnPressed -= MHelpButtonPressed;
}
public void OnStateEntered(LobbyState state)
{
if (LobbyMHelpButton != null)
{
// То же самое для лобби, см. Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs (метод OnStateEntered)
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
LobbyMHelpButton.OnPressed += MHelpButtonPressed;
LobbyMHelpButton.Pressed = UIHelper?.IsOpen ?? false;
UpdateButtonStyling();
}
}
public void OnStateExited(LobbyState state)
{
if (LobbyMHelpButton != null)
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
}
private void MHelpButtonPressed(BaseButton.ButtonEventArgs obj)
{
Open();
}
private void OnAdminStatusUpdated()
{
if (UIHelper == null || !UIHelper.IsOpen)
return;
EnsureUIHelper();
}
private void OnTicketUpdated(object? sender, MentorHelpTicketUpdateMessage message)
{
// Новый тикет
var isNewTicket = !_ticketDataById.ContainsKey(message.Ticket.Id);
_ticketDataById[message.Ticket.Id] = message.Ticket;
EnsureUIHelper();
if (UIHelper == null)
return;
// Звук для менторов только при появлении нового тикета
if (isNewTicket && _mentorHelpSoundEnabled && _adminManager.HasFlag(AdminFlags.Mentor) && IsRelevantTicket(message.Ticket.Id))
{
_audio.PlayGlobal(MentorHelpSound, Filter.Local(), false);
if (!UIHelper.IsOpen)
_clyde.RequestWindowAttention();
}
if (!IsRelevantTicket(message.Ticket.Id))
_unreadTicketIds.Remove(message.Ticket.Id);
UpdateButtonStyling();
UIHelper.TicketUpdated(message.Ticket);
if (UIHelper.IsOpen && UIHelper.CurrentTicketId == message.Ticket.Id)
{
_mentorHelpSystem?.RequestTicketMessages(message.Ticket.Id);
}
}
private void OnTicketsListReceived(object? sender, MentorHelpTicketsListMessage message)
{
EnsureUIHelper();
foreach (var ticket in message.Tickets)
{
_ticketDataById[ticket.Id] = ticket;
if (!IsRelevantTicket(ticket.Id))
_unreadTicketIds.Remove(ticket.Id);
}
UpdateButtonStyling();
if (UIHelper == null)
return;
UIHelper.TicketsListReceived(message.Tickets);
}
private void OnTicketMessagesReceived(object? sender, MentorHelpTicketMessagesMessage message)
{
EnsureUIHelper();
UpdateUnreadTickets(message.TicketId, message.Messages);
// Звук только на новые входящие сообщения
TryPlaySoundForNewMessage(message.TicketId, message.Messages);
var autoOpen = _config.GetCVar(SunriseCCVars.MentorHelpAutoOpenOnNewMessage);
var shouldAutoOpen = autoOpen && ShouldAutoOpenTicket(message.TicketId, message.Messages);
// Автооткрытие работает только когда окно закрыто, ибо раздражает постоянными переключениями
if (shouldAutoOpen && UIHelper != null && !UIHelper.IsOpen)
UIHelper.OpenTicket(message.TicketId);
if (UIHelper == null)
return;
UIHelper.TicketMessagesReceived(message.TicketId, message.Messages);
}
/// <summary>
/// Проигрывает звук только на новое входящее сообщение по тикету
/// </summary>
private void TryPlaySoundForNewMessage(int ticketId, List<MentorHelpMessageData> messages)
{
if (!_mentorHelpSoundEnabled || messages.Count == 0)
return;
var lastMessage = messages[^1];
if (!_lastMessageIdByTicket.TryGetValue(ticketId, out var previousMessageId))
{
_lastMessageIdByTicket[ticketId] = lastMessage.Id;
return;
}
// Не новое сообщение
if (previousMessageId == lastMessage.Id)
return;
_lastMessageIdByTicket[ticketId] = lastMessage.Id;
var localUser = _playerManager.LocalUser;
// Без звука свое сообщение
if (localUser == null || lastMessage.SenderUserId == localUser.Value)
return;
// Тикет не релевантентен
if (!IsRelevantTicket(ticketId))
return;
var isViewingTicket = UIHelper != null && UIHelper.IsOpen && UIHelper.CurrentTicketId == ticketId;
var isWindowClosed = UIHelper != null && !UIHelper.IsOpen;
// Окно открыто, но мы не в этом тикете
if (!isViewingTicket && !isWindowClosed)
return;
// Новое входящее сообщение
_audio.PlayGlobal(MentorHelpSound, Filter.Local(), false);
if (isWindowClosed)
_clyde.RequestWindowAttention();
}
private void OnPlayerTypingUpdated(object? sender, MentorHelpPlayerTypingUpdated message)
{
UIHelper?.PlayerTypingUpdated(message.TicketId, message.PlayerName, message.Typing);
}
public void EnsureUIHelper()
{
var hasMentorPerms = _adminManager.HasFlag(AdminFlags.Mentor);
if (UIHelper != null && UIHelper.HasMentorPermissions == hasMentorPerms)
return;
UIHelper?.Dispose();
var localUser = _playerManager.LocalUser;
if (localUser == null)
return;
var ownerUserId = localUser.Value;
UIHelper = hasMentorPerms
? new MentorMentorHelpUIHandler(ownerUserId, _mentorHelpSystem)
: new PlayerMentorHelpUIHandler(ownerUserId, _mentorHelpSystem);
UIHelper.OnClose += () => { SetMentorHelpPressed(false); };
}
/// <summary>
/// Open the mentor help window
/// </summary>
public void Open()
{
if (_playerManager.LocalUser == null)
return;
EnsureUIHelper();
if (UIHelper == null)
return;
UIHelper.OpenWindow();
SetMentorHelpPressed(true);
}
/// <summary>
/// Close the mentor help window
/// </summary>
public void Close()
{
UIHelper?.CloseWindow();
SetMentorHelpPressed(false);
}
private void SetMentorHelpPressed(bool pressed)
{
UIManager.ClickSound();
UnreadTicketsRead();
if (GameMHelpButton != null)
{
GameMHelpButton.Pressed = pressed;
}
if (LobbyMHelpButton != null)
{
LobbyMHelpButton.Pressed = pressed;
}
}
private void UnreadTicketsRead()
{
_unreadTicketIds.Clear();
UpdateButtonStyling();
}
private void UpdateUnreadTickets(int ticketId, List<MentorHelpMessageData> messages)
{
var localUser = _playerManager.LocalUser;
if (localUser == null)
return;
if (!IsRelevantTicket(ticketId))
{
_unreadTicketIds.Remove(ticketId);
UpdateButtonStyling();
return;
}
if (messages.Count == 0)
{
_unreadTicketIds.Remove(ticketId);
UpdateButtonStyling();
return;
}
var lastMessage = messages[^1];
var isIncoming = lastMessage.SenderUserId != localUser.Value;
var isViewingTicket = UIHelper != null && UIHelper.IsOpen && UIHelper.CurrentTicketId == ticketId;
var unread = isIncoming && !isViewingTicket;
if (unread)
_unreadTicketIds.Add(ticketId);
else
_unreadTicketIds.Remove(ticketId);
UpdateButtonStyling();
}
private bool IsRelevantTicket(int ticketId)
{
var localUser = _playerManager.LocalUser;
if (localUser == null || !_ticketDataById.TryGetValue(ticketId, out var ticket))
return false;
var localId = localUser.Value;
var isMentor = _adminManager.HasFlag(AdminFlags.Mentor);
if (isMentor)
return ticket.AssignedToUserId == null || ticket.AssignedToUserId == localId;
return ticket.PlayerId == localId;
}
private bool ShouldAutoOpenTicket(int ticketId, List<MentorHelpMessageData> messages)
{
var localUser = _playerManager.LocalUser;
if (localUser == null || !_ticketDataById.TryGetValue(ticketId, out var ticket))
return false;
var localId = localUser.Value;
var isOwner = ticket.PlayerId == localId;
var isAssignedToMe = ticket.AssignedToUserId == localId;
// Авто-открытие только у автора тикета и назначенного ментора
if (!isOwner && !isAssignedToMe)
return false;
if (messages.Count == 0)
return false;
var lastMessage = messages[^1];
return lastMessage.SenderUserId != localId;
}
private void UpdateButtonStyling()
{
var unreadCount = _unreadTicketIds.Count;
var hasUnread = unreadCount > 0;
var displayCount = unreadCount;
if (hasUnread)
{
GameMHelpButton?.StyleClasses.Add("StyleClassRedTopButton");
LobbyMHelpButton?.StyleClasses.Add("ButtonColorRed");
}
else
{
GameMHelpButton?.StyleClasses.Remove("StyleClassRedTopButton");
LobbyMHelpButton?.StyleClasses.Remove("ButtonColorRed");
}
if (LobbyMHelpButton != null)
{
var baseText = _loc.GetString("ui-lobby-mhelp-button");
LobbyMHelpButton.Text = displayCount > 0 ? $"{baseText} ({displayCount})" : baseText;
}
if (GameMHelpButton != null)
{
var baseTooltip = _loc.GetString("ui-options-function-open-mentor-help");
GameMHelpButton.ToolTip = displayCount > 0 ? $"{baseTooltip} ({displayCount})" : baseTooltip;
}
}
}
/// <summary>
/// Interface for mentor help UI handlers
/// </summary>
public interface IMentorHelpUIHandler : IDisposable
{
bool IsOpen { get; }
bool HasMentorPermissions { get; }
int? CurrentTicketId { get; }
event Action? OnClose;
void OpenWindow();
void CloseWindow();
void OpenTicket(int ticketId);
void TicketUpdated(MentorHelpTicketData ticket);
void TicketsListReceived(List<MentorHelpTicketData> tickets);
void TicketMessagesReceived(int ticketId, List<MentorHelpMessageData> messages);
void PlayerTypingUpdated(int ticketId, string playerName, bool typing);
}
public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
{
public bool IsOpen { get; private set; }
public bool HasMentorPermissions => false;
public event Action? OnClose;
public int? CurrentTicketId { get; private set; }
private readonly NetUserId _ownerUserId;
private readonly MentorHelpSystem? _mentorHelpSystem;
private MentorHelpWindow? _window;
public PlayerMentorHelpUIHandler(NetUserId ownerUserId, MentorHelpSystem? mentorHelpSystem)
{
_ownerUserId = ownerUserId;
_mentorHelpSystem = mentorHelpSystem;
}
public void OpenWindow()
{
if (_window != null)
{
_window.MoveToFront();
IsOpen = true;
return;
}
_window = new MentorHelpWindow();
_window.MentorHelp.Initialize(_mentorHelpSystem, _ownerUserId, false);
_window.OnClose += () =>
{
IsOpen = false;
CurrentTicketId = null;
OnClose?.Invoke();
_window = null;
};
_window.OpenCentered();
IsOpen = true;
_mentorHelpSystem?.RequestTickets(onlyMine: true);
}
public void OpenTicket(int ticketId)
{
CurrentTicketId = ticketId;
// Ensure window is open
OpenWindow();
// Ask control to focus the ticket if possible
_window?.MentorHelp.TryOpenTicket(ticketId);
// Also request messages from server in case they're not loaded yet
_mentorHelpSystem?.RequestTicketMessages(ticketId);
}
public void CloseWindow()
{
CurrentTicketId = null;
if (_window == null)
{
IsOpen = false;
return;
}
_window.Close();
}
public void TicketUpdated(MentorHelpTicketData ticket)
{
_window?.MentorHelp.UpdateTicket(ticket);
}
public void TicketsListReceived(List<MentorHelpTicketData> tickets)
{
_window?.MentorHelp.UpdateTicketsList(tickets);
}
public void TicketMessagesReceived(int ticketId, List<MentorHelpMessageData> messages)
{
_window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
}
// Обновление статуса печати, все еще печатает ли игрок
public void PlayerTypingUpdated(int ticketId, string playerName, bool typing)
{
_window?.MentorHelp.UpdatePlayerTyping(ticketId, playerName, typing);
}
public void Dispose()
{
CloseWindow();
}
}
/// <summary>
/// UI handler for mentors/admins (can see and manage all tickets)
/// </summary>
public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
{
public int? CurrentTicketId { get; private set; }
public bool IsOpen { get; private set; }
public bool HasMentorPermissions => true;
public event Action? OnClose;
private readonly NetUserId _ownerUserId;
private readonly MentorHelpSystem? _mentorHelpSystem;
private MentorHelpWindow? _window;
public MentorMentorHelpUIHandler(NetUserId ownerUserId, MentorHelpSystem? mentorHelpSystem)
{
_ownerUserId = ownerUserId;
_mentorHelpSystem = mentorHelpSystem;
}
public void OpenWindow()
{
if (_window != null)
{
_window.MoveToFront();
IsOpen = true;
return;
}
_window = new MentorHelpWindow();
_window.MentorHelp.Initialize(_mentorHelpSystem, _ownerUserId, true);
_window.OnClose += () =>
{
IsOpen = false;
CurrentTicketId = null;
OnClose?.Invoke();
_window = null;
};
_window.OpenCentered();
IsOpen = true;
_mentorHelpSystem?.RequestTickets(onlyMine: false);
}
public void OpenTicket(int ticketId)
{
CurrentTicketId = ticketId;
OpenWindow();
_window?.MentorHelp.TryOpenTicket(ticketId);
_mentorHelpSystem?.RequestTicketMessages(ticketId);
}
public void CloseWindow()
{
CurrentTicketId = null;
if (_window == null)
{
IsOpen = false;
return;
}
_window.Close();
}
public void TicketUpdated(MentorHelpTicketData ticket)
{
_window?.MentorHelp.UpdateTicket(ticket);
}
public void TicketsListReceived(List<MentorHelpTicketData> tickets)
{
_window?.MentorHelp.UpdateTicketsList(tickets);
}
public void TicketMessagesReceived(int ticketId, List<MentorHelpMessageData> messages)
{
_window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
}
// Обновление статуса печати, все еще печатает ли ментор
public void PlayerTypingUpdated(int ticketId, string playerName, bool typing)
{
_window?.MentorHelp.UpdatePlayerTyping(ticketId, playerName, typing);
}
public void Dispose()
{
CloseWindow();
}
}