JoinQueue (#883)

* JoinQueue stalker port

* fix queues

---------

Co-authored-by: JerryImMouse <toperkas@gmail.com>
This commit is contained in:
Ed
2025-02-12 00:35:54 +03:00
committed by GitHub
parent 33495266b4
commit 811104ada8
14 changed files with 393 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
using Content.Client._CP14.Discord;
using Content.Client._CP14.JoinQueue;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -46,6 +47,7 @@ namespace Content.Client.Entry
{
//CP14
[Dependency] private readonly DiscordAuthManager _discordAuth = default!;
[Dependency] private readonly JoinQueueManager _joinQueueManager = default!;
//CP14 end
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IGameController _gameController = default!;
@@ -167,6 +169,7 @@ namespace Content.Client.Entry
//CP14
_overlayManager.AddOverlay(new CP14BasePostProcessOverlay());
_discordAuth.Initialize();
_joinQueueManager.Initialize();
//CP14 end
_overlayManager.AddOverlay(new SingularityOverlay());
_overlayManager.AddOverlay(new RadiationPulseOverlay());

View File

@@ -1,4 +1,5 @@
using Content.Client._CP14.Discord;
using Content.Client._CP14.JoinQueue;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -36,6 +37,7 @@ namespace Content.Client.IoC
//CP14
collection.Register<DiscordAuthManager>();
collection.Register<JoinQueueManager>();
//CP14 end
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();

View File

@@ -0,0 +1,26 @@
using Content.Shared._CP14.JoinQueue;
using Robust.Client.State;
using Robust.Shared.Network;
namespace Content.Client._CP14.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);
}
}

View File

@@ -0,0 +1,33 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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>

View File

@@ -0,0 +1,41 @@
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client._CP14.JoinQueue;
[GenerateTypedNameReferences]
public sealed partial class QueueGui : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public event Action? QuitPressed;
public QueueGui()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
QuitButton.OnPressed += (_) => QuitPressed?.Invoke();
// Disable "priority join" button on Steam builds
// since it violates Valve's rules about alternative storefronts.
PriorityJoinButton.Visible = !_cfg.GetCVar(CCVars.BrandingSteam);
PriorityJoinButton.OnPressed += (_) =>
{
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon);
IoCManager.Resolve<IUriOpener>().OpenUri(linkPatreon);
};
}
public void UpdateInfo(int total, int position)
{
QueueTotal.Text = total.ToString();
QueuePosition.Text = position.ToString();
}
}

View File

@@ -0,0 +1,53 @@
using Content.Client._CP14.JoinQueue;
using Content.Shared._CP14.JoinQueue;
using Robust.Client.Audio;
using Robust.Client.Console;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
namespace Content.Client._CP14.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");
}
}

View File

@@ -42,6 +42,8 @@ namespace Content.Server.Connection
/// <param name="duration">How long the bypass should last for.</param>
void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
Task<bool> HavePrivilegedJoin(NetUserId userId); //CP14 Join Queue
void Update();
}
@@ -363,5 +365,17 @@ namespace Content.Server.Connection
await _db.AssignUserIdAsync(name, assigned);
return assigned;
}
//CP14 Join Queue
public async Task<bool> HavePrivilegedJoin(NetUserId userId)
{
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && await _db.GetAdminDataForAsync(userId) != null;
//var havePriorityJoin = _sponsors
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
return adminBypass || wasInGame;
}
//CP14 Join Queue end
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server._CP14.Discord;
using Content.Server._CP14.JoinQueue;
using Content.Server.Acz;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
@@ -104,6 +105,7 @@ namespace Content.Server.Entry
//CP14
IoCManager.Resolve<DiscordAuthManager>().Initialize();
IoCManager.Resolve<JoinQueueManager>().Initialize();
//CP14 end
IoCManager.Resolve<IAdminLogManager>().Initialize();

View File

@@ -57,7 +57,7 @@ namespace Content.Server.GameTicking
// Make the player actually join the game.
// timer time must be > tick length
//Timer.Spawn(0, () => _playerManager.JoinGame(args.Session)); //CP14 Discord AuthManager
//Timer.Spawn(0, () => _playerManager.JoinGame(args.Session)); //CP14 Discord AuthManager | Moved to DiscordAuthManager & JoinQueueManager
var record = await _db.GetPlayerRecordByUserId(args.Session.UserId);
var firstConnection = record != null &&

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Text.Json.Nodes;
using Content.Server._CP14.JoinQueue;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Robust.Server.ServerStatus;
@@ -29,6 +30,11 @@ namespace Content.Server.GameTicking
/// </summary>
[Dependency] private readonly SharedGameTicker _gameTicker = default!;
/// <summary>
/// For getting actual players count
/// </summary>
[Dependency] private readonly JoinQueueManager _joinQueue = default!;
//CP14 - JoinQueue
private void InitializeStatusShell()
{
IoCManager.Resolve<IStatusHost>().OnStatusRequest += GetStatusResponse;
@@ -44,9 +50,7 @@ namespace Content.Server.GameTicking
jObject["name"] = _baseServer.ServerName;
jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName;
jObject["round_id"] = _gameTicker.RoundId;
jObject["players"] = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount)
? _playerManager.PlayerCount
: _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count();
jObject["players"] = _joinQueue.ActualPlayersCount; //CP14 - JoinQueue
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
jObject["run_level"] = (int) _runLevel;

