Merge remote-tracking branch 'space-sunrise/master'
This commit is contained in:
commit
ed619598f1
88 changed files with 9293 additions and 82 deletions
53
.github/workflows/publish-test.yml
vendored
Normal file
53
.github/workflows/publish-test.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Publish Test
|
||||
|
||||
concurrency:
|
||||
group: publish
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pip install pyyaml requests
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Packaging
|
||||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
|
||||
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py --fork-id sunrise_station_test
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
|
||||
31
Content.Client/Administration/Systems/AdminWhoSystem.cs
Normal file
31
Content.Client/Administration/Systems/AdminWhoSystem.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared.Administration;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Client system for handling admin who requests
|
||||
/// </summary>
|
||||
public sealed class AdminWhoSystem : EntitySystem
|
||||
{
|
||||
public event Action<List<AdminWhoEntry>>? OnAdminWhoUpdate;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<AdminWhoResponseEvent>(OnAdminWhoResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request the list of online administrators from the server
|
||||
/// </summary>
|
||||
public void RequestAdminWho()
|
||||
{
|
||||
RaiseNetworkEvent(new RequestAdminWhoEvent());
|
||||
}
|
||||
|
||||
private void OnAdminWhoResponse(AdminWhoResponseEvent args, EntitySessionEventArgs session)
|
||||
{
|
||||
OnAdminWhoUpdate?.Invoke(args.Admins);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,13 +12,25 @@ namespace Content.Client.Administration.Systems
|
|||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public event EventHandler<BwoinkTextMessage>? OnBwoinkTextMessageRecieved;
|
||||
public event EventHandler<BwoinkCooldownMessage>? OnBwoinkCooldownReceived;
|
||||
private (TimeSpan Timestamp, bool Typing) _lastTypingUpdateSent;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<BwoinkCooldownMessage>(OnBwoinkCooldownMessage);
|
||||
}
|
||||
|
||||
protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnBwoinkTextMessageRecieved?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnBwoinkCooldownMessage(BwoinkCooldownMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnBwoinkCooldownReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
public void Send(NetUserId channelId, string text, bool playSound, bool adminOnly)
|
||||
{
|
||||
// Reuse the channel ID as the 'true sender'.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
using Content.Client.Administration.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class AdminWhoUIController : UIController, IOnSystemChanged<AdminWhoSystem>
|
||||
{
|
||||
private AdminWhoWindow? _dialog;
|
||||
private AdminWhoSystem? _adminWhoSystem;
|
||||
|
||||
protected override string SawmillName => "c.c.admin.adminwho";
|
||||
|
||||
public void OnSystemLoaded(AdminWhoSystem system)
|
||||
{
|
||||
_adminWhoSystem = system;
|
||||
|
||||
_dialog = new AdminWhoWindow();
|
||||
_dialog.Initialize(system);
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(AdminWhoSystem system)
|
||||
{
|
||||
if (_dialog != null)
|
||||
{
|
||||
_dialog.Close();
|
||||
_dialog.Uninitialize();
|
||||
_dialog.Dispose();
|
||||
_dialog = null;
|
||||
}
|
||||
|
||||
_adminWhoSystem = null;
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_dialog == null)
|
||||
{
|
||||
if (_adminWhoSystem == null)
|
||||
return;
|
||||
|
||||
_dialog = new AdminWhoWindow();
|
||||
_dialog.Initialize(_adminWhoSystem);
|
||||
}
|
||||
|
||||
_dialog.OpenCentered();
|
||||
_dialog.RefreshAdminList();
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
if (_dialog?.IsOpen == true)
|
||||
Close();
|
||||
else
|
||||
Open();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_dialog?.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Content.Client/Administration/UI/Bwoink/AdminWhoWindow.xaml
Normal file
23
Content.Client/Administration/UI/Bwoink/AdminWhoWindow.xaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<ui:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
VerticalExpand="True" HorizontalExpand="True"
|
||||
Title="{Loc admin-who-title}"
|
||||
SetSize="400 300"
|
||||
Resizable="False">
|
||||
<PanelContainer StyleClasses="BackgroundDark">
|
||||
<BoxContainer Orientation="Vertical" Margin="8">
|
||||
<Label Name="LoadingLabel" Access="Public" Text="{Loc admin-who-loading}" StyleClasses="LabelText"
|
||||
HorizontalAlignment="Center" Visible="False" />
|
||||
<Label Name="NoAdminsLabel" Access="Public" Text="{Loc admin-who-no-admins}" StyleClasses="LabelText"
|
||||
HorizontalAlignment="Center" Visible="False" />
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Name="AdminsContainer" Access="Public" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 8 0 0">
|
||||
<Button Name="RefreshButton" Access="Public" Text="{Loc admin-who-refresh}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</ui:FancyWindow>
|
||||
102
Content.Client/Administration/UI/Bwoink/AdminWhoWindow.xaml.cs
Normal file
102
Content.Client/Administration/UI/Bwoink/AdminWhoWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Text;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminWhoWindow : FancyWindow
|
||||
{
|
||||
private AdminWhoSystem? _adminWhoSystem;
|
||||
|
||||
public AdminWhoWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
RefreshButton.OnPressed += _ => RefreshAdminList();
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
}
|
||||
|
||||
public void Initialize(AdminWhoSystem system)
|
||||
{
|
||||
_adminWhoSystem = system;
|
||||
_adminWhoSystem.OnAdminWhoUpdate += OnAdminListReceived;
|
||||
|
||||
// Request the list when the window is initialized
|
||||
RefreshAdminList();
|
||||
}
|
||||
|
||||
public void Uninitialize()
|
||||
{
|
||||
if (_adminWhoSystem != null)
|
||||
{
|
||||
_adminWhoSystem.OnAdminWhoUpdate -= OnAdminListReceived;
|
||||
_adminWhoSystem = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshAdminList()
|
||||
{
|
||||
AdminsContainer.RemoveAllChildren();
|
||||
|
||||
NoAdminsLabel.Visible = false;
|
||||
LoadingLabel.Visible = true;
|
||||
|
||||
_adminWhoSystem?.RequestAdminWho();
|
||||
}
|
||||
|
||||
private void OnAdminListReceived(List<AdminWhoEntry> admins)
|
||||
{
|
||||
LoadingLabel.Visible = false;
|
||||
|
||||
if (admins.Count == 0)
|
||||
{
|
||||
NoAdminsLabel.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NoAdminsLabel.Visible = false;
|
||||
|
||||
// Add each admin
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
var adminText = new StringBuilder();
|
||||
adminText.Append(admin.Name);
|
||||
|
||||
if (!string.IsNullOrEmpty(admin.Title))
|
||||
adminText.Append($": [{admin.Title}]");
|
||||
|
||||
if (admin.IsStealth)
|
||||
adminText.Append(" (S)");
|
||||
|
||||
if (admin.IsAfk)
|
||||
adminText.Append(" [AFK]");
|
||||
|
||||
var adminLabel = new Label
|
||||
{
|
||||
Text = adminText.ToString(),
|
||||
StyleClasses = { "LabelText" },
|
||||
HorizontalAlignment = Control.HAlignment.Left,
|
||||
Margin = new Thickness(16, 2, 8, 2)
|
||||
};
|
||||
AdminsContainer.AddChild(adminLabel);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Uninitialize();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
|
||||
<Control HorizontalExpand="True" MinWidth="5" />
|
||||
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
|
||||
<Control HorizontalExpand="True" MinWidth="5" />
|
||||
<Button Visible="True" Name="AdminWho" Access="Public" Text="{Loc 'admin-who-button'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
|
||||
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
|
||||
|
|
|
|||
|
|
@ -203,6 +203,14 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
{
|
||||
uiController.PopOut();
|
||||
};
|
||||
|
||||
// Sunrise-Start
|
||||
AdminWho.OnPressed += _ =>
|
||||
{
|
||||
var ctrl = _ui.GetUIController<AdminWhoUIController>();
|
||||
ctrl.Toggle();
|
||||
};
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
public void OnBwoink(NetUserId channel)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@
|
|||
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" />
|
||||
<RichTextLabel Name="RelayedToDiscordLabel" Access="Public" Visible="False" />
|
||||
<!-- Sunrise-Start -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 4 0 0">
|
||||
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}" Visible="False" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
<!-- Sunrise-End -->
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Threading;
|
||||
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BwoinkPanel : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private readonly Action<string> _messageSender;
|
||||
|
||||
public int Unread { get; private set; } = 0;
|
||||
|
|
@ -20,11 +24,14 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
// Sunrise-Start
|
||||
private DateTime? _lastDateHeader;
|
||||
public bool LoadDb { get; set; }
|
||||
private DateTime _cooldownEnd = DateTime.MinValue;
|
||||
private CancellationTokenSource? _cooldownCancellationTokenSource;
|
||||
// Sunrise-End
|
||||
|
||||
public BwoinkPanel(Action<string> messageSender)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this); // Sunrise-Edit
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
msg.PushColor(Color.LightGray);
|
||||
|
|
@ -41,6 +48,15 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
};
|
||||
SenderLineEdit.OnTextEntered += Input_OnTextEntered;
|
||||
SenderLineEdit.OnTextChanged += Input_OnTextChanged;
|
||||
|
||||
// Sunrise-Start
|
||||
AdminWhoButton.OnPressed += _ =>
|
||||
{
|
||||
var ctrl = _ui.GetUIController<AdminWhoUIController>();
|
||||
ctrl.Toggle();
|
||||
};
|
||||
// Sunrise-End
|
||||
|
||||
UpdateTypingIndicator();
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +65,10 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
if (string.IsNullOrWhiteSpace(args.Text))
|
||||
return;
|
||||
|
||||
// Check if we're still on cooldown
|
||||
if (DateTime.Now < _cooldownEnd)
|
||||
return;
|
||||
|
||||
_messageSender.Invoke(args.Text);
|
||||
SenderLineEdit.Clear();
|
||||
}
|
||||
|
|
@ -107,7 +127,7 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
return;
|
||||
|
||||
PeopleTyping.Add(name);
|
||||
Timer.Spawn(TimeSpan.FromSeconds(10), () =>
|
||||
Robust.Shared.Timing.Timer.Spawn(TimeSpan.FromSeconds(10), () =>
|
||||
{
|
||||
if (Disposed)
|
||||
return;
|
||||
|
|
@ -124,11 +144,38 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
UpdateTypingIndicator();
|
||||
}
|
||||
|
||||
public void OnCooldownReceived(BwoinkCooldownMessage message)
|
||||
{
|
||||
// Set cooldown end time
|
||||
_cooldownEnd = DateTime.Now.Add(message.RemainingCooldown);
|
||||
|
||||
// Disable input field and show feedback
|
||||
SenderLineEdit.Editable = false;
|
||||
SenderLineEdit.PlaceHolder = Loc.GetString("bwoink-cooldown-message",
|
||||
("seconds", $"{message.RemainingCooldown.TotalSeconds:F1}"));
|
||||
|
||||
// Clean up existing timer
|
||||
_cooldownCancellationTokenSource?.Cancel();
|
||||
_cooldownCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Set timer to re-enable input
|
||||
Robust.Shared.Timing.Timer.Spawn(message.RemainingCooldown, () =>
|
||||
{
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
SenderLineEdit.Editable = true;
|
||||
SenderLineEdit.PlaceHolder = Loc.GetString("bwoink-input-placeholder");
|
||||
}, _cooldownCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
InputTextChanged = null;
|
||||
_cooldownCancellationTokenSource?.Cancel();
|
||||
_cooldownCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ namespace Content.Client.Communications.UI
|
|||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
_menu.OnToggleRelay += ToggleRelayPressed; // Sunrise-Edit
|
||||
}
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
|
|
@ -58,6 +59,13 @@ namespace Content.Client.Communications.UI
|
|||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void ToggleRelayPressed()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleToggleRelayMessage());
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
public void CallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleCallEmergencyShuttleMessage());
|
||||
|
|
@ -91,6 +99,14 @@ namespace Content.Client.Communications.UI
|
|||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
|
||||
// Sunrise-Start
|
||||
_menu.CanRelay = commsState.CanRelay;
|
||||
_menu.IsRelaying = commsState.IsRelaying;
|
||||
_menu.RelayCooldownRemaining = commsState.RelayCooldownRemaining;
|
||||
_menu.RelayTimeRemaining = commsState.RelayTimeRemaining;
|
||||
_menu.UpdateRelayUi();
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,25 @@
|
|||
VerticalExpand="True"
|
||||
Margin="6 6 6 5">
|
||||
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<!-- ButtonsPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom"
|
||||
SeparationOverride="4">
|
||||
Margin="0 2">
|
||||
|
||||
<!-- AnnouncePart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-announcement-header'}"
|
||||
Name="AnnouncementLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
|
||||
|
|
@ -33,30 +35,66 @@
|
|||
Margin="0 0 1 0"
|
||||
Disabled="True"/>
|
||||
|
||||
<Button Name="BroadcastButton"
|
||||
<Button Name="BroadcastButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
</BoxContainer>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-alert-level-header'}"
|
||||
Name="AlertLevelLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-relay-header'}"
|
||||
Name="RelayLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Button Name="RelayButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
Text="{Loc 'comms-console-menu-relay-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-relay-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<RichTextLabel Name="RelayStatusLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<!-- EmergencyPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6">
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-emergency-header'}"
|
||||
Name="EmergencyLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
Text="Placeholder Text"
|
||||
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ namespace Content.Client.Communications.UI
|
|||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
// Sunrise-Start
|
||||
public bool CanRelay;
|
||||
public bool IsRelaying;
|
||||
public float RelayCooldownRemaining;
|
||||
public float RelayTimeRemaining;
|
||||
// Sunrise-End
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
|
|
@ -28,6 +34,7 @@ namespace Content.Client.Communications.UI
|
|||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
public event Action? OnToggleRelay; // Sunrise-Edit
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
{
|
||||
|
|
@ -58,6 +65,11 @@ namespace Content.Client.Communications.UI
|
|||
BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
BroadcastButton.Disabled = !CanBroadcast;
|
||||
|
||||
// Sunrise-Start
|
||||
RelayButton.OnPressed += _ => OnToggleRelay?.Invoke();
|
||||
RelayButton.Disabled = !CanRelay;
|
||||
// Sunrise-End
|
||||
|
||||
AlertLevelButton.OnItemSelected += args =>
|
||||
{
|
||||
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
|
||||
|
|
@ -78,6 +90,7 @@ namespace Content.Client.Communications.UI
|
|||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
UpdateRelayUi(); // Sunrise-Edit
|
||||
}
|
||||
|
||||
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
|
||||
|
|
@ -122,6 +135,7 @@ namespace Content.Client.Communications.UI
|
|||
if (!CountdownStarted)
|
||||
{
|
||||
CountdownLabel.SetMessage(string.Empty);
|
||||
CountdownLabel.Visible = false; // Sunrise-Edit
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle");
|
||||
return;
|
||||
}
|
||||
|
|
@ -132,6 +146,36 @@ namespace Content.Client.Communications.UI
|
|||
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
|
||||
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
|
||||
CountdownLabel.SetMessage(infoText);
|
||||
CountdownLabel.Visible = true; // Sunrise-Edit
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public void UpdateRelayUi()
|
||||
{
|
||||
RelayButton.Disabled = !CanRelay && !IsRelaying;
|
||||
if (IsRelaying)
|
||||
{
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-stop");
|
||||
var remaining = TimeSpan.FromSeconds(Math.Max(0f, RelayTimeRemaining));
|
||||
var text = Loc.GetString("comms-console-menu-relay-time-left", ("time", remaining.ToString(@"mm\:ss", CultureInfo.CurrentCulture)));
|
||||
RelayStatusLabel.Visible = true;
|
||||
RelayStatusLabel.SetMessage(text);
|
||||
}
|
||||
else if (RelayCooldownRemaining > 0f)
|
||||
{
|
||||
var remaining = TimeSpan.FromSeconds(RelayCooldownRemaining);
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-button");
|
||||
RelayStatusLabel.SetMessage(Loc.GetString("comms-console-menu-relay-cooldown", ("time", remaining.ToString(@"mm\:ss", CultureInfo.CurrentCulture))));
|
||||
RelayStatusLabel.Visible = true;
|
||||
RelayButton.Disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-button");
|
||||
RelayStatusLabel.Visible = false;
|
||||
RelayStatusLabel.SetMessage(string.Empty);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +177,11 @@
|
|||
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"/>
|
||||
<Button Name="OptionsButton" Access="Public" Text="{Loc 'ui-lobby-options-button'}"
|
||||
StyleClasses="ButtonBig"/>
|
||||
<Button Name="LeaveButton" Access="Public" Text="{Loc 'ui-lobby-leave-button'}"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ using Robust.Client.Graphics;
|
|||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Lobby.UI
|
||||
{
|
||||
|
|
@ -27,6 +28,7 @@ namespace Content.Client.Lobby.UI
|
|||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IUriOpener _uriOpener = default!;
|
||||
|
||||
public string LobbyParallax = "FastSpace"; // Sunrise-edit
|
||||
public bool ShowParallax; // Sunrise-edit
|
||||
|
|
@ -53,6 +55,7 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
|
||||
OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
|
||||
ReplaysButton.OnPressed += _ => _uriOpener.OpenUri(_configurationManager.GetCVar(SunriseCCVars.InfoLinksReplays));
|
||||
|
||||
//CollapseButton.OnPressed += _ => TogglePanel(false);
|
||||
//ExpandButton.OnPressed += _ => TogglePanel(true);
|
||||
|
|
@ -146,6 +149,7 @@ namespace Content.Client.Lobby.UI
|
|||
_configurationManager.OnValueChanged(SunriseCCVars.ServersHubEnable, OnServersHubEnableChanged, true);
|
||||
_configurationManager.OnValueChanged(SunriseCCVars.ServiceAuthEnabled, OnServiceAuthEnableChanged, true);
|
||||
_configurationManager.OnValueChanged(SunriseCCVars.ServerName, OnServerNameChanged, true);
|
||||
_configurationManager.OnValueChanged(SunriseCCVars.InfoLinksReplays, OnReplaysLinkChanged, true);
|
||||
|
||||
Chat.SetChatOpacity();
|
||||
|
||||
|
|
@ -216,6 +220,11 @@ namespace Content.Client.Lobby.UI
|
|||
{
|
||||
UserProfileBox.Visible = enable;
|
||||
}
|
||||
|
||||
private void OnReplaysLinkChanged(string replaysUrl)
|
||||
{
|
||||
ReplaysButton.Visible = !string.IsNullOrEmpty(replaysUrl);
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
private void OnLobbyOpacityChanged(float opacity)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<!-- Sunrise-Start -->
|
||||
<Button Access="Public" Name="RoadmapButton" Text="{Loc 'ui-roadmap'}" StyleClasses="Caution" />
|
||||
<Button Access="Public" Name="DonateButton" Text="{Loc 'ui-escape-donate'}" />
|
||||
<Button Access="Public" Name="ReplaysButton" Text="{Loc 'ui-escape-replays'}" />
|
||||
<!-- Sunrise-End -->
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
|
||||
<Button Access="Public" Name="RulesButton" Text="{Loc 'ui-escape-rules'}" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
|||
{
|
||||
_bwoinkSystem = system;
|
||||
_bwoinkSystem.OnBwoinkTextMessageRecieved += ReceivedBwoink;
|
||||
_bwoinkSystem.OnBwoinkCooldownReceived += ReceivedCooldown;
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenAHelp,
|
||||
|
|
@ -114,6 +115,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
|||
|
||||
DebugTools.Assert(_bwoinkSystem != null);
|
||||
_bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;
|
||||
_bwoinkSystem!.OnBwoinkCooldownReceived -= ReceivedCooldown;
|
||||
_bwoinkSystem = null;
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +160,12 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
|||
UIHelper!.Receive(message);
|
||||
}
|
||||
|
||||
private void ReceivedCooldown(object? sender, BwoinkCooldownMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper?.OnCooldownReceived(message);
|
||||
}
|
||||
|
||||
private void DiscordRelayUpdated(BwoinkDiscordRelayUpdated args, EntitySessionEventArgs session)
|
||||
{
|
||||
_discordRelayActive = args.DiscordRelayEnabled;
|
||||
|
|
@ -342,6 +350,7 @@ public interface IAHelpUIHandler : IDisposable
|
|||
public void ToggleWindow();
|
||||
public void DiscordRelayChanged(bool active);
|
||||
public void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args);
|
||||
public void OnCooldownReceived(BwoinkCooldownMessage message);
|
||||
public event Action OnClose;
|
||||
public event Action OnOpen;
|
||||
public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
|
||||
|
|
@ -432,6 +441,16 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
|||
panel.UpdatePlayerTyping(args.PlayerName, args.Typing);
|
||||
}
|
||||
|
||||
public void OnCooldownReceived(BwoinkCooldownMessage message)
|
||||
{
|
||||
// For admins, we might want to show a message in the currently active panel
|
||||
// For now, we'll pass it to all panels to handle
|
||||
foreach (var (_, panel) in _activePanelMap)
|
||||
{
|
||||
panel.OnCooldownReceived(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public void SetLoadDb(NetUserId userId)
|
||||
{
|
||||
|
|
@ -621,6 +640,11 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
|||
{
|
||||
}
|
||||
|
||||
public void OnCooldownReceived(BwoinkCooldownMessage message)
|
||||
{
|
||||
_chatPanel?.OnCooldownReceived(message);
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
|
||||
|
|
@ -637,6 +661,10 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
|||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true, false));
|
||||
// Sunrise-Start
|
||||
_chatPanel.AHelpDescLabel.Visible = true;
|
||||
_chatPanel.AdminWhoButton.Visible = true;
|
||||
// Sunrise-End
|
||||
_chatPanel.InputTextChanged += text => InputTextChanged?.Invoke(_ownerId, text);
|
||||
_chatPanel.RelayedToDiscordLabel.Visible = relayActive;
|
||||
_window = new DefaultWindow()
|
||||
|
|
@ -644,7 +672,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(); };
|
||||
|
|
|
|||
|
|
@ -106,6 +106,11 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
|||
{
|
||||
_uri.OpenUri(_cfg.GetCVar(SunriseCCVars.InfoLinksDonate));
|
||||
};
|
||||
|
||||
_escapeWindow.ReplaysButton.OnPressed += _ =>
|
||||
{
|
||||
_uri.OpenUri(_cfg.GetCVar(SunriseCCVars.InfoLinksReplays));
|
||||
};
|
||||
// Sunrise-end
|
||||
|
||||
_escapeWindow.WikiButton.OnPressed += _ =>
|
||||
|
|
@ -123,6 +128,8 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
|||
|
||||
_escapeWindow.DonateButton.Visible = _cfg.GetCVar(SunriseCCVars.InfoLinksDonate) != ""; // Sunrise-Sponsors
|
||||
|
||||
_escapeWindow.ReplaysButton.Visible = _cfg.GetCVar(SunriseCCVars.InfoLinksReplays) != ""; // Sunrise-Replays
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(EngineKeyFunctions.EscapeMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
|
|
|
|||
|
|
@ -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" Margin="0 0 10 0"/>
|
||||
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}" Margin="0 0 10 0" />
|
||||
<Button Name="StatisticsButton" Access="Public" Text="{Loc 'mentor-help-statistics'}"
|
||||
StyleClasses="ButtonBig" Visible="False" Margin="0 0 10 0" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Name="BackToListButton" Access="Public" Text="{Loc 'mentor-help-back-to-list'}"
|
||||
StyleClasses="ButtonBig" HorizontalAlignment="Right" 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>
|
||||
434
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
Normal file
434
Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
using System.Linq;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI.Bwoink;
|
||||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.State;
|
||||
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;
|
||||
|
||||
AdminWhoButton.OnPressed += _ =>
|
||||
{
|
||||
var ctrl = _ui.GetUIController<AdminWhoUIController>();
|
||||
ctrl.Toggle();
|
||||
};
|
||||
|
||||
// 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(256)", maxLength: 256, 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(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.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(256)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
Content.Server/Administration/Systems/AdminWhoSystem.cs
Normal file
64
Content.Server/Administration/Systems/AdminWhoSystem.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Afk;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Server system for handling admin who requests
|
||||
/// </summary>
|
||||
public sealed class AdminWhoSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAfkManager _afkManager = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<RequestAdminWhoEvent>(OnRequestAdminWho);
|
||||
}
|
||||
|
||||
private void OnRequestAdminWho(RequestAdminWhoEvent args, EntitySessionEventArgs session)
|
||||
{
|
||||
if (session.SenderSession == null)
|
||||
return;
|
||||
|
||||
var seeStealth = false;
|
||||
var seeAfk = false;
|
||||
|
||||
// Check if the requesting player can see stealth admins and AFK status
|
||||
if (session.SenderSession.AttachedEntity != null)
|
||||
{
|
||||
var playerData = _adminManager.GetAdminData(session.SenderSession);
|
||||
if (playerData != null)
|
||||
{
|
||||
seeStealth = playerData.CanStealth();
|
||||
seeAfk = _adminManager.HasAdminFlag(session.SenderSession, AdminFlags.Admin);
|
||||
}
|
||||
}
|
||||
|
||||
var adminList = new List<AdminWhoEntry>();
|
||||
|
||||
foreach (var admin in _adminManager.ActiveAdmins)
|
||||
{
|
||||
var adminData = _adminManager.GetAdminData(admin);
|
||||
DebugTools.AssertNotNull(adminData);
|
||||
|
||||
if (adminData!.Stealth && !seeStealth)
|
||||
continue;
|
||||
|
||||
var isAfk = seeAfk && _afkManager.IsAfk(admin);
|
||||
|
||||
adminList.Add(new AdminWhoEntry(
|
||||
admin.Name,
|
||||
adminData.Title,
|
||||
adminData.Stealth,
|
||||
isAfk
|
||||
));
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new AdminWhoResponseEvent(adminList), session.SenderSession);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]";
|
||||
}
|
||||
|
|
@ -762,8 +762,12 @@ namespace Content.Server.Administration.Systems
|
|||
// Based on Starlight Build: https://github.com/ss14Starlight/space-station-14/pull/85
|
||||
var currentTime = _timing.RealTime;
|
||||
|
||||
if (IsOnCooldown(message.UserId, currentTime))
|
||||
if (IsOnCooldown(message.UserId, currentTime, out var remainingCooldown))
|
||||
{
|
||||
// Send cooldown feedback to the client
|
||||
RaiseNetworkEvent(new BwoinkCooldownMessage(remainingCooldown), senderSession.Channel);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsSpam(message.UserId, message.Text))
|
||||
_banManager.CreateServerBan(senderSession.UserId, senderSession.Name, null, null, null, 180, NoteSeverity.High, Loc.GetString("ahelp-antispam-ban-reason"));
|
||||
|
|
@ -1019,14 +1023,31 @@ namespace Content.Server.Administration.Systems
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsOnCooldown(NetUserId channelId, TimeSpan currentTime)
|
||||
private bool IsOnCooldown(NetUserId channelId, TimeSpan currentTime, out TimeSpan remainingCooldown)
|
||||
{
|
||||
remainingCooldown = TimeSpan.Zero;
|
||||
|
||||
var lastMessage = _recentMessages
|
||||
.Where(msg => msg.Channel == channelId)
|
||||
.OrderByDescending(msg => msg.Timestamp)
|
||||
.FirstOrDefault();
|
||||
|
||||
return lastMessage != default && (currentTime - lastMessage.Timestamp) < _messageCooldown;
|
||||
if (lastMessage == default)
|
||||
return false;
|
||||
|
||||
var timeSinceLastMessage = currentTime - lastMessage.Timestamp;
|
||||
if (timeSinceLastMessage < _messageCooldown)
|
||||
{
|
||||
remainingCooldown = _messageCooldown - timeSinceLastMessage;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsOnCooldown(NetUserId channelId, TimeSpan currentTime)
|
||||
{
|
||||
return IsOnCooldown(channelId, currentTime, out _);
|
||||
}
|
||||
|
||||
private bool IsSpam(NetUserId channelId, string text)
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
// Sunrise-end
|
||||
|
||||
// Sunrise-start - Use speaker network for station announcements
|
||||
if (playTts && (playDefault || announcementSound != null))
|
||||
if (playTts)
|
||||
{
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
|
|
|||
|
|
@ -80,6 +80,24 @@ namespace Content.Server.Communications
|
|||
// Sunrise-Start
|
||||
[DataField("announceVoice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
|
||||
public string AnnounceVoice = "Hanson";
|
||||
// Sunrise-Start
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsRelaying;
|
||||
|
||||
[ViewVariables]
|
||||
public float RelayTimeRemaining;
|
||||
|
||||
[ViewVariables]
|
||||
public float RelayCooldownRemaining;
|
||||
|
||||
[DataField]
|
||||
public float RelayDuration = 60f;
|
||||
|
||||
[DataField]
|
||||
public float RelayCooldown = 300f;
|
||||
|
||||
[DataField]
|
||||
public float RelayRange = 7f;
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ using Content.Server.AlertLevel;
|
|||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Screens.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared._Sunrise.TTS; // Sunrise-edit
|
||||
using Content.Shared._Sunrise.TTS;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
|
|
@ -18,6 +19,9 @@ using Content.Shared.DeviceNetwork;
|
|||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
|
|
@ -55,6 +59,11 @@ namespace Content.Server.Communications
|
|||
|
||||
// On console init, set cooldown
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, MapInitEvent>(OnCommunicationsConsoleMapInit);
|
||||
|
||||
// Sunrise-Start
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleToggleRelayMessage>(OnToggleRelayMessage);
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, ListenEvent>(OnEntitySpokeNearbyRelay);
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
|
@ -68,6 +77,23 @@ namespace Content.Server.Communications
|
|||
comp.AnnouncementCooldownRemaining -= frameTime;
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
if (comp.RelayCooldownRemaining > 0f)
|
||||
comp.RelayCooldownRemaining -= frameTime;
|
||||
|
||||
if (comp.IsRelaying)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
StopRelay(uid, comp, announce: true);
|
||||
else
|
||||
{
|
||||
comp.RelayTimeRemaining -= frameTime;
|
||||
if (comp.RelayTimeRemaining <= 0f)
|
||||
StopRelay(uid, comp, announce: true);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
comp.UIUpdateAccumulator += frameTime;
|
||||
|
||||
if (comp.UIUpdateAccumulator < UIUpdateInterval)
|
||||
|
|
@ -159,13 +185,20 @@ namespace Content.Server.Communications
|
|||
}
|
||||
}
|
||||
|
||||
var canRelay = comp.RelayCooldownRemaining <= 0f && !comp.IsRelaying && this.IsPowered(uid, EntityManager); // Sunrise-Edit
|
||||
_uiSystem.SetUiState(uid, CommunicationsConsoleUiKey.Key, new CommunicationsConsoleInterfaceState(
|
||||
CanAnnounce(comp),
|
||||
CanCallOrRecall(comp),
|
||||
levels,
|
||||
currentLevel,
|
||||
currentDelay,
|
||||
_roundEndSystem.ExpectedCountdownEnd
|
||||
_roundEndSystem.ExpectedCountdownEnd,
|
||||
// Sunrise-Start
|
||||
canRelay,
|
||||
comp.IsRelaying,
|
||||
MathF.Max(0f, comp.RelayCooldownRemaining),
|
||||
MathF.Max(0f, comp.RelayTimeRemaining)
|
||||
// Sunrise-End
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -338,6 +371,67 @@ namespace Content.Server.Communications
|
|||
_roundEndSystem.CancelRoundEndCountdown(uid);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(message.Actor):player} has recalled the shuttle.");
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void OnToggleRelayMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleToggleRelayMessage message)
|
||||
{
|
||||
if (comp.IsRelaying)
|
||||
{
|
||||
StopRelay(uid, comp, announce: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp.RelayCooldownRemaining > 0f)
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
comp.IsRelaying = true;
|
||||
comp.RelayTimeRemaining = comp.RelayDuration;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
EnsureComp<ActiveListenerComponent>(uid).Range = comp.RelayRange;
|
||||
|
||||
var startText = Loc.GetString("comms-console-relay-started");
|
||||
var title = Loc.GetString(comp.Title);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, startText, sender: title, playDefault: true, playTts: true, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice, announcementSound: comp.Sound);
|
||||
}
|
||||
|
||||
private void OnEntitySpokeNearbyRelay(EntityUid uid, CommunicationsConsoleComponent comp, ListenEvent ev)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
var voice = comp.AnnounceVoice;
|
||||
if (TryComp<TTSComponent>(ev.Source, out var ttsComponent))
|
||||
{
|
||||
voice = ttsComponent.VoicePrototypeId;
|
||||
}
|
||||
_chatSystem.DispatchStationAnnouncement(uid, ev.Message, sender: Loc.GetString(comp.Title), playDefault: false, playTts: true, colorOverride: comp.Color, announceVoice: voice);
|
||||
}
|
||||
|
||||
private void StopRelay(EntityUid uid, CommunicationsConsoleComponent comp, bool announce)
|
||||
{
|
||||
if (!comp.IsRelaying && comp.RelayCooldownRemaining > 0f)
|
||||
{
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
return;
|
||||
}
|
||||
|
||||
comp.IsRelaying = false;
|
||||
comp.RelayTimeRemaining = 0f;
|
||||
comp.RelayCooldownRemaining = comp.RelayCooldown;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
RemCompDeferred<ActiveListenerComponent>(uid);
|
||||
|
||||
if (announce)
|
||||
{
|
||||
var stopText = Loc.GetString("comms-console-relay-stopped");
|
||||
var title = Loc.GetString(comp.Title);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, stopText, sender: title, playDefault: true, playTts: true, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice, announcementSound: comp.Sound);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -59,11 +59,10 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
|
||||
SubscribeLocalEvent<EnergyDomeGeneratorComponent, ComponentRemove>(OnComponentRemove);
|
||||
|
||||
//Dome events
|
||||
SubscribeLocalEvent<EnergyDomeComponent, DamageChangedEvent>(OnDomeDamaged);
|
||||
SubscribeLocalEvent<EnergyDomeProtectedUserComponent, EntParentChangedMessage>(OnProtectedEntityParentChanged);
|
||||
}
|
||||
|
||||
|
||||
private void OnInit(Entity<EnergyDomeGeneratorComponent> generator, ref MapInitEvent args)
|
||||
{
|
||||
if (generator.Comp.CanDeviceNetworkUse)
|
||||
|
|
@ -159,7 +158,6 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
if (args.Handled)
|
||||
return;
|
||||
|
||||
// Sunrise-Start
|
||||
if (!_containerSystem.ContainsEntity(args.Performer, generator.Owner))
|
||||
return;
|
||||
|
||||
|
|
@ -168,7 +166,6 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
if (!_biocodeSystem.CanUse(args.Performer, biocodedComponent.Factions))
|
||||
return;
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
AttemptToggle(generator, !generator.Comp.Enabled);
|
||||
|
||||
|
|
@ -251,6 +248,12 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
|
||||
public bool AttemptToggle(Entity<EnergyDomeGeneratorComponent> generator, bool status)
|
||||
{
|
||||
var parent = Transform(generator.Owner).ParentUid;
|
||||
if (HasComp<ContainerManagerComponent>(Transform(parent).ParentUid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryComp<UseDelayComponent>(generator, out var useDelay) && _useDelay.IsDelayed(new (generator, useDelay)))
|
||||
{
|
||||
_audio.PlayPvs(generator.Comp.TurnOffSound, generator);
|
||||
|
|
@ -321,6 +324,9 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
domeComp.Generator = generator;
|
||||
}
|
||||
|
||||
var protectedComp = EnsureComp<EnergyDomeProtectedUserComponent>(protectedEntity);
|
||||
protectedComp.DomeEntity = newDome;
|
||||
|
||||
if (TryComp<PowerCellDrawComponent>(generator.Owner, out var powerCellDrawComponent))
|
||||
{
|
||||
_powerCell.SetDrawEnabled(generator.Owner, true);
|
||||
|
|
@ -335,6 +341,23 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
generator.Comp.Enabled = true;
|
||||
}
|
||||
|
||||
// Sunrise: обработчик смены парента защищаемой сущности
|
||||
private void OnProtectedEntityParentChanged(Entity<EnergyDomeProtectedUserComponent> ent, ref EntParentChangedMessage args)
|
||||
{
|
||||
if (ent.Comp.DomeEntity == null)
|
||||
return;
|
||||
if (HasComp<ContainerManagerComponent>(Transform(ent).ParentUid))
|
||||
{
|
||||
if (TryComp<EnergyDomeComponent>(ent.Comp.DomeEntity.Value, out var domeComp) && domeComp.Generator != null)
|
||||
{
|
||||
if (TryComp<EnergyDomeGeneratorComponent>(domeComp.Generator.Value, out var genComp))
|
||||
{
|
||||
TurnOff((domeComp.Generator.Value, genComp), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TurnOff(Entity<EnergyDomeGeneratorComponent> generator, bool startReloading)
|
||||
{
|
||||
if (!generator.Comp.Enabled)
|
||||
|
|
@ -343,6 +366,11 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
generator.Comp.Enabled = false;
|
||||
QueueDel(generator.Comp.SpawnedDome);
|
||||
|
||||
if (generator.Comp.DomeParentEntity != null && HasComp<EnergyDomeProtectedUserComponent>(generator.Comp.DomeParentEntity.Value))
|
||||
{
|
||||
RemCompDeferred<EnergyDomeProtectedUserComponent>(generator.Comp.DomeParentEntity.Value);
|
||||
}
|
||||
|
||||
if (TryComp<PowerCellDrawComponent>(generator.Owner, out var powerCellDrawComponent))
|
||||
{
|
||||
_powerCell.SetDrawEnabled(generator.Owner, false);
|
||||
|
|
@ -372,3 +400,4 @@ public sealed partial class EnergyDomeSystem : EntitySystem
|
|||
: entity;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Content.Server.Ghost;
|
|||
using Content.Server.Roles.Jobs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Server.Player;
|
||||
|
|
@ -14,6 +15,9 @@ using Robust.Shared.Player;
|
|||
using Robust.Shared.Timing;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Voting;
|
||||
|
||||
|
|
@ -28,6 +32,8 @@ public sealed class VotingSystem : EntitySystem
|
|||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -75,13 +81,47 @@ public sealed class VotingSystem : EntitySystem
|
|||
public string GetPlayerVoteListName(EntityUid attached)
|
||||
{
|
||||
TryComp<MindContainerComponent>(attached, out var mind);
|
||||
|
||||
var jobName = _jobs.MindTryGetJobName(mind?.Mind);
|
||||
|
||||
var jobName = GetNonAntagJobName(mind?.Mind);
|
||||
var playerInfo = $"{Comp<MetaDataComponent>(attached).EntityName} ({jobName})";
|
||||
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job name for a mind, excluding antagonist roles to prevent revealing game mode information in voting.
|
||||
/// Returns the first non-antagonist job role found, or "Unknown" if none exists.
|
||||
/// </summary>
|
||||
private string GetNonAntagJobName(EntityUid? mindId)
|
||||
{
|
||||
if (mindId == null)
|
||||
return Loc.GetString("generic-unknown-title");
|
||||
|
||||
if (!TryComp<MindComponent>(mindId.Value, out var mindComponent))
|
||||
return Loc.GetString("generic-unknown-title");
|
||||
|
||||
// Look for a job role that is NOT an antagonist
|
||||
foreach (var roleEnt in mindComponent.MindRoleContainer.ContainedEntities)
|
||||
{
|
||||
if (!TryComp<MindRoleComponent>(roleEnt, out var roleComponent))
|
||||
continue;
|
||||
|
||||
// Skip antagonist roles to prevent revealing game mode
|
||||
if (roleComponent.Antag || roleComponent.ExclusiveAntag)
|
||||
continue;
|
||||
|
||||
// We found a non-antagonist role with a job
|
||||
if (roleComponent.JobPrototype != null &&
|
||||
_prototypes.TryIndex(roleComponent.JobPrototype.Value, out var jobPrototype))
|
||||
{
|
||||
return jobPrototype.LocalizedName;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no non-antagonist job found
|
||||
return Loc.GetString("generic-unknown-title");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether the player initiating a votekick is allowed to do so serverside.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Server._Sunrise.EnergyShield;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server._Sunrise.EnergyShield;
|
||||
|
||||
|
|
@ -21,6 +15,7 @@ public sealed class EnergyShieldSystem : EntitySystem
|
|||
[Dependency] private readonly ItemToggleSystem _itemToggle = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
|
|||
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>
|
||||
|
|
|
|||
45
Content.Shared/Administration/AdminWhoSystem.cs
Normal file
45
Content.Shared/Administration/AdminWhoSystem.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
/// <summary>
|
||||
/// Request event to get the list of online administrators
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RequestAdminWhoEvent : EntityEventArgs
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response event containing the list of online administrators
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AdminWhoResponseEvent : EntityEventArgs
|
||||
{
|
||||
public readonly List<AdminWhoEntry> Admins;
|
||||
|
||||
public AdminWhoResponseEvent(List<AdminWhoEntry> admins)
|
||||
{
|
||||
Admins = admins;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single administrator
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AdminWhoEntry
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string? Title;
|
||||
public readonly bool IsStealth;
|
||||
public readonly bool IsAfk;
|
||||
|
||||
public AdminWhoEntry(string name, string? title, bool isStealth, bool isAfk)
|
||||
{
|
||||
Name = name;
|
||||
Title = title;
|
||||
IsStealth = isStealth;
|
||||
IsAfk = isAfk;
|
||||
}
|
||||
}
|
||||
|
|
@ -137,4 +137,18 @@ namespace Content.Shared.Administration
|
|||
Typing = typing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent by server to notify a client when their message was blocked due to cooldown.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BwoinkCooldownMessage : EntityEventArgs
|
||||
{
|
||||
public TimeSpan RemainingCooldown { get; }
|
||||
|
||||
public BwoinkCooldownMessage(TimeSpan remainingCooldown)
|
||||
{
|
||||
RemainingCooldown = remainingCooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,14 @@ namespace Content.Shared.Communications
|
|||
public List<string>? AlertLevels;
|
||||
public string CurrentAlert;
|
||||
public float CurrentAlertDelay;
|
||||
// Sunrise-Start
|
||||
public readonly bool CanRelay;
|
||||
public readonly bool IsRelaying;
|
||||
public readonly float RelayCooldownRemaining;
|
||||
public readonly float RelayTimeRemaining;
|
||||
// Sunrise-End
|
||||
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null, bool canRelay = false, bool isRelaying = false, float relayCooldownRemaining = 0f, float relayTimeRemaining = 0f) // Sunrise-Edit
|
||||
{
|
||||
CanAnnounce = canAnnounce;
|
||||
CanCall = canCall;
|
||||
|
|
@ -28,6 +34,12 @@ namespace Content.Shared.Communications
|
|||
AlertLevels = alertLevels;
|
||||
CurrentAlert = currentAlert;
|
||||
CurrentAlertDelay = currentAlertDelay;
|
||||
// Sunrise-Start
|
||||
CanRelay = canRelay;
|
||||
IsRelaying = isRelaying;
|
||||
RelayCooldownRemaining = relayCooldownRemaining;
|
||||
RelayTimeRemaining = relayTimeRemaining;
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +75,13 @@ namespace Content.Shared.Communications
|
|||
}
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CommunicationsConsoleToggleRelayMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CommunicationsConsoleCallEmergencyShuttleMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Content.Shared.EnergyDome;
|
||||
|
||||
/// <summary>
|
||||
/// marker component that allows linking the dome generator with the dome itself
|
||||
/// </summary>
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class EnergyDomeProtectedUserComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntityUid? DomeEntity;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,6 +191,12 @@ public sealed partial class SunriseCCVars : CVars
|
|||
public static readonly CVarDef<string> InfoLinksDonate =
|
||||
CVarDef.Create("infolinks.donate", "", CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Link to replays to show in menus.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> InfoLinksReplays =
|
||||
CVarDef.Create("infolinks.replays", "https://t.me/ss14_replays", CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/**
|
||||
* Lobby
|
||||
*/
|
||||
|
|
@ -559,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.
|
|
@ -21972,3 +21972,132 @@
|
|||
id: 1435
|
||||
time: '2025-09-04T23:34:23.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3137
|
||||
- author: ReWAFFlution
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
|
||||
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0447\u0438\u043D\u0438\u0442\
|
||||
\u044C \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0449\u0438\u0442\
|
||||
\u044B \u0441\u0432\u0430\u0440\u043A\u043E\u0439."
|
||||
type: Add
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
|
||||
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u043A \u043E\u0441\u043C\u043E\
|
||||
\u0442\u0440\u0443 \u043F\u043E\u0432\u0440\u0435\u0436\u0434\u0435\u043D\u0438\
|
||||
\u0439 \u0449\u0438\u0442\u043E\u0432."
|
||||
type: Add
|
||||
id: 1436
|
||||
time: '2025-09-07T09:06:53.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3082
|
||||
- author: Copilot AI
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u043A\u043D\u043E\
|
||||
\u043F\u043A\u0430 \"\u0420\u0435\u043F\u043B\u0435\u0438\" \u0432 \u043C\u0435\
|
||||
\u043D\u044E ESC \u0438 \u043B\u043E\u0431\u0431\u0438 \u0434\u043B\u044F \u0434\
|
||||
\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\
|
||||
\u0435 \u0440\u0435\u043F\u043B\u0435\u0435\u0432."
|
||||
type: Add
|
||||
id: 1437
|
||||
time: '2025-09-08T17:26:10.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3173
|
||||
- author: Hero_010
|
||||
changes:
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u043B\u043E\
|
||||
\u043A\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F \u0445\u0438\u0440\u0443\
|
||||
\u0440\u0433\u0438\u0438 \u0443 \u0410\u0440\u043A\u0430\u043D."
|
||||
type: Fix
|
||||
id: 1438
|
||||
time: '2025-09-08T17:26:50.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3115
|
||||
- author: KaiserMaus
|
||||
changes:
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B ERROR\u043A\
|
||||
\u0438 \u043F\u0440\u0438 \u043F\u0438\u0441\u044C\u043C\u0435 \u0443 \u0434\
|
||||
\u0438\u043E\u043D."
|
||||
type: Fix
|
||||
id: 1439
|
||||
time: '2025-09-09T13:19:34.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3185
|
||||
- author: VigersRay, Copilot AI
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u043E\u0442\u0434\u0435\
|
||||
\u043B\u044C\u043D\u044B\u0439 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\
|
||||
\u0441 \u0434\u043B\u044F \u043C\u0435\u043D\u0442\u043E\u0440\u043E\u0432 \u0440\
|
||||
\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u043F\u043E \u043F\u0440\
|
||||
\u0438\u043D\u0446\u0438\u043F\u0443 \u0442\u0438\u043A\u0435\u0442\u043E\u0432\
|
||||
."
|
||||
type: Add
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0441\u0442\u0430\
|
||||
\u0442\u0438\u0441\u0442\u0438\u043A\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\
|
||||
\u043E\u0441\u0442\u0438 \u0434\u043B\u044F \u043C\u0435\u043D\u0442\u043E\u0440\
|
||||
\u043E\u0432."
|
||||
type: Add
|
||||
- message: "F1 \u0442\u0435\u043F\u0435\u0440\u044C \u043E\u0442\u043A\u0440\u044B\
|
||||
\u0432\u0430\u0435\u0442 \u0432\u044B\u0431\u043E\u0440 \u043C\u0435\u0436\u0434\
|
||||
\u0443 \u0430\u0445\u0435\u043B\u043F\u043E\u043C \u0438 \u043C\u0435\u043D\u0442\
|
||||
\u043E\u0440 \u0445\u0435\u043B\u043F\u043E\u043C."
|
||||
type: Tweak
|
||||
id: 1440
|
||||
time: '2025-09-09T17:54:52.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/2915
|
||||
- author: Copilot AI, VigersRay
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u043A\u043D\u043E\
|
||||
\u043F\u043A\u0430 \"\u0410\u0434\u043C\u0438\u043D\u044B \u043E\u043D\u043B\
|
||||
\u0430\u0439\u043D\" \u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\
|
||||
\u0441 AHelp \u0438 Mhelp \u0434\u043B\u044F \u0443\u0434\u043E\u0431\u043D\u043E\
|
||||
\u0433\u043E \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0441\u043F\
|
||||
\u0438\u0441\u043A\u0430 \u043E\u043D\u043B\u0430\u0439\u043D \u0430\u0434\u043C\
|
||||
\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u043E\u0432 \u0438\
|
||||
\ \u043C\u0435\u043D\u0442\u043E\u0440\u043E\u0432."
|
||||
type: Add
|
||||
id: 1441
|
||||
time: '2025-09-09T19:06:57.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/2918
|
||||
- author: VigersRay
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
|
||||
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0432\u043A\u043B\u044E\u0447\
|
||||
\u0438\u0442\u044C \u043D\u0430 60 \u0441\u0435\u043A\u0443\u043D\u0434 \u0442\
|
||||
\u0440\u0430\u043D\u0441\u043B\u044F\u0446\u0438\u044E \u0440\u0435\u0447\u0438\
|
||||
\ \u0447\u0435\u0440\u0435\u0437 \u043A\u043E\u043D\u0441\u043E\u043B\u044C\
|
||||
\ \u0441\u0432\u044F\u0437\u0438. \u041F\u0435\u0440\u0435\u0437\u0430\u0440\
|
||||
\u044F\u0434\u043A\u0430 5 \u043C\u0438\u043D\u0443\u0442."
|
||||
type: Add
|
||||
- message: "\u0423\u043B\u0443\u0447\u0448\u0435\u043D \u0438\u043D\u0442\u0435\u0440\
|
||||
\u0444\u0435\u0439\u0441 \u043A\u043E\u043D\u0441\u043E\u043B\u0438 \u0441\u0432\
|
||||
\u044F\u0437\u0438."
|
||||
type: Tweak
|
||||
id: 1442
|
||||
time: '2025-09-09T19:15:38.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3070
|
||||
- author: Copilot AI
|
||||
changes:
|
||||
- message: "\u0412 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u0438\
|
||||
\ \u0437\u0430 \u043A\u0438\u043A \u0430\u043D\u0442\u0430\u0433\u043E\u043D\
|
||||
\u0438\u0441\u0442\u044B \u0442\u0435\u043F\u0435\u0440\u044C \u0431\u0443\u0434\
|
||||
\u0443\u0442 \u0438\u043C\u0435\u0442\u044C \u0434\u0440\u0443\u0433\u0438\u0435\
|
||||
\ \u0438\u043C\u0435\u043D\u0430 \u0434\u043B\u044F \u043F\u0440\u0435\u0434\
|
||||
\u043E\u0442\u0432\u0440\u0430\u0449\u0435\u043D\u0438\u044F \u043F\u043E\u043B\
|
||||
\u0443\u0447\u0435\u043D\u0438\u044F \u043C\u0435\u0442\u0430 \u0438\u043D\u0444\
|
||||
\u043E\u0440\u043C\u0430\u0446\u0438\u0438."
|
||||
type: Fix
|
||||
id: 1443
|
||||
time: '2025-09-09T19:18:06.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3012
|
||||
- author: Copilot AI
|
||||
changes:
|
||||
- message: "\u0417\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u0430 \u0430\u043A\u0442\
|
||||
\u0438\u0432\u0430\u0446\u0438\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0442\
|
||||
\u043E\u0440\u0430 \u0449\u0438\u0442\u0430 \u0432\u043D\u0443\u0442\u0440\u0438\
|
||||
\ \u043C\u0435\u0445\u043E\u0432 \u0438 \u043A\u043E\u043D\u0442\u0435\u0439\
|
||||
\u043D\u0435\u0440\u043E\u0432."
|
||||
type: Fix
|
||||
- message: "\u0413\u0435\u043D\u0435\u0440\u0430\u0442\u043E\u0440\u044B \u0449\u0438\
|
||||
\u0442\u0430 \u0442\u0435\u043F\u0435\u0440\u044C \u0430\u0432\u0442\u043E\u043C\
|
||||
\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0442\u043A\u043B\u044E\
|
||||
\u0447\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0438 \u0432\u0445\u043E\u0434\
|
||||
\u0435 \u0432 \u043C\u0435\u0445\u0438 \u0438\u043B\u0438 \u043A\u043E\u043D\
|
||||
\u0442\u0435\u0439\u043D\u0435\u0440\u044B."
|
||||
type: Fix
|
||||
id: 1444
|
||||
time: '2025-09-09T23:46:20.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/2997
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
admin-who-button = Admins Online
|
||||
admin-who-title = Online Administrators
|
||||
admin-who-refresh = Refresh
|
||||
admin-who-close = Close
|
||||
admin-who-no-admins = No administrators are currently online.
|
||||
admin-who-loading = Loading administrators...
|
||||
admin-who-info = This feature displays which administrators are currently online and available to help with any issues you may have.
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
ui-escape-donate = Донат
|
||||
ui-escape-forum = Форум
|
||||
ui-escape-replays = Реплеи
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
health-examinable-shield-none = There is no obvious damage to be seen.
|
||||
|
||||
health-examinable-shield-Blunt-8 = [color=orange]Shield has minor dents.[/color]
|
||||
health-examinable-shield-Blunt-15 = [color=red]Shield has several visible dents.[/color]
|
||||
health-examinable-shield-Blunt-30 = [color=crimson]Shield is badly deformed from impacts![/color]
|
||||
health-examinable-shield-Blunt-50 = [color=crimson]Shield is nearly shattered![/color]
|
||||
|
||||
health-examinable-shield-Slash-8 = [color=orange]Shield has light scratches.[/color]
|
||||
health-examinable-shield-Slash-15 = [color=red]Shield is visibly cut in several places.[/color]
|
||||
health-examinable-shield-Slash-30 = [color=crimson]Shield is deeply gashed and cracked![/color]
|
||||
health-examinable-shield-Slash-50 = [color=crimson]Shield is nearly shattered![/color]
|
||||
|
||||
health-examinable-shield-Piercing-8 = [color=orange]Shield has small puncture marks.[/color]
|
||||
health-examinable-shield-Piercing-15 = [color=red]Shield is riddled with holes.[/color]
|
||||
health-examinable-shield-Piercing-30 = [color=crimson]Shield has large, dangerous breaches![/color]
|
||||
health-examinable-shield-Piercing-50 = [color=crimson]Shield is nearly shattered![/color]
|
||||
|
||||
health-examinable-shield-Heat-8 = [color=orange]Shield surface is slightly scorched.[/color]
|
||||
health-examinable-shield-Heat-15 = [color=red]Shield is charred and blackened.[/color]
|
||||
health-examinable-shield-Heat-30 = [color=crimson]Shield is melting under extreme heat![/color]
|
||||
health-examinable-shield-Heat-50 = [color=crimson]Shield almost burns in your hand![/color]
|
||||
|
||||
health-examinable-shield-Mangleness-15 = [color=orange]Shield looks fused.[/color]
|
||||
health-examinable-shield-Mangleness-35 = [color=red]Shield looks like scrap metal![/color]
|
||||
|
|
@ -26,6 +26,8 @@ admin-bwoink-play-sound = Bwoink?
|
|||
bwoink-title-none-selected = None selected
|
||||
|
||||
bwoink-system-rate-limited = System: you are sending messages too quickly.
|
||||
bwoink-cooldown-message = Too fast! Wait {$seconds}s before sending another message.
|
||||
bwoink-input-placeholder = Type your message here...
|
||||
bwoink-system-player-disconnecting = has disconnected.
|
||||
bwoink-system-player-reconnecting = has reconnected.
|
||||
bwoink-system-player-banned = has been banned for: {$banReason}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ ui-lobby-title = Lobby: {$serverName}
|
|||
ui-lobby-ahelp-button = AHelp
|
||||
ui-lobby-options-button = Options
|
||||
ui-lobby-leave-button = Leave
|
||||
ui-lobby-replays-button = Replays
|
||||
ui-lobby-observe-button = Observe
|
||||
ui-lobby-ready-up-button = Ready Up
|
||||
ui-lobby-online-players-block = Online Players
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ ent-LeftArmDemon = левая рука арканы
|
|||
.desc = { ent-PartDemon.desc }
|
||||
ent-RightArmDemon = правая рука арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-LeftHandDemon = левая рука арканы
|
||||
ent-LeftHandDemon = левая кисть арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-RightHandDemon = правая рука арканы
|
||||
ent-RightHandDemon = правая кисть арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-LeftLegDemon = левая нога арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-RightLegDemon = правая нога арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-LeftFootDemon = левая нога арканы
|
||||
ent-LeftFootDemon = левая стопа арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
ent-RightFootDemon = правая нога арканы
|
||||
ent-RightFootDemon = правая стопа арканы
|
||||
.desc = { ent-PartDemon.desc }
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
ent-PartSwine = свинячья часть тела
|
||||
ent-PartSwine = часть тела свиньи
|
||||
.desc = { ent-BasePart.desc }
|
||||
ent-TorsoSwine = свинячий торс
|
||||
ent-TorsoSwine = торс свиньи
|
||||
.desc = { ent-BaseTorso.desc }
|
||||
ent-HeadSwine = свинячья голова
|
||||
ent-HeadSwine = голова свиньи
|
||||
.desc = { ent-BaseHead.desc }
|
||||
ent-LeftArmSwine = левая свинячья рука
|
||||
ent-LeftArmSwine = левая рука свиньи
|
||||
.desc = { ent-BaseLeftArm.desc }
|
||||
ent-RightArmSwine = правая свинячья рука
|
||||
ent-RightArmSwine = правая рука свиньи
|
||||
.desc = { ent-BaseRightArm.desc }
|
||||
ent-LeftHandSwine = левая свинячья рука (кисть)
|
||||
ent-LeftHandSwine = левая кисть свиньи
|
||||
.desc = { ent-BaseLeftHand.desc }
|
||||
ent-RightHandSwine = правая свинячья рука (кисть)
|
||||
ent-RightHandSwine = правая кисть свиньи
|
||||
.desc = { ent-BaseRightHand.desc }
|
||||
ent-LeftLegSwine = левая свинячья нога
|
||||
ent-LeftLegSwine = левая нога свиньи
|
||||
.desc = { ent-BaseLeftLeg.desc }
|
||||
ent-RightLegSwine = правая свинячья нога
|
||||
ent-RightLegSwine = правая нога свиньи
|
||||
.desc = { ent-BaseRightLeg.desc }
|
||||
ent-LeftFootSwine = левая свинячья стопа
|
||||
ent-LeftFootSwine = левая стопа свиньи
|
||||
.desc = { ent-BaseLeftFoot.desc }
|
||||
ent-RightFootSwine = правая свинячья стопа
|
||||
ent-RightFootSwine = правая стопа свиньи
|
||||
.desc = { ent-BaseRightFoot.desc }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
admin-who-button = Админы онлайн
|
||||
admin-who-title = Администраторы онлайн
|
||||
admin-who-refresh = Обновить
|
||||
admin-who-close = Закрыть
|
||||
admin-who-no-admins = В данный момент нет администраторов онлайн.
|
||||
admin-who-loading = Загрузка администраторов...
|
||||
admin-who-info = Эта функция показывает, какие администраторы в данный момент онлайн и доступны для помощи с любыми вопросами.
|
||||
|
|
@ -1 +1,12 @@
|
|||
comms-console-announcement-title-prison = Космическая тюрьма
|
||||
comms-console-announcement-title-prison = Космическая тюрьма
|
||||
comms-console-menu-relay-button = Включить
|
||||
comms-console-menu-relay-button-tooltip = Включить или выключить вещание голоса на динамики.
|
||||
comms-console-menu-relay-stop = Выключить
|
||||
comms-console-menu-relay-cooldown = Перезарядка: { $time }.
|
||||
comms-console-menu-announcement-header = Оповещения
|
||||
comms-console-menu-relay-header = Вещание
|
||||
comms-console-menu-relay-time-left = Осталось: { $time }
|
||||
comms-console-menu-emergency-header = Эвакуация
|
||||
comms-console-menu-alert-level-header = Код угрозы
|
||||
comms-console-relay-started = Начато вещание через консоль связи.
|
||||
comms-console-relay-stopped = Вещание через консоль связи остановлено.
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
ui-escape-donate = Донат
|
||||
ui-escape-forum = Форум
|
||||
ui-escape-forum = Форум
|
||||
ui-escape-replays = Реплеи
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
health-examinable-shield-none = Видимые повреждения щита отсутствуют.
|
||||
|
||||
health-examinable-shield-Blunt-8 = [color=orange]Щит имеет небольшие вмятины.[/color]
|
||||
health-examinable-shield-Blunt-15 = [color=red]На щите несколько заметных вмятин.[/color]
|
||||
health-examinable-shield-Blunt-30 = [color=crimson]Щит сильно деформирован от ударов![/color]
|
||||
health-examinable-shield-Blunt-50 = [color=crimson]Щит почти раскрошен от сокрушительной силы![/color]
|
||||
|
||||
health-examinable-shield-Slash-8 = [color=orange]Щит имеет лёгкие царапины.[/color]
|
||||
health-examinable-shield-Slash-15 = [color=red]На щите видны глубокие порезы.[/color]
|
||||
health-examinable-shield-Slash-30 = [color=crimson]Щит изрезан и потрескался![/color]
|
||||
health-examinable-shield-Slash-50 = [color=crimson]Щит едва держится, разодранный порезами![/color]
|
||||
|
||||
health-examinable-shield-Piercing-8 = [color=orange]Щит имеет маленькие пробоины.[/color]
|
||||
health-examinable-shield-Piercing-15 = [color=red]Щит изрешечён дырами.[/color]
|
||||
health-examinable-shield-Piercing-30 = [color=crimson]В щите зияют крупные опасные пробоины![/color]
|
||||
health-examinable-shield-Piercing-50 = [color=crimson]Щит разорван на части от колющего урона![/color]
|
||||
|
||||
health-examinable-shield-Heat-8 = [color=orange]Поверхность щита слегка оплавлена.[/color]
|
||||
health-examinable-shield-Heat-15 = [color=red]Щит почернел и обуглился.[/color]
|
||||
health-examinable-shield-Heat-30 = [color=crimson]Щит плавится от экстремального жара![/color]
|
||||
health-examinable-shield-Heat-50 = [color=crimson]Щит почти рассыпался в пламени![/color]
|
||||
|
||||
health-examinable-shield-Mangleness-15 = [color=orange]Щит выглядит сплавленным.[/color]
|
||||
health-examinable-shield-Mangleness-35 = [color=red]Щит выглядит как металлолом![/color]
|
||||
|
|
@ -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]
|
||||
|
|
@ -18,6 +18,8 @@ admin-ahelp-admin-only-tooltip =
|
|||
admin-bwoink-play-sound = Бвоинк?
|
||||
bwoink-title-none-selected = Ничего не выбрано
|
||||
bwoink-system-rate-limited = Система: вы отправляете сообщения слишком быстро.
|
||||
bwoink-cooldown-message = Слишком быстро! Подождите {$seconds}с перед отправкой следующего сообщения.
|
||||
bwoink-input-placeholder = Введите ваше сообщение здесь...
|
||||
bwoink-system-player-disconnecting = отключился.
|
||||
bwoink-system-player-reconnecting = переподключился.
|
||||
bwoink-system-player-banned = был забанен за: { $banReason }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ ui-lobby-title = Лобби: { $serverName }
|
|||
ui-lobby-ahelp-button = AHelp
|
||||
ui-lobby-options-button = Настройки
|
||||
ui-lobby-leave-button = Выйти
|
||||
ui-lobby-replays-button = Реплеи
|
||||
ui-lobby-observe-button = Наблюдать
|
||||
ui-lobby-ready-up-button = Готовность
|
||||
ui-lobby-online-players-block = Текущие игроки
|
||||
|
|
|
|||
|
|
@ -59,12 +59,23 @@
|
|||
max: 2
|
||||
- type: StaticPrice
|
||||
price: 100
|
||||
- type: HealthExaminable #Sunrise-Start
|
||||
examinableTypes:
|
||||
- Blunt
|
||||
- Slash
|
||||
- Piercing
|
||||
- Heat
|
||||
- Mangleness
|
||||
locPrefix: shield
|
||||
- type: ClothingSpeedModifier
|
||||
sprintModifier: 0.95
|
||||
- type: HeldSpeedModifier #Sunrise-End
|
||||
|
||||
#Security Shields
|
||||
|
||||
- type: entity
|
||||
name: riot shield
|
||||
parent: [ BaseShield, BaseSecurityContraband ]
|
||||
parent: [ RepairableShield, BaseSecurityContraband, BaseShield ] # Sunrise-Edit
|
||||
id: RiotShield
|
||||
description: A large tower shield. Good for controlling crowds.
|
||||
components:
|
||||
|
|
@ -97,7 +108,7 @@
|
|||
|
||||
- type: entity
|
||||
name: riot strobe shield
|
||||
parent: [ BaseShield, BaseSecurityContraband, MachineComponentSounds ]
|
||||
parent: [ RepairableShield, BaseSecurityContraband, MachineComponentSounds, BaseShield ] # Sunrise-Edit
|
||||
id: RiotShieldStrobe
|
||||
description: A large tower shield with atached Flash. Good for controlling crowds.
|
||||
components:
|
||||
|
|
@ -191,7 +202,7 @@
|
|||
|
||||
- type: entity
|
||||
name: laser shield
|
||||
parent: [ BaseShield, BaseSecurityContraband ]
|
||||
parent: [ RepairableShield, BaseSecurityContraband, BaseShield ] # Sunrise-Edit
|
||||
id: RiotLaserShield
|
||||
description: A shield built for withstanding lasers, but not much else.
|
||||
components:
|
||||
|
|
@ -199,6 +210,7 @@
|
|||
state: riot_laser-icon
|
||||
- type: Item
|
||||
heldPrefix: riot_laser
|
||||
size: Ginormous # Sunrise-Edit Я не ебу почему он стал маленьким
|
||||
- type: Blocking
|
||||
passiveBlockModifier:
|
||||
coefficients:
|
||||
|
|
@ -217,13 +229,14 @@
|
|||
|
||||
- type: entity
|
||||
name: ballistic shield
|
||||
parent: [ BaseShield, BaseSecurityContraband ]
|
||||
parent: [ RepairableShield, BaseSecurityContraband, BaseShield ] # Sunrise-Edit
|
||||
id: RiotBulletShield
|
||||
description: A shield built for protecting against ballistics, but not much else.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: riot_bullet-icon
|
||||
- type: Item
|
||||
size: Ginormous # Sunrise-Edit Я не ебу почему он стал маленьким
|
||||
heldPrefix: riot_bullet
|
||||
- type: Blocking
|
||||
passiveBlockModifier:
|
||||
|
|
@ -346,8 +359,8 @@
|
|||
|
||||
- type: entity
|
||||
name: makeshift shield
|
||||
parent: [ BaseShield, RepairableShield ] # Sunrise-Edit
|
||||
id: MakeshiftShield
|
||||
parent: BaseShield
|
||||
description: A rundown looking shield, not good for much.
|
||||
components:
|
||||
- type: Sprite
|
||||
|
|
@ -626,6 +639,9 @@
|
|||
startingCharge: 1500
|
||||
- type: PowerCellDraw
|
||||
useRate: 2.5
|
||||
- type: ClothingSpeedModifier
|
||||
sprintModifier: 1
|
||||
- type: HeldSpeedModifier
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -644,7 +660,7 @@
|
|||
|
||||
- type: entity
|
||||
name: telescopic shield
|
||||
parent: BaseShield
|
||||
parent: [ RepairableShield, BaseShield, BaseSecurityContraband ] # Sunrise-Edit
|
||||
id: TelescopicShield
|
||||
description: An advanced riot shield made of lightweight materials that collapses for easy storage.
|
||||
components:
|
||||
|
|
@ -711,3 +727,23 @@
|
|||
max: 2
|
||||
- type: StaticPrice
|
||||
price: 150
|
||||
|
||||
- type: entity # Sunrise-Start
|
||||
name: repairable shield
|
||||
parent: BaseItem
|
||||
id: RepairableShield
|
||||
description: A shield!
|
||||
abstract: true
|
||||
components:
|
||||
- type: Item
|
||||
size: Ginormous
|
||||
- type: Repairable
|
||||
doAfterDelay: 4
|
||||
damage:
|
||||
types:
|
||||
Blunt: -25
|
||||
Slash: -25
|
||||
Piercing: -25
|
||||
Heat: -25
|
||||
Structural: -25
|
||||
Mangleness: 7.5 # Sunrise-End
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- type: entity
|
||||
name: paladin shield
|
||||
parent: [ BaseItem, BaseMajorContraband ] # Easier to remake it than inheret it, due to wiz keeping all shields in one folder and mine doing the opposite, due to the suitslot sprites.
|
||||
parent: [ RepairableShield, BaseMajorContraband ] # Sunrise-Edit
|
||||
id: PaladinShield
|
||||
description: A shield for a true paladin! Good at deflecting hits with swords and absorbing the impact from bats, but not much else.
|
||||
components:
|
||||
|
|
@ -67,6 +67,17 @@
|
|||
SheetSteel:
|
||||
min: 5
|
||||
max: 13
|
||||
- type: HealthExaminable #Sunrise-Start
|
||||
examinableTypes:
|
||||
- Blunt
|
||||
- Slash
|
||||
- Piercing
|
||||
- Heat
|
||||
- Mangleness
|
||||
locPrefix: shield
|
||||
- type: ClothingSpeedModifier
|
||||
sprintModifier: 0.95
|
||||
- type: HeldSpeedModifier #Sunrise-End
|
||||
|
||||
- type: entity
|
||||
name: paladin greatshield
|
||||
|
|
|
|||
|
|
@ -29,3 +29,16 @@
|
|||
Greenshift: [0, 200]
|
||||
Secret: [50, 200]
|
||||
Extra: [50, 100]
|
||||
|
||||
- type: gamePresetPool
|
||||
id: AllPresetPool
|
||||
presets:
|
||||
Nukeops: [30, 200]
|
||||
Traitor: [5, 200]
|
||||
Zombie: [10, 200]
|
||||
Survival: [5, 200]
|
||||
Revolutionary: [10, 200]
|
||||
Wizard: [30, 200]
|
||||
BloodCult: [30, 200]
|
||||
AssaultOps: [10, 200]
|
||||
FleshCult: [30, 200]
|
||||
|
|
|
|||
|
|
@ -88,6 +88,18 @@
|
|||
{
|
||||
"name": "diona2"
|
||||
},
|
||||
{
|
||||
"name": "diona3",
|
||||
"delays": [
|
||||
[
|
||||
0.2,
|
||||
0.3,
|
||||
0.3,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gingerbread0",
|
||||
"delays": [
|
||||
|
|
|
|||
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