From 76c6ee08e7f6d5dd3559eb3ca35c87cc6fd6c590 Mon Sep 17 00:00:00 2001 From: wrexbe <81056464+wrexbe@users.noreply.github.com> Date: Mon, 10 Jan 2022 11:24:41 -0800 Subject: [PATCH] Refactored RoundEndSystem (2) (#6115) * No RestartRound if round id changed * Refactored RoundEndSystem * Fix round end + add test --- .../Tests/RoundEndTest.cs | 115 +++++++++++++++ .../CommunicationsConsoleComponent.cs | 12 +- .../Commands/RestartRoundCommand.cs | 5 +- Content.Server/RoundEnd/RoundEndSystem.cs | 138 +++++++++--------- 4 files changed, 188 insertions(+), 82 deletions(-) create mode 100644 Content.IntegrationTests/Tests/RoundEndTest.cs diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs new file mode 100644 index 0000000000..1e0501b2d8 --- /dev/null +++ b/Content.IntegrationTests/Tests/RoundEndTest.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameTicking; +using Content.Server.RoundEnd; +using Content.Shared.CCVar; +using NUnit.Framework; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Content.IntegrationTests.Tests +{ + [TestFixture] + public class RoundEndTest : ContentIntegrationTest, IEntityEventSubscriber + { + [Test] + public async Task Test() + { + var eventCount = 0; + + var (_, server) = await StartConnectedServerClientPair(); + + await server.WaitAssertion(() => + { + var ticker = IoCManager.Resolve().GetEntitySystem(); + ticker.RestartRound(); + var config = IoCManager.Resolve(); + config.SetCVar(CCVars.GameLobbyEnabled, true); + + var roundEndSystem = IoCManager.Resolve().GetEntitySystem(); + roundEndSystem.DefaultCooldownDuration = TimeSpan.FromMilliseconds(250); + roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMilliseconds(500); + roundEndSystem.DefaultRestartRoundDuration = TimeSpan.FromMilliseconds(250); + }); + + await server.WaitAssertion(() => + { + var bus = IoCManager.Resolve().EventBus; + bus.SubscribeEvent(EventSource.Local, this, _ => { + Interlocked.Increment(ref eventCount); + }); + var roundEndSystem = IoCManager.Resolve().GetEntitySystem(); + // Press the shuttle call button + roundEndSystem.RequestRoundEnd(); + Assert.That(roundEndSystem.ExpectedCountdownEnd, Is.Not.Null, "Shuttle was called, but countdown time was not set"); + Assert.That(roundEndSystem.CanCall(), Is.False, "Started the shuttle, but didn't have to wait cooldown to press cancel button"); + // Check that we can't recall the shuttle yet + roundEndSystem.CancelRoundEndCountdown(); + Assert.That(roundEndSystem.ExpectedCountdownEnd, Is.Not.Null, "Shuttle was cancelled, even though the button was on cooldown"); + }); + + await WaitForEvent(); // Wait for Cooldown + + await server.WaitAssertion(() => + { + var roundEndSystem = IoCManager.Resolve().GetEntitySystem(); + + Assert.That(roundEndSystem.CanCall(), Is.True, "We waited a while, but the cooldown is not expired"); + Assert.That(roundEndSystem.ExpectedCountdownEnd, Is.Not.Null, "We were waiting for the cooldown, but the round also ended"); + // Recall the shuttle, which should trigger the cooldown again + roundEndSystem.CancelRoundEndCountdown(); + Assert.That(roundEndSystem.ExpectedCountdownEnd, Is.Null, "Recalled shuttle, but countdown has not ended"); + Assert.That(roundEndSystem.CanCall(), Is.False, "Recalled shuttle, but cooldown has not been enabled"); + }); + + await WaitForEvent(); // Wait for Cooldown + + await server.WaitAssertion(() => + { + var roundEndSystem = IoCManager.Resolve().GetEntitySystem(); + Assert.That(roundEndSystem.CanCall(), Is.True, "We waited a while, but the cooldown is not expired"); + // Press the shuttle call button + roundEndSystem.RequestRoundEnd(); + }); + + await WaitForEvent(); // Wait for Cooldown + + await server.WaitAssertion(() => + { + var roundEndSystem = IoCManager.Resolve().GetEntitySystem(); + Assert.That(roundEndSystem.CanCall(), Is.True, "We waited a while, but the cooldown is not expired"); + Assert.That(roundEndSystem.ExpectedCountdownEnd, Is.Not.Null, "The countdown ended, but we just wanted the cooldown to end"); + }); + + await WaitForEvent(); // Wait for countdown to end round + + await CheckRunLevel(GameRunLevel.PostRound); + + await WaitForEvent(); // Wait for Restart + + await CheckRunLevel(GameRunLevel.PreRoundLobby); + + Task CheckRunLevel(GameRunLevel level) + { + return server.WaitAssertion(() => + { + var ticker = IoCManager.Resolve().GetEntitySystem(); + Assert.That(ticker.RunLevel, Is.EqualTo(level)); + }); + } + + async Task WaitForEvent() + { + var timeout = Task.Delay(TimeSpan.FromSeconds(10)); + var currentCount = Thread.VolatileRead(ref eventCount); + while (currentCount == Thread.VolatileRead(ref eventCount) && !timeout.IsCompleted) + { + await server.WaitRunTicks(1); + } + if (timeout.IsCompleted) throw new TimeoutException("Event took too long to trigger"); + } + } + } +} diff --git a/Content.Server/Communications/CommunicationsConsoleComponent.cs b/Content.Server/Communications/CommunicationsConsoleComponent.cs index 7d8b826bcb..8a0104ea32 100644 --- a/Content.Server/Communications/CommunicationsConsoleComponent.cs +++ b/Content.Server/Communications/CommunicationsConsoleComponent.cs @@ -17,11 +17,12 @@ using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.Communications { [RegisterComponent] - public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent + public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent, IEntityEventSubscriber { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; private bool Powered => !_entities.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) || receiver.Powered; @@ -42,10 +43,7 @@ namespace Content.Server.Communications UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; } - RoundEndSystem.OnRoundEndCountdownStarted += UpdateBoundInterface; - RoundEndSystem.OnRoundEndCountdownCancelled += UpdateBoundInterface; - RoundEndSystem.OnRoundEndCountdownFinished += UpdateBoundInterface; - RoundEndSystem.OnCallCooldownEnded += UpdateBoundInterface; + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, (s) => UpdateBoundInterface()); } protected override void Startup() @@ -76,9 +74,7 @@ namespace Content.Server.Communications protected override void OnRemove() { - RoundEndSystem.OnRoundEndCountdownStarted -= UpdateBoundInterface; - RoundEndSystem.OnRoundEndCountdownCancelled -= UpdateBoundInterface; - RoundEndSystem.OnRoundEndCountdownFinished -= UpdateBoundInterface; + _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); base.OnRemove(); } diff --git a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs index 63a6610b26..4341c0ed07 100644 --- a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs @@ -1,10 +1,9 @@ -using System; +using System; using Content.Server.Administration; using Content.Server.RoundEnd; using Content.Shared.Administration; using Robust.Shared.Console; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; namespace Content.Server.GameTicking.Commands { @@ -13,7 +12,7 @@ namespace Content.Server.GameTicking.Commands { public string Command => "restartround"; public string Description => "Ends the current round and starts the countdown for the next lobby."; - public string Help => String.Empty; + public string Help => string.Empty; public void Execute(IConsoleShell shell, string argStr, string[] args) { diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 51237af694..44cf1018d1 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -19,79 +19,61 @@ namespace Content.Server.RoundEnd { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly AdminLogSystem _adminLog = default!; - public const float RestartRoundTime = 20f; - private CancellationTokenSource _roundEndCancellationTokenSource = new(); - private CancellationTokenSource _callCooldownEndedTokenSource = new(); - public bool IsRoundEndCountdownStarted { get; private set; } - public TimeSpan RoundEndCountdownTime { get; set; } = TimeSpan.FromMinutes(4); - public TimeSpan? ExpectedCountdownEnd = null; + public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(4); + public TimeSpan DefaultRestartRoundDuration { get; set; } = TimeSpan.FromSeconds(20); - public TimeSpan LastCallTime { get; private set; } - - public TimeSpan CallCooldown { get; } = TimeSpan.FromSeconds(30); - - // TODO: Make these regular eventbus events... - public delegate void RoundEndCountdownStarted(); - public event RoundEndCountdownStarted? OnRoundEndCountdownStarted; - - public delegate void RoundEndCountdownCancelled(); - public event RoundEndCountdownCancelled? OnRoundEndCountdownCancelled; - - public delegate void RoundEndCountdownFinished(); - public event RoundEndCountdownFinished? OnRoundEndCountdownFinished; - - public delegate void CallCooldownEnded(); - public event CallCooldownEnded? OnCallCooldownEnded; + private CancellationTokenSource? _countdownTokenSource = null; + private CancellationTokenSource? _cooldownTokenSource = null; + public TimeSpan? ExpectedCountdownEnd { get; set; } = null; public override void Initialize() { base.Initialize(); - - SubscribeLocalEvent(Reset); + SubscribeLocalEvent(_ => Reset()); } - void Reset(RoundRestartCleanupEvent ev) + private void Reset() { - IsRoundEndCountdownStarted = false; - _roundEndCancellationTokenSource.Cancel(); - _roundEndCancellationTokenSource = new CancellationTokenSource(); - _callCooldownEndedTokenSource.Cancel(); - _callCooldownEndedTokenSource = new CancellationTokenSource(); + if (_countdownTokenSource != null) + { + _countdownTokenSource.Cancel(); + _countdownTokenSource = null; + } + + if (_cooldownTokenSource != null) + { + _cooldownTokenSource.Cancel(); + _cooldownTokenSource = null; + } + ExpectedCountdownEnd = null; - LastCallTime = default; + RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public bool CanCall() { - return _gameTiming.CurTime >= LastCallTime + CallCooldown; - } - - private void ActivateCooldown() - { - _callCooldownEndedTokenSource.Cancel(); - _callCooldownEndedTokenSource = new CancellationTokenSource(); - LastCallTime = _gameTiming.CurTime; - Timer.Spawn(CallCooldown, () => OnCallCooldownEnded?.Invoke(), _callCooldownEndedTokenSource.Token); + return _cooldownTokenSource == null; } public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true) { - RequestRoundEnd(RoundEndCountdownTime, requester, checkCooldown); + RequestRoundEnd(DefaultCountdownDuration, requester, checkCooldown); } public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true) { - if (IsRoundEndCountdownStarted) - return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) return; - if (checkCooldown && !CanCall()) - { - return; - } + if (checkCooldown && _cooldownTokenSource != null) return; + + if (_countdownTokenSource != null) return; + _countdownTokenSource = new(); if (requester != null) { @@ -102,29 +84,25 @@ namespace Content.Server.RoundEnd _adminLog.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called"); } - IsRoundEndCountdownStarted = true; - _chatManager.DispatchStationAnnouncement(Loc.GetString("round-end-system-shuttle-called-announcement",("minutes", countdownTime.Minutes)), Loc.GetString("Station"), false); SoundSystem.Play(Filter.Broadcast(), "/Audio/Announcements/shuttlecalled.ogg"); ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime; - Timer.Spawn(countdownTime, EndRound, _roundEndCancellationTokenSource.Token); + Timer.Spawn(countdownTime, EndRound, _countdownTokenSource.Token); ActivateCooldown(); - - OnRoundEndCountdownStarted?.Invoke(); + RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true) { - if (!IsRoundEndCountdownStarted) - return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) return; + if (checkCooldown && _cooldownTokenSource != null) return; - if (checkCooldown && !CanCall()) - { - return; - } + if (_countdownTokenSource == null) return; + _countdownTokenSource.Cancel(); + _countdownTokenSource = null; if (requester != null) { @@ -135,31 +113,49 @@ namespace Content.Server.RoundEnd _adminLog.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled"); } - IsRoundEndCountdownStarted = false; - _chatManager.DispatchStationAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"), Loc.GetString("Station"), false); SoundSystem.Play(Filter.Broadcast(), "/Audio/Announcements/shuttlerecalled.ogg"); - _roundEndCancellationTokenSource.Cancel(); - _roundEndCancellationTokenSource = new CancellationTokenSource(); - ExpectedCountdownEnd = null; - ActivateCooldown(); - - OnRoundEndCountdownCancelled?.Invoke(); + RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public void EndRound() { - OnRoundEndCountdownFinished?.Invoke(); - var gameTicker = Get(); - gameTicker.EndRound(); + if (_gameTicker.RunLevel != GameRunLevel.InRound) return; + ExpectedCountdownEnd = null; + RaiseLocalEvent(RoundEndSystemChangedEvent.Default); + _gameTicker.EndRound(); + _countdownTokenSource?.Cancel(); + _countdownTokenSource = new(); + _chatManager.DispatchServerAnnouncement(Loc.GetString("round-end-system-round-restart-eta-announcement", ("seconds", DefaultRestartRoundDuration.Seconds))); + Timer.Spawn(DefaultRestartRoundDuration, AfterEndRoundRestart, _countdownTokenSource.Token); + } - _chatManager.DispatchServerAnnouncement(Loc.GetString("round-end-system-round-restart-eta-announcement", ("seconds", RestartRoundTime))); + private void AfterEndRoundRestart() + { + if (_gameTicker.RunLevel != GameRunLevel.PostRound) return; + Reset(); + _gameTicker.RestartRound(); + } - Timer.Spawn(TimeSpan.FromSeconds(RestartRoundTime), () => gameTicker.RestartRound(), CancellationToken.None); + private void ActivateCooldown() + { + _cooldownTokenSource?.Cancel(); + _cooldownTokenSource = new(); + Timer.Spawn(DefaultCooldownDuration, () => + { + _cooldownTokenSource.Cancel(); + _cooldownTokenSource = null; + RaiseLocalEvent(RoundEndSystemChangedEvent.Default); + }, _cooldownTokenSource.Token); } } + + public class RoundEndSystemChangedEvent : EntityEventArgs + { + public static RoundEndSystemChangedEvent Default { get; } = new(); + } }