1
0

RoundEndSystem.cs 15 KB

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