From d91a8c49257fb243b8d1d632b2c883824d66a1c0 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Sun, 21 Jun 2020 22:05:47 +0200 Subject: [PATCH] Add delaystart and forcepreset commands (#1163) * Add extendroundstart message to extend lobby start timer * Rename StartExtend to DelayStart * Fix delaystart amounts above 59 not working * Change delaystart seconds type from int to uint * Change delaystart wrong args amount message * Add forcegamepreset command * Rename forcegamepreset to forcepreset and merged start and forcestart preset methods * Fix index out of bounds exception when forcing suspicion to start * Change game preset to match regardless of casing * Add forcepreset unknown preset message * Add and move in lobby checks * Remove testing changes * Change delaystart to pause/resume the timer when no seconds are specified * Change pause message * Remove testing code * Change 0 seconds to not be a valid amount of seconds * Replace MsgTickerLobbyCountdown Seconds with DateTime instead of uint * Add one entire dot * Replace Math.Min + Math.Max with Math.Clamp Co-authored-by: ComicIronic Co-authored-by: ComicIronic --- .../GameTicking/ClientGameTicker.cs | 8 ++ .../Interfaces/IClientGameTicker.cs | 1 + Content.Client/State/LobbyState.cs | 30 +++-- Content.IntegrationTests/DummyGameTicker.cs | 27 ++++- Content.Server/GameTicking/GamePreset.cs | 2 +- .../GamePresets/PresetDeathMatch.cs | 2 +- .../GameTicking/GamePresets/PresetSandbox.cs | 2 +- .../GamePresets/PresetSuspicion.cs | 8 +- Content.Server/GameTicking/GameTicker.cs | 104 +++++++++++++++--- .../GameTicking/GameTickerCommands.cs | 74 +++++++++++++ .../Interfaces/GameTicking/IGameTicker.cs | 15 ++- Content.Shared/SharedGameTicker.cs | 34 ++++++ Resources/Groups/groups.yml | 4 + 13 files changed, 274 insertions(+), 37 deletions(-) diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs index 1d3d278a67..194f600657 100644 --- a/Content.Client/GameTicking/ClientGameTicker.cs +++ b/Content.Client/GameTicking/ClientGameTicker.cs @@ -24,6 +24,7 @@ namespace Content.Client.GameTicking [ViewVariables] public bool IsGameStarted { get; private set; } [ViewVariables] public string ServerInfoBlob { get; private set; } [ViewVariables] public DateTime StartTime { get; private set; } + [ViewVariables] public bool Paused { get; private set; } public event Action InfoBlobUpdated; public event Action LobbyStatusUpdated; @@ -36,6 +37,7 @@ namespace Content.Client.GameTicking _netManager.RegisterNetMessage(nameof(MsgTickerJoinGame), JoinGame); _netManager.RegisterNetMessage(nameof(MsgTickerLobbyStatus), LobbyStatus); _netManager.RegisterNetMessage(nameof(MsgTickerLobbyInfo), LobbyInfo); + _netManager.RegisterNetMessage(nameof(MsgTickerLobbyCountdown), LobbyCountdown); _netManager.RegisterNetMessage(nameof(MsgRoundEndMessage), RoundEnd); _initialized = true; @@ -69,6 +71,12 @@ namespace Content.Client.GameTicking _stateManager.RequestStateChange(); } + private void LobbyCountdown(MsgTickerLobbyCountdown message) + { + StartTime = message.StartTime; + Paused = message.Paused; + } + private void RoundEnd(MsgRoundEndMessage message) { diff --git a/Content.Client/Interfaces/IClientGameTicker.cs b/Content.Client/Interfaces/IClientGameTicker.cs index 5e92e3d27c..8d47a0e1dc 100644 --- a/Content.Client/Interfaces/IClientGameTicker.cs +++ b/Content.Client/Interfaces/IClientGameTicker.cs @@ -8,6 +8,7 @@ namespace Content.Client.Interfaces string ServerInfoBlob { get; } bool AreWeReady { get; } DateTime StartTime { get; } + bool Paused { get; } void Initialize(); event Action InfoBlobUpdated; diff --git a/Content.Client/State/LobbyState.cs b/Content.Client/State/LobbyState.cs index 2fd25666df..3209e1d01b 100644 --- a/Content.Client/State/LobbyState.cs +++ b/Content.Client/State/LobbyState.cs @@ -123,21 +123,29 @@ namespace Content.Client.State } string text; - var difference = _clientGameTicker.StartTime - DateTime.UtcNow; - if (difference.Ticks < 0) + + if (_clientGameTicker.Paused) { - if (difference.TotalSeconds < -5) - { - text = Loc.GetString("Right Now?"); - } - else - { - text = Loc.GetString("Right Now"); - } + text = Loc.GetString("Paused"); } else { - text = $"{(int) Math.Floor(difference.TotalMinutes)}:{difference.Seconds:D2}"; + var difference = _clientGameTicker.StartTime - DateTime.UtcNow; + if (difference.Ticks < 0) + { + if (difference.TotalSeconds < -5) + { + text = Loc.GetString("Right Now?"); + } + else + { + text = Loc.GetString("Right Now"); + } + } + else + { + text = $"{(int) Math.Floor(difference.TotalMinutes)}:{difference.Seconds:D2}"; + } } _lobby.StartTime.Text = Loc.GetString("Round Starts In: {0}", text); diff --git a/Content.IntegrationTests/DummyGameTicker.cs b/Content.IntegrationTests/DummyGameTicker.cs index 1f944841ab..5a5b5e6425 100644 --- a/Content.IntegrationTests/DummyGameTicker.cs +++ b/Content.IntegrationTests/DummyGameTicker.cs @@ -37,7 +37,7 @@ namespace Content.IntegrationTests { } - public void StartRound() + public void StartRound(bool force = false) { } @@ -81,12 +81,33 @@ namespace Content.IntegrationTests public IEnumerable ActiveGameRules { get; } = Array.Empty(); - public void SetStartPreset(Type type) + public bool TryGetPreset(string name, out Type type) + { + type = default; + return false; + } + + public void SetStartPreset(Type type, bool force = false) { } - public void SetStartPreset(string type) + public void SetStartPreset(string name, bool force = false) { } + + public bool DelayStart(TimeSpan time) + { + return true; + } + + public bool PauseStart(bool pause = true) + { + return true; + } + + public bool TogglePause() + { + return false; + } } } diff --git a/Content.Server/GameTicking/GamePreset.cs b/Content.Server/GameTicking/GamePreset.cs index 0075f7ae1b..5b4fdadf2d 100644 --- a/Content.Server/GameTicking/GamePreset.cs +++ b/Content.Server/GameTicking/GamePreset.cs @@ -8,7 +8,7 @@ namespace Content.Server.GameTicking /// public abstract class GamePreset { - public abstract bool Start(IReadOnlyList players); + public abstract bool Start(IReadOnlyList readyPlayers, bool force = false); public virtual string ModeTitle => "Sandbox"; public virtual string Description => "Secret!"; } diff --git a/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs b/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs index f45fa125b3..04e663b6da 100644 --- a/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs +++ b/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs @@ -12,7 +12,7 @@ namespace Content.Server.GameTicking.GamePresets [Dependency] private readonly IGameTicker _gameTicker; #pragma warning restore 649 - public override bool Start(IReadOnlyList readyPlayers) + public override bool Start(IReadOnlyList readyPlayers, bool force = false) { _gameTicker.AddGameRule(); return true; diff --git a/Content.Server/GameTicking/GamePresets/PresetSandbox.cs b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs index 2eeab4a049..7ceebaf108 100644 --- a/Content.Server/GameTicking/GamePresets/PresetSandbox.cs +++ b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs @@ -11,7 +11,7 @@ namespace Content.Server.GameTicking.GamePresets [Dependency] private readonly ISandboxManager _sandboxManager; #pragma warning restore 649 - public override bool Start(IReadOnlyList readyPlayers) + public override bool Start(IReadOnlyList readyPlayers, bool force = false) { _sandboxManager.IsSandboxEnabled = true; return true; diff --git a/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs b/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs index 4616888dd5..b59e7d7ae5 100644 --- a/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs +++ b/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Content.Server.GameTicking.GameRules; using Content.Server.Interfaces.Chat; using Content.Server.Interfaces.GameTicking; @@ -28,16 +27,17 @@ namespace Content.Server.GameTicking.GamePresets public int MinTraitors { get; set; } = 2; public int PlayersPerTraitor { get; set; } = 5; - public override bool Start(IReadOnlyList readyPlayers) + public override bool Start(IReadOnlyList readyPlayers, bool force = false) { - if (readyPlayers.Count < MinPlayers) + if (!force && readyPlayers.Count < MinPlayers) { _chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {readyPlayers.Count} players readied up out of {MinPlayers} needed."); return false; } var list = new List(readyPlayers); - var numTraitors = Math.Max(readyPlayers.Count() % PlayersPerTraitor, MinTraitors); + var numTraitors = Math.Clamp(readyPlayers.Count % PlayersPerTraitor, + MinTraitors, readyPlayers.Count); for (var i = 0; i < numTraitors; i++) { diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 9620eaf93c..6da5f59c4a 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -81,6 +81,7 @@ namespace Content.Server.GameTicking [ViewVariables] private Type _presetType; + [ViewVariables] private DateTime _pauseTime; [ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; private DateTime _roundStartTimeUtc; [ViewVariables] private GameRunLevel _runLevel; @@ -92,6 +93,8 @@ namespace Content.Server.GameTicking private CancellationTokenSource _updateShutdownCts; + [ViewVariables] public bool Paused { get; private set; } + [ViewVariables] public GameRunLevel RunLevel { @@ -128,6 +131,7 @@ namespace Content.Server.GameTicking _netManager.RegisterNetMessage(nameof(MsgTickerJoinGame)); _netManager.RegisterNetMessage(nameof(MsgTickerLobbyStatus)); _netManager.RegisterNetMessage(nameof(MsgTickerLobbyInfo)); + _netManager.RegisterNetMessage(nameof(MsgTickerLobbyCountdown)); _netManager.RegisterNetMessage(nameof(MsgRoundEndMessage)); SetStartPreset(_configurationManager.GetCVar("game.defaultpreset")); @@ -156,9 +160,13 @@ namespace Content.Server.GameTicking RoundLengthMetric.Inc(frameEventArgs.DeltaSeconds); } - if (RunLevel != GameRunLevel.PreRoundLobby || _roundStartTimeUtc > DateTime.UtcNow || + if (RunLevel != GameRunLevel.PreRoundLobby || + Paused || + _roundStartTimeUtc > DateTime.UtcNow || _roundStartCountdownHasNotStartedYetDueToNoPlayers) + { return; + } StartRound(); } @@ -197,7 +205,7 @@ namespace Content.Server.GameTicking } } - public void StartRound() + public void StartRound(bool force = false) { DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); Logger.InfoS("ticker", "Starting round!"); @@ -247,13 +255,15 @@ namespace Content.Server.GameTicking // Time to start the preset. var preset = MakeGamePreset(); - if (!preset.Start(assignedJobs.Keys.ToList())) + if (!preset.Start(assignedJobs.Keys.ToList(), force)) { SetStartPreset(_configurationManager.GetCVar("game.fallbackpreset")); var newPreset = MakeGamePreset(); _chatManager.DispatchServerAnnouncement($"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}..."); - if(!newPreset.Start(readyPlayers)) + if (!newPreset.Start(readyPlayers, force)) + { throw new ApplicationException("Fallback preset failed to start!"); + } } _roundStartTimeSpan = IoCManager.Resolve().RealTime; @@ -297,7 +307,7 @@ namespace Content.Server.GameTicking { PlayerOOCName = ply.Name, PlayerICName = mind.CurrentEntity.Name, - Role = antag ? mind.AllRoles.First(role => role.Antag).Name : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unkown"), + Role = antag ? mind.AllRoles.First(role => role.Antag).Name : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"), Antag = antag }; listOfPlayerInfo.Add(playerEndRoundInfo); @@ -377,22 +387,90 @@ namespace Content.Server.GameTicking public IEnumerable ActiveGameRules => _gameRules; - public void SetStartPreset(Type type) + public bool TryGetPreset(string name, out Type type) + { + type = name.ToLower() switch + { + "sandbox" => typeof(PresetSandbox), + "deathmatch" => typeof(PresetDeathMatch), + "suspicion" => typeof(PresetSuspicion), + _ => default + }; + + return type != default; + } + + public void SetStartPreset(Type type, bool force = false) { if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset"); _presetType = type; UpdateInfoText(); + + if (force) + { + StartRound(true); + } } - public void SetStartPreset(string type) => - SetStartPreset(type switch + public void SetStartPreset(string name, bool force = false) + { + if (!TryGetPreset(name, out var type)) { - "Sandbox" => typeof(PresetSandbox), - "DeathMatch" => typeof(PresetDeathMatch), - "Suspicion" => typeof(PresetSuspicion), - _ => throw new NotSupportedException() - }); + throw new NotSupportedException(); + } + + SetStartPreset(type, force); + } + + public bool DelayStart(TimeSpan time) + { + if (_runLevel != GameRunLevel.PreRoundLobby) + { + return false; + } + + _roundStartTimeUtc += time; + + var lobbyCountdownMessage = _netManager.CreateNetMessage(); + lobbyCountdownMessage.StartTime = _roundStartTimeUtc; + lobbyCountdownMessage.Paused = Paused; + _netManager.ServerSendToAll(lobbyCountdownMessage); + + return true; + } + + public bool PauseStart(bool pause = true) + { + if (Paused == pause) + { + return false; + } + + Paused = pause; + + if (pause) + { + _pauseTime = DateTime.UtcNow; + } + else if (_pauseTime != default) + { + _roundStartTimeUtc += DateTime.UtcNow - _pauseTime; + } + + var lobbyCountdownMessage = _netManager.CreateNetMessage(); + lobbyCountdownMessage.StartTime = _roundStartTimeUtc; + lobbyCountdownMessage.Paused = Paused; + _netManager.ServerSendToAll(lobbyCountdownMessage); + + return true; + } + + public bool TogglePause() + { + PauseStart(!Paused); + return Paused; + } private IEntity _spawnPlayerMob(Job job, bool lateJoin = true) { diff --git a/Content.Server/GameTicking/GameTickerCommands.cs b/Content.Server/GameTicking/GameTickerCommands.cs index 3308b1f922..0c12e593c5 100644 --- a/Content.Server/GameTicking/GameTickerCommands.cs +++ b/Content.Server/GameTicking/GameTickerCommands.cs @@ -9,6 +9,47 @@ using Robust.Shared.Network; namespace Content.Server.GameTicking { + class DelayStartCommand : IClientCommand + { + public string Command => "delaystart"; + public string Description => "Delays the round start."; + public string Help => $"Usage: {Command} \nPauses/Resumes the countdown if no argument is provided."; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + var ticker = IoCManager.Resolve(); + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + shell.SendText(player, "This can only be executed while the game is in the pre-round lobby."); + return; + } + + if (args.Length == 0) + { + var paused = ticker.TogglePause(); + shell.SendText(player, paused ? "Paused the countdown." : "Resumed the countdown."); + return; + } + + if (args.Length != 1) + { + shell.SendText(player, "Need zero or one arguments."); + return; + } + + if (!uint.TryParse(args[0], out var seconds) || seconds == 0) + { + shell.SendText(player, $"{args[0]} isn't a valid amount of seconds."); + return; + } + + var time = TimeSpan.FromSeconds(seconds); + if (!ticker.DelayStart(time)) + { + shell.SendText(player, "An unknown error has occurred."); + } + } + } class StartRoundCommand : IClientCommand { @@ -193,4 +234,37 @@ namespace Content.Server.GameTicking ticker.SetStartPreset(args[0]); } } + + class ForcePresetCommand : IClientCommand + { + public string Command => "forcepreset"; + public string Description => "Forces a specific game preset to start for the current lobby."; + public string Help => $"Usage: {Command} "; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + var ticker = IoCManager.Resolve(); + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + shell.SendText(player, "This can only be executed while the game is in the pre-round lobby."); + return; + } + + if (args.Length != 1) + { + shell.SendText(player, "Need exactly one argument."); + return; + } + + var name = args[0]; + if (!ticker.TryGetPreset(name, out var type)) + { + shell.SendText(player, $"No preset exists with name {name}."); + return; + } + + ticker.SetStartPreset(type, true); + shell.SendText(player, $"Forced the game to start with preset {name}."); + } + } } diff --git a/Content.Server/Interfaces/GameTicking/IGameTicker.cs b/Content.Server/Interfaces/GameTicking/IGameTicker.cs index 56e04a84ef..b0eadb8094 100644 --- a/Content.Server/Interfaces/GameTicking/IGameTicker.cs +++ b/Content.Server/Interfaces/GameTicking/IGameTicker.cs @@ -21,7 +21,7 @@ namespace Content.Server.Interfaces.GameTicking void Update(FrameEventArgs frameEventArgs); void RestartRound(); - void StartRound(); + void StartRound(bool force = false); void EndRound(); void Respawn(IPlayerSession targetPlayer); @@ -39,7 +39,16 @@ namespace Content.Server.Interfaces.GameTicking void RemoveGameRule(GameRule rule); IEnumerable ActiveGameRules { get; } - void SetStartPreset(Type type); - void SetStartPreset(string type); + bool TryGetPreset(string name, out Type type); + void SetStartPreset(Type type, bool force = false); + void SetStartPreset(string name, bool force = false); + + /// true if changed, false otherwise + bool PauseStart(bool pause = true); + + /// true if paused, false otherwise + bool TogglePause(); + + bool DelayStart(TimeSpan time); } } diff --git a/Content.Shared/SharedGameTicker.cs b/Content.Shared/SharedGameTicker.cs index 0aa4ea4b4c..c4dd450c8b 100644 --- a/Content.Shared/SharedGameTicker.cs +++ b/Content.Shared/SharedGameTicker.cs @@ -116,6 +116,40 @@ namespace Content.Shared buffer.Write(TextBlob); } } + + protected class MsgTickerLobbyCountdown : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgTickerLobbyCountdown); + public MsgTickerLobbyCountdown(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + /// + /// The total amount of seconds to go until the countdown finishes + /// + public DateTime StartTime { get; set; } + + /// + /// Whether or not the countdown is paused + /// + public bool Paused { get; set; } + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + StartTime = new DateTime(buffer.ReadInt64(), DateTimeKind.Utc); + Paused = buffer.ReadBoolean(); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(StartTime.Ticks); + buffer.Write(Paused); + } + } + public struct RoundEndPlayerInfo { public string PlayerOOCName; diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index 9d11fce6d0..57728a024f 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -55,6 +55,8 @@ - tp - tpgrid - setgamepreset + - forcepreset + - delaystart - startround - endround - restartround @@ -98,6 +100,8 @@ - tp - tpgrid - setgamepreset + - forcepreset + - delaystart - startround - endround - restartround