using System.Threading; using Content.Server.Administration.Logs; using Content.Server.AlertLevel; using Content.Server.Chat; using Content.Server.Chat.Managers; using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Shuttles.Systems; using Content.Server.Station.Systems; using Content.Shared.Database; using Content.Shared.GameTicking; using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.RoundEnd { /// /// Handles ending rounds normally and also via requesting it (e.g. via comms console) /// If you request a round end then an escape shuttle will be used. /// public sealed class RoundEndSystem : EntitySystem { [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly ShuttleSystem _shuttle = default!; [Dependency] private readonly StationSystem _stationSystem = default!; public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30); /// /// Countdown to use where there is no station alert countdown to be found. /// public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(4); public TimeSpan DefaultRestartRoundDuration { get; set; } = TimeSpan.FromMinutes(1); private CancellationTokenSource? _countdownTokenSource = null; private CancellationTokenSource? _cooldownTokenSource = null; public TimeSpan? LastCountdownStart { get; set; } = null; public TimeSpan? ExpectedCountdownEnd { get; set; } = null; public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart; public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(_ => Reset()); } private void Reset() { if (_countdownTokenSource != null) { _countdownTokenSource.Cancel(); _countdownTokenSource = null; } if (_cooldownTokenSource != null) { _cooldownTokenSource.Cancel(); _cooldownTokenSource = null; } LastCountdownStart = null; ExpectedCountdownEnd = null; RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public bool CanCallOrRecall() { return _cooldownTokenSource == null; } public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true) { var duration = DefaultCountdownDuration; if (requester != null) { var stationUid = _stationSystem.GetOwningStation(requester.Value); if (TryComp(stationUid, out var alertLevel)) { duration = _protoManager .Index(AlertLevelSystem.DefaultAlertLevelSet) .Levels[alertLevel.CurrentLevel].ShuttleTime; } } RequestRoundEnd(duration, requester, checkCooldown); } public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true) { if (_gameTicker.RunLevel != GameRunLevel.InRound) return; if (checkCooldown && _cooldownTokenSource != null) return; if (_countdownTokenSource != null) return; _countdownTokenSource = new(); if (requester != null) { _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):user}"); } else { _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called"); } // I originally had these set up here but somehow time gets passed as 0 to Loc so IDEK. int time; string units; if (countdownTime.TotalSeconds < 60) { time = countdownTime.Seconds; units = "eta-units-seconds"; } else { time = countdownTime.Minutes; units = "eta-units-minutes"; } _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-called-announcement", ("time", time), ("units", Loc.GetString(units))), Loc.GetString("Station"), false, null, Color.Gold); SoundSystem.Play("/Audio/Announcements/shuttlecalled.ogg", Filter.Broadcast()); LastCountdownStart = _gameTiming.CurTime; ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime; Timer.Spawn(countdownTime, _shuttle.CallEmergencyShuttle, _countdownTokenSource.Token); ActivateCooldown(); RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true) { if (_gameTicker.RunLevel != GameRunLevel.InRound) return; if (checkCooldown && _cooldownTokenSource != null) return; if (_countdownTokenSource == null) return; _countdownTokenSource.Cancel(); _countdownTokenSource = null; if (requester != null) { _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):user}"); } else { _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled"); } _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"), Loc.GetString("Station"), false, colorOverride: Color.Gold); SoundSystem.Play("/Audio/Announcements/shuttlerecalled.ogg", Filter.Broadcast()); LastCountdownStart = null; ExpectedCountdownEnd = null; ActivateCooldown(); RaiseLocalEvent(RoundEndSystemChangedEvent.Default); } public void EndRound() { if (_gameTicker.RunLevel != GameRunLevel.InRound) return; LastCountdownStart = null; ExpectedCountdownEnd = null; RaiseLocalEvent(RoundEndSystemChangedEvent.Default); _gameTicker.EndRound(); _countdownTokenSource?.Cancel(); _countdownTokenSource = new(); _chatManager.DispatchServerAnnouncement(Loc.GetString("round-end-system-round-restart-eta-announcement", ("minutes", DefaultRestartRoundDuration.Minutes))); Timer.Spawn(DefaultRestartRoundDuration, AfterEndRoundRestart, _countdownTokenSource.Token); } private void AfterEndRoundRestart() { if (_gameTicker.RunLevel != GameRunLevel.PostRound) return; Reset(); _gameTicker.RestartRound(); } private void ActivateCooldown() { _cooldownTokenSource?.Cancel(); _cooldownTokenSource = new(); Timer.Spawn(DefaultCooldownDuration, () => { _cooldownTokenSource.Cancel(); _cooldownTokenSource = null; RaiseLocalEvent(RoundEndSystemChangedEvent.Default); }, _cooldownTokenSource.Token); } } public sealed class RoundEndSystemChangedEvent : EntityEventArgs { public static RoundEndSystemChangedEvent Default { get; } = new(); } }