diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml
index 31f46c5500..b1bd283ab8 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical"
HorizontalExpand="True">
+
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index efc3e8caf9..4fc346e375 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -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);
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml b/Content.Client/Lobby/UI/LobbyGui.xaml
index 4963b9b1bf..fd775f5c45 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml
@@ -177,6 +177,8 @@
HorizontalAlignment="Right" SizeFlagsStretchRatio="1">
+
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index c334294b57..c01e493ebc 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -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
diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
index a99afeba75..4aecedd160 100644
--- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
+++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
@@ -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(); };
diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
index 2c09666fdf..63a41a8c40 100644
--- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
+++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
@@ -110,6 +110,16 @@
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="42 64"
HorizontalExpand="True"
+ AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
+ />
+
diff --git a/Content.Client/_Sunrise/HelpChoice/HelpChoiceUIController.cs b/Content.Client/_Sunrise/HelpChoice/HelpChoiceUIController.cs
new file mode 100644
index 0000000000..56bdfdbaae
--- /dev/null
+++ b/Content.Client/_Sunrise/HelpChoice/HelpChoiceUIController.cs
@@ -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
+{
+ [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().Open();
+ };
+
+ local.MentorHelpButton.OnPressed += _ =>
+ {
+ local.Close();
+ _uiManager.GetUIController().Open();
+ };
+
+ _dialog.OnClose += () => _dialog = null;
+ }
+
+ _dialog.OpenCentered();
+ }
+}
diff --git a/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml b/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml
new file mode 100644
index 0000000000..3108e258d8
--- /dev/null
+++ b/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml.cs b/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml.cs
new file mode 100644
index 0000000000..650eb9db8f
--- /dev/null
+++ b/Content.Client/_Sunrise/HelpChoice/HelpChoiceWindow.xaml.cs
@@ -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);
+ }
+ }
+}
+
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml b/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml
new file mode 100644
index 0000000000..5c5e0a7289
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
new file mode 100644
index 0000000000..9e0f321f8c
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
@@ -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
+{
+ ///
+ /// Main control for mentor help interface
+ ///
+ [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 _tickets = new();
+ private MentorHelpTicketData? _selectedTicket;
+ private Dictionary> _ticketMessages = new();
+
+ private readonly Dictionary _openTicketControls = new();
+ private readonly Dictionary _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().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);
+ }
+
+ ///
+ /// Попытаться открыть тикет с указанным id. Если тикет ещё не загружен, запросит список тикетов
+ /// и откроет тикет когда список придёт.
+ ///
+ 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 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 tickets, Dictionary 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 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 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);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml b/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml
new file mode 100644
index 0000000000..bc6ec52aa0
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml.cs
new file mode 100644
index 0000000000..ec192339d3
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpNewTicketDialog.xaml.cs
@@ -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
+{
+ ///
+ /// Dialog for creating a new mentor help ticket
+ ///
+ [GenerateTypedNameReferences]
+ public sealed partial class MentorHelpNewTicketDialog : DefaultWindow
+ {
+ public event Action? 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);
+ };
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml
new file mode 100644
index 0000000000..6699076397
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml.cs
new file mode 100644
index 0000000000..826f59beba
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsDialog.xaml.cs
@@ -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 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);
+ }
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsUIController.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsUIController.cs
new file mode 100644
index 0000000000..3be8b4a48b
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpStatisticsUIController.cs
@@ -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
+ {
+ 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();
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpSystem.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpSystem.cs
new file mode 100644
index 0000000000..a05823675d
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpSystem.cs
@@ -0,0 +1,154 @@
+using Content.Shared._Sunrise.MentorHelp;
+using JetBrains.Annotations;
+
+namespace Content.Client._Sunrise.MentorHelp
+{
+ ///
+ /// Client-side mentor help system
+ ///
+ [UsedImplicitly]
+ public sealed class MentorHelpSystem : SharedMentorHelpSystem
+ {
+ public event EventHandler? OnTicketUpdated;
+ public event EventHandler? OnTicketsListReceived;
+ public event EventHandler? OnTicketMessagesReceived;
+ public event EventHandler? OnStatisticsReceived;
+ public event EventHandler? 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(OnTicketUpdate);
+ SubscribeNetworkEvent(OnTicketsList);
+ SubscribeNetworkEvent(OnTicketMessages);
+ SubscribeNetworkEvent(OnStatistics);
+ SubscribeNetworkEvent(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);
+ }
+
+ ///
+ /// Create a new mentor help ticket
+ ///
+ private void OnStatistics(MentorHelpStatisticsMessage message, EntitySessionEventArgs eventArgs)
+ {
+ OnStatisticsReceived?.Invoke(this, message);
+ }
+
+ public void CreateTicket(string subject, string message)
+ {
+ RaiseNetworkEvent(new MentorHelpCreateTicketMessage(subject, message));
+ }
+
+ ///
+ /// Claim a mentor help ticket
+ ///
+ public void ClaimTicket(int ticketId)
+ {
+ RaiseNetworkEvent(new MentorHelpClaimTicketMessage(ticketId));
+ }
+
+ ///
+ /// Unassign a mentor help ticket
+ ///
+ public void UnassignTicket(int ticketId)
+ {
+ RaiseNetworkEvent(new MentorHelpUnassignTicketMessage(ticketId));
+ }
+
+ ///
+ /// Reply to a mentor help ticket
+ ///
+ public void ReplyToTicket(int ticketId, string message, bool isStaffOnly = false)
+ {
+ RaiseNetworkEvent(new MentorHelpReplyMessage(ticketId, message, isStaffOnly));
+ }
+
+ ///
+ /// Close a mentor help ticket
+ ///
+ public void CloseTicket(int ticketId)
+ {
+ RaiseNetworkEvent(new MentorHelpCloseTicketMessage(ticketId));
+ }
+
+ ///
+ /// Request tickets (either all for mentors/admins, or only own for players)
+ ///
+ public void RequestTickets(bool onlyMine = false)
+ {
+ RaiseNetworkEvent(new MentorHelpRequestTicketsMessage(onlyMine));
+ }
+
+ ///
+ /// Request messages for a specific ticket
+ ///
+ public void RequestTicketMessages(int ticketId)
+ {
+ // Send a request to the server to fetch messages for the given ticket
+ RaiseNetworkEvent(new MentorHelpRequestTicketMessagesMessage(ticketId));
+ }
+
+ ///
+ /// Request mentor help statistics
+ ///
+ public void RequestStatistics()
+ {
+ RaiseNetworkEvent(new MentorHelpRequestStatisticsMessage());
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs
new file mode 100644
index 0000000000..36e86d566a
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpUIController.cs
@@ -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;
+
+///
+/// UI controller for mentor help system
+///
+[UsedImplicitly]
+public sealed class MentorHelpUIController : UIController, IOnSystemChanged, IOnStateChanged, IOnStateChanged
+{
+ [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()?.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();
+ }
+
+ 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();
+
+ 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); };
+ }
+
+ ///
+ /// Open the mentor help window
+ ///
+ public void Open()
+ {
+ EnsureUIHelper();
+ UIHelper!.OpenWindow();
+ SetMentorHelpPressed(true);
+ }
+
+ ///
+ /// Toggle the mentor help window
+ ///
+ public void ToggleWindow()
+ {
+ EnsureUIHelper();
+
+ if (UIHelper!.IsOpen)
+ {
+ UIHelper.CloseWindow();
+ SetMentorHelpPressed(false);
+ }
+ else
+ {
+ UIHelper.OpenWindow();
+ SetMentorHelpPressed(true);
+ }
+ }
+
+ ///
+ /// Close the mentor help window
+ ///
+ 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");
+ }
+ }
+}
+
+///
+/// Interface for mentor help UI handlers
+///
+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 tickets);
+ void TicketMessagesReceived(int ticketId, List 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 tickets)
+ {
+ _window?.MentorHelp.UpdateTicketsList(tickets);
+ }
+
+ public void TicketMessagesReceived(int ticketId, List messages)
+ {
+ _window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
+ }
+
+ public void Dispose()
+ {
+ CloseWindow();
+ }
+}
+
+///
+/// UI handler for mentors/admins (can see and manage all tickets)
+///
+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 tickets)
+ {
+ _window?.MentorHelp.UpdateTicketsList(tickets);
+ }
+
+ public void TicketMessagesReceived(int ticketId, List messages)
+ {
+ _window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
+ }
+
+ public void Dispose()
+ {
+ CloseWindow();
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml b/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml
new file mode 100644
index 0000000000..07d9b7e184
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml.cs b/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml.cs
new file mode 100644
index 0000000000..6044814452
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/MentorHelpWindow.xaml.cs
@@ -0,0 +1,18 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.MentorHelp
+{
+ ///
+ /// Mentor help window wrapper
+ ///
+ [GenerateTypedNameReferences]
+ public sealed partial class MentorHelpWindow : DefaultWindow
+ {
+ public MentorHelpWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml b/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml
new file mode 100644
index 0000000000..68c5bedaae
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml.cs b/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml.cs
new file mode 100644
index 0000000000..be2bc6f8e9
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/StatisticsEntryControl.xaml.cs
@@ -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();
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml b/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml
new file mode 100644
index 0000000000..ab071d50aa
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml.cs b/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml.cs
new file mode 100644
index 0000000000..a74d0bef53
--- /dev/null
+++ b/Content.Client/_Sunrise/MentorHelp/TicketEntryControl.xaml.cs
@@ -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? 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")
+ };
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20250825044815_MentorHelp.Designer.cs b/Content.Server.Database/Migrations/Postgres/20250825044815_MentorHelp.Designer.cs
new file mode 100644
index 0000000000..53a81f10bf
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20250825044815_MentorHelp.Designer.cs
@@ -0,0 +1,2326 @@
+//
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20250825044815_MentorHelp")]
+ partial class MentorHelp
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.AHelpMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ahelp_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminOnly")
+ .HasColumnType("boolean")
+ .HasColumnName("admin_only");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlaySound")
+ .HasColumnType("boolean")
+ .HasColumnName("play_sound");
+
+ b.Property("ReceiverUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("receiver_user_id");
+
+ b.Property("SenderUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("sender_user_id");
+
+ b.Property("SentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("sent_at");
+
+ b.HasKey("Id")
+ .HasName("PK_ahelp_messages");
+
+ b.HasIndex("ReceiverUserId")
+ .HasDatabaseName("IX_ahelp_messages_receiver_user_id");
+
+ b.ToTable("ahelp_messages", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Deadminned")
+ .HasColumnType("boolean")
+ .HasColumnName("deadminned");
+
+ b.Property("Suspended")
+ .HasColumnType("boolean")
+ .HasColumnName("suspended");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("CurTime")
+ .HasColumnType("bigint")
+ .HasColumnName("cur_time");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("RoundId", "Id")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.HasKey("RoundId", "LogId", "PlayerUserId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+ b.ToTable("admin_log_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Dismissed")
+ .HasColumnType("boolean")
+ .HasColumnName("dismissed");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Seen")
+ .HasColumnType("boolean")
+ .HasColumnName("seen");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_messages");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_messages_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_messages_round_id");
+
+ b.ToTable("admin_messages", null, t =>
+ {
+ t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_notes");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_notes_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_notes_round_id");
+
+ b.ToTable("admin_notes", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank");
+
+ b.ToTable("admin_rank", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank_flag");
+
+ b.HasIndex("AdminRankId");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_watchlists");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_watchlists_round_id");
+
+ b.ToTable("admin_watchlists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_antag");
+
+ b.HasIndex("ProfileId", "AntagName")
+ .IsUnique();
+
+ b.ToTable("antag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_assigned_user_id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("assigned_user_id", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_template_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("Length")
+ .HasColumnType("interval")
+ .HasColumnName("length");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_template");
+
+ b.ToTable("ban_template", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_blacklist");
+
+ b.ToTable("blacklist", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("Trust")
+ .HasColumnType("real")
+ .HasColumnName("trust");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_connection_log");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_connection_log_server_id");
+
+ b.HasIndex("Time");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("connection_log", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ipintel_cache_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Score")
+ .HasColumnType("real")
+ .HasColumnName("score");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.HasKey("Id")
+ .HasName("PK_ipintel_cache");
+
+ b.ToTable("ipintel_cache", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_job");
+
+ b.HasIndex("ProfileId");
+
+ b.HasIndex("ProfileId", "JobName")
+ .IsUnique();
+
+ b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+ .IsUnique()
+ .HasFilter("priority = 3");
+
+ b.ToTable("job", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.MentorHelpMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("mentor_help_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsStaffOnly")
+ .HasColumnType("boolean")
+ .HasColumnName("is_staff_only");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("SenderUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("sender_user_id");
+
+ b.Property("SentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("sent_at");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("mentor_help_tickets_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AssignedToUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_user_id");
+
+ b.Property("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property("ClosedByUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_user_id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("PlayerId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("ServerId")
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasColumnName("subject");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("play_time_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("PlayerId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_id");
+
+ b.Property("TimeSpent")
+ .HasColumnType("interval")
+ .HasColumnName("time_spent");
+
+ b.Property("Tracker")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("tracker");
+
+ b.HasKey("Id")
+ .HasName("PK_play_time");
+
+ b.HasIndex("PlayerId", "Tracker")
+ .IsUnique();
+
+ b.ToTable("play_time", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FirstSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("first_seen_time");
+
+ b.Property("LastReadRules")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_read_rules");
+
+ b.Property("LastSeenAddress")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("last_seen_address");
+
+ b.Property("LastSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_seen_time");
+
+ b.Property("LastSeenUserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("last_seen_user_name");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_player");
+
+ b.HasAlternateKey("UserId")
+ .HasName("ak_player_user_id");
+
+ b.HasIndex("LastSeenUserName");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("player", null, t =>
+ {
+ t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminOOCColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("admin_ooc_color");
+
+ b.PrimitiveCollection>("ConstructionFavorites")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("construction_favorites");
+
+ b.Property("SelectedCharacterSlot")
+ .HasColumnType("integer")
+ .HasColumnName("selected_character_slot");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_preference");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("preference", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Age")
+ .HasColumnType("integer")
+ .HasColumnName("age");
+
+ b.Property("BodyType")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("body_type");
+
+ b.Property("CharacterName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("char_name");
+
+ b.Property("EyeColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("eye_color");
+
+ b.Property("FacialHairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_color");
+
+ b.Property("FacialHairColorType")
+ .HasColumnType("integer")
+ .HasColumnName("facial_hair_color_type");
+
+ b.Property("FacialHairExtendedColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_extended_color");
+
+ b.Property("FacialHairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_name");
+
+ b.Property("FlavorText")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flavor_text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("gender");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_color");
+
+ b.Property("HairColorType")
+ .HasColumnType("integer")
+ .HasColumnName("hair_color_type");
+
+ b.Property("HairExtendedColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_extended_color");
+
+ b.Property("HairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_name");
+
+ b.Property("Height")
+ .HasColumnType("real")
+ .HasColumnName("height");
+
+ b.Property("Markings")
+ .HasColumnType("jsonb")
+ .HasColumnName("markings");
+
+ b.Property("PreferenceId")
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ b.Property("PreferenceUnavailable")
+ .HasColumnType("integer")
+ .HasColumnName("pref_unavailable");
+
+ b.Property("Sex")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("sex");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("skin_color");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasColumnName("slot");
+
+ b.Property("SpawnPriority")
+ .HasColumnType("integer")
+ .HasColumnName("spawn_priority");
+
+ b.Property("Species")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("species");
+
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+
+ b.Property("Width")
+ .HasColumnType("real")
+ .HasColumnName("width");
+
+ b.HasKey("Id")
+ .HasName("PK_profile");
+
+ b.HasIndex("PreferenceId")
+ .HasDatabaseName("IX_profile_preference_id");
+
+ b.HasIndex("Slot", "PreferenceId")
+ .IsUnique();
+
+ b.ToTable("profile", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("loadout_name");
+
+ b.Property("ProfileLoadoutGroupId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout");
+
+ b.HasIndex("ProfileLoadoutGroupId");
+
+ b.ToTable("profile_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("group_name");
+
+ b.Property("ProfileRoleLoadoutId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout_group");
+
+ b.HasIndex("ProfileRoleLoadoutId");
+
+ b.ToTable("profile_loadout_group", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("EntityName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("entity_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.Property("RoleName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_name");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_role_loadout");
+
+ b.HasIndex("ProfileId");
+
+ b.ToTable("profile_role_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+ {
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("RoleId")
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("PlayerUserId", "RoleId")
+ .HasName("PK_role_whitelists");
+
+ b.ToTable("role_whitelists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ServerId")
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_date");
+
+ b.HasKey("Id")
+ .HasName("PK_round");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_round_server_id");
+
+ b.HasIndex("StartDate");
+
+ b.ToTable("round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Server", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_server");
+
+ b.ToTable("server", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property