RoundEndSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. using System.Threading;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.AlertLevel;
  4. using Content.Shared.CCVar;
  5. using Content.Server.Chat.Managers;
  6. using Content.Server.Chat.Systems;
  7. using Content.Server.DeviceNetwork;
  8. using Content.Server.DeviceNetwork.Components;
  9. using Content.Server.DeviceNetwork.Systems;
  10. using Content.Server.GameTicking;
  11. using Content.Server.Screens.Components;
  12. using Content.Server.Shuttles.Components;
  13. using Content.Server.Shuttles.Systems;
  14. using Content.Server.Station.Components;
  15. using Content.Server.Station.Systems;
  16. using Content.Shared.Database;
  17. using Content.Shared.DeviceNetwork;
  18. using Content.Shared.GameTicking;
  19. using Robust.Shared.Audio.Systems;
  20. using Robust.Shared.Configuration;
  21. using Robust.Shared.Player;
  22. using Robust.Shared.Prototypes;
  23. using Robust.Shared.Timing;
  24. using Timer = Robust.Shared.Timing.Timer;
  25. namespace Content.Server.RoundEnd
  26. {
  27. /// <summary>
  28. /// Handles ending rounds normally and also via requesting it (e.g. via comms console)
  29. /// If you request a round end then an escape shuttle will be used.
  30. /// </summary>
  31. public sealed class RoundEndSystem : EntitySystem
  32. {
  33. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  34. [Dependency] private readonly IConfigurationManager _cfg = default!;
  35. [Dependency] private readonly IChatManager _chatManager = default!;
  36. [Dependency] private readonly IGameTiming _gameTiming = default!;
  37. [Dependency] private readonly IPrototypeManager _protoManager = default!;
  38. [Dependency] private readonly ChatSystem _chatSystem = default!;
  39. [Dependency] private readonly GameTicker _gameTicker = default!;
  40. [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
  41. [Dependency] private readonly EmergencyShuttleSystem _shuttle = default!;
  42. [Dependency] private readonly SharedAudioSystem _audio = default!;
  43. [Dependency] private readonly StationSystem _stationSystem = default!;
  44. public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30);
  45. /// <summary>
  46. /// Countdown to use where there is no station alert countdown to be found.
  47. /// </summary>
  48. public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(10);
  49. private CancellationTokenSource? _countdownTokenSource = null;
  50. private CancellationTokenSource? _cooldownTokenSource = null;
  51. public TimeSpan? LastCountdownStart { get; set; } = null;
  52. public TimeSpan? ExpectedCountdownEnd { get; set; } = null;
  53. public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart;
  54. public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime;
  55. public TimeSpan AutoCallStartTime;
  56. private bool _autoCalledBefore = false;
  57. public override void Initialize()
  58. {
  59. base.Initialize();
  60. SubscribeLocalEvent<RoundRestartCleanupEvent>(_ => Reset());
  61. SetAutoCallTime();
  62. }
  63. private void SetAutoCallTime()
  64. {
  65. AutoCallStartTime = _gameTiming.CurTime;
  66. }
  67. private void Reset()
  68. {
  69. if (_countdownTokenSource != null)
  70. {
  71. _countdownTokenSource.Cancel();
  72. _countdownTokenSource = null;
  73. }
  74. if (_cooldownTokenSource != null)
  75. {
  76. _cooldownTokenSource.Cancel();
  77. _cooldownTokenSource = null;
  78. }
  79. LastCountdownStart = null;
  80. ExpectedCountdownEnd = null;
  81. SetAutoCallTime();
  82. _autoCalledBefore = false;
  83. RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
  84. }
  85. /// <summary>
  86. /// Attempts to get the MapUid of the station using <see cref="StationSystem.GetLargestGrid"/>
  87. /// </summary>
  88. public EntityUid? GetStation()
  89. {
  90. AllEntityQuery<StationEmergencyShuttleComponent, StationDataComponent>().MoveNext(out _, out _, out var data);
  91. if (data == null)
  92. return null;
  93. var targetGrid = _stationSystem.GetLargestGrid(data);
  94. return targetGrid == null ? null : Transform(targetGrid.Value).MapUid;
  95. }
  96. /// <summary>
  97. /// Attempts to get centcomm's MapUid
  98. /// </summary>
  99. public EntityUid? GetCentcomm()
  100. {
  101. AllEntityQuery<StationCentcommComponent>().MoveNext(out var centcomm);
  102. return centcomm == null ? null : centcomm.MapEntity;
  103. }
  104. public bool CanCallOrRecall()
  105. {
  106. return _cooldownTokenSource == null;
  107. }
  108. public bool IsRoundEndRequested()
  109. {
  110. return _countdownTokenSource != null;
  111. }
  112. public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
  113. {
  114. var duration = DefaultCountdownDuration;
  115. if (requester != null)
  116. {
  117. var stationUid = _stationSystem.GetOwningStation(requester.Value);
  118. if (TryComp<AlertLevelComponent>(stationUid, out var alertLevel))
  119. {
  120. duration = _protoManager
  121. .Index<AlertLevelPrototype>(AlertLevelSystem.DefaultAlertLevelSet)
  122. .Levels[alertLevel.CurrentLevel].ShuttleTime;
  123. }
  124. }
  125. RequestRoundEnd(duration, requester, checkCooldown, text, name);
  126. }
  127. public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
  128. {
  129. if (_gameTicker.RunLevel != GameRunLevel.InRound)
  130. return;
  131. if (checkCooldown && _cooldownTokenSource != null)
  132. return;
  133. if (_countdownTokenSource != null)
  134. return;
  135. _countdownTokenSource = new();
  136. if (requester != null)
  137. {
  138. _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):user}");
  139. }
  140. else
  141. {
  142. _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called");
  143. }
  144. // I originally had these set up here but somehow time gets passed as 0 to Loc so IDEK.
  145. int time;
  146. string units;
  147. if (countdownTime.TotalSeconds < 60)
  148. {
  149. time = countdownTime.Seconds;
  150. units = "eta-units-seconds";
  151. }
  152. else
  153. {
  154. time = countdownTime.Minutes;
  155. units = "eta-units-minutes";
  156. }
  157. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString(text,
  158. ("time", time),
  159. ("units", Loc.GetString(units))),
  160. Loc.GetString(name),
  161. false,
  162. null,
  163. Color.Gold);
  164. _audio.PlayGlobal("/Audio/Announcements/shuttlecalled.ogg", Filter.Broadcast(), true);
  165. LastCountdownStart = _gameTiming.CurTime;
  166. ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime;
  167. // TODO full game saves
  168. Timer.Spawn(countdownTime, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token);
  169. ActivateCooldown();
  170. RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
  171. var shuttle = _shuttle.GetShuttle();
  172. if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
  173. {
  174. var payload = new NetworkPayload
  175. {
  176. [ShuttleTimerMasks.ShuttleMap] = shuttle,
  177. [ShuttleTimerMasks.SourceMap] = GetCentcomm(),
  178. [ShuttleTimerMasks.DestMap] = GetStation(),
  179. [ShuttleTimerMasks.ShuttleTime] = countdownTime,
  180. [ShuttleTimerMasks.SourceTime] = countdownTime + TimeSpan.FromSeconds(_shuttle.TransitTime + _cfg.GetCVar(CCVars.EmergencyShuttleDockTime)),
  181. [ShuttleTimerMasks.DestTime] = countdownTime,
  182. };
  183. _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency);
  184. }
  185. }
  186. public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true)
  187. {
  188. if (_gameTicker.RunLevel != GameRunLevel.InRound) return;
  189. if (checkCooldown && _cooldownTokenSource != null) return;
  190. if (_countdownTokenSource == null) return;
  191. _countdownTokenSource.Cancel();
  192. _countdownTokenSource = null;
  193. if (requester != null)
  194. {
  195. _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):user}");
  196. }
  197. else
  198. {
  199. _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled");
  200. }
  201. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"),
  202. Loc.GetString("Station"), false, colorOverride: Color.Gold);
  203. _audio.PlayGlobal("/Audio/Announcements/shuttlerecalled.ogg", Filter.Broadcast(), true);
  204. LastCountdownStart = null;
  205. ExpectedCountdownEnd = null;
  206. ActivateCooldown();
  207. RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
  208. // remove active clientside evac shuttle timers by zeroing the target time
  209. var zero = TimeSpan.Zero;
  210. var shuttle = _shuttle.GetShuttle();
  211. if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
  212. {
  213. var payload = new NetworkPayload
  214. {
  215. [ShuttleTimerMasks.ShuttleMap] = shuttle,
  216. [ShuttleTimerMasks.SourceMap] = GetCentcomm(),
  217. [ShuttleTimerMasks.DestMap] = GetStation(),
  218. [ShuttleTimerMasks.ShuttleTime] = zero,
  219. [ShuttleTimerMasks.SourceTime] = zero,
  220. [ShuttleTimerMasks.DestTime] = zero,
  221. };
  222. _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency);
  223. }
  224. }
  225. public void EndRound(TimeSpan? countdownTime = null)
  226. {
  227. if (_gameTicker.RunLevel != GameRunLevel.InRound) return;
  228. LastCountdownStart = null;
  229. ExpectedCountdownEnd = null;
  230. RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
  231. _gameTicker.EndRound();
  232. _countdownTokenSource?.Cancel();
  233. _countdownTokenSource = new();
  234. countdownTime ??= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.RoundRestartTime));
  235. int time;
  236. string unitsLocString;
  237. if (countdownTime.Value.TotalSeconds < 60)
  238. {
  239. time = countdownTime.Value.Seconds;
  240. unitsLocString = "eta-units-seconds";
  241. }
  242. else
  243. {
  244. time = countdownTime.Value.Minutes;
  245. unitsLocString = "eta-units-minutes";
  246. }
  247. _chatManager.DispatchServerAnnouncement(
  248. Loc.GetString(
  249. "round-end-system-round-restart-eta-announcement",
  250. ("time", time),
  251. ("units", Loc.GetString(unitsLocString))));
  252. Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token);
  253. }
  254. /// <summary>
  255. /// Starts a behavior to end the round
  256. /// </summary>
  257. /// <param name="behavior">The way in which the round will end</param>
  258. /// <param name="time"></param>
  259. /// <param name="sender"></param>
  260. /// <param name="textCall"></param>
  261. /// <param name="textAnnounce"></param>
  262. public void DoRoundEndBehavior(RoundEndBehavior behavior,
  263. TimeSpan time,
  264. string sender = "comms-console-announcement-title-centcom",
  265. string textCall = "round-end-system-shuttle-called-announcement",
  266. string textAnnounce = "round-end-system-shuttle-already-called-announcement")
  267. {
  268. switch (behavior)
  269. {
  270. case RoundEndBehavior.InstantEnd:
  271. EndRound();
  272. break;
  273. case RoundEndBehavior.ShuttleCall:
  274. // Check is shuttle called or not. We should only dispatch announcement if it's already called
  275. if (IsRoundEndRequested())
  276. {
  277. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString(textAnnounce),
  278. Loc.GetString(sender),
  279. colorOverride: Color.Gold);
  280. }
  281. else
  282. {
  283. RequestRoundEnd(time, null, false, textCall,
  284. Loc.GetString(sender));
  285. }
  286. break;
  287. }
  288. }
  289. private void AfterEndRoundRestart()
  290. {
  291. if (_gameTicker.RunLevel != GameRunLevel.PostRound) return;
  292. Reset();
  293. _gameTicker.RestartRound();
  294. }
  295. private void ActivateCooldown()
  296. {
  297. _cooldownTokenSource?.Cancel();
  298. _cooldownTokenSource = new();
  299. // TODO full game saves
  300. Timer.Spawn(DefaultCooldownDuration, () =>
  301. {
  302. _cooldownTokenSource.Cancel();
  303. _cooldownTokenSource = null;
  304. RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
  305. }, _cooldownTokenSource.Token);
  306. }
  307. public override void Update(float frameTime)
  308. {
  309. // Check if we should auto-call.
  310. int mins = _autoCalledBefore ? _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallExtensionTime)
  311. : _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime);
  312. if (mins != 0 && _gameTiming.CurTime - AutoCallStartTime > TimeSpan.FromMinutes(mins))
  313. {
  314. if (!_shuttle.EmergencyShuttleArrived && ExpectedCountdownEnd is null)
  315. {
  316. RequestRoundEnd(null, false, "round-end-system-shuttle-auto-called-announcement");
  317. _autoCalledBefore = true;
  318. }
  319. // Always reset auto-call in case of a recall.
  320. SetAutoCallTime();
  321. }
  322. }
  323. }
  324. public sealed class RoundEndSystemChangedEvent : EntityEventArgs
  325. {
  326. public static RoundEndSystemChangedEvent Default { get; } = new();
  327. }
  328. public enum RoundEndBehavior : byte
  329. {
  330. /// <summary>
  331. /// Instantly end round
  332. /// </summary>
  333. InstantEnd,
  334. /// <summary>
  335. /// Call shuttle with custom announcement
  336. /// </summary>
  337. ShuttleCall,
  338. /// <summary>
  339. /// Do nothing
  340. /// </summary>
  341. Nothing
  342. }
  343. }