Implement Mentor Help System (MHelp) - Complete Ticket-based Help for Game Questions (#2915)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com> Co-authored-by: Vigers Ray <vigersray@gmail.com>
This commit is contained in:
parent
ddbdadb72b
commit
aba00f7e8f
44 changed files with 8082 additions and 10 deletions
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True">
|
||||
<RichTextLabel Name="AHelpDescLabel" Text="{Loc 'help-choice-ahelp-desc-label'}" Visible="False" Access="Public"/> <!-- Sunrise-Edit -->
|
||||
<OutputPanel Name="TextOutput" VerticalExpand="true" Access="Public" /> <!-- Sunrise-Edit -->
|
||||
<RichTextLabel Name="TypingIndicator" Access="Public" />
|
||||
<HistoryLineEdit Name="SenderLineEdit" />
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ namespace Content.Client.Input
|
|||
common.AddFunction(ContentKeyFunctions.CycleChatChannelBackward);
|
||||
common.AddFunction(ContentKeyFunctions.EscapeContext);
|
||||
common.AddFunction(ContentKeyFunctions.ExamineEntity);
|
||||
common.AddFunction(ContentKeyFunctions.OpenAHelp);
|
||||
// Sunrise-Edit
|
||||
//common.AddFunction(ContentKeyFunctions.OpenAHelp);
|
||||
common.AddFunction(ContentKeyFunctions.TakeScreenshot);
|
||||
common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI);
|
||||
common.AddFunction(ContentKeyFunctions.ToggleFullscreen);
|
||||
|
|
@ -49,6 +50,11 @@ namespace Content.Client.Input
|
|||
// Not in engine so that the RCD can rotate objects
|
||||
common.AddFunction(EngineKeyFunctions.EditorRotateObject);
|
||||
|
||||
// Sunrise-Start
|
||||
common.AddFunction(ContentKeyFunctions.OpenMentorHelp);
|
||||
common.AddFunction(ContentKeyFunctions.OpenHelpChoice);
|
||||
// Sunrise-End
|
||||
|
||||
var human = contexts.GetContext("human");
|
||||
human.AddFunction(EngineKeyFunctions.MoveUp);
|
||||
human.AddFunction(EngineKeyFunctions.MoveDown);
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@
|
|||
HorizontalAlignment="Right" SizeFlagsStretchRatio="1">
|
||||
<Button Name="AHelpButton" Access="Public" Text="{Loc 'ui-lobby-ahelp-button'}"
|
||||
StyleClasses="ButtonBig"/>
|
||||
<Button Name="MHelpButton" Access="Public" Text="{Loc 'ui-lobby-mhelp-button'}"
|
||||
StyleClasses="ButtonBig"/>
|
||||
<vote:VoteCallMenuButton Name="CallVoteButton" StyleClasses="ButtonBig" />
|
||||
<Button Name="ReplaysButton" Access="Public" Text="{Loc 'ui-lobby-replays-button'}"
|
||||
StyleClasses="ButtonBig"/>
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ namespace Content.Client.Options.UI.Tabs
|
|||
AddButton(ContentKeyFunctions.Reloading);
|
||||
AddButton(ContentKeyFunctions.Interact);
|
||||
AddButton(ContentKeyFunctions.LookUp);
|
||||
AddButton(ContentKeyFunctions.OpenMentorHelp);
|
||||
AddButton(ContentKeyFunctions.OpenHelpChoice);
|
||||
AddCheckBox("ui-options-function-hold-look-up", _cfg.GetCVar(SunriseCCVars.HoldLookUp), HandleHoldLookUp);
|
||||
// Sunrise-End
|
||||
|
||||
|
|
|
|||
|
|
@ -637,6 +637,7 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
|||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true, false));
|
||||
_chatPanel.AHelpDescLabel.Visible = true; // Sunrise-Edit
|
||||
_chatPanel.InputTextChanged += text => InputTextChanged?.Invoke(_ownerId, text);
|
||||
_chatPanel.RelayedToDiscordLabel.Visible = relayActive;
|
||||
_window = new DefaultWindow()
|
||||
|
|
@ -644,7 +645,7 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
|||
TitleClass="windowTitleAlert",
|
||||
HeaderClass="windowHeaderAlert",
|
||||
Title=Loc.GetString("bwoink-user-title"),
|
||||
MinSize = new Vector2(500, 300),
|
||||
MinSize = new Vector2(900, 500), // Sunrise-Edit
|
||||
};
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
|
|
|
|||
|
|
@ -110,6 +110,16 @@
|
|||
ToolTip="{Loc 'ui-options-function-open-a-help'}"
|
||||
MinSize="42 64"
|
||||
HorizontalExpand="True"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="MHelpButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/mentor.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenMentorHelp}"
|
||||
ToolTip="{Loc 'ui-options-function-open-mentor-help'}"
|
||||
MinSize="42 64"
|
||||
HorizontalExpand="True"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
|
||||
/>
|
||||
</widgets:GameTopMenuBar>
|
||||
|
|
|
|||
64
Content.Client/_Sunrise/HelpChoice/HelpChoiceUIController.cs
Normal file
64
Content.Client/_Sunrise/HelpChoice/HelpChoiceUIController.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using Content.Client._Sunrise.MentorHelp;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Input.Binding;
|
||||
|
||||
namespace Content.Client._Sunrise.HelpChoice;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class HelpChoiceUIController: UIController, IOnSystemChanged<MentorHelpSystem>
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
|
||||
private HelpChoiceWindow? _dialog;
|
||||
|
||||
public void OnSystemLoaded(MentorHelpSystem system)
|
||||
{
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenHelpChoice,
|
||||
InputCmdHandler.FromDelegate(_ => ShowHelpChoiceDialog()));
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(MentorHelpSystem system)
|
||||
{
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenHelpChoice, null);
|
||||
}
|
||||
|
||||
private void ShowHelpChoiceDialog()
|
||||
{
|
||||
if (_dialog != null && _dialog.IsOpen)
|
||||
{
|
||||
_dialog.Close();
|
||||
_dialog = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dialog == null)
|
||||
{
|
||||
_dialog = new HelpChoiceWindow();
|
||||
|
||||
var local = _dialog;
|
||||
|
||||
local.AHelpButton.OnPressed += _ =>
|
||||
{
|
||||
local.Close();
|
||||
_uiManager.GetUIController<AHelpUIController>().Open();
|
||||
};
|
||||
|
||||
local.MentorHelpButton.OnPressed += _ =>
|
||||
{
|
||||
local.Close();
|
||||
_uiManager.GetUIController<MentorHelpUIController>().Open();
|
||||
};
|
||||
|
||||
_dialog.OnClose += () => _dialog = null;
|
||||
}
|
||||
|
||||
_dialog.OpenCentered();
|
||||
}
|
||||
}
|
||||
14
Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml
Normal file
14
Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
SetSize="450 200"
|
||||
Title="{Loc 'help-choice-title'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<RichTextLabel Text="{Loc 'help-choice-title-label'}" HorizontalAlignment="Center"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" SeparationOverride="20">
|
||||
<Button Name="AHelpButton" Text="{Loc 'help-choice-ahelp-button'}" SetSize="160 45" HorizontalAlignment="Center" Access="Public"/>
|
||||
<Button Name="MentorHelpButton" Text="{Loc 'help-choice-mhelp-button'}" SetSize="160 45" HorizontalAlignment="Center" Access="Public"/>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="AHelpDescLabel" Text="{Loc 'help-choice-ahelp-desc-label'}"/>
|
||||
<RichTextLabel Name="MHelpDescLabel" Text="{Loc 'help-choice-mhelp-desc-label'}"/>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
16
Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml.cs
Normal file
16
Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.HelpChoice
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HelpChoiceWindow : DefaultWindow
|
||||
{
|
||||
public HelpChoiceWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml
Normal file
102
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<Control
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||
<PanelContainer StyleClasses="BackgroundDark">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" SetHeight="40" HorizontalExpand="True" Margin="5">
|
||||
<Button Name="NewTicketButton" Access="Public" Text="{Loc 'mentor-help-new-ticket'}"
|
||||
StyleClasses="ButtonBig" />
|
||||
<Control SetWidth="10" />
|
||||
<Button Name="StatisticsButton" Access="Public" Text="{Loc 'mentor-help-statistics'}"
|
||||
StyleClasses="ButtonBig" Visible="False" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Name="BackToListButton" Access="Public" Text="{Loc 'mentor-help-back-to-list'}"
|
||||
StyleClasses="ButtonBig" Visible="False" />
|
||||
</BoxContainer>
|
||||
|
||||
<Control Name="DefaultState" VerticalExpand="True" HorizontalExpand="True">
|
||||
<TabContainer Name="TicketsTabContainer" Access="Public" VerticalExpand="True" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-id'}" SizeFlagsStretchRatio="1" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-player'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-status'}" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-assigned'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-subject'}" SizeFlagsStretchRatio="5" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True" MinHeight="150">
|
||||
<BoxContainer Name="OpenTicketsList" Access="Public" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-id'}" SizeFlagsStretchRatio="1" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-player'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-status'}" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-assigned'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-subject'}" SizeFlagsStretchRatio="5" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True" MinHeight="150">
|
||||
<BoxContainer Name="ClosedTicketsList" Access="Public" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
</Control>
|
||||
|
||||
<Control Name="TicketViewState" Visible="False" VerticalExpand="True" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer StyleClasses="PanelColorMedium" HorizontalExpand="True" Margin="5">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'mentor-help-label-id'}" Margin="0 0 5 0" />
|
||||
<Label Name="TicketIdLabel" Access="Public" StyleClasses="LabelHeading" Margin="6 0 20 0" />
|
||||
<Label Text="{Loc 'mentor-help-label-subject'}" Margin="0 0 5 0" />
|
||||
<Label Name="TicketSubjectLabel" Access="Public" StyleClasses="LabelHeading" Margin="8 0 0 0" />
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="3" HorizontalAlignment="Stretch" HorizontalExpand="True" Margin="0 6 0 0">
|
||||
<Label Name="TicketStatus" Access="Public" StyleClasses="PdaContentFooterText" HorizontalExpand="True" HorizontalAlignment="Left"/>
|
||||
<Label Name="TicketAssigned" Access="Public" StyleClasses="PdaContentFooterText" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<Label Name="TicketCreated" Access="Public" StyleClasses="PdaContentFooterText" HorizontalExpand="True" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<ScrollContainer Name="MessagesScroll" Access="Public" VerticalExpand="True" HorizontalExpand="True" Margin="5">
|
||||
<BoxContainer Name="MessagesContainer" Access="Public" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
|
||||
<BoxContainer Name="ReplyPanel" Access="Public" Orientation="Vertical" HorizontalExpand="True" Margin="5">
|
||||
<LineEdit Name="ReplyInput" Access="Public" PlaceHolder="{Loc 'mentor-help-reply-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
|
||||
<Button Name="ClaimButton" Access="Public" Text="{Loc 'mentor-help-claim'}"
|
||||
StyleClasses="ButtonBig" />
|
||||
<Button Name="UnassignButton" Access="Public" Text="{Loc 'mentor-help-unassign'}"
|
||||
StyleClasses="ButtonBig" />
|
||||
<Button Name="CloseTicketButton" Access="Public" Text="{Loc 'mentor-help-close-ticket'}"
|
||||
StyleClasses="ButtonBig" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
426
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
Normal file
426
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
using System.Linq;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Main control for mentor help interface
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MentorHelpControl : Control
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
private NetUserId _ownerUserId;
|
||||
private bool _hasMentorPermissions;
|
||||
private List<MentorHelpTicketData> _tickets = new();
|
||||
private MentorHelpTicketData? _selectedTicket;
|
||||
private Dictionary<int, List<MentorHelpMessageData>> _ticketMessages = new();
|
||||
|
||||
private readonly Dictionary<int, TicketEntryControl> _openTicketControls = new();
|
||||
private readonly Dictionary<int, TicketEntryControl> _closedTicketControls = new();
|
||||
|
||||
private enum ViewState
|
||||
{
|
||||
TicketsList,
|
||||
TicketView
|
||||
}
|
||||
|
||||
private MentorHelpNewTicketDialog? _newTicketDialog;
|
||||
private int? _pendingOpenTicketId;
|
||||
|
||||
public MentorHelpControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
// Wire up button events
|
||||
NewTicketButton.OnPressed += _ => OpenNewTicketDialog();
|
||||
StatisticsButton.OnPressed += _ => _ui.GetUIController<MentorHelpStatisticsUIController>().ToggleStatistics();
|
||||
BackToListButton.OnPressed += _ => SwitchState(ViewState.TicketsList);
|
||||
|
||||
// Wire up ticket action buttons
|
||||
ClaimButton.OnPressed += _ => ClaimTicket();
|
||||
UnassignButton.OnPressed += _ => UnassignTicket();
|
||||
CloseTicketButton.OnPressed += _ => CloseTicket();
|
||||
|
||||
// Handle enter key in reply input
|
||||
ReplyInput.OnTextEntered += _ => SendReply();
|
||||
|
||||
// Setup tab container like in AdminMenuWindow
|
||||
TicketsTabContainer.SetTabTitle(0, Loc.GetString("mentor-help-tab-open"));
|
||||
TicketsTabContainer.SetTabTitle(1, Loc.GetString("mentor-help-tab-closed"));
|
||||
|
||||
// Handle tab changes to load closed tickets when needed
|
||||
TicketsTabContainer.OnTabChanged += OnTabChanged;
|
||||
|
||||
// Set initial state
|
||||
SwitchState(ViewState.TicketsList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Попытаться открыть тикет с указанным id. Если тикет ещё не загружен, запросит список тикетов
|
||||
/// и откроет тикет когда список придёт.
|
||||
/// </summary>
|
||||
public void TryOpenTicket(int ticketId)
|
||||
{
|
||||
// If we already have the ticket loaded, select it immediately
|
||||
var ticket = _tickets.FirstOrDefault(t => t.Id == ticketId);
|
||||
if (ticket != null)
|
||||
{
|
||||
OnTicketSelected(ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, remember to open when the list arrives and request tickets from server
|
||||
_pendingOpenTicketId = ticketId;
|
||||
_mentorHelpSystem?.RequestTickets(onlyMine: !_hasMentorPermissions);
|
||||
}
|
||||
|
||||
public void Initialize(MentorHelpSystem? mentorHelpSystem, NetUserId ownerUserId, bool hasMentorPermissions)
|
||||
{
|
||||
_mentorHelpSystem = mentorHelpSystem;
|
||||
_ownerUserId = ownerUserId;
|
||||
_hasMentorPermissions = hasMentorPermissions;
|
||||
|
||||
// Show/hide buttons based on permissions
|
||||
StatisticsButton.Visible = _hasMentorPermissions;
|
||||
UpdateTicketActionButtons();
|
||||
}
|
||||
|
||||
private void OnTabChanged(int tabIndex)
|
||||
{
|
||||
// When switching to closed tickets tab, request all tickets including closed ones
|
||||
if (tabIndex == 1 && _hasMentorPermissions)
|
||||
{
|
||||
_mentorHelpSystem?.RequestTickets(onlyMine: false);
|
||||
}
|
||||
else if (tabIndex == 1 && !_hasMentorPermissions)
|
||||
{
|
||||
_mentorHelpSystem?.RequestTickets(onlyMine: true);
|
||||
}
|
||||
}
|
||||
|
||||
// State switching like in LobbyGui
|
||||
private void SwitchState(ViewState state)
|
||||
{
|
||||
DefaultState.Visible = false;
|
||||
TicketViewState.Visible = false;
|
||||
BackToListButton.Visible = false;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ViewState.TicketsList:
|
||||
DefaultState.Visible = true;
|
||||
_selectedTicket = null;
|
||||
break;
|
||||
case ViewState.TicketView:
|
||||
TicketViewState.Visible = true;
|
||||
BackToListButton.Visible = true;
|
||||
UpdateTicketHeader();
|
||||
UpdateTicketActionButtons();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTicketsList(List<MentorHelpTicketData> tickets)
|
||||
{
|
||||
_tickets = tickets;
|
||||
RefreshTicketsList();
|
||||
|
||||
// If we were asked to open a specific ticket once the list arrives, do it now
|
||||
if (_pendingOpenTicketId.HasValue)
|
||||
{
|
||||
var id = _pendingOpenTicketId.Value;
|
||||
_pendingOpenTicketId = null;
|
||||
var ticket = _tickets.FirstOrDefault(t => t.Id == id);
|
||||
if (ticket != null)
|
||||
{
|
||||
OnTicketSelected(ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh tickets like in ContributorsTop
|
||||
private void RefreshTicketsList()
|
||||
{
|
||||
var openTickets = _tickets.Where(t => t.Status != MentorHelpTicketStatus.Closed).ToList();
|
||||
var closedTickets = _tickets.Where(t => t.Status == MentorHelpTicketStatus.Closed).ToList();
|
||||
|
||||
// Filter for players to show only their tickets
|
||||
if (!_hasMentorPermissions)
|
||||
{
|
||||
openTickets = openTickets.Where(t => t.PlayerId == _ownerUserId).ToList();
|
||||
closedTickets = closedTickets.Where(t => t.PlayerId == _ownerUserId).ToList();
|
||||
}
|
||||
|
||||
// Update open tickets
|
||||
RefreshTicketTab(openTickets, _openTicketControls, OpenTicketsList);
|
||||
|
||||
// Update closed tickets
|
||||
RefreshTicketTab(closedTickets, _closedTicketControls, ClosedTicketsList);
|
||||
}
|
||||
|
||||
private void RefreshTicketTab(List<MentorHelpTicketData> tickets, Dictionary<int, TicketEntryControl> controls, BoxContainer container)
|
||||
{
|
||||
var sortedTickets = tickets.OrderByDescending(t => t.UpdatedAt).ToList();
|
||||
|
||||
// Remove tickets that no longer exist
|
||||
var toRemove = controls.Keys.Where(id => !sortedTickets.Any(t => t.Id == id)).ToList();
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
if (controls.TryGetValue(id, out var control))
|
||||
{
|
||||
container.RemoveChild(control);
|
||||
controls.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update existing tickets
|
||||
foreach (var ticket in sortedTickets)
|
||||
{
|
||||
if (controls.TryGetValue(ticket.Id, out var existingControl))
|
||||
{
|
||||
existingControl.UpdateData(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
var control = new TicketEntryControl();
|
||||
control.UpdateData(ticket);
|
||||
control.OnTicketSelected += OnTicketSelected;
|
||||
controls[ticket.Id] = control;
|
||||
container.AddChild(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTicketSelected(MentorHelpTicketData ticket)
|
||||
{
|
||||
_selectedTicket = ticket;
|
||||
SwitchState(ViewState.TicketView);
|
||||
|
||||
// Clear previous messages first
|
||||
MessagesContainer.RemoveAllChildren();
|
||||
|
||||
// Request messages for this ticket
|
||||
_mentorHelpSystem?.RequestTicketMessages(ticket.Id);
|
||||
}
|
||||
|
||||
public void UpdateTicketMessages(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
_ticketMessages[ticketId] = messages;
|
||||
|
||||
if (_selectedTicket?.Id == ticketId && TicketViewState.Visible)
|
||||
{
|
||||
DisplayTicketMessages(messages);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTicket(MentorHelpTicketData ticket)
|
||||
{
|
||||
// Update ticket in our list
|
||||
var index = _tickets.FindIndex(t => t.Id == ticket.Id);
|
||||
if (index >= 0)
|
||||
{
|
||||
_tickets[index] = ticket;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tickets.Add(ticket);
|
||||
}
|
||||
|
||||
// Update selected ticket if it's the one that changed
|
||||
if (_selectedTicket?.Id == ticket.Id)
|
||||
{
|
||||
_selectedTicket = ticket;
|
||||
if (TicketViewState.Visible)
|
||||
{
|
||||
UpdateTicketHeader();
|
||||
UpdateTicketActionButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// If admin closed the ticket while player was viewing it, return to list
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed &&
|
||||
_selectedTicket?.Id == ticket.Id &&
|
||||
!_hasMentorPermissions &&
|
||||
TicketViewState.Visible)
|
||||
{
|
||||
SwitchState(ViewState.TicketsList);
|
||||
}
|
||||
|
||||
// Refresh the list to show updated status
|
||||
RefreshTicketsList();
|
||||
}
|
||||
|
||||
private void UpdateTicketHeader()
|
||||
{
|
||||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
// Отдельные метки для ID и темы (чтобы занимали меньше места и были выровнены)
|
||||
TicketIdLabel.Text = $"#{_selectedTicket.Id}";
|
||||
TicketSubjectLabel.Text = _selectedTicket.Subject;
|
||||
|
||||
// Компактная строка: статус | назначен | создано
|
||||
TicketStatus.Text = Loc.GetString("mentor-help-status-label", ("status", GetStatusText(_selectedTicket.Status)));
|
||||
TicketAssigned.Text = Loc.GetString("mentor-help-assigned-label",
|
||||
("assigned", _selectedTicket.AssignedToName ?? Loc.GetString("mentor-help-unassigned")));
|
||||
TicketCreated.Text = Loc.GetString("mentor-help-created-label",
|
||||
("created", _selectedTicket.CreatedAt.ToString("dd.MM.yyyy HH:mm")));
|
||||
}
|
||||
|
||||
private void UpdateTicketActionButtons()
|
||||
{
|
||||
if (_selectedTicket == null)
|
||||
{
|
||||
ReplyPanel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var canReply = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||||
var isAssignedToMe = _selectedTicket.AssignedToUserId == _ownerUserId;
|
||||
var isOpen = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||||
|
||||
// Hide reply panel for closed tickets
|
||||
ReplyPanel.Visible = canReply;
|
||||
|
||||
if (_hasMentorPermissions)
|
||||
{
|
||||
ClaimButton.Visible = isOpen && !isAssignedToMe;
|
||||
UnassignButton.Visible = isOpen && isAssignedToMe;
|
||||
CloseTicketButton.Visible = isOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClaimButton.Visible = false;
|
||||
UnassignButton.Visible = false;
|
||||
CloseTicketButton.Visible = isOpen; // Players can close their own tickets
|
||||
}
|
||||
}
|
||||
|
||||
private string GetStatusText(MentorHelpTicketStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MentorHelpTicketStatus.Open => Loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => Loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => Loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => Loc.GetString("mentor-help-status-closed"),
|
||||
_ => Loc.GetString("mentor-help-status-unknown")
|
||||
};
|
||||
}
|
||||
|
||||
private void DisplayTicketMessages(List<MentorHelpMessageData> messages)
|
||||
{
|
||||
MessagesContainer.RemoveAllChildren();
|
||||
|
||||
DateTime? lastDate = null;
|
||||
|
||||
foreach (var message in messages.OrderBy(m => m.SentAt))
|
||||
{
|
||||
// Ensure we show a date header when the day changes
|
||||
var sentAt = message.SentAt;
|
||||
var sentDate = sentAt.Date;
|
||||
if (lastDate == null || lastDate.Value != sentDate)
|
||||
{
|
||||
lastDate = sentDate;
|
||||
var dateLabel = new RichTextLabel
|
||||
{
|
||||
Text = $"[center=\"{sentAt:dd.MM.yyyy}\"]",
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
MessagesContainer.AddChild(dateLabel);
|
||||
}
|
||||
|
||||
var messageBox = new PanelContainer
|
||||
{
|
||||
StyleClasses = { "PanelColorMedium" },
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 2)
|
||||
};
|
||||
|
||||
var vbox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
// Format: [HH:mm] Author: message
|
||||
var content = new RichTextLabel
|
||||
{
|
||||
Text = $"[bold]{sentAt:HH:mm}[/bold] {message.FormattedSender}: {message.Message}",
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
vbox.AddChild(content);
|
||||
messageBox.AddChild(vbox);
|
||||
MessagesContainer.AddChild(messageBox);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenNewTicketDialog()
|
||||
{
|
||||
if (_newTicketDialog != null)
|
||||
{
|
||||
_newTicketDialog.Close();
|
||||
_newTicketDialog = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_newTicketDialog = new MentorHelpNewTicketDialog();
|
||||
_newTicketDialog.OnTicketCreated += (subject, message) =>
|
||||
{
|
||||
_newTicketDialog?.Close();
|
||||
_mentorHelpSystem?.CreateTicket(subject, message);
|
||||
};
|
||||
_newTicketDialog.OnClose += () => _newTicketDialog = null;
|
||||
_newTicketDialog.OpenCentered();
|
||||
}
|
||||
|
||||
private void SendReply()
|
||||
{
|
||||
if (_selectedTicket == null || string.IsNullOrWhiteSpace(ReplyInput.Text))
|
||||
return;
|
||||
|
||||
_mentorHelpSystem?.ReplyToTicket(_selectedTicket.Id, ReplyInput.Text.Trim());
|
||||
ReplyInput.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void ClaimTicket()
|
||||
{
|
||||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
_mentorHelpSystem?.ClaimTicket(_selectedTicket.Id);
|
||||
}
|
||||
|
||||
private void UnassignTicket()
|
||||
{
|
||||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
_mentorHelpSystem?.UnassignTicket(_selectedTicket.Id);
|
||||
}
|
||||
|
||||
private void CloseTicket()
|
||||
{
|
||||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
_mentorHelpSystem?.CloseTicket(_selectedTicket.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="{Loc 'mentor-help-new-ticket-title'}"
|
||||
SetSize="450,400"
|
||||
Resizable="False">
|
||||
<BoxContainer Name="VBox" Orientation="Vertical" SeparationOverride="10" HorizontalExpand="True" VerticalExpand="True">
|
||||
<RichTextLabel Name="Instructions" SetHeight="60" Text="{Loc 'mentor-help-new-ticket-instructions'}" />
|
||||
<Label Name="SubjectLabel" Text="{Loc 'mentor-help-new-ticket-subject-label'}" />
|
||||
<LineEdit Name="SubjectInput" PlaceHolder="{Loc 'mentor-help-new-ticket-subject-placeholder'}" SetHeight="30" />
|
||||
<Label Name="MessageLabel" Text="{Loc 'mentor-help-new-ticket-message-label'}" />
|
||||
<TextEdit Name="MessageInput" VerticalExpand="True" />
|
||||
<Label Name="ErrorLabel" Modulate="#FF3333" Visible="False" />
|
||||
<BoxContainer Name="ButtonContainer" Orientation="Horizontal" SeparationOverride="10" SetHeight="30">
|
||||
<Button Name="CancelButton" Text="{Loc 'mentor-help-cancel'}" />
|
||||
<Button Name="CreateButton" Text="{Loc 'mentor-help-new-ticket-create-button'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog for creating a new mentor help ticket
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MentorHelpNewTicketDialog : DefaultWindow
|
||||
{
|
||||
public event Action<string, string>? OnTicketCreated;
|
||||
|
||||
public MentorHelpNewTicketDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
CancelButton.OnPressed += _ => Close();
|
||||
CreateButton.OnPressed += _ =>
|
||||
{
|
||||
var subject = SubjectInput.Text.Trim();
|
||||
var message = Rope.Collapse(MessageInput.TextRope).Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
{
|
||||
ErrorLabel.Text = Loc.GetString("mentor-help-new-ticket-error-subject");
|
||||
ErrorLabel.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
ErrorLabel.Text = Loc.GetString("mentor-help-new-ticket-error-message");
|
||||
ErrorLabel.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorLabel.Visible = false;
|
||||
OnTicketCreated?.Invoke(subject, message);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
SetSize="600 400"
|
||||
Title="{Loc 'mentor-help-statistics-title'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="10">
|
||||
<Label Name="HeaderLabel" Access="Public"
|
||||
Text="{Loc 'mentor-help-statistics-header'}"
|
||||
StyleClasses="LabelHeading"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Ментор" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Взятых тикетов" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Сообщений" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Name="StatisticsContainer" Access="Public" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MentorHelpStatisticsDialog : DefaultWindow
|
||||
{
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
|
||||
public MentorHelpStatisticsDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
}
|
||||
|
||||
public void Initialize(MentorHelpSystem mentorHelpSystem)
|
||||
{
|
||||
if (_mentorHelpSystem != null)
|
||||
return;
|
||||
|
||||
_mentorHelpSystem = mentorHelpSystem;
|
||||
_mentorHelpSystem.OnStatisticsReceived += OnStatisticsReceived;
|
||||
|
||||
_mentorHelpSystem.RequestStatistics();
|
||||
}
|
||||
|
||||
public void Uninitialize()
|
||||
{
|
||||
if (_mentorHelpSystem != null)
|
||||
{
|
||||
_mentorHelpSystem.OnStatisticsReceived -= OnStatisticsReceived;
|
||||
_mentorHelpSystem = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnStatisticsReceived(object? sender, MentorHelpStatisticsMessage message)
|
||||
{
|
||||
UpdateStatistics(message.Statistics);
|
||||
}
|
||||
|
||||
private void UpdateStatistics(List<MentorHelpStatisticsData> statistics)
|
||||
{
|
||||
StatisticsContainer.RemoveAllChildren();
|
||||
|
||||
if (statistics.Count == 0)
|
||||
{
|
||||
var noDataLabel = new Label
|
||||
{
|
||||
Text = "Нет данных о статистике менторов", // а не локализирую, бугагагага
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
StyleClasses = { "LabelSubText" }
|
||||
};
|
||||
StatisticsContainer.AddChild(noDataLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var stat in statistics)
|
||||
{
|
||||
var entry = new StatisticsEntryControl();
|
||||
entry.UpdateData(stat);
|
||||
StatisticsContainer.AddChild(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using Content.Client.Administration.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class MentorHelpStatisticsUIController : UIController, IOnSystemChanged<MentorHelpSystem>
|
||||
{
|
||||
private MentorHelpStatisticsDialog? _dialog;
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
|
||||
protected override string SawmillName => "c.s.go.es.mhelp.stats";
|
||||
|
||||
public void OnSystemLoaded(MentorHelpSystem system)
|
||||
{
|
||||
_mentorHelpSystem = system;
|
||||
|
||||
_dialog = new MentorHelpStatisticsDialog();
|
||||
_dialog.Initialize(system);
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(MentorHelpSystem system)
|
||||
{
|
||||
if (_dialog != null)
|
||||
{
|
||||
_dialog.Close();
|
||||
_dialog.Uninitialize();
|
||||
_dialog = null;
|
||||
}
|
||||
|
||||
_mentorHelpSystem = null;
|
||||
}
|
||||
|
||||
public void OpenStatistics()
|
||||
{
|
||||
if (_dialog == null)
|
||||
{
|
||||
if (_mentorHelpSystem == null)
|
||||
return;
|
||||
|
||||
_dialog = new MentorHelpStatisticsDialog();
|
||||
_dialog.Initialize(_mentorHelpSystem);
|
||||
}
|
||||
|
||||
_dialog.OpenCentered();
|
||||
}
|
||||
|
||||
public void ToggleStatistics()
|
||||
{
|
||||
if (_dialog?.IsOpen == true)
|
||||
{
|
||||
CloseStatistics();
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseStatistics()
|
||||
{
|
||||
_dialog?.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
154
Content.Client/_Sunrise/MentorHelp/MentorHelpSystem.cs
Normal file
154
Content.Client/_Sunrise/MentorHelp/MentorHelpSystem.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side mentor help system
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class MentorHelpSystem : SharedMentorHelpSystem
|
||||
{
|
||||
public event EventHandler<MentorHelpTicketUpdateMessage>? OnTicketUpdated;
|
||||
public event EventHandler<MentorHelpTicketsListMessage>? OnTicketsListReceived;
|
||||
public event EventHandler<MentorHelpTicketMessagesMessage>? OnTicketMessagesReceived;
|
||||
public event EventHandler<MentorHelpStatisticsMessage>? OnStatisticsReceived;
|
||||
public event EventHandler<MentorHelpOpenTicketMessage>? OnOpenTicketReceived;
|
||||
|
||||
protected override void OnCreateTicketMessage(MentorHelpCreateTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnClaimTicketMessage(MentorHelpClaimTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnReplyMessage(MentorHelpReplyMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnCloseTicketMessage(MentorHelpCloseTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnRequestTicketsMessage(MentorHelpRequestTicketsMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnUnassignTicketMessage(MentorHelpUnassignTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
protected override void OnRequestStatisticsMessage(MentorHelpRequestStatisticsMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
// Client doesn't handle this directly
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<MentorHelpTicketUpdateMessage>(OnTicketUpdate);
|
||||
SubscribeNetworkEvent<MentorHelpTicketsListMessage>(OnTicketsList);
|
||||
SubscribeNetworkEvent<MentorHelpTicketMessagesMessage>(OnTicketMessages);
|
||||
SubscribeNetworkEvent<MentorHelpStatisticsMessage>(OnStatistics);
|
||||
SubscribeNetworkEvent<MentorHelpOpenTicketMessage>(OnOpenTicket);
|
||||
}
|
||||
|
||||
private void OnOpenTicket(MentorHelpOpenTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnOpenTicketReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnTicketUpdate(MentorHelpTicketUpdateMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnTicketUpdated?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnTicketsList(MentorHelpTicketsListMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnTicketsListReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnTicketMessages(MentorHelpTicketMessagesMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnTicketMessagesReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new mentor help ticket
|
||||
/// </summary>
|
||||
private void OnStatistics(MentorHelpStatisticsMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnStatisticsReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
public void CreateTicket(string subject, string message)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpCreateTicketMessage(subject, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Claim a mentor help ticket
|
||||
/// </summary>
|
||||
public void ClaimTicket(int ticketId)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpClaimTicketMessage(ticketId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unassign a mentor help ticket
|
||||
/// </summary>
|
||||
public void UnassignTicket(int ticketId)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpUnassignTicketMessage(ticketId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reply to a mentor help ticket
|
||||
/// </summary>
|
||||
public void ReplyToTicket(int ticketId, string message, bool isStaffOnly = false)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpReplyMessage(ticketId, message, isStaffOnly));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close a mentor help ticket
|
||||
/// </summary>
|
||||
public void CloseTicket(int ticketId)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpCloseTicketMessage(ticketId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request tickets (either all for mentors/admins, or only own for players)
|
||||
/// </summary>
|
||||
public void RequestTickets(bool onlyMine = false)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpRequestTicketsMessage(onlyMine));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request messages for a specific ticket
|
||||
/// </summary>
|
||||
public void RequestTicketMessages(int ticketId)
|
||||
{
|
||||
// Send a request to the server to fetch messages for the given ticket
|
||||
RaiseNetworkEvent(new MentorHelpRequestTicketMessagesMessage(ticketId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request mentor help statistics
|
||||
/// </summary>
|
||||
public void RequestStatistics()
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpRequestStatisticsMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
446
Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs
Normal file
446
Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
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.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
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 IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[UISystemDependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
public IMentorHelpUIHandler? UIHelper;
|
||||
private bool _hasMentorPermissions;
|
||||
private bool _hasUnreadTickets;
|
||||
private bool _mentorHelpSoundEnabled;
|
||||
private string? _mentorHelpSound;
|
||||
|
||||
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.MentorHelpSound, v => _mentorHelpSound = v, true); // Reuse ahelp sound for now
|
||||
_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;
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenMentorHelp,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<MentorHelpUIController>();
|
||||
}
|
||||
|
||||
private void OnOpenTicketReceived(object? sender, MentorHelpOpenTicketMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
|
||||
// Open the window and instruct UI to open the specific ticket
|
||||
Open();
|
||||
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 = null;
|
||||
}
|
||||
|
||||
if (GameMHelpButton != null)
|
||||
GameMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
|
||||
if (LobbyMHelpButton != null)
|
||||
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
SubscribeToButtons();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
// Keep UI helper for potential return to game
|
||||
}
|
||||
|
||||
public void OnStateEntered(LobbyState state)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
SubscribeToButtons();
|
||||
}
|
||||
|
||||
public void OnStateExited(LobbyState state)
|
||||
{
|
||||
// Keep UI helper for potential return to lobby
|
||||
}
|
||||
|
||||
private void SubscribeToButtons()
|
||||
{
|
||||
if (GameMHelpButton != null)
|
||||
GameMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
|
||||
if (LobbyMHelpButton != null)
|
||||
LobbyMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
}
|
||||
|
||||
private void MHelpButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void OnAdminStatusUpdated()
|
||||
{
|
||||
_hasMentorPermissions = _adminManager.HasFlag(AdminFlags.Mentor);
|
||||
|
||||
if (UIHelper is not { IsOpen: true })
|
||||
return;
|
||||
|
||||
EnsureUIHelper();
|
||||
}
|
||||
|
||||
private void OnTicketUpdated(object? sender, MentorHelpTicketUpdateMessage message)
|
||||
{
|
||||
if (_mentorHelpSound != null && _mentorHelpSoundEnabled)
|
||||
{
|
||||
_audio.PlayGlobal(_mentorHelpSound, Filter.Local(), false);
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
EnsureUIHelper();
|
||||
|
||||
if (!UIHelper!.IsOpen)
|
||||
{
|
||||
UnreadTicketReceived();
|
||||
}
|
||||
|
||||
UIHelper!.TicketUpdated(message.Ticket);
|
||||
}
|
||||
|
||||
private void OnTicketsListReceived(object? sender, MentorHelpTicketsListMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper!.TicketsListReceived(message.Tickets);
|
||||
}
|
||||
|
||||
private void OnTicketMessagesReceived(object? sender, MentorHelpTicketMessagesMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper!.TicketMessagesReceived(message.TicketId, message.Messages);
|
||||
}
|
||||
|
||||
public void EnsureUIHelper()
|
||||
{
|
||||
var hasMentorPerms = _adminManager.HasFlag(AdminFlags.Mentor);
|
||||
|
||||
if (UIHelper != null && UIHelper.HasMentorPermissions == hasMentorPerms)
|
||||
return;
|
||||
|
||||
UIHelper?.Dispose();
|
||||
var ownerUserId = _playerManager.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()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper!.OpenWindow();
|
||||
SetMentorHelpPressed(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the mentor help window
|
||||
/// </summary>
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
|
||||
if (UIHelper!.IsOpen)
|
||||
{
|
||||
UIHelper.CloseWindow();
|
||||
SetMentorHelpPressed(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
UnreadTicketRead();
|
||||
|
||||
if (GameMHelpButton != null)
|
||||
{
|
||||
GameMHelpButton.Pressed = pressed;
|
||||
}
|
||||
|
||||
if (LobbyMHelpButton != null)
|
||||
{
|
||||
LobbyMHelpButton.Pressed = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnreadTicketReceived()
|
||||
{
|
||||
_hasUnreadTickets = true;
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
|
||||
private void UnreadTicketRead()
|
||||
{
|
||||
_hasUnreadTickets = false;
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
|
||||
private void UpdateButtonStyling()
|
||||
{
|
||||
if (_hasUnreadTickets)
|
||||
{
|
||||
GameMHelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
|
||||
LobbyMHelpButton?.StyleClasses.Add("ButtonColorRed");
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMHelpButton?.StyleClasses.Remove(MenuButton.StyleClassRedTopButton);
|
||||
LobbyMHelpButton?.StyleClasses.Remove("ButtonColorRed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for mentor help UI handlers
|
||||
/// </summary>
|
||||
public interface IMentorHelpUIHandler : IDisposable
|
||||
{
|
||||
bool IsOpen { get; }
|
||||
bool HasMentorPermissions { 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);
|
||||
}
|
||||
|
||||
public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
||||
{
|
||||
public bool IsOpen { get; private set; }
|
||||
public bool HasMentorPermissions => false;
|
||||
public event Action? OnClose;
|
||||
|
||||
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;
|
||||
OnClose?.Invoke();
|
||||
_window = null;
|
||||
};
|
||||
|
||||
_window.OpenCentered();
|
||||
IsOpen = true;
|
||||
|
||||
_mentorHelpSystem?.RequestTickets(onlyMine: true);
|
||||
}
|
||||
|
||||
public void OpenTicket(int 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()
|
||||
{
|
||||
_window?.Close();
|
||||
IsOpen = false;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
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 Dispose()
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI handler for mentors/admins (can see and manage all tickets)
|
||||
/// </summary>
|
||||
public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
||||
{
|
||||
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;
|
||||
OnClose?.Invoke();
|
||||
_window = null;
|
||||
};
|
||||
|
||||
_window.OpenCentered();
|
||||
IsOpen = true;
|
||||
|
||||
_mentorHelpSystem?.RequestTickets(onlyMine: false);
|
||||
}
|
||||
|
||||
public void OpenTicket(int ticketId)
|
||||
{
|
||||
OpenWindow();
|
||||
_window?.MentorHelp.TryOpenTicket(ticketId);
|
||||
_mentorHelpSystem?.RequestTicketMessages(ticketId);
|
||||
}
|
||||
|
||||
public void CloseWindow()
|
||||
{
|
||||
_window?.Close();
|
||||
IsOpen = false;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
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 Dispose()
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
}
|
||||
8
Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml
Normal file
8
Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:mentorHelp="clr-namespace:Content.Client._Sunrise.MentorHelp"
|
||||
SetSize="900 700"
|
||||
HeaderClass="windowHeaderHelp"
|
||||
TitleClass="windowTitleHelp"
|
||||
Title="{Loc 'mentor-help-window-title'}" >
|
||||
<mentorHelp:MentorHelpControl Name="MentorHelp" Access="Public"/>
|
||||
</DefaultWindow>
|
||||
18
Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml.cs
Normal file
18
Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Mentor help window wrapper
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MentorHelpWindow : DefaultWindow
|
||||
{
|
||||
public MentorHelpWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Name="BackgroundColorPanel"
|
||||
MouseFilter="Stop">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<customControls:HSeparator/>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 2">
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="MentorNameLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="TicketsClaimedLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="MessagesCountLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StatisticsEntryControl : PanelContainer
|
||||
{
|
||||
private static readonly Color NormalColor = Color.FromHex("#202023");
|
||||
private static readonly Color HoverColor = Color.FromHex("#2F2F33");
|
||||
|
||||
public StatisticsEntryControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
TooltipDelay = 0.5f;
|
||||
|
||||
BackgroundColorPanel.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = NormalColor
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnMouseEntered += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = HoverColor;
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnMouseExited += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = NormalColor;
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateData(MentorHelpStatisticsData data)
|
||||
{
|
||||
MentorNameLabel.Text = data.MentorName;
|
||||
TicketsClaimedLabel.Text = data.TicketsClaimed.ToString();
|
||||
MessagesCountLabel.Text = data.MessagesCount.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml
Normal file
46
Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||
<PanelContainer Name="BackgroundColorPanel"
|
||||
MouseFilter="Stop">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<customControls:HSeparator/>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 2">
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="IdLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="PlayerLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="StatusLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="AssignedLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="SubjectLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TicketEntryControl : Control
|
||||
{
|
||||
private static readonly Color NormalColor = Color.FromHex("#202023");
|
||||
private static readonly Color HoverColor = Color.FromHex("#2F2F33");
|
||||
|
||||
private MentorHelpTicketData? _ticketData;
|
||||
|
||||
public event Action<MentorHelpTicketData>? OnTicketSelected;
|
||||
|
||||
public TicketEntryControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
TooltipDelay = 0.5f;
|
||||
|
||||
BackgroundColorPanel.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = NormalColor
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnMouseEntered += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = HoverColor;
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnMouseExited += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = NormalColor;
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
if (_ticketData != null)
|
||||
OnTicketSelected?.Invoke(_ticketData);
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateData(MentorHelpTicketData ticketData)
|
||||
{
|
||||
_ticketData = ticketData;
|
||||
|
||||
IdLabel.Text = $"#{ticketData.Id}";
|
||||
PlayerLabel.Text = ticketData.PlayerName;
|
||||
|
||||
var statusText = GetStatusText(ticketData.Status);
|
||||
StatusLabel.Text = statusText;
|
||||
|
||||
AssignedLabel.Text = ticketData.AssignedToName ?? Loc.GetString("mentor-help-unassigned");
|
||||
|
||||
var subjectText = ticketData.Subject;
|
||||
if (ticketData.HasUnreadMessages)
|
||||
subjectText = "* " + subjectText;
|
||||
SubjectLabel.Text = subjectText;
|
||||
}
|
||||
|
||||
private string GetStatusText(MentorHelpTicketStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MentorHelpTicketStatus.Open => Loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => Loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => Loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => Loc.GetString("mentor-help-status-closed"),
|
||||
_ => Loc.GetString("mentor-help-status-unknown")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
2326
Content.Server.Database/Migrations/Postgres/20250825044815_MentorHelp.Designer.cs
generated
Normal file
2326
Content.Server.Database/Migrations/Postgres/20250825044815_MentorHelp.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MentorHelp : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "mentor_help_tickets",
|
||||
columns: table => new
|
||||
{
|
||||
mentor_help_tickets_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
player_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
assigned_to_user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
subject = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
closed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
closed_by_user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
round_id = table.Column<int>(type: "integer", nullable: true),
|
||||
server_id = table.Column<int>(type: "integer", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_mentor_help_tickets", x => x.mentor_help_tickets_id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "mentor_help_messages",
|
||||
columns: table => new
|
||||
{
|
||||
mentor_help_messages_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ticket_id = table.Column<int>(type: "integer", nullable: false),
|
||||
sender_user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
message = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
sent_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
is_staff_only = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_mentor_help_messages", x => x.mentor_help_messages_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_mentor_help_messages_mentor_help_tickets_ticket_id",
|
||||
column: x => x.ticket_id,
|
||||
principalTable: "mentor_help_tickets",
|
||||
principalColumn: "mentor_help_tickets_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_messages_sent_at",
|
||||
table: "mentor_help_messages",
|
||||
column: "sent_at");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_messages_ticket_id",
|
||||
table: "mentor_help_messages",
|
||||
column: "ticket_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_assigned_to_user_id",
|
||||
table: "mentor_help_tickets",
|
||||
column: "assigned_to_user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_player_id",
|
||||
table: "mentor_help_tickets",
|
||||
column: "player_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_status",
|
||||
table: "mentor_help_tickets",
|
||||
column: "status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "mentor_help_messages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "mentor_help_tickets");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -747,6 +747,115 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
b.ToTable("job", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpMessage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("mentor_help_messages_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsStaffOnly")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_staff_only");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Guid>("SenderUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("sender_user_id");
|
||||
|
||||
b.Property<DateTimeOffset>("SentAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("sent_at");
|
||||
|
||||
b.Property<int>("TicketId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ticket_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_mentor_help_messages");
|
||||
|
||||
b.HasIndex("SentAt")
|
||||
.HasDatabaseName("IX_mentor_help_messages_sent_at");
|
||||
|
||||
b.HasIndex("TicketId")
|
||||
.HasDatabaseName("IX_mentor_help_messages_ticket_id");
|
||||
|
||||
b.ToTable("mentor_help_messages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpTicket", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("mentor_help_tickets_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<Guid?>("AssignedToUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("assigned_to_user_id");
|
||||
|
||||
b.Property<DateTimeOffset?>("ClosedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("closed_at");
|
||||
|
||||
b.Property<Guid?>("ClosedByUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("closed_by_user_id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("PlayerId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("player_id");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int?>("ServerId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("server_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("subject");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_mentor_help_tickets");
|
||||
|
||||
b.HasIndex("AssignedToUserId")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_assigned_to_user_id");
|
||||
|
||||
b.HasIndex("PlayerId")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_player_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_status");
|
||||
|
||||
b.ToTable("mentor_help_tickets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1796,6 +1905,18 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpMessage", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.MentorHelpTicket", "Ticket")
|
||||
.WithMany()
|
||||
.HasForeignKey("TicketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_mentor_help_messages_mentor_help_tickets_ticket_id");
|
||||
|
||||
b.Navigation("Ticket");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Player", b =>
|
||||
{
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
|
||||
|
|
|
|||
2243
Content.Server.Database/Migrations/Sqlite/20250825044803_MentorHelp.Designer.cs
generated
Normal file
2243
Content.Server.Database/Migrations/Sqlite/20250825044803_MentorHelp.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MentorHelp : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "mentor_help_tickets",
|
||||
columns: table => new
|
||||
{
|
||||
mentor_help_tickets_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
assigned_to_user_id = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
subject = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false),
|
||||
status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
closed_at = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
closed_by_user_id = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
round_id = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
server_id = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_mentor_help_tickets", x => x.mentor_help_tickets_id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "mentor_help_messages",
|
||||
columns: table => new
|
||||
{
|
||||
mentor_help_messages_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ticket_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
sender_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
message = table.Column<string>(type: "TEXT", maxLength: 4096, nullable: false),
|
||||
sent_at = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
is_staff_only = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_mentor_help_messages", x => x.mentor_help_messages_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_mentor_help_messages_mentor_help_tickets_ticket_id",
|
||||
column: x => x.ticket_id,
|
||||
principalTable: "mentor_help_tickets",
|
||||
principalColumn: "mentor_help_tickets_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_messages_sent_at",
|
||||
table: "mentor_help_messages",
|
||||
column: "sent_at");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_messages_ticket_id",
|
||||
table: "mentor_help_messages",
|
||||
column: "ticket_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_assigned_to_user_id",
|
||||
table: "mentor_help_tickets",
|
||||
column: "assigned_to_user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_player_id",
|
||||
table: "mentor_help_tickets",
|
||||
column: "player_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_mentor_help_tickets_status",
|
||||
table: "mentor_help_tickets",
|
||||
column: "status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "mentor_help_messages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "mentor_help_tickets");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -707,6 +707,111 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
b.ToTable("job", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpMessage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("mentor_help_messages_id");
|
||||
|
||||
b.Property<bool>("IsStaffOnly")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_staff_only");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Guid>("SenderUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sender_user_id");
|
||||
|
||||
b.Property<DateTimeOffset>("SentAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sent_at");
|
||||
|
||||
b.Property<int>("TicketId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ticket_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_mentor_help_messages");
|
||||
|
||||
b.HasIndex("SentAt")
|
||||
.HasDatabaseName("IX_mentor_help_messages_sent_at");
|
||||
|
||||
b.HasIndex("TicketId")
|
||||
.HasDatabaseName("IX_mentor_help_messages_ticket_id");
|
||||
|
||||
b.ToTable("mentor_help_messages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpTicket", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("mentor_help_tickets_id");
|
||||
|
||||
b.Property<Guid?>("AssignedToUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("assigned_to_user_id");
|
||||
|
||||
b.Property<DateTimeOffset?>("ClosedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("closed_at");
|
||||
|
||||
b.Property<Guid?>("ClosedByUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("closed_by_user_id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("PlayerId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("player_id");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int?>("ServerId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("server_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("subject");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_mentor_help_tickets");
|
||||
|
||||
b.HasIndex("AssignedToUserId")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_assigned_to_user_id");
|
||||
|
||||
b.HasIndex("PlayerId")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_player_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_mentor_help_tickets_status");
|
||||
|
||||
b.ToTable("mentor_help_tickets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1717,6 +1822,18 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.MentorHelpMessage", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.MentorHelpTicket", "Ticket")
|
||||
.WithMany()
|
||||
.HasForeignKey("TicketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_mentor_help_messages_mentor_help_tickets_ticket_id");
|
||||
|
||||
b.Navigation("Ticket");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Player", b =>
|
||||
{
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ namespace Content.Server.Database
|
|||
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
|
||||
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
|
||||
public DbSet<AHelpMessage> AHelpMessages { get; set; } = default!;
|
||||
public DbSet<MentorHelpTicket> MentorHelpTickets { get; set; } = default!;
|
||||
public DbSet<MentorHelpMessage> MentorHelpMessages { get; set; } = default!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
|
@ -1365,4 +1367,107 @@ namespace Content.Server.Database
|
|||
public bool PlaySound { get; set; }
|
||||
public bool AdminOnly { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mentor help ticket
|
||||
/// </summary>
|
||||
[Table("mentor_help_tickets"), Index(nameof(PlayerId)), Index(nameof(AssignedToUserId)), Index(nameof(Status))]
|
||||
public class MentorHelpTicket
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player who created the ticket
|
||||
/// </summary>
|
||||
[ForeignKey("Player")]
|
||||
public Guid PlayerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mentor/admin who claimed this ticket (null if unclaimed)
|
||||
/// </summary>
|
||||
[ForeignKey("Player")]
|
||||
public Guid? AssignedToUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject/title of the ticket
|
||||
/// </summary>
|
||||
[Required, MaxLength(256)]
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current status of the ticket
|
||||
/// </summary>
|
||||
public MentorHelpTicketStatus Status { get; set; } = MentorHelpTicketStatus.Open;
|
||||
|
||||
/// <summary>
|
||||
/// When the ticket was created
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the ticket was last updated
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the ticket was closed (null if still open)
|
||||
/// </summary>
|
||||
public DateTimeOffset? ClosedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Who closed the ticket
|
||||
/// </summary>
|
||||
[ForeignKey("Player")]
|
||||
public Guid? ClosedByUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Round ID when the ticket was created
|
||||
/// </summary>
|
||||
public int? RoundId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Server ID where the ticket was created
|
||||
/// </summary>
|
||||
public int? ServerId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message in a mentor help ticket
|
||||
/// </summary>
|
||||
[Table("mentor_help_messages"), Index(nameof(TicketId)), Index(nameof(SentAt))]
|
||||
public class MentorHelpMessage
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ticket this message belongs to
|
||||
/// </summary>
|
||||
[ForeignKey("MentorHelpTicket")]
|
||||
public int TicketId { get; set; }
|
||||
public MentorHelpTicket Ticket { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Who sent this message
|
||||
/// </summary>
|
||||
[ForeignKey("Player")]
|
||||
public Guid SenderUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message content
|
||||
/// </summary>
|
||||
[Required, MaxLength(4096)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the message was sent
|
||||
/// </summary>
|
||||
public DateTimeOffset SentAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this message is only visible to mentors/admins
|
||||
/// </summary>
|
||||
public bool IsStaffOnly { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ namespace Content.Server.Administration.Systems
|
|||
|
||||
if (senderAdmin is not null &&
|
||||
senderAdmin.Value.dat.Flags ==
|
||||
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
|
||||
AdminFlags.Mentor) // Mentor. Not full admin. That's why it's colored differently.
|
||||
{
|
||||
bwoinkText = $"[color=purple]{adminPrefix}{username}[/color]";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared._Sunrise.MarkingEffects;
|
||||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
|
|
@ -1373,8 +1374,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
|||
MakePlayerRecord(ban.CreatedBy),
|
||||
ban.BanTime,
|
||||
MakePlayerRecord(ban.LastEditedBy),
|
||||
ban.LastEditedAt,
|
||||
ban.ExpirationTime,
|
||||
NormalizeDatabaseTime(ban.LastEditedAt),
|
||||
NormalizeDatabaseTime(ban.ExpirationTime),
|
||||
ban.Hidden,
|
||||
MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
|
||||
? null
|
||||
|
|
@ -1415,8 +1416,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
|||
MakePlayerRecord(ban.CreatedBy),
|
||||
ban.BanTime,
|
||||
MakePlayerRecord(ban.LastEditedBy),
|
||||
ban.LastEditedAt,
|
||||
ban.ExpirationTime,
|
||||
NormalizeDatabaseTime(ban.LastEditedAt),
|
||||
NormalizeDatabaseTime(ban.ExpirationTime),
|
||||
ban.Hidden,
|
||||
new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
|
||||
MakePlayerRecord(unbanningAdmin),
|
||||
|
|
@ -1817,6 +1818,137 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
|||
return messages;
|
||||
}
|
||||
|
||||
# endregion
|
||||
|
||||
# region MentorHelp
|
||||
|
||||
public async Task AddMentorHelpTicketAsync(MentorHelpTicket ticket)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.MentorHelpTickets.Add(ticket);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<MentorHelpTicket?> GetMentorHelpTicketAsync(int ticketId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return await db.DbContext.MentorHelpTickets
|
||||
.FirstOrDefaultAsync(t => t.Id == ticketId);
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpStatistics>> GetMentorHelpStatisticsAsync()
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
// Получаем количество тикетов, взятых каждым ментором
|
||||
var tickets = await db.DbContext.MentorHelpTickets
|
||||
.Where(t => t.AssignedToUserId != null)
|
||||
.GroupBy(t => t.AssignedToUserId!.Value)
|
||||
.Select(g => new { MentorUserId = g.Key, TicketsClaimed = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
// Получаем количество сообщений, отправленных каждым ментором
|
||||
var messages = await db.DbContext.MentorHelpMessages
|
||||
.GroupBy(m => m.SenderUserId)
|
||||
.Select(g => new { MentorUserId = g.Key, MessagesCount = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
// Объединяем статистику по MentorUserId
|
||||
var stats = new Dictionary<Guid, MentorHelpStatistics>();
|
||||
|
||||
foreach (var t in tickets)
|
||||
{
|
||||
stats[t.MentorUserId] = new MentorHelpStatistics
|
||||
{
|
||||
MentorUserId = t.MentorUserId,
|
||||
TicketsClaimed = t.TicketsClaimed,
|
||||
MessagesCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var m in messages)
|
||||
{
|
||||
if (stats.TryGetValue(m.MentorUserId, out var stat))
|
||||
{
|
||||
stat.MessagesCount = m.MessagesCount;
|
||||
stats[m.MentorUserId] = stat;
|
||||
}
|
||||
else
|
||||
{
|
||||
stats[m.MentorUserId] = new MentorHelpStatistics
|
||||
{
|
||||
MentorUserId = m.MentorUserId,
|
||||
TicketsClaimed = 0,
|
||||
MessagesCount = m.MessagesCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return stats.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task UpdateMentorHelpTicketAsync(MentorHelpTicket ticket)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.MentorHelpTickets.Update(ticket);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpTicket>> GetMentorHelpTicketsByPlayerAsync(Guid playerId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return (await db.DbContext.MentorHelpTickets
|
||||
.Where(t => t.PlayerId == playerId)
|
||||
.ToListAsync())
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpTicket>> GetOpenMentorHelpTicketsAsync()
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return (await db.DbContext.MentorHelpTickets
|
||||
.Where(t => t.Status != MentorHelpTicketStatus.Closed)
|
||||
.ToListAsync())
|
||||
.OrderByDescending(t => t.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpTicket>> GetAssignedMentorHelpTicketsAsync(Guid mentorId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return (await db.DbContext.MentorHelpTickets
|
||||
.Where(t => t.AssignedToUserId == mentorId && t.Status != MentorHelpTicketStatus.Closed)
|
||||
.ToListAsync())
|
||||
.OrderByDescending(t => t.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpTicket>> GetClosedMentorHelpTicketsAsync()
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return (await db.DbContext.MentorHelpTickets
|
||||
.Where(t => t.Status == MentorHelpTicketStatus.Closed)
|
||||
.ToListAsync())
|
||||
.OrderByDescending(t => t.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task AddMentorHelpMessageAsync(MentorHelpMessage message)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.MentorHelpMessages.Add(message);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<MentorHelpMessage>> GetMentorHelpMessagesByTicketAsync(int ticketId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return await db.DbContext.MentorHelpMessages
|
||||
.Where(m => m.TicketId == ticketId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
# endregion
|
||||
// Sunrise-End
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Text.Json;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
|
|
@ -376,6 +377,21 @@ namespace Content.Server.Database
|
|||
|
||||
public Task<List<AHelpMessage>> GetAHelpMessagesByReceiverAsync(Guid receiverUserId);
|
||||
|
||||
#endregion
|
||||
|
||||
#region MentorHelp
|
||||
|
||||
Task<List<MentorHelpStatistics>> GetMentorHelpStatisticsAsync();
|
||||
Task AddMentorHelpTicketAsync(MentorHelpTicket ticket);
|
||||
Task<MentorHelpTicket?> GetMentorHelpTicketAsync(int ticketId);
|
||||
Task UpdateMentorHelpTicketAsync(MentorHelpTicket ticket);
|
||||
Task<List<MentorHelpTicket>> GetMentorHelpTicketsByPlayerAsync(Guid playerId);
|
||||
Task<List<MentorHelpTicket>> GetOpenMentorHelpTicketsAsync();
|
||||
Task<List<MentorHelpTicket>> GetAssignedMentorHelpTicketsAsync(Guid mentorId);
|
||||
Task AddMentorHelpMessageAsync(MentorHelpMessage message);
|
||||
Task<List<MentorHelpMessage>> GetMentorHelpMessagesByTicketAsync(int ticketId);
|
||||
Task<List<MentorHelpTicket>> GetClosedMentorHelpTicketsAsync();
|
||||
|
||||
#endregion
|
||||
// Sunrise-End
|
||||
}
|
||||
|
|
@ -1093,6 +1109,60 @@ namespace Content.Server.Database
|
|||
return RunDbCommand(() => _db.GetAHelpMessagesByReceiverAsync(receiverUserId));
|
||||
}
|
||||
|
||||
// MentorHelp implementations
|
||||
public Task AddMentorHelpTicketAsync(MentorHelpTicket ticket)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.AddMentorHelpTicketAsync(ticket));
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpStatistics>> GetMentorHelpStatisticsAsync()
|
||||
{
|
||||
return RunDbCommand(() => _db.GetMentorHelpStatisticsAsync());
|
||||
}
|
||||
|
||||
public Task<MentorHelpTicket?> GetMentorHelpTicketAsync(int ticketId)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetMentorHelpTicketAsync(ticketId));
|
||||
}
|
||||
|
||||
public Task UpdateMentorHelpTicketAsync(MentorHelpTicket ticket)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.UpdateMentorHelpTicketAsync(ticket));
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpTicket>> GetMentorHelpTicketsByPlayerAsync(Guid playerId)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetMentorHelpTicketsByPlayerAsync(playerId));
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpTicket>> GetOpenMentorHelpTicketsAsync()
|
||||
{
|
||||
return RunDbCommand(() => _db.GetOpenMentorHelpTicketsAsync());
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpTicket>> GetAssignedMentorHelpTicketsAsync(Guid mentorId)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetAssignedMentorHelpTicketsAsync(mentorId));
|
||||
}
|
||||
|
||||
public Task AddMentorHelpMessageAsync(MentorHelpMessage message)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.AddMentorHelpMessageAsync(message));
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpMessage>> GetMentorHelpMessagesByTicketAsync(int ticketId)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetMentorHelpMessagesByTicketAsync(ticketId));
|
||||
}
|
||||
|
||||
public Task<List<MentorHelpTicket>> GetClosedMentorHelpTicketsAsync()
|
||||
{
|
||||
return RunDbCommand(() => _db.GetClosedMentorHelpTicketsAsync());
|
||||
}
|
||||
|
||||
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
|
|
|
|||
653
Content.Server/_Sunrise/MentorHelp/MentorHelpSystem.cs
Normal file
653
Content.Server/_Sunrise/MentorHelp/MentorHelpSystem.cs
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Players.RateLimiting;
|
||||
using Content.Sunrise.Interfaces.Shared;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side mentor help system for managing tickets
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class MentorHelpSystem : SharedMentorHelpSystem
|
||||
{
|
||||
private const string RateLimitKey = "MentorHelp";
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
|
||||
private ISharedSponsorsManager? _sponsorsManager; // Sunrise-Sponsors
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private List<MentorHelpStatisticsData>? _mentorStatsCache;
|
||||
private DateTimeOffset? _mentorStatsCacheTime;
|
||||
private readonly float _mentorCacheInterval = 10;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("MHELP");
|
||||
|
||||
_rateLimit.Register(
|
||||
RateLimitKey,
|
||||
new RateLimitRegistration(SunriseCCVars.MentorHelpRateLimitPeriod, // Reuse ahelp rate limit config
|
||||
SunriseCCVars.MentorHelpRateLimitCount,
|
||||
PlayerRateLimitedAction)
|
||||
);
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
IoCManager.Instance!.TryResolveType(out _sponsorsManager); // Sunrise-Sponsors
|
||||
}
|
||||
|
||||
private void PlayerRateLimitedAction(ICommonSession session)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) was rate limited for mentor help");
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
// Could notify mentors about player connection status for active tickets
|
||||
// For now, keep it simple
|
||||
}
|
||||
|
||||
protected override async void OnCreateTicketMessage(MentorHelpCreateTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
// Rate limiting
|
||||
if (_rateLimit.CountAction(session, RateLimitKey) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(message.Subject) || string.IsNullOrWhiteSpace(message.Message))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with empty subject or message");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Subject.Length > 256 || message.Message.Length > 4096)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with too long subject or message");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var ticket = new MentorHelpTicket
|
||||
{
|
||||
PlayerId = session.UserId.UserId,
|
||||
Subject = message.Subject.Trim(),
|
||||
Status = MentorHelpTicketStatus.Open,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
RoundId = _gameTicker.RoundId,
|
||||
ServerId = await GetServerIdAsync()
|
||||
};
|
||||
|
||||
await _dbManager.AddMentorHelpTicketAsync(ticket);
|
||||
|
||||
var ticketMessage = new MentorHelpMessage
|
||||
{
|
||||
TicketId = ticket.Id,
|
||||
SenderUserId = session.UserId.UserId,
|
||||
Message = message.Message.Trim(),
|
||||
SentAt = now,
|
||||
IsStaffOnly = false
|
||||
};
|
||||
await _dbManager.AddMentorHelpMessageAsync(ticketMessage);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) created mentor help ticket #{ticket.Id}: {ticket.Subject}");
|
||||
|
||||
// Notify player
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), session.Channel);
|
||||
// Instruct the player's client to open the newly created ticket
|
||||
RaiseNetworkEvent(new MentorHelpOpenTicketMessage(ticket.Id), session.Channel);
|
||||
|
||||
// Notify mentors/admins
|
||||
await NotifyMentorsOfNewTicket(ticketData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error creating mentor help ticket for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnClaimTicketMessage(MentorHelpClaimTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
// Check permissions
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to claim mentor help ticket without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Claim the ticket
|
||||
ticket.AssignedToUserId = session.UserId.UserId;
|
||||
ticket.Status = MentorHelpTicketStatus.Assigned;
|
||||
ticket.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Mentor {session.Name} ({session.UserId}) claimed ticket #{ticket.Id}");
|
||||
|
||||
// Notify all relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error claiming mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnReplyMessage(MentorHelpReplyMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
// Rate limiting
|
||||
if (_rateLimit.CountAction(session, RateLimitKey) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions - player can reply to their own ticket, mentors/admins can reply to any
|
||||
var isTicketOwner = ticket.PlayerId == session.UserId.UserId;
|
||||
var hasMentorPerms = HasMentorPermissions(session);
|
||||
|
||||
if (!isTicketOwner && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to ticket #{message.TicketId} without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Staff-only messages can only be sent by mentors/admins
|
||||
if (message.IsStaffOnly && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to send staff-only message without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if (string.IsNullOrWhiteSpace(message.Message) || message.Message.Length > 4096)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to send invalid message to ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the message
|
||||
var ticketMessage = new MentorHelpMessage
|
||||
{
|
||||
TicketId = message.TicketId,
|
||||
SenderUserId = session.UserId.UserId,
|
||||
Message = message.Message.Trim(),
|
||||
SentAt = DateTimeOffset.UtcNow,
|
||||
IsStaffOnly = message.IsStaffOnly
|
||||
};
|
||||
|
||||
await _dbManager.AddMentorHelpMessageAsync(ticketMessage);
|
||||
|
||||
// Update ticket status
|
||||
if (hasMentorPerms && ticket.Status == MentorHelpTicketStatus.Open)
|
||||
{
|
||||
// Mentor replied to open ticket, mark as assigned
|
||||
ticket.AssignedToUserId = session.UserId.UserId;
|
||||
ticket.Status = MentorHelpTicketStatus.Assigned;
|
||||
}
|
||||
else if (hasMentorPerms)
|
||||
{
|
||||
// Mentor replied, awaiting player response
|
||||
ticket.Status = MentorHelpTicketStatus.AwaitingResponse;
|
||||
}
|
||||
else if (isTicketOwner && ticket.Status == MentorHelpTicketStatus.AwaitingResponse)
|
||||
{
|
||||
// Player replied, mark as assigned again
|
||||
ticket.Status = MentorHelpTicketStatus.Assigned;
|
||||
}
|
||||
|
||||
ticket.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) replied to ticket #{message.TicketId}");
|
||||
|
||||
// Notify relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
var messageData = await ConvertToMessageDataAsync(ticketMessage);
|
||||
await NotifyTicketMessage(ticketData, messageData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error adding reply to mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnCloseTicketMessage(MentorHelpCloseTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close already closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions - player can close their own ticket, mentors/admins can close any
|
||||
var isTicketOwner = ticket.PlayerId == session.UserId.UserId;
|
||||
var hasMentorPerms = HasMentorPermissions(session);
|
||||
|
||||
if (!isTicketOwner && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close ticket #{message.TicketId} without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the ticket
|
||||
ticket.Status = MentorHelpTicketStatus.Closed;
|
||||
ticket.ClosedAt = DateTimeOffset.UtcNow;
|
||||
ticket.ClosedByUserId = session.UserId.UserId;
|
||||
ticket.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) closed ticket #{ticket.Id}");
|
||||
|
||||
// Notify relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error closing mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnRequestTicketsMessage(MentorHelpRequestTicketsMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
try
|
||||
{
|
||||
List<MentorHelpTicket> tickets;
|
||||
|
||||
if (message.OnlyMine)
|
||||
{
|
||||
// Player requesting their own tickets (both open and closed)
|
||||
tickets = await _dbManager.GetMentorHelpTicketsByPlayerAsync(session.UserId.UserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mentor/admin requesting all tickets (both open and closed)
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to request all mentor help tickets without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get both open and closed tickets for mentors
|
||||
var openTickets = await _dbManager.GetOpenMentorHelpTicketsAsync();
|
||||
var closedTickets = await _dbManager.GetClosedMentorHelpTicketsAsync();
|
||||
tickets = openTickets.Concat(closedTickets).ToList();
|
||||
}
|
||||
|
||||
var ticketDataList = new List<MentorHelpTicketData>();
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
ticketDataList.Add(await ConvertToTicketDataAsync(ticket));
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new MentorHelpTicketsListMessage(ticketDataList), session.Channel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help tickets for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnUnassignTicketMessage(MentorHelpUnassignTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to unassign mentor help ticket without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
ticket.AssignedToUserId = null;
|
||||
ticket.Status = MentorHelpTicketStatus.Open;
|
||||
ticket.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Mentor {session.Name} ({session.UserId}) unassigned ticket #{ticket.Id}");
|
||||
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error unassigning mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasMentorPermissions(ICommonSession session)
|
||||
{
|
||||
var adminData = _adminManager.GetAdminData(session);
|
||||
return adminData?.HasFlag(AdminFlags.Mentor) ?? false;
|
||||
}
|
||||
|
||||
private async Task<int?> GetServerIdAsync()
|
||||
{
|
||||
// Implementation would depend on how server ID is tracked
|
||||
// For now, return null
|
||||
return null;
|
||||
}
|
||||
protected override async void OnRequestStatisticsMessage(MentorHelpRequestStatisticsMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to request mentor help statistics without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var cacheValid = _mentorStatsCache != null && _mentorStatsCacheTime != null && (now - _mentorStatsCacheTime.Value).TotalMinutes < _mentorCacheInterval;
|
||||
List<MentorHelpStatisticsData> statisticsData;
|
||||
if (cacheValid)
|
||||
{
|
||||
statisticsData = _mentorStatsCache!;
|
||||
}
|
||||
else
|
||||
{
|
||||
var statistics = await _dbManager.GetMentorHelpStatisticsAsync();
|
||||
statisticsData = new List<MentorHelpStatisticsData>();
|
||||
|
||||
foreach (var stat in statistics)
|
||||
{
|
||||
var adminData = await _adminManager.LoadAdminData(new NetUserId(stat.MentorUserId));
|
||||
if (adminData == null)
|
||||
continue;
|
||||
if (!adminData.Value.dat.Flags.HasFlag(AdminFlags.Mentor))
|
||||
continue;
|
||||
var mentorName = await GetPlayerNameAsync(stat.MentorUserId);
|
||||
statisticsData.Add(new MentorHelpStatisticsData
|
||||
{
|
||||
MentorName = mentorName,
|
||||
TicketsClaimed = stat.TicketsClaimed,
|
||||
MessagesCount = stat.MessagesCount
|
||||
});
|
||||
}
|
||||
|
||||
statisticsData = statisticsData.OrderByDescending((s) => s.TicketsClaimed).ToList();
|
||||
_mentorStatsCache = statisticsData;
|
||||
_mentorStatsCacheTime = now;
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new MentorHelpStatisticsMessage(statisticsData), session.Channel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help statistics for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnRequestTicketMessagesMessage(MentorHelpRequestTicketMessagesMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
_sawmill.Info("Received RequestTicketMessages for ticket #{0} from {1} ({2})", message.TicketId, session.Name, session.UserId);
|
||||
|
||||
try
|
||||
{
|
||||
var allMessages = await _dbManager.GetMentorHelpMessagesByTicketAsync(message.TicketId);
|
||||
var messageDatas = new List<MentorHelpMessageData>();
|
||||
foreach (var msg in allMessages.OrderBy(m => m.SentAt))
|
||||
{
|
||||
messageDatas.Add(await ConvertToMessageDataAsync(msg));
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new MentorHelpTicketMessagesMessage(message.TicketId, messageDatas), session.Channel);
|
||||
_sawmill.Info("Sent {0} messages for ticket #{1} to {2} ({3})", messageDatas.Count, message.TicketId, session.Name, session.UserId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help messages for ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<MentorHelpTicketData> ConvertToTicketDataAsync(MentorHelpTicket ticket)
|
||||
{
|
||||
var playerName = await GetPlayerNameAsync(ticket.PlayerId);
|
||||
var assignedToName = ticket.AssignedToUserId.HasValue ? await GetPlayerNameAsync(ticket.AssignedToUserId.Value) : null;
|
||||
var closedByName = ticket.ClosedByUserId.HasValue ? await GetPlayerNameAsync(ticket.ClosedByUserId.Value) : null;
|
||||
|
||||
return new MentorHelpTicketData
|
||||
{
|
||||
Id = ticket.Id,
|
||||
PlayerId = new NetUserId(ticket.PlayerId),
|
||||
PlayerName = playerName,
|
||||
AssignedToUserId = ticket.AssignedToUserId.HasValue ? new NetUserId(ticket.AssignedToUserId.Value) : null,
|
||||
AssignedToName = assignedToName,
|
||||
Subject = ticket.Subject,
|
||||
Status = ticket.Status,
|
||||
CreatedAt = ticket.CreatedAt.DateTime,
|
||||
UpdatedAt = ticket.UpdatedAt.DateTime,
|
||||
ClosedAt = ticket.ClosedAt?.DateTime,
|
||||
ClosedByUserId = ticket.ClosedByUserId.HasValue ? new NetUserId(ticket.ClosedByUserId.Value) : null,
|
||||
ClosedByName = closedByName,
|
||||
RoundId = ticket.RoundId,
|
||||
HasUnreadMessages = false // Would need to implement read tracking
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<MentorHelpMessageData> ConvertToMessageDataAsync(MentorHelpMessage message)
|
||||
{
|
||||
var senderUserId = new NetUserId(message.SenderUserId);
|
||||
var senderAdminData = await _adminManager.LoadAdminData(senderUserId);
|
||||
var senderData = await _dbManager.GetPlayerRecordByUserId(senderUserId);
|
||||
var username = "";
|
||||
if (senderData != null)
|
||||
{
|
||||
username = senderData.LastSeenUserName;
|
||||
}
|
||||
|
||||
string formatterSender;
|
||||
var adminPrefix = "";
|
||||
|
||||
if (_config.GetCVar(SunriseCCVars.MentorHelpAdminPrefix) && senderAdminData is not null && senderAdminData.Value.dat.Title is not null)
|
||||
{
|
||||
adminPrefix = $"[bold]\\[{senderAdminData.Value.dat.Title}\\][/bold] ";
|
||||
}
|
||||
|
||||
if (senderAdminData is not null &&
|
||||
senderAdminData.Value.dat.Flags ==
|
||||
AdminFlags.Mentor)
|
||||
{
|
||||
formatterSender = $"[color=purple]{adminPrefix}{username}[/color]";
|
||||
}
|
||||
else if (senderAdminData is not null && senderAdminData.Value.dat.Flags.HasFlag(AdminFlags.Mentor))
|
||||
{
|
||||
formatterSender = $"[color=red]{adminPrefix}{username}[/color]";
|
||||
}
|
||||
else if (_sponsorsManager != null)
|
||||
{
|
||||
_sponsorsManager.TryGetOocColor(senderUserId, out var oocColor);
|
||||
_sponsorsManager.TryGetOocTitle(senderUserId, out var oocTitle);
|
||||
var sponsorTitle = oocTitle is null ? "" : $"\\[{oocTitle}\\]";
|
||||
if (oocColor != null)
|
||||
{
|
||||
formatterSender = $"[color={oocColor.Value.ToHex()}]{sponsorTitle} {username}[/color]";
|
||||
}
|
||||
else
|
||||
{
|
||||
formatterSender = $"{sponsorTitle} {username}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
formatterSender = $"{username}";
|
||||
}
|
||||
|
||||
return new MentorHelpMessageData
|
||||
{
|
||||
Id = message.Id,
|
||||
TicketId = message.TicketId,
|
||||
SenderUserId = senderUserId,
|
||||
SenderName = username,
|
||||
FormattedSender = formatterSender,
|
||||
Message = message.Message,
|
||||
SentAt = message.SentAt.DateTime,
|
||||
IsStaffOnly = message.IsStaffOnly
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> GetPlayerNameAsync(Guid userId)
|
||||
{
|
||||
var playerData = await _dbManager.GetPlayerRecordByUserId(new NetUserId(userId));
|
||||
var name = playerData?.LastSeenUserName;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Logger.Warning($"GetPlayerNameAsync: No name found for userId {userId}, returning 'Unknown'.");
|
||||
return "Unknown";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private async Task NotifyMentorsOfNewTicket(MentorHelpTicketData ticketData)
|
||||
{
|
||||
var mentors = GetTargetMentors();
|
||||
foreach (var mentor in mentors)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), mentor);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyTicketUpdate(MentorHelpTicketData ticketData)
|
||||
{
|
||||
// Notify the player
|
||||
if (_playerManager.TryGetSessionById(ticketData.PlayerId, out var playerSession))
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), playerSession.Channel);
|
||||
}
|
||||
|
||||
// Notify mentors
|
||||
var mentors = GetTargetMentors();
|
||||
foreach (var mentor in mentors)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), mentor);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyTicketMessage(MentorHelpTicketData ticketData, MentorHelpMessageData messageData)
|
||||
{
|
||||
var allMessages = await _dbManager.GetMentorHelpMessagesByTicketAsync(ticketData.Id);
|
||||
var messageDatas = new List<MentorHelpMessageData>();
|
||||
foreach (var msg in allMessages.OrderBy(m => m.SentAt)) // сортировка теперь точно по объектам
|
||||
{
|
||||
messageDatas.Add(await ConvertToMessageDataAsync(msg));
|
||||
}
|
||||
|
||||
// Notify the player (if not staff-only)
|
||||
if (!messageData.IsStaffOnly && _playerManager.TryGetSessionById(ticketData.PlayerId, out var playerSession))
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketMessagesMessage(ticketData.Id, messageDatas), playerSession.Channel);
|
||||
}
|
||||
|
||||
// Notify mentors
|
||||
var mentors = GetTargetMentors();
|
||||
foreach (var mentor in mentors)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketMessagesMessage(ticketData.Id, messageDatas), mentor);
|
||||
}
|
||||
}
|
||||
|
||||
private IList<INetChannel> GetTargetMentors()
|
||||
{
|
||||
return _adminManager.ActiveAdmins
|
||||
.Where(p => _adminManager.GetAdminData(p)?.HasFlag(AdminFlags.Mentor) ?? false)
|
||||
.Select(p => p.Channel)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
Content.Shared.Database/MentorHelpTicketStatus.cs
Normal file
28
Content.Shared.Database/MentorHelpTicketStatus.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace Content.Shared.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Status values for mentor help tickets
|
||||
/// </summary>
|
||||
public enum MentorHelpTicketStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticket is open and awaiting assignment or response
|
||||
/// </summary>
|
||||
Open = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Ticket has been claimed by a mentor/admin
|
||||
/// </summary>
|
||||
Assigned = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Ticket has been responded to, awaiting player response
|
||||
/// </summary>
|
||||
AwaitingResponse = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ticket has been resolved/closed
|
||||
/// </summary>
|
||||
Closed = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,11 @@
|
|||
/// </summary>
|
||||
NameColor = 1 << 21,
|
||||
|
||||
/// <summary>
|
||||
/// Lets you use the mentor help system.
|
||||
/// </summary>
|
||||
Mentor = 1 << 22,
|
||||
|
||||
/// <summary>
|
||||
/// Dangerous host permissions like scsi.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ namespace Content.Shared.Input
|
|||
public static readonly BoundKeyFunction Reloading = "Reloading";
|
||||
public static readonly BoundKeyFunction Interact = "Interact";
|
||||
public static readonly BoundKeyFunction LookUp = "LookUp";
|
||||
public static readonly BoundKeyFunction OpenMentorHelp = "OpenMentorHelp";
|
||||
public static readonly BoundKeyFunction OpenHelpChoice = "OpenHelpChoice";
|
||||
// Sunrise-End
|
||||
|
||||
public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp";
|
||||
|
|
|
|||
276
Content.Shared/_Sunrise/MentorHelp/SharedMentorHelpSystem.cs
Normal file
276
Content.Shared/_Sunrise/MentorHelp/SharedMentorHelpSystem.cs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
using Content.Shared.Database;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.MentorHelp
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared base class for mentor help system
|
||||
/// </summary>
|
||||
public abstract class SharedMentorHelpSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<MentorHelpCreateTicketMessage>(OnCreateTicketMessage);
|
||||
SubscribeNetworkEvent<MentorHelpClaimTicketMessage>(OnClaimTicketMessage);
|
||||
SubscribeNetworkEvent<MentorHelpReplyMessage>(OnReplyMessage);
|
||||
SubscribeNetworkEvent<MentorHelpCloseTicketMessage>(OnCloseTicketMessage);
|
||||
SubscribeNetworkEvent<MentorHelpRequestTicketsMessage>(OnRequestTicketsMessage);
|
||||
SubscribeNetworkEvent<MentorHelpUnassignTicketMessage>(OnUnassignTicketMessage);
|
||||
SubscribeNetworkEvent<MentorHelpRequestStatisticsMessage>(OnRequestStatisticsMessage);
|
||||
SubscribeNetworkEvent<MentorHelpRequestTicketMessagesMessage>(OnRequestTicketMessagesMessage);
|
||||
}
|
||||
|
||||
protected virtual void OnCreateTicketMessage(MentorHelpCreateTicketMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnClaimTicketMessage(MentorHelpClaimTicketMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnReplyMessage(MentorHelpReplyMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnCloseTicketMessage(MentorHelpCloseTicketMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnRequestTicketsMessage(MentorHelpRequestTicketsMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnUnassignTicketMessage(MentorHelpUnassignTicketMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnRequestStatisticsMessage(MentorHelpRequestStatisticsMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
protected virtual void OnRequestTicketMessagesMessage(MentorHelpRequestTicketMessagesMessage message, EntitySessionEventArgs eventArgs) { }
|
||||
}
|
||||
|
||||
public struct MentorHelpStatistics
|
||||
{
|
||||
public Guid MentorUserId { get; set; }
|
||||
public int TicketsClaimed { get; set; }
|
||||
public int MessagesCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to create a new mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpCreateTicketMessage : EntityEventArgs
|
||||
{
|
||||
public string Subject { get; }
|
||||
public string Message { get; }
|
||||
|
||||
public MentorHelpCreateTicketMessage(string subject, string message)
|
||||
{
|
||||
Subject = subject;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to claim a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpClaimTicketMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpClaimTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to reply to a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpReplyMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
public string Message { get; }
|
||||
public bool IsStaffOnly { get; }
|
||||
|
||||
public MentorHelpReplyMessage(int ticketId, string message, bool isStaffOnly = false)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
Message = message;
|
||||
IsStaffOnly = isStaffOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to unassign a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpUnassignTicketMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpUnassignTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to close a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpCloseTicketMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpCloseTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to request tickets (from client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpRequestTicketsMessage : EntityEventArgs
|
||||
{
|
||||
public bool OnlyMine { get; }
|
||||
|
||||
public MentorHelpRequestTicketsMessage(bool onlyMine = false)
|
||||
{
|
||||
OnlyMine = onlyMine;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with ticket update (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketUpdateMessage : EntityEventArgs
|
||||
{
|
||||
public MentorHelpTicketData Ticket { get; }
|
||||
|
||||
public MentorHelpTicketUpdateMessage(MentorHelpTicketData ticket)
|
||||
{
|
||||
Ticket = ticket;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with tickets list (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketsListMessage : EntityEventArgs
|
||||
{
|
||||
public List<MentorHelpTicketData> Tickets { get; }
|
||||
|
||||
public MentorHelpTicketsListMessage(List<MentorHelpTicketData> tickets)
|
||||
{
|
||||
Tickets = tickets;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with ticket messages (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketMessagesMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
public List<MentorHelpMessageData> Messages { get; }
|
||||
|
||||
public MentorHelpTicketMessagesMessage(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
Messages = messages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable ticket data for networking
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketData
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public NetUserId PlayerId { get; set; }
|
||||
public string PlayerName { get; set; } = string.Empty;
|
||||
public NetUserId? AssignedToUserId { get; set; }
|
||||
public string? AssignedToName { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public MentorHelpTicketStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
public NetUserId? ClosedByUserId { get; set; }
|
||||
public string? ClosedByName { get; set; }
|
||||
public int? RoundId { get; set; }
|
||||
public bool HasUnreadMessages { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable message data for networking
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpMessageData
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int TicketId { get; set; }
|
||||
public NetUserId SenderUserId { get; set; }
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
public string FormattedSender { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public DateTime SentAt { get; set; }
|
||||
public bool IsStaffOnly { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO для передачи статистики по менторам
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpStatisticsData
|
||||
{
|
||||
public string MentorName { get; set; } = string.Empty;
|
||||
public int TicketsClaimed { get; set; }
|
||||
public int MessagesCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение-запрос статистики по менторам
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpRequestStatisticsMessage : EntityEventArgs
|
||||
{
|
||||
// Пустой класс-запрос
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение с результатами статистики по менторам
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpStatisticsMessage : EntityEventArgs
|
||||
{
|
||||
public List<MentorHelpStatisticsData> Statistics { get; }
|
||||
public MentorHelpStatisticsMessage(List<MentorHelpStatisticsData> statistics)
|
||||
{
|
||||
Statistics = statistics;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to request messages for a specific ticket (from client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpRequestTicketMessagesMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpRequestTicketMessagesMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message sent from server to client instructing it to open (focus) a specific ticket in the UI.
|
||||
/// This is used immediately after creating a ticket so the creating player sees their new ticket.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpOpenTicketMessage : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpOpenTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -565,4 +565,19 @@ public sealed partial class SunriseCCVars : CVars
|
|||
/// </summary>
|
||||
public static readonly CVarDef<string> DocumentTemplatePool =
|
||||
CVarDef.Create("doc.template_pool", "Sunrise", CVar.SERVER | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<bool> MentorHelpAdminPrefix =
|
||||
CVarDef.Create("mentor_help.admin_prefix", true, CVar.SERVERONLY);
|
||||
|
||||
public static readonly CVarDef<float> MentorHelpRateLimitPeriod =
|
||||
CVarDef.Create("mentor_help.rate_limit_period", 2f, CVar.SERVERONLY);
|
||||
|
||||
public static readonly CVarDef<int> MentorHelpRateLimitCount =
|
||||
CVarDef.Create("mentor_help.rate_limit_count", 10, CVar.SERVERONLY);
|
||||
|
||||
public static readonly CVarDef<string> MentorHelpSound =
|
||||
CVarDef.Create("mentor_help.mentor_help_sound", "/Audio/_Sunrise/Effects/adminticketopen.ogg", CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<bool> MentorHelpSoundEnabled =
|
||||
CVarDef.Create("mentor_help.mentor_help_sound_enabled", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
}
|
||||
|
|
|
|||
BIN
Resources/Audio/_Sunrise/Effects/adminticketopen.ogg
Normal file
BIN
Resources/Audio/_Sunrise/Effects/adminticketopen.ogg
Normal file
Binary file not shown.
|
|
@ -0,0 +1,72 @@
|
|||
ui-lobby-mhelp-button = MHelp
|
||||
ui-options-function-open-mentor-help = Открыть ментор помощь
|
||||
ui-options-function-open-help-choice = Открыть выбор помощи
|
||||
|
||||
# Mentor Help Window
|
||||
mentor-help-window-title = Ментор помощь
|
||||
|
||||
# Tabs
|
||||
mentor-help-tab-open = Открытые
|
||||
mentor-help-tab-closed = Закрытые
|
||||
|
||||
# Buttons
|
||||
mentor-help-statistics = Статистика
|
||||
mentor-help-new-ticket = Новый тикет
|
||||
mentor-help-back-to-list = Назад к списку
|
||||
mentor-help-send-reply = Отправить
|
||||
mentor-help-claim = Взять
|
||||
mentor-help-unassign = Освободить
|
||||
mentor-help-close-ticket = Закрыть
|
||||
mentor-help-cancel = Отмена
|
||||
mentor-help-close = Закрыть
|
||||
|
||||
# Table columns
|
||||
mentor-help-column-id = ID
|
||||
mentor-help-column-player = Игрок
|
||||
mentor-help-column-status = Статус
|
||||
mentor-help-column-assigned = Назначен
|
||||
mentor-help-column-subject = Тема
|
||||
|
||||
# Status
|
||||
mentor-help-status-open = Открыт
|
||||
mentor-help-status-assigned = Назначен
|
||||
mentor-help-status-awaiting = Ожидает ответа
|
||||
mentor-help-status-closed = Закрыт
|
||||
mentor-help-status-unknown = Неизвестно
|
||||
mentor-help-unassigned = Не назначен
|
||||
|
||||
# Ticket info
|
||||
mentor-help-ticket-info = Статус: {$status} | Назначен: {$assigned} | Создан: {$created}
|
||||
|
||||
# Reply input
|
||||
mentor-help-reply-placeholder = Введите ваш ответ...
|
||||
|
||||
# New ticket dialog
|
||||
mentor-help-new-ticket-title = Новый тикет ментор помощи
|
||||
mentor-help-new-ticket-instructions = Опишите вашу проблему или вопрос. Менторы помогут вам с игровыми механиками и правилами.
|
||||
mentor-help-new-ticket-subject-label = Тема:
|
||||
mentor-help-new-ticket-subject-placeholder = Кратко опишите вашу проблему
|
||||
mentor-help-new-ticket-message-label = Сообщение:
|
||||
mentor-help-new-ticket-create-button = Создать тикет
|
||||
mentor-help-new-ticket-error-subject = Пожалуйста, укажите тему тикета
|
||||
mentor-help-new-ticket-error-message = Пожалуйста, опишите вашу проблему
|
||||
|
||||
# Statistics dialog
|
||||
mentor-help-statistics-title = Статистика ментор помощи
|
||||
mentor-help-statistics-header = Статистика работы менторов
|
||||
mentor-help-statistics-content = Здесь будет отображаться статистика по количеству взятых тикетов каждым ментором.
|
||||
|
||||
|
||||
mentor-help-status-label = Статус: {$status}
|
||||
mentor-help-assigned-label = Назначен: {$assigned}
|
||||
mentor-help-created-label = Создан: {$created}
|
||||
mentor-help-label-id = ID:
|
||||
mentor-help-label-subject = Тема:
|
||||
|
||||
|
||||
help-choice-title = Выберите тип помощи
|
||||
help-choice-title-label = [color=white][font size=16]Какой тип помощи вам нужен?[/font][/color]
|
||||
help-choice-ahelp-button = Админ-помощь
|
||||
help-choice-mhelp-button = Ментор-помощь
|
||||
help-choice-ahelp-desc-label = [color=#CCCCCC][font size=12]• Админ-помощь - для жалоб на игроков, сообщений о багах и нарушениях правил[/font][/color]
|
||||
help-choice-mhelp-desc-label = [color=#CCCCCC][font size=12]• Ментор-помощь - для вопросов о механиках игры и помощи новичкам[/font][/color]
|
||||
BIN
Resources/Textures/Interface/mentor.svg.192dpi.png
Normal file
BIN
Resources/Textures/Interface/mentor.svg.192dpi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -241,9 +241,10 @@ binds:
|
|||
- function: OpenGuidebook
|
||||
type: State
|
||||
key: NumpadNum0
|
||||
- function: OpenAHelp
|
||||
type: State
|
||||
key: F1
|
||||
# Sunrise-Edit
|
||||
#- function: OpenAHelp
|
||||
# type: State
|
||||
# key: F1
|
||||
- function: OpenInventoryMenu
|
||||
type: State
|
||||
key: I
|
||||
|
|
@ -647,4 +648,10 @@ binds:
|
|||
- function: MeleeGunAttack
|
||||
type: State
|
||||
key: MouseMiddle
|
||||
- function: OpenMentorHelp
|
||||
type: State
|
||||
key: O
|
||||
- function: OpenHelpChoice
|
||||
type: State
|
||||
key: F1
|
||||
# Sunrise-End
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue