mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 03:50:54 +01:00
@@ -1,26 +0,0 @@
|
||||
using Content.Shared.Corvax.JoinQueue;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Client.Corvax.JoinQueue;
|
||||
|
||||
public sealed class JoinQueueManager
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgQueueUpdate>(OnQueueUpdate);
|
||||
}
|
||||
|
||||
private void OnQueueUpdate(MsgQueueUpdate msg)
|
||||
{
|
||||
if (_stateManager.CurrentState is not QueueState)
|
||||
{
|
||||
_stateManager.RequestStateChange<QueueState>();
|
||||
}
|
||||
|
||||
((QueueState) _stateManager.CurrentState).OnQueueUpdate(msg);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:parallax="clr-namespace:Content.Client.Parallax"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<parallax:ParallaxControl />
|
||||
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<BoxContainer Orientation="Vertical" MinSize="200 200">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Margin="8 0 0 0" Text="{Loc 'queue-title'}"
|
||||
StyleClasses="LabelHeading" VAlign="Center" />
|
||||
<Button Name="QuitButton" Text="{Loc 'queue-quit'}"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<controls:HighDivider />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 20 0 0">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<Label Text="{Loc 'queue-position'}" Align="Center" />
|
||||
<Label Name="QueuePosition" StyleClasses="LabelHeading" Align="Center" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 10 0 0">
|
||||
<Label Text="{Loc 'queue-total'}" Align="Center" />
|
||||
<Label Name="QueueTotal" StyleClasses="LabelHeading" Align="Center" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalAlignment="Bottom" Margin="0 20 0 0">
|
||||
<Button Name="PriorityJoinButton" Text="{Loc 'queue-priority-join'}" HorizontalExpand="True" StyleClasses="OpenRight" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</Control>
|
||||
@@ -1,31 +0,0 @@
|
||||
using Content.Client.Links;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Corvax.JoinQueue;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class QueueGui : Control
|
||||
{
|
||||
public event Action? QuitPressed;
|
||||
|
||||
public QueueGui()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
|
||||
|
||||
QuitButton.OnPressed += (_) => QuitPressed?.Invoke();
|
||||
PriorityJoinButton.OnPressed += (_) =>
|
||||
{
|
||||
IoCManager.Resolve<IUriOpener>().OpenUri(UILinks.Patreon);
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateInfo(int total, int position)
|
||||
{
|
||||
QueueTotal.Text = total.ToString();
|
||||
QueuePosition.Text = position.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Content.Shared.Corvax.JoinQueue;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Corvax.JoinQueue;
|
||||
|
||||
public sealed class QueueState : State
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
|
||||
private const string JoinSoundPath = "/Audio/Effects/voteding.ogg";
|
||||
|
||||
private QueueGui? _gui;
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
_gui = new QueueGui();
|
||||
_userInterfaceManager.StateRoot.AddChild(_gui);
|
||||
|
||||
_gui.QuitPressed += OnQuitPressed;
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
_gui!.QuitPressed -= OnQuitPressed;
|
||||
_gui.Dispose();
|
||||
|
||||
Ding();
|
||||
}
|
||||
|
||||
private void Ding()
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TrySystem<AudioSystem>(out var audio))
|
||||
{
|
||||
audio.PlayGlobal(JoinSoundPath, Filter.Local(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnQueueUpdate(MsgQueueUpdate msg)
|
||||
{
|
||||
_gui?.UpdateInfo(msg.Total, msg.Position);
|
||||
}
|
||||
|
||||
private void OnQuitPressed()
|
||||
{
|
||||
_consoleHost.ExecuteCommand("quit");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Corvax.JoinQueue;
|
||||
using Content.Client.Corvax.Sponsors;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
@@ -75,7 +74,6 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
[Dependency] private readonly ContentLocalizationManager _contentLoc = default!;
|
||||
[Dependency] private readonly ClientSponsorsManager _sponsorsManager = default!; // Corvax-Sponsors
|
||||
[Dependency] private readonly JoinQueueManager _queueManager = default!; // Corvax-Queue
|
||||
|
||||
public const int NetBufferSizeOverride = 2;
|
||||
|
||||
@@ -177,7 +175,6 @@ namespace Content.Client.Entry
|
||||
_networkResources.Initialize();
|
||||
_userInterfaceManager.SetDefaultTheme("SS14DefaultTheme");
|
||||
_sponsorsManager.Initialize(); // Corvax-Sponsors
|
||||
_queueManager.Initialize(); // Corvax-Queue
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Clickable;
|
||||
using Content.Client.Corvax.JoinQueue;
|
||||
using Content.Client.Corvax.Sponsors;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
@@ -45,7 +44,6 @@ namespace Content.Client.IoC
|
||||
IoCManager.Register<ExtendedDisconnectInformationManager>();
|
||||
IoCManager.Register<PlayTimeTrackingManager>();
|
||||
IoCManager.Register<ClientSponsorsManager>(); // Corvax-Sponsors
|
||||
IoCManager.Register<JoinQueueManager>(); // Corvax-Queue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace Content.Server.Connection
|
||||
public interface IConnectionManager
|
||||
{
|
||||
void Initialize();
|
||||
Task<bool> HavePrivilegedJoin(NetUserId userId); // Corvax-Queue
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,6 +102,7 @@ namespace Content.Server.Connection
|
||||
}
|
||||
|
||||
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
|
||||
var sponsorData = _sponsorsManager.GetSponsorInfo(e.UserId); // Corvax-Sponsors
|
||||
|
||||
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled))
|
||||
{
|
||||
@@ -115,11 +115,11 @@ namespace Content.Server.Connection
|
||||
}
|
||||
}
|
||||
|
||||
// Corvax-Queue-Start
|
||||
var isPrivileged = await HavePrivilegedJoin(e.UserId);
|
||||
var isQueueEnabled = _cfg.GetCVar(CCVars.QueueEnabled);
|
||||
if (_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled)
|
||||
// Corvax-Queue-End
|
||||
var havePriorityJoin = sponsorData?.HavePriorityJoin == true; // Corvax-Sponsors
|
||||
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
|
||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame;
|
||||
if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && adminData is null && !havePriorityJoin) && !wasInGame)
|
||||
{
|
||||
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
|
||||
}
|
||||
@@ -158,21 +158,5 @@ namespace Content.Server.Connection
|
||||
await _db.AssignUserIdAsync(name, assigned);
|
||||
return assigned;
|
||||
}
|
||||
|
||||
// Corvax-Queue-Start: Make these conditions in one place, for checks in the connection and in the queue
|
||||
public async Task<bool> HavePrivilegedJoin(NetUserId userId)
|
||||
{
|
||||
var adminData = await _dbManager.GetAdminDataForAsync(userId);
|
||||
var sponsorData = _sponsorsManager.GetSponsorInfo(userId); // Corvax-Sponsors
|
||||
|
||||
var havePriorityJoin = sponsorData?.HavePriorityJoin == true; // Corvax-Sponsors
|
||||
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
|
||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame;
|
||||
return adminData != null ||
|
||||
havePriorityJoin || // Corvax-Sponsors
|
||||
wasInGame;
|
||||
}
|
||||
// Corvax-Queue-End
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Connection;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Corvax.JoinQueue;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Corvax.JoinQueue;
|
||||
|
||||
/// <summary>
|
||||
/// Manages new player connections when the server is full and queues them up, granting access when a slot becomes free
|
||||
/// </summary>
|
||||
public sealed class JoinQueueManager
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IConnectionManager _connectionManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Queue of active player sessions
|
||||
/// </summary>
|
||||
private readonly List<IPlayerSession> _queue = new(); // Real Queue class can't delete disconnected users
|
||||
|
||||
private bool _isEnabled = false;
|
||||
|
||||
public int PlayerInQueueCount => _queue.Count;
|
||||
public int ActualPlayersCount => _playerManager.PlayerCount - PlayerInQueueCount; // Now it's only real value with actual players count that in game
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgQueueUpdate>();
|
||||
|
||||
_cfg.OnValueChanged(CCVars.QueueEnabled, OnQueueCVarChanged, true);
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnQueueCVarChanged(bool value)
|
||||
{
|
||||
_isEnabled = value;
|
||||
|
||||
if (!value)
|
||||
{
|
||||
foreach (var session in _queue)
|
||||
{
|
||||
session.ConnectedClient.Disconnect("Queue was disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
switch (e.NewStatus)
|
||||
{
|
||||
case SessionStatus.Connected:
|
||||
{
|
||||
if (!_isEnabled)
|
||||
{
|
||||
SendToGame(e.Session);
|
||||
return;
|
||||
}
|
||||
|
||||
var isPrivileged = await _connectionManager.HavePrivilegedJoin(e.Session.UserId);
|
||||
var haveFreeSlot = _playerManager.PlayerCount < _cfg.GetCVar(CCVars.SoftMaxPlayers);
|
||||
if (isPrivileged || haveFreeSlot)
|
||||
{
|
||||
SendToGame(e.Session);
|
||||
return;
|
||||
}
|
||||
|
||||
_queue.Add(e.Session);
|
||||
ProcessQueue(false);
|
||||
break;
|
||||
}
|
||||
case SessionStatus.Disconnected:
|
||||
{
|
||||
_queue.Remove(e.Session);
|
||||
ProcessQueue(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If possible, takes the first player in the queue and sends him into the game
|
||||
/// </summary>
|
||||
private void ProcessQueue(bool isDisconnect)
|
||||
{
|
||||
var players = ActualPlayersCount;
|
||||
if (isDisconnect)
|
||||
players--; // Decrease currently disconnected session but that has not yet been deleted
|
||||
|
||||
var haveFreeSlot = players < _cfg.GetCVar(CCVars.SoftMaxPlayers);
|
||||
var queueContains = _queue.Count > 0;
|
||||
if ((!_isEnabled || haveFreeSlot) && queueContains)
|
||||
{
|
||||
var session = _queue.First();
|
||||
_queue.Remove(session);
|
||||
SendToGame(session);
|
||||
}
|
||||
|
||||
SendUpdateMessages();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends messages to all players in the queue with the current state of the queue
|
||||
/// </summary>
|
||||
private void SendUpdateMessages()
|
||||
{
|
||||
for (var i = 0; i < _queue.Count; i++)
|
||||
{
|
||||
_queue[i].ConnectedClient.SendMessage(new MsgQueueUpdate
|
||||
{
|
||||
Total = _queue.Count,
|
||||
Position = i + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Letting player's session into game, change player state
|
||||
/// </summary>
|
||||
private void SendToGame(IPlayerSession s)
|
||||
{
|
||||
Timer.Spawn(0, s.JoinGame);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using Content.Server.Administration.Managers;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Connection;
|
||||
using Content.Server.Corvax.JoinQueue;
|
||||
using Content.Server.Corvax.Sponsors;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.EUI;
|
||||
@@ -99,7 +98,6 @@ namespace Content.Server.Entry
|
||||
IoCManager.Resolve<NetworkResourceManager>().Initialize();
|
||||
IoCManager.Resolve<GhostKickManager>().Initialize();
|
||||
IoCManager.Resolve<ServerSponsorsManager>().Initialize(); // Corvax-Sponsors
|
||||
IoCManager.Resolve<JoinQueueManager>().Initialize(); // Corvax-Sponsors
|
||||
|
||||
_voteManager.Initialize();
|
||||
_updateManager.Initialize();
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
|
||||
{
|
||||
var session = args.Session;
|
||||
@@ -35,7 +36,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
// Make the player actually join the game.
|
||||
// timer time must be > tick length
|
||||
// Timer.Spawn(0, args.Session.JoinGame); // Corvax-Queue: Moved to `JoinQueueManager`
|
||||
Timer.Spawn(0, args.Session.JoinGame);
|
||||
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("player-join-message", ("name", args.Session.Name)));
|
||||
|
||||
@@ -87,8 +88,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
|
||||
|
||||
if (_playerGameStatuses.ContainsKey(args.Session.UserId)) // Corvax-Queue: Delete data only if player was in game
|
||||
_userDb.ClientDisconnected(session);
|
||||
_userDb.ClientDisconnected(session);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Content.Server.Corvax.JoinQueue;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server.ServerStatus;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -23,7 +22,6 @@ namespace Content.Server.GameTicking
|
||||
/// For access to CVars in status responses.
|
||||
/// </summary>
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly JoinQueueManager _queueManager = default!; // Corvax-Queue
|
||||
|
||||
private void InitializeStatusShell()
|
||||
{
|
||||
@@ -36,7 +34,7 @@ namespace Content.Server.GameTicking
|
||||
lock (_statusShellLock)
|
||||
{
|
||||
jObject["name"] = _baseServer.ServerName;
|
||||
jObject["players"] = _queueManager.ActualPlayersCount; // Corvax-Queue
|
||||
jObject["players"] = _playerManager.PlayerCount;
|
||||
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
|
||||
jObject["run_level"] = (int) _runLevel;
|
||||
if (_runLevel >= GameRunLevel.InRound)
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Server.Administration.Notes;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Connection;
|
||||
using Content.Server.Corvax.JoinQueue;
|
||||
using Content.Server.Corvax.Sponsors;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.EUI;
|
||||
@@ -58,7 +57,6 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<PlayTimeTrackingManager>();
|
||||
IoCManager.Register<UserDbDataManager>();
|
||||
IoCManager.Register<ServerSponsorsManager>(); // Corvax-Sponsors
|
||||
IoCManager.Register<JoinQueueManager>(); // Corvax-Queue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1345,7 +1345,7 @@ namespace Content.Shared.CCVar
|
||||
PlayTimeSaveInterval = CVarDef.Create("playtime.save_interval", 900f, CVar.SERVERONLY);
|
||||
|
||||
/**
|
||||
* Corvax | Sponsors
|
||||
* SPONSORS
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
@@ -1353,16 +1353,5 @@ namespace Content.Shared.CCVar
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> SponsorsApiUrl =
|
||||
CVarDef.Create("sponsor.api_url", "", CVar.SERVERONLY);
|
||||
|
||||
|
||||
/*
|
||||
* Corvax | Queue
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Controls if the connections queue is enabled. If enabled stop kicking new players after `SoftMaxPlayers` cap and instead add them to queue.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool>
|
||||
QueueEnabled = CVarDef.Create("queue.enabled", false, CVar.SERVERONLY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using Lidgren.Network;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Corvax.JoinQueue;
|
||||
|
||||
/// <summary>
|
||||
/// Sent from server to client with queue state for player
|
||||
/// Also initiates queue state on client
|
||||
/// </summary>
|
||||
public sealed class MsgQueueUpdate : NetMessage
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.Command;
|
||||
|
||||
/// <summary>
|
||||
/// Total players in queue
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Player current position in queue (starts from 1)
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
Total = buffer.ReadInt32();
|
||||
Position = buffer.ReadInt32();
|
||||
}
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
buffer.Write(Total);
|
||||
buffer.Write(Position);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
queue-title = Очередь
|
||||
queue-quit = Выйти
|
||||
queue-position = Ваша позиция:
|
||||
queue-total = Всего в очереди:
|
||||
queue-priority-join = Приоритетный вход
|
||||
Reference in New Issue
Block a user