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 <comicironic@gmail.com> Co-authored-by: ComicIronic <comicironic@gmail.com>
This commit is contained in:
@@ -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<MsgTickerJoinGame>(nameof(MsgTickerJoinGame), JoinGame);
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyStatus>(nameof(MsgTickerLobbyStatus), LobbyStatus);
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyInfo>(nameof(MsgTickerLobbyInfo), LobbyInfo);
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyCountdown>(nameof(MsgTickerLobbyCountdown), LobbyCountdown);
|
||||
_netManager.RegisterNetMessage<MsgRoundEndMessage>(nameof(MsgRoundEndMessage), RoundEnd);
|
||||
|
||||
_initialized = true;
|
||||
@@ -69,6 +71,12 @@ namespace Content.Client.GameTicking
|
||||
_stateManager.RequestStateChange<GameScreen>();
|
||||
}
|
||||
|
||||
private void LobbyCountdown(MsgTickerLobbyCountdown message)
|
||||
{
|
||||
StartTime = message.StartTime;
|
||||
Paused = message.Paused;
|
||||
}
|
||||
|
||||
private void RoundEnd(MsgRoundEndMessage message)
|
||||
{
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<GameRule> ActiveGameRules { get; } = Array.Empty<GameRule>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking
|
||||
/// </summary>
|
||||
public abstract class GamePreset
|
||||
{
|
||||
public abstract bool Start(IReadOnlyList<IPlayerSession> players);
|
||||
public abstract bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false);
|
||||
public virtual string ModeTitle => "Sandbox";
|
||||
public virtual string Description => "Secret!";
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Content.Server.GameTicking.GamePresets
|
||||
[Dependency] private readonly IGameTicker _gameTicker;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers)
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
_gameTicker.AddGameRule<RuleDeathMatch>();
|
||||
return true;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Content.Server.GameTicking.GamePresets
|
||||
[Dependency] private readonly ISandboxManager _sandboxManager;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers)
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
_sandboxManager.IsSandboxEnabled = true;
|
||||
return true;
|
||||
|
||||
@@ -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<IPlayerSession> readyPlayers)
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> 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<IPlayerSession>(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++)
|
||||
{
|
||||
|
||||
@@ -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<MsgTickerJoinGame>(nameof(MsgTickerJoinGame));
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyStatus>(nameof(MsgTickerLobbyStatus));
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyInfo>(nameof(MsgTickerLobbyInfo));
|
||||
_netManager.RegisterNetMessage<MsgTickerLobbyCountdown>(nameof(MsgTickerLobbyCountdown));
|
||||
_netManager.RegisterNetMessage<MsgRoundEndMessage>(nameof(MsgRoundEndMessage));
|
||||
|
||||
SetStartPreset(_configurationManager.GetCVar<string>("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<string>("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<IGameTiming>().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<GameRule> 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<MsgTickerLobbyCountdown>();
|
||||
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<MsgTickerLobbyCountdown>();
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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} <seconds>\nPauses/Resumes the countdown if no argument is provided.";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
|
||||
{
|
||||
var ticker = IoCManager.Resolve<IGameTicker>();
|
||||
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} <preset>";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
|
||||
{
|
||||
var ticker = IoCManager.Resolve<IGameTicker>();
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GameRule> 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);
|
||||
|
||||
/// <returns>true if changed, false otherwise</returns>
|
||||
bool PauseStart(bool pause = true);
|
||||
|
||||
/// <returns>true if paused, false otherwise</returns>
|
||||
bool TogglePause();
|
||||
|
||||
bool DelayStart(TimeSpan time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of seconds to go until the countdown finishes
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the countdown is paused
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
- tp
|
||||
- tpgrid
|
||||
- setgamepreset
|
||||
- forcepreset
|
||||
- delaystart
|
||||
- startround
|
||||
- endround
|
||||
- restartround
|
||||
@@ -98,6 +100,8 @@
|
||||
- tp
|
||||
- tpgrid
|
||||
- setgamepreset
|
||||
- forcepreset
|
||||
- delaystart
|
||||
- startround
|
||||
- endround
|
||||
- restartround
|
||||
|
||||
Reference in New Issue
Block a user