View File

@@ -1,4 +1,5 @@
using Content.Server._CP14.Discord;
using Content.Server._CP14.JoinQueue;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
@@ -39,6 +40,7 @@ namespace Content.Server.IoC
{
//CP14
IoCManager.Register<DiscordAuthManager>();
IoCManager.Register<JoinQueueManager>();
//CP14 end
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<ISharedChatManager, ChatManager>();

View File

@@ -42,12 +42,6 @@ public sealed class DiscordAuthManager
_netMgr.RegisterNetMessage<MsgDiscordAuthCheck>(OnAuthCheck);
_playerMgr.PlayerStatusChanged += OnPlayerStatusChanged;
PlayerVerified += OnPlayerVerified;
}
private void OnPlayerVerified(object? obj, ICommonSession session)
{
Timer.Spawn(0, () => _playerMgr.JoinGame(session));
}
private async void OnAuthCheck(MsgDiscordAuthCheck msg)

View File

@@ -0,0 +1,173 @@
using System.Linq;
using Content.Server._CP14.Discord;
using Content.Server.Connection;
using Content.Shared._CP14.JoinQueue;
using Content.Shared.CCVar;
using Prometheus;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._CP14.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
{
private static readonly Gauge QueueCount = Metrics.CreateGauge(
"join_queue_count",
"Amount of players in queue.");
private static readonly Counter QueueBypassCount = Metrics.CreateCounter(
"join_queue_bypass_count",
"Amount of players who bypassed queue by privileges.");
private static readonly Histogram QueueTimings = Metrics.CreateHistogram(
"join_queue_timings",
"Timings of players in queue",
new HistogramConfiguration()
{
LabelNames = new[] {"type"},
Buckets = Histogram.ExponentialBuckets(1, 2, 14),
});
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConnectionManager _connectionManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly DiscordAuthManager _discordAuthManager = default!;
//[Dependency] private readonly SponsorsManager _sponsors = default!;
private ISawmill _sawmill = default!;
/// <summary>
/// Queue of active player sessions
/// </summary>
private readonly List<ICommonSession> _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;
_discordAuthManager.PlayerVerified += OnPlayerVerified;
_sawmill = Logger.GetSawmill("queue");
}
private void OnQueueCVarChanged(bool value)
{
_isEnabled = value;
if (!value)
{
foreach (var session in _queue)
{
session.Channel.Disconnect("Queue was disabled");
}
}
}
private async void OnPlayerVerified(object? sender, ICommonSession session)
{
if (!_isEnabled)
{
SendToGame(session);
return;
}
var isPrivileged = await _connectionManager.HavePrivilegedJoin(session.UserId);
var currentOnline = _playerManager.PlayerCount - 1; // Do not count current session in general online, because we are still deciding her fate
var haveFreeSlot = currentOnline < _cfg.GetCVar(CCVars.SoftMaxPlayers);
if (isPrivileged || haveFreeSlot)
{
SendToGame(session);
if (isPrivileged && !haveFreeSlot)
{
QueueBypassCount.Inc();
_sawmill.Info($"{session.Name} bypassed soft slots by privileges");
}
return;
}
_queue.Add(session);
ProcessQueue(false, session.ConnectedTime);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
{
var wasInQueue = _queue.Remove(e.Session);
if (!wasInQueue && e.OldStatus != SessionStatus.InGame) // Process queue only if player disconnected from InGame or from queue
return;
ProcessQueue(true, e.Session.ConnectedTime);
if (wasInQueue)
QueueTimings.WithLabels("Unwaited").Observe((DateTime.UtcNow - e.Session.ConnectedTime).TotalSeconds);
}
}
/// <summary>
/// If possible, takes the first player in the queue and sends him into the game
/// </summary>
/// <param name="isDisconnect">Is method called on disconnect event</param>
/// <param name="connectedTime">Session connected time for histogram metrics</param>
private void ProcessQueue(bool isDisconnect, DateTime connectedTime)
{
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 (haveFreeSlot && queueContains)
{
var session = _queue.First();
_queue.Remove(session);
SendToGame(session);
QueueTimings.WithLabels("Waited").Observe((DateTime.UtcNow - connectedTime).TotalSeconds);
}
SendUpdateMessages();
QueueCount.Set(_queue.Count);
}
/// <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].Channel.SendMessage(new MsgQueueUpdate
{
Total = _queue.Count,
Position = i + 1,
});
}
}
/// <summary>
/// Letting player's session into game, change player state
/// </summary>
/// <param name="s">Player session that will be sent to game</param>
private void SendToGame(ICommonSession s)
{
Timer.Spawn(0, () => _playerManager.JoinGame(s));
}
}

View File

@@ -0,0 +1,36 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.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);
}
}