1
0

EmergencyShuttleSystem.Console.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. using System.Threading;
  2. using Content.Server.DeviceNetwork.Components;
  3. using Content.Server.Screens.Components;
  4. using Content.Server.Shuttles.Components;
  5. using Content.Server.Shuttles.Events;
  6. using Content.Shared.Access;
  7. using Content.Shared.CCVar;
  8. using Content.Shared.Database;
  9. using Content.Shared.DeviceNetwork;
  10. using Content.Shared.Popups;
  11. using Content.Shared.Shuttles.BUIStates;
  12. using Content.Shared.Shuttles.Events;
  13. using Content.Shared.Shuttles.Systems;
  14. using Content.Shared.UserInterface;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Player;
  17. using Timer = Robust.Shared.Timing.Timer;
  18. namespace Content.Server.Shuttles.Systems;
  19. // TODO full game saves
  20. // Move state data into the emergency shuttle component
  21. public sealed partial class EmergencyShuttleSystem
  22. {
  23. /*
  24. * Handles the emergency shuttle's console and early launching.
  25. */
  26. /// <summary>
  27. /// Has the emergency shuttle arrived?
  28. /// </summary>
  29. public bool EmergencyShuttleArrived { get; private set; }
  30. public bool EarlyLaunchAuthorized { get; private set; }
  31. /// <summary>
  32. /// How much time remaining until the shuttle consoles for emergency shuttles are unlocked?
  33. /// </summary>
  34. private float _consoleAccumulator = float.MinValue;
  35. /// <summary>
  36. /// How long after the transit is over to end the round.
  37. /// </summary>
  38. private readonly TimeSpan _bufferTime = TimeSpan.FromSeconds(5);
  39. /// <summary>
  40. /// <see cref="CCVars.EmergencyShuttleMinTransitTime"/>
  41. /// </summary>
  42. public float MinimumTransitTime { get; private set; }
  43. /// <summary>
  44. /// <see cref="CCVars.EmergencyShuttleMaxTransitTime"/>
  45. /// </summary>
  46. public float MaximumTransitTime { get; private set; }
  47. /// <summary>
  48. /// How long it will take for the emergency shuttle to arrive at CentComm.
  49. /// </summary>
  50. public float TransitTime;
  51. /// <summary>
  52. /// <see cref="CCVars.EmergencyShuttleAuthorizeTime"/>
  53. /// </summary>
  54. private float _authorizeTime;
  55. private CancellationTokenSource? _roundEndCancelToken;
  56. [ValidatePrototypeId<AccessLevelPrototype>]
  57. private const string EmergencyRepealAllAccess = "EmergencyShuttleRepealAll";
  58. private static readonly Color DangerColor = Color.Red;
  59. /// <summary>
  60. /// Have the emergency shuttles been authorised to launch at CentCom?
  61. /// </summary>
  62. private bool _launchedShuttles;
  63. /// <summary>
  64. /// Have the emergency shuttles left for CentCom?
  65. /// </summary>
  66. public bool ShuttlesLeft;
  67. /// <summary>
  68. /// Have we announced the launch?
  69. /// </summary>
  70. private bool _announced;
  71. private void InitializeEmergencyConsole()
  72. {
  73. Subs.CVar(_configManager, CCVars.EmergencyShuttleMinTransitTime, SetMinTransitTime, true);
  74. Subs.CVar(_configManager, CCVars.EmergencyShuttleMaxTransitTime, SetMaxTransitTime, true);
  75. Subs.CVar(_configManager, CCVars.EmergencyShuttleAuthorizeTime, SetAuthorizeTime, true);
  76. SubscribeLocalEvent<EmergencyShuttleConsoleComponent, ComponentStartup>(OnEmergencyStartup);
  77. SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleAuthorizeMessage>(OnEmergencyAuthorize);
  78. SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleRepealMessage>(OnEmergencyRepeal);
  79. SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleRepealAllMessage>(OnEmergencyRepealAll);
  80. SubscribeLocalEvent<EmergencyShuttleConsoleComponent, ActivatableUIOpenAttemptEvent>(OnEmergencyOpenAttempt);
  81. }
  82. private void OnEmergencyOpenAttempt(EntityUid uid, EmergencyShuttleConsoleComponent component, ActivatableUIOpenAttemptEvent args)
  83. {
  84. // I'm hoping ActivatableUI checks it's open before allowing these messages.
  85. if (!_configManager.GetCVar(CCVars.EmergencyEarlyLaunchAllowed))
  86. {
  87. args.Cancel();
  88. _popup.PopupEntity(Loc.GetString("emergency-shuttle-console-no-early-launches"), uid, args.User);
  89. }
  90. }
  91. private void SetAuthorizeTime(float obj)
  92. {
  93. _authorizeTime = obj;
  94. }
  95. private void SetMinTransitTime(float obj)
  96. {
  97. MinimumTransitTime = obj;
  98. MaximumTransitTime = Math.Max(MaximumTransitTime, MinimumTransitTime);
  99. }
  100. private void SetMaxTransitTime(float obj)
  101. {
  102. MaximumTransitTime = Math.Max(MinimumTransitTime, obj);
  103. }
  104. private void OnEmergencyStartup(EntityUid uid, EmergencyShuttleConsoleComponent component, ComponentStartup args)
  105. {
  106. UpdateConsoleState(uid, component);
  107. }
  108. private void UpdateEmergencyConsole(float frameTime)
  109. {
  110. // Add some buffer time so eshuttle always first.
  111. var minTime = -(TransitTime - (_shuttle.DefaultStartupTime + _shuttle.DefaultTravelTime + 1f));
  112. // TODO: I know this is shit but I already just cleaned up a billion things.
  113. // This is very cursed spaghetti code. I don't even know what the fuck this is doing or why it exists.
  114. // But I think it needs to be less than or equal to zero or the shuttle might never leave???
  115. // TODO Shuttle AAAAAAAAAAAAAAAAAAAAAAAAA
  116. // Clean this up, just have a single timer with some state system.
  117. // I.e., dont infer state from the current interval that the accumulator is in???
  118. minTime = Math.Min(0, minTime); // ????
  119. if (_consoleAccumulator < minTime)
  120. {
  121. return;
  122. }
  123. _consoleAccumulator -= frameTime;
  124. // No early launch but we're under the timer.
  125. if (!_launchedShuttles && _consoleAccumulator <= _authorizeTime)
  126. {
  127. if (!EarlyLaunchAuthorized)
  128. AnnounceLaunch();
  129. }
  130. // Imminent departure
  131. if (!_launchedShuttles && _consoleAccumulator <= _shuttle.DefaultStartupTime)
  132. {
  133. _launchedShuttles = true;
  134. var dataQuery = AllEntityQuery<StationEmergencyShuttleComponent>();
  135. while (dataQuery.MoveNext(out var stationUid, out var comp))
  136. {
  137. if (!TryComp<ShuttleComponent>(comp.EmergencyShuttle, out var shuttle) ||
  138. !TryComp<StationCentcommComponent>(stationUid, out var centcomm))
  139. {
  140. continue;
  141. }
  142. if (!Deleted(centcomm.Entity))
  143. {
  144. _shuttle.FTLToDock(comp.EmergencyShuttle.Value, shuttle,
  145. centcomm.Entity.Value, _consoleAccumulator, TransitTime);
  146. continue;
  147. }
  148. if (!Deleted(centcomm.MapEntity))
  149. {
  150. // TODO: Need to get non-overlapping positions.
  151. _shuttle.FTLToCoordinates(comp.EmergencyShuttle.Value, shuttle,
  152. new EntityCoordinates(centcomm.MapEntity.Value,
  153. _random.NextVector2(1000f)), _consoleAccumulator, TransitTime);
  154. }
  155. }
  156. var podQuery = AllEntityQuery<EscapePodComponent>();
  157. // Stagger launches coz funny
  158. while (podQuery.MoveNext(out _, out var pod))
  159. {
  160. pod.LaunchTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(0.05f, 0.75f));
  161. }
  162. }
  163. var podLaunchQuery = EntityQueryEnumerator<EscapePodComponent, ShuttleComponent>();
  164. while (podLaunchQuery.MoveNext(out var uid, out var pod, out var shuttle))
  165. {
  166. var stationUid = _station.GetOwningStation(uid);
  167. if (!TryComp<StationCentcommComponent>(stationUid, out var centcomm) ||
  168. Deleted(centcomm.Entity) ||
  169. pod.LaunchTime == null ||
  170. pod.LaunchTime > _timing.CurTime)
  171. {
  172. continue;
  173. }
  174. // Don't dock them. If you do end up doing this then stagger launch.
  175. _shuttle.FTLToDock(uid, shuttle, centcomm.Entity.Value, hyperspaceTime: TransitTime);
  176. RemCompDeferred<EscapePodComponent>(uid);
  177. }
  178. // Departed
  179. if (!ShuttlesLeft && _consoleAccumulator <= 0f)
  180. {
  181. ShuttlesLeft = true;
  182. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("emergency-shuttle-left", ("transitTime", $"{TransitTime:0}")));
  183. Timer.Spawn((int)(TransitTime * 1000) + _bufferTime.Milliseconds, () => _roundEnd.EndRound(), _roundEndCancelToken?.Token ?? default);
  184. }
  185. // All the others.
  186. if (_consoleAccumulator < minTime)
  187. {
  188. var query = AllEntityQuery<StationCentcommComponent, TransformComponent>();
  189. // Guarantees that emergency shuttle arrives first before anyone else can FTL.
  190. while (query.MoveNext(out var comp, out var centcommXform))
  191. {
  192. if (Deleted(comp.Entity))
  193. continue;
  194. if (_shuttle.TryAddFTLDestination(centcommXform.MapID, true, out var ftlComp))
  195. {
  196. _shuttle.SetFTLWhitelist((centcommXform.MapUid!.Value, ftlComp), null);
  197. }
  198. }
  199. }
  200. }
  201. private void OnEmergencyRepealAll(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleRepealAllMessage args)
  202. {
  203. var player = args.Actor;
  204. if (!_reader.FindAccessTags(player).Contains(EmergencyRepealAllAccess))
  205. {
  206. _popup.PopupCursor(Loc.GetString("emergency-shuttle-console-denied"), player, PopupType.Medium);
  207. return;
  208. }
  209. if (component.AuthorizedEntities.Count == 0)
  210. return;
  211. _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch REPEAL ALL by {args.Actor:user}");
  212. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("emergency-shuttle-console-auth-revoked", ("remaining", component.AuthorizationsRequired)));
  213. component.AuthorizedEntities.Clear();
  214. UpdateAllEmergencyConsoles();
  215. }
  216. private void OnEmergencyRepeal(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleRepealMessage args)
  217. {
  218. var player = args.Actor;
  219. if (!_idSystem.TryFindIdCard(player, out var idCard) || !_reader.IsAllowed(idCard, uid))
  220. {
  221. _popup.PopupCursor(Loc.GetString("emergency-shuttle-console-denied"), player, PopupType.Medium);
  222. return;
  223. }
  224. // TODO: This is fucking bad
  225. if (!component.AuthorizedEntities.Remove(MetaData(idCard).EntityName))
  226. return;
  227. _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch REPEAL by {args.Actor:user}");
  228. var remaining = component.AuthorizationsRequired - component.AuthorizedEntities.Count;
  229. _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("emergency-shuttle-console-auth-revoked", ("remaining", remaining)));
  230. CheckForLaunch(component);
  231. UpdateAllEmergencyConsoles();
  232. }
  233. private void OnEmergencyAuthorize(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleAuthorizeMessage args)
  234. {
  235. var player = args.Actor;
  236. if (!_idSystem.TryFindIdCard(player, out var idCard) || !_reader.IsAllowed(idCard, uid))
  237. {
  238. _popup.PopupCursor(Loc.GetString("emergency-shuttle-console-denied"), args.Actor, PopupType.Medium);
  239. return;
  240. }
  241. // TODO: This is fucking bad
  242. if (!component.AuthorizedEntities.Add(MetaData(idCard).EntityName))
  243. return;
  244. _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch AUTH by {args.Actor:user}");
  245. var remaining = component.AuthorizationsRequired - component.AuthorizedEntities.Count;
  246. if (remaining > 0)
  247. _chatSystem.DispatchGlobalAnnouncement(
  248. Loc.GetString("emergency-shuttle-console-auth-left", ("remaining", remaining)),
  249. playSound: false, colorOverride: DangerColor);
  250. if (!CheckForLaunch(component))
  251. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), recordReplay: true);
  252. UpdateAllEmergencyConsoles();
  253. }
  254. private void CleanupEmergencyConsole()
  255. {
  256. // Realistically most of this shit needs moving to a station component so each station has their own emergency shuttle
  257. // and timer and all that jazz so I don't really care about debugging if it works on cleanup vs start.
  258. _announced = false;
  259. ShuttlesLeft = false;
  260. _launchedShuttles = false;
  261. _consoleAccumulator = float.MinValue;
  262. EarlyLaunchAuthorized = false;
  263. EmergencyShuttleArrived = false;
  264. TransitTime = MinimumTransitTime + (MaximumTransitTime - MinimumTransitTime) * _random.NextFloat();
  265. // Round to nearest 10
  266. TransitTime = MathF.Round(TransitTime / 10f) * 10f;
  267. }
  268. private void UpdateAllEmergencyConsoles()
  269. {
  270. var query = AllEntityQuery<EmergencyShuttleConsoleComponent>();
  271. while (query.MoveNext(out var uid, out var comp))
  272. {
  273. UpdateConsoleState(uid, comp);
  274. }
  275. }
  276. private void UpdateConsoleState(EntityUid uid, EmergencyShuttleConsoleComponent component)
  277. {
  278. var auths = new List<string>();
  279. foreach (var auth in component.AuthorizedEntities)
  280. {
  281. auths.Add(auth);
  282. }
  283. if (_uiSystem.HasUi(uid, EmergencyConsoleUiKey.Key))
  284. _uiSystem.SetUiState(
  285. uid,
  286. EmergencyConsoleUiKey.Key,
  287. new EmergencyConsoleBoundUserInterfaceState()
  288. {
  289. EarlyLaunchTime = EarlyLaunchAuthorized ? _timing.CurTime + TimeSpan.FromSeconds(_consoleAccumulator) : null,
  290. Authorizations = auths,
  291. AuthorizationsRequired = component.AuthorizationsRequired,
  292. }
  293. );
  294. }
  295. private bool CheckForLaunch(EmergencyShuttleConsoleComponent component)
  296. {
  297. if (component.AuthorizedEntities.Count < component.AuthorizationsRequired || EarlyLaunchAuthorized)
  298. return false;
  299. EarlyLaunch();
  300. return true;
  301. }
  302. /// <summary>
  303. /// Attempts to early launch the emergency shuttle if not already done.
  304. /// </summary>
  305. public bool EarlyLaunch()
  306. {
  307. if (EarlyLaunchAuthorized || !EmergencyShuttleArrived || _consoleAccumulator <= _authorizeTime) return false;
  308. _logger.Add(LogType.EmergencyShuttle, LogImpact.Extreme, $"Emergency shuttle launch authorized");
  309. _consoleAccumulator = _authorizeTime;
  310. EarlyLaunchAuthorized = true;
  311. RaiseLocalEvent(new EmergencyShuttleAuthorizedEvent());
  312. AnnounceLaunch();
  313. UpdateAllEmergencyConsoles();
  314. var time = TimeSpan.FromSeconds(_authorizeTime);
  315. var shuttle = GetShuttle();
  316. if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
  317. {
  318. var payload = new NetworkPayload
  319. {
  320. [ShuttleTimerMasks.ShuttleMap] = shuttle,
  321. [ShuttleTimerMasks.SourceMap] = _roundEnd.GetStation(),
  322. [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(),
  323. [ShuttleTimerMasks.ShuttleTime] = time,
  324. [ShuttleTimerMasks.SourceTime] = time,
  325. [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime),
  326. [ShuttleTimerMasks.Docked] = true
  327. };
  328. _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency);
  329. }
  330. return true;
  331. }
  332. private void AnnounceLaunch()
  333. {
  334. if (_announced) return;
  335. _announced = true;
  336. _chatSystem.DispatchGlobalAnnouncement(
  337. Loc.GetString("emergency-shuttle-launch-time", ("consoleAccumulator", $"{_consoleAccumulator:0}")),
  338. playSound: false,
  339. colorOverride: DangerColor);
  340. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), recordReplay: true);
  341. }
  342. public bool DelayEmergencyRoundEnd()
  343. {
  344. if (_roundEndCancelToken == null)
  345. return false;
  346. _roundEndCancelToken?.Cancel();
  347. _roundEndCancelToken = null;
  348. return true;
  349. }
  350. }