1
0

EmergencyShuttleSystem.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. using System.Linq;
  2. using System.Numerics;
  3. using System.Threading;
  4. using Content.Server.Access.Systems;
  5. using Content.Server.Administration.Logs;
  6. using Content.Server.Administration.Managers;
  7. using Content.Server.Chat.Systems;
  8. using Content.Server.Communications;
  9. using Content.Server.DeviceNetwork.Components;
  10. using Content.Server.DeviceNetwork.Systems;
  11. using Content.Server.GameTicking.Events;
  12. using Content.Server.Pinpointer;
  13. using Content.Server.Popups;
  14. using Content.Server.RoundEnd;
  15. using Content.Server.Screens.Components;
  16. using Content.Server.Shuttles.Components;
  17. using Content.Server.Shuttles.Events;
  18. using Content.Server.Station.Components;
  19. using Content.Server.Station.Events;
  20. using Content.Server.Station.Systems;
  21. using Content.Shared.Access.Systems;
  22. using Content.Shared.CCVar;
  23. using Content.Shared.Database;
  24. using Content.Shared.DeviceNetwork;
  25. using Content.Shared.GameTicking;
  26. using Content.Shared.Localizations;
  27. using Content.Shared.Shuttles.Components;
  28. using Content.Shared.Shuttles.Events;
  29. using Content.Shared.Tag;
  30. using Content.Shared.Tiles;
  31. using Robust.Server.GameObjects;
  32. using Robust.Shared.Audio.Systems;
  33. using Robust.Shared.Configuration;
  34. using Robust.Shared.EntitySerialization.Systems;
  35. using Robust.Shared.Map.Components;
  36. using Robust.Shared.Player;
  37. using Robust.Shared.Random;
  38. using Robust.Shared.Timing;
  39. using Robust.Shared.Utility;
  40. namespace Content.Server.Shuttles.Systems;
  41. public sealed partial class EmergencyShuttleSystem : EntitySystem
  42. {
  43. /*
  44. * Handles the escape shuttle + CentCom.
  45. */
  46. [Dependency] private readonly IAdminLogManager _logger = default!;
  47. [Dependency] private readonly IAdminManager _admin = default!;
  48. [Dependency] private readonly IConfigurationManager _configManager = default!;
  49. [Dependency] private readonly IGameTiming _timing = default!;
  50. [Dependency] private readonly IRobustRandom _random = default!;
  51. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  52. [Dependency] private readonly AccessReaderSystem _reader = default!;
  53. [Dependency] private readonly ChatSystem _chatSystem = default!;
  54. [Dependency] private readonly CommunicationsConsoleSystem _commsConsole = default!;
  55. [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
  56. [Dependency] private readonly DockingSystem _dock = default!;
  57. [Dependency] private readonly IdCardSystem _idSystem = default!;
  58. [Dependency] private readonly NavMapSystem _navMap = default!;
  59. [Dependency] private readonly MapLoaderSystem _loader = default!;
  60. [Dependency] private readonly MetaDataSystem _metaData = default!;
  61. [Dependency] private readonly PopupSystem _popup = default!;
  62. [Dependency] private readonly RoundEndSystem _roundEnd = default!;
  63. [Dependency] private readonly SharedAudioSystem _audio = default!;
  64. [Dependency] private readonly ShuttleSystem _shuttle = default!;
  65. [Dependency] private readonly StationSystem _station = default!;
  66. [Dependency] private readonly TransformSystem _transformSystem = default!;
  67. [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
  68. private const float ShuttleSpawnBuffer = 1f;
  69. private bool _emergencyShuttleEnabled;
  70. [ValidatePrototypeId<TagPrototype>]
  71. private const string DockTag = "DockEmergency";
  72. public override void Initialize()
  73. {
  74. _emergencyShuttleEnabled = _configManager.GetCVar(CCVars.EmergencyShuttleEnabled);
  75. // Don't immediately invoke as roundstart will just handle it.
  76. Subs.CVar(_configManager, CCVars.EmergencyShuttleEnabled, SetEmergencyShuttleEnabled);
  77. SubscribeLocalEvent<RoundStartingEvent>(OnRoundStart);
  78. SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundCleanup);
  79. SubscribeLocalEvent<StationEmergencyShuttleComponent, StationPostInitEvent>(OnStationStartup);
  80. SubscribeLocalEvent<StationCentcommComponent, ComponentShutdown>(OnCentcommShutdown);
  81. SubscribeLocalEvent<StationCentcommComponent, MapInitEvent>(OnStationInit);
  82. SubscribeLocalEvent<EmergencyShuttleComponent, FTLStartedEvent>(OnEmergencyFTL);
  83. SubscribeLocalEvent<EmergencyShuttleComponent, FTLCompletedEvent>(OnEmergencyFTLComplete);
  84. SubscribeNetworkEvent<EmergencyShuttleRequestPositionMessage>(OnShuttleRequestPosition);
  85. InitializeEmergencyConsole();
  86. }
  87. private void OnRoundStart(RoundStartingEvent ev)
  88. {
  89. CleanupEmergencyConsole();
  90. _roundEndCancelToken = new CancellationTokenSource();
  91. }
  92. private void OnRoundCleanup(RoundRestartCleanupEvent ev)
  93. {
  94. _roundEndCancelToken?.Cancel();
  95. _roundEndCancelToken = null;
  96. }
  97. private void OnCentcommShutdown(EntityUid uid, StationCentcommComponent component, ComponentShutdown args)
  98. {
  99. ClearCentcomm(component);
  100. }
  101. private void ClearCentcomm(StationCentcommComponent component)
  102. {
  103. QueueDel(component.Entity);
  104. QueueDel(component.MapEntity);
  105. component.Entity = null;
  106. component.MapEntity = null;
  107. }
  108. /// <summary>
  109. /// Attempts to get the EntityUid of the emergency shuttle
  110. /// </summary>
  111. public EntityUid? GetShuttle()
  112. {
  113. AllEntityQuery<EmergencyShuttleComponent>().MoveNext(out var shuttle, out _);
  114. return shuttle;
  115. }
  116. private void SetEmergencyShuttleEnabled(bool value)
  117. {
  118. if (_emergencyShuttleEnabled == value)
  119. return;
  120. _emergencyShuttleEnabled = value;
  121. if (value)
  122. {
  123. SetupEmergencyShuttle();
  124. }
  125. else
  126. {
  127. CleanupEmergencyShuttle();
  128. }
  129. }
  130. private void CleanupEmergencyShuttle()
  131. {
  132. var query = AllEntityQuery<StationCentcommComponent>();
  133. while (query.MoveNext(out var uid, out _))
  134. {
  135. RemCompDeferred<StationCentcommComponent>(uid);
  136. }
  137. }
  138. public override void Update(float frameTime)
  139. {
  140. base.Update(frameTime);
  141. UpdateEmergencyConsole(frameTime);
  142. }
  143. /// <summary>
  144. /// If the client is requesting debug info on where an emergency shuttle would dock.
  145. /// </summary>
  146. private void OnShuttleRequestPosition(EmergencyShuttleRequestPositionMessage msg, EntitySessionEventArgs args)
  147. {
  148. if (!_admin.IsAdmin(args.SenderSession))
  149. return;
  150. var player = args.SenderSession.AttachedEntity;
  151. if (player is null)
  152. return;
  153. var station = _station.GetOwningStation(player.Value);
  154. if (!TryComp<StationEmergencyShuttleComponent>(station, out var stationShuttle) ||
  155. !HasComp<ShuttleComponent>(stationShuttle.EmergencyShuttle))
  156. {
  157. return;
  158. }
  159. var targetGrid = _station.GetLargestGrid(Comp<StationDataComponent>(station.Value));
  160. if (targetGrid == null)
  161. return;
  162. var config = _dock.GetDockingConfig(stationShuttle.EmergencyShuttle.Value, targetGrid.Value, DockTag);
  163. if (config == null)
  164. return;
  165. RaiseNetworkEvent(new EmergencyShuttlePositionMessage()
  166. {
  167. StationUid = GetNetEntity(targetGrid),
  168. Position = config.Area,
  169. });
  170. }
  171. /// <summary>
  172. /// Escape shuttle FTL event handler. The only escape shuttle FTL transit should be from station to centcomm at round end
  173. /// </summary>
  174. private void OnEmergencyFTL(EntityUid uid, EmergencyShuttleComponent component, ref FTLStartedEvent args)
  175. {
  176. var ftlTime = TimeSpan.FromSeconds
  177. (
  178. TryComp<FTLComponent>(uid, out var ftlComp) ? ftlComp.TravelTime : _shuttle.DefaultTravelTime
  179. );
  180. if (TryComp<DeviceNetworkComponent>(uid, out var netComp))
  181. {
  182. var payload = new NetworkPayload
  183. {
  184. [ShuttleTimerMasks.ShuttleMap] = uid,
  185. [ShuttleTimerMasks.SourceMap] = args.FromMapUid,
  186. [ShuttleTimerMasks.DestMap] = _transformSystem.GetMap(args.TargetCoordinates),
  187. [ShuttleTimerMasks.ShuttleTime] = ftlTime,
  188. [ShuttleTimerMasks.SourceTime] = ftlTime,
  189. [ShuttleTimerMasks.DestTime] = ftlTime
  190. };
  191. _deviceNetworkSystem.QueuePacket(uid, null, payload, netComp.TransmitFrequency);
  192. }
  193. }
  194. /// <summary>
  195. /// When the escape shuttle finishes FTL (docks at centcomm), have the timers display the round end countdown
  196. /// </summary>
  197. private void OnEmergencyFTLComplete(EntityUid uid, EmergencyShuttleComponent component, ref FTLCompletedEvent args)
  198. {
  199. var countdownTime = TimeSpan.FromSeconds(_configManager.GetCVar(CCVars.RoundRestartTime));
  200. var shuttle = args.Entity;
  201. if (TryComp<DeviceNetworkComponent>(shuttle, out var net))
  202. {
  203. var payload = new NetworkPayload
  204. {
  205. [ShuttleTimerMasks.ShuttleMap] = shuttle,
  206. [ShuttleTimerMasks.SourceMap] = _roundEnd.GetCentcomm(),
  207. [ShuttleTimerMasks.DestMap] = _roundEnd.GetStation(),
  208. [ShuttleTimerMasks.ShuttleTime] = countdownTime,
  209. [ShuttleTimerMasks.SourceTime] = countdownTime,
  210. [ShuttleTimerMasks.DestTime] = countdownTime,
  211. };
  212. // by popular request
  213. // https://discord.com/channels/310555209753690112/770682801607278632/1189989482234126356
  214. if (_random.Next(1000) == 0)
  215. {
  216. payload.Add(ScreenMasks.Text, ShuttleTimerMasks.Kill);
  217. payload.Add(ScreenMasks.Color, Color.Red);
  218. }
  219. else
  220. payload.Add(ScreenMasks.Text, ShuttleTimerMasks.Bye);
  221. _deviceNetworkSystem.QueuePacket(shuttle, null, payload, net.TransmitFrequency);
  222. }
  223. }
  224. /// <summary>
  225. /// Attempts to dock a station's emergency shuttle.
  226. /// </summary>
  227. /// <seealso cref="DockEmergencyShuttle"/>
  228. public ShuttleDockResult? DockSingleEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null)
  229. {
  230. if (!Resolve(stationUid, ref stationShuttle))
  231. return null;
  232. if (!TryComp(stationShuttle.EmergencyShuttle, out TransformComponent? xform) ||
  233. !TryComp<ShuttleComponent>(stationShuttle.EmergencyShuttle, out var shuttle))
  234. {
  235. Log.Error($"Attempted to call an emergency shuttle for an uninitialized station? Station: {ToPrettyString(stationUid)}. Shuttle: {ToPrettyString(stationShuttle.EmergencyShuttle)}");
  236. return null;
  237. }
  238. var targetGrid = _station.GetLargestGrid(Comp<StationDataComponent>(stationUid));
  239. // UHH GOOD LUCK
  240. if (targetGrid == null)
  241. {
  242. _logger.Add(
  243. LogType.EmergencyShuttle,
  244. LogImpact.High,
  245. $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}");
  246. return new ShuttleDockResult
  247. {
  248. Station = (stationUid, stationShuttle),
  249. ResultType = ShuttleDockResultType.GoodLuck,
  250. };
  251. }
  252. ShuttleDockResultType resultType;
  253. if (_shuttle.TryFTLDock(stationShuttle.EmergencyShuttle.Value, shuttle, targetGrid.Value, out var config, DockTag))
  254. {
  255. _logger.Add(
  256. LogType.EmergencyShuttle,
  257. LogImpact.High,
  258. $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations");
  259. resultType = _dock.IsConfigPriority(config, DockTag)
  260. ? ShuttleDockResultType.PriorityDock
  261. : ShuttleDockResultType.OtherDock;
  262. }
  263. else
  264. {
  265. _logger.Add(
  266. LogType.EmergencyShuttle,
  267. LogImpact.High,
  268. $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}");
  269. resultType = ShuttleDockResultType.NoDock;
  270. }
  271. return new ShuttleDockResult
  272. {
  273. Station = (stationUid, stationShuttle),
  274. DockingConfig = config,
  275. ResultType = resultType,
  276. TargetGrid = targetGrid,
  277. };
  278. }
  279. /// <summary>
  280. /// Do post-shuttle-dock setup. Announce to the crew and set up shuttle timers.
  281. /// </summary>
  282. public void AnnounceShuttleDock(ShuttleDockResult result, bool extended)
  283. {
  284. var shuttle = result.Station.Comp.EmergencyShuttle;
  285. DebugTools.Assert(shuttle != null);
  286. if (result.ResultType == ShuttleDockResultType.GoodLuck)
  287. {
  288. _chatSystem.DispatchStationAnnouncement(
  289. result.Station,
  290. Loc.GetString("emergency-shuttle-good-luck"),
  291. playDefaultSound: false);
  292. // TODO: Need filter extensions or something don't blame me.
  293. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true);
  294. return;
  295. }
  296. DebugTools.Assert(result.TargetGrid != null);
  297. // Send station announcement.
  298. var targetXform = Transform(result.TargetGrid.Value);
  299. var angle = _dock.GetAngle(
  300. shuttle.Value,
  301. Transform(shuttle.Value),
  302. result.TargetGrid.Value,
  303. targetXform);
  304. var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
  305. var location = FormattedMessage.RemoveMarkupPermissive(
  306. _navMap.GetNearestBeaconString((shuttle.Value, Transform(shuttle.Value))));
  307. var extendedText = extended ? Loc.GetString("emergency-shuttle-extended") : "";
  308. var locKey = result.ResultType == ShuttleDockResultType.NoDock
  309. ? "emergency-shuttle-nearby"
  310. : "emergency-shuttle-docked";
  311. _chatSystem.DispatchStationAnnouncement(
  312. result.Station,
  313. Loc.GetString(
  314. locKey,
  315. ("time", $"{_consoleAccumulator:0}"),
  316. ("direction", direction),
  317. ("location", location),
  318. ("extended", extendedText)),
  319. playDefaultSound: false);
  320. // Trigger shuttle timers on the shuttle.
  321. var time = TimeSpan.FromSeconds(_consoleAccumulator);
  322. if (TryComp<DeviceNetworkComponent>(shuttle, out var netComp))
  323. {
  324. var payload = new NetworkPayload
  325. {
  326. [ShuttleTimerMasks.ShuttleMap] = shuttle,
  327. [ShuttleTimerMasks.SourceMap] = targetXform.MapUid,
  328. [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(),
  329. [ShuttleTimerMasks.ShuttleTime] = time,
  330. [ShuttleTimerMasks.SourceTime] = time,
  331. [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime),
  332. [ShuttleTimerMasks.Docked] = true,
  333. };
  334. _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, netComp.TransmitFrequency);
  335. }
  336. // Play announcement audio.
  337. var audioFile = result.ResultType == ShuttleDockResultType.NoDock
  338. ? "/Audio/Misc/notice1.ogg"
  339. : "/Audio/Announcements/shuttle_dock.ogg";
  340. // TODO: Need filter extensions or something don't blame me.
  341. _audio.PlayGlobal(audioFile, Filter.Broadcast(), true);
  342. }
  343. private void OnStationInit(EntityUid uid, StationCentcommComponent component, MapInitEvent args)
  344. {
  345. // This is handled on map-init, so that centcomm has finished initializing by the time the StationPostInitEvent
  346. // gets raised
  347. if (!_emergencyShuttleEnabled)
  348. return;
  349. // Post mapinit? fancy
  350. if (TryComp(component.Entity, out TransformComponent? xform))
  351. {
  352. component.MapEntity = xform.MapUid;
  353. return;
  354. }
  355. AddCentcomm(uid, component);
  356. }
  357. private void OnStationStartup(Entity<StationEmergencyShuttleComponent> ent, ref StationPostInitEvent args)
  358. {
  359. AddEmergencyShuttle((ent, ent));
  360. }
  361. /// <summary>
  362. /// Teleports the emergency shuttle to its station and starts the countdown until it launches.
  363. /// </summary>
  364. /// <remarks>
  365. /// If the emergency shuttle is disabled, this immediately ends the round.
  366. /// </remarks>
  367. public void DockEmergencyShuttle()
  368. {
  369. if (EmergencyShuttleArrived)
  370. return;
  371. if (!_emergencyShuttleEnabled)
  372. {
  373. _roundEnd.EndRound();
  374. return;
  375. }
  376. _consoleAccumulator = _configManager.GetCVar(CCVars.EmergencyShuttleDockTime);
  377. EmergencyShuttleArrived = true;
  378. var query = AllEntityQuery<StationEmergencyShuttleComponent>();
  379. var dockResults = new List<ShuttleDockResult>();
  380. while (query.MoveNext(out var uid, out var comp))
  381. {
  382. if (DockSingleEmergencyShuttle(uid, comp) is { } dockResult)
  383. dockResults.Add(dockResult);
  384. }
  385. // Make the shuttle wait longer if it couldn't dock in the normal spot.
  386. // We have to handle the possibility of there being multiple stations, so since the shuttle timer is global,
  387. // use the WORST value we have.
  388. var worstResult = dockResults.Max(x => x.ResultType);
  389. var multiplier = worstResult switch
  390. {
  391. ShuttleDockResultType.OtherDock => _configManager.GetCVar(
  392. CCVars.EmergencyShuttleDockTimeMultiplierOtherDock),
  393. ShuttleDockResultType.NoDock => _configManager.GetCVar(
  394. CCVars.EmergencyShuttleDockTimeMultiplierNoDock),
  395. // GoodLuck doesn't get a multiplier.
  396. // Quite frankly at that point the round is probably so fucked that you'd rather it be over ASAP.
  397. _ => 1,
  398. };
  399. _consoleAccumulator *= multiplier;
  400. foreach (var shuttleDockResult in dockResults)
  401. {
  402. AnnounceShuttleDock(shuttleDockResult, multiplier > 1);
  403. }
  404. _commsConsole.UpdateCommsConsoleInterface();
  405. }
  406. private void SetupEmergencyShuttle()
  407. {
  408. if (!_emergencyShuttleEnabled)
  409. return;
  410. var centcommQuery = AllEntityQuery<StationCentcommComponent>();
  411. while (centcommQuery.MoveNext(out var uid, out var centcomm))
  412. {
  413. AddCentcomm(uid, centcomm);
  414. }
  415. var query = AllEntityQuery<StationEmergencyShuttleComponent>();
  416. while (query.MoveNext(out var uid, out var comp))
  417. {
  418. AddEmergencyShuttle((uid, comp));
  419. }
  420. }
  421. private void AddCentcomm(EntityUid station, StationCentcommComponent component)
  422. {
  423. DebugTools.Assert(LifeStage(station) >= EntityLifeStage.MapInitialized);
  424. if (component.MapEntity != null || component.Entity != null)
  425. {
  426. Log.Warning("Attempted to re-add an existing centcomm map.");
  427. return;
  428. }
  429. // Check for existing centcomms and just point to that
  430. var query = AllEntityQuery<StationCentcommComponent>();
  431. while (query.MoveNext(out var otherComp))
  432. {
  433. if (otherComp == component)
  434. continue;
  435. if (!Exists(otherComp.MapEntity) || !Exists(otherComp.Entity))
  436. {
  437. Log.Error($"Discovered invalid centcomm component?");
  438. ClearCentcomm(otherComp);
  439. continue;
  440. }
  441. component.MapEntity = otherComp.MapEntity;
  442. component.Entity = otherComp.Entity;
  443. component.ShuttleIndex = otherComp.ShuttleIndex;
  444. return;
  445. }
  446. if (string.IsNullOrEmpty(component.Map.ToString()))
  447. {
  448. Log.Warning("No CentComm map found, skipping setup.");
  449. return;
  450. }
  451. var map = _mapSystem.CreateMap(out var mapId);
  452. if (!_loader.TryLoadGrid(mapId, component.Map, out var grid))
  453. {
  454. Log.Error($"Failed to set up centcomm grid!");
  455. return;
  456. }
  457. if (!Exists(map))
  458. {
  459. Log.Error($"Failed to set up centcomm map!");
  460. QueueDel(grid);
  461. return;
  462. }
  463. if (!Exists(grid))
  464. {
  465. Log.Error($"Failed to set up centcomm grid!");
  466. QueueDel(map);
  467. return;
  468. }
  469. var xform = Transform(grid.Value);
  470. if (xform.ParentUid != map || xform.MapUid != map)
  471. {
  472. Log.Error($"Centcomm grid is not parented to its own map?");
  473. QueueDel(map);
  474. QueueDel(grid);
  475. return;
  476. }
  477. component.MapEntity = map;
  478. _metaData.SetEntityName(map, Loc.GetString("map-name-centcomm"));
  479. component.Entity = grid;
  480. _shuttle.TryAddFTLDestination(mapId, true, out _);
  481. Log.Info($"Created centcomm grid {ToPrettyString(grid)} on map {ToPrettyString(map)} for station {ToPrettyString(station)}");
  482. }
  483. public HashSet<EntityUid> GetCentcommMaps()
  484. {
  485. var query = AllEntityQuery<StationCentcommComponent>();
  486. var maps = new HashSet<EntityUid>(Count<StationCentcommComponent>());
  487. while (query.MoveNext(out var comp))
  488. {
  489. if (comp.MapEntity != null)
  490. maps.Add(comp.MapEntity.Value);
  491. }
  492. return maps;
  493. }
  494. private void AddEmergencyShuttle(Entity<StationEmergencyShuttleComponent?, StationCentcommComponent?> ent)
  495. {
  496. if (!Resolve(ent.Owner, ref ent.Comp1, ref ent.Comp2))
  497. return;
  498. if (!_emergencyShuttleEnabled)
  499. return;
  500. if (ent.Comp1.EmergencyShuttle != null)
  501. {
  502. if (Exists(ent.Comp1.EmergencyShuttle))
  503. {
  504. Log.Error($"Attempted to add an emergency shuttle to {ToPrettyString(ent)}, despite a shuttle already existing?");
  505. return;
  506. }
  507. Log.Error($"Encountered deleted emergency shuttle during initialization of {ToPrettyString(ent)}");
  508. ent.Comp1.EmergencyShuttle = null;
  509. }
  510. if (!TryComp(ent.Comp2.MapEntity, out MapComponent? map))
  511. {
  512. Log.Error($"Failed to add emergency shuttle - centcomm has not been initialized? {ToPrettyString(ent)}");
  513. return;
  514. }
  515. // Load escape shuttle
  516. var shuttlePath = ent.Comp1.EmergencyShuttlePath;
  517. if (!_loader.TryLoadGrid(map.MapId,
  518. shuttlePath,
  519. out var shuttle,
  520. // Should be far enough... right? I'm too lazy to bounds check CentCom rn.
  521. offset: new Vector2(500f + ent.Comp2.ShuttleIndex, 0f)))
  522. {
  523. Log.Error($"Unable to spawn emergency shuttle {shuttlePath} for {ToPrettyString(ent)}");
  524. return;
  525. }
  526. ent.Comp2.ShuttleIndex += Comp<MapGridComponent>(shuttle.Value).LocalAABB.Width + ShuttleSpawnBuffer;
  527. // Update indices for all centcomm comps pointing to same map
  528. var query = AllEntityQuery<StationCentcommComponent>();
  529. while (query.MoveNext(out var comp))
  530. {
  531. if (comp == ent.Comp2 || comp.MapEntity != ent.Comp2.MapEntity)
  532. continue;
  533. comp.ShuttleIndex = ent.Comp2.ShuttleIndex;
  534. }
  535. ent.Comp1.EmergencyShuttle = shuttle;
  536. EnsureComp<ProtectedGridComponent>(shuttle.Value);
  537. EnsureComp<PreventPilotComponent>(shuttle.Value);
  538. EnsureComp<EmergencyShuttleComponent>(shuttle.Value);
  539. Log.Info($"Added emergency shuttle {ToPrettyString(shuttle)} for station {ToPrettyString(ent)} and centcomm {ToPrettyString(ent.Comp2.Entity)}");
  540. }
  541. /// <summary>
  542. /// Returns whether a target is escaping on the emergency shuttle, but only if evac has arrived.
  543. /// </summary>
  544. public bool IsTargetEscaping(EntityUid target)
  545. {
  546. // if evac isn't here then sitting in a pod doesn't return true
  547. if (!EmergencyShuttleArrived)
  548. return false;
  549. // check each emergency shuttle
  550. var xform = Transform(target);
  551. foreach (var stationData in EntityQuery<StationEmergencyShuttleComponent>())
  552. {
  553. if (stationData.EmergencyShuttle == null)
  554. continue;
  555. if (IsOnGrid(xform, stationData.EmergencyShuttle.Value))
  556. {
  557. return true;
  558. }
  559. }
  560. return false;
  561. }
  562. private bool IsOnGrid(TransformComponent xform, EntityUid shuttle, MapGridComponent? grid = null, TransformComponent? shuttleXform = null)
  563. {
  564. if (!Resolve(shuttle, ref grid, ref shuttleXform))
  565. return false;
  566. return _transformSystem.GetWorldMatrix(shuttleXform).TransformBox(grid.LocalAABB).Contains(_transformSystem.GetWorldPosition(xform));
  567. }
  568. /// <summary>
  569. /// A result of a shuttle dock operation done by <see cref="EmergencyShuttleSystem.DockSingleEmergencyShuttle"/>.
  570. /// </summary>
  571. /// <seealso cref="ShuttleDockResultType"/>
  572. public sealed class ShuttleDockResult
  573. {
  574. /// <summary>
  575. /// The station for which the emergency shuttle got docked.
  576. /// </summary>
  577. public Entity<StationEmergencyShuttleComponent> Station;
  578. /// <summary>
  579. /// The target grid of the station that the shuttle tried to dock to.
  580. /// </summary>
  581. /// <remarks>
  582. /// Not present if <see cref="ResultType"/> is <see cref="ShuttleDockResultType.GoodLuck"/>.
  583. /// </remarks>
  584. public EntityUid? TargetGrid;
  585. /// <summary>
  586. /// Enum code describing the dock result.
  587. /// </summary>
  588. public ShuttleDockResultType ResultType;
  589. /// <summary>
  590. /// The docking config used to actually dock to the station.
  591. /// </summary>
  592. /// <remarks>
  593. /// Only present if <see cref="ResultType"/> is <see cref="ShuttleDockResultType.PriorityDock"/>
  594. /// or <see cref="ShuttleDockResultType.NoDock"/>.
  595. /// </remarks>
  596. public DockingConfig? DockingConfig;
  597. }
  598. /// <summary>
  599. /// Emergency shuttle dock result codes used by <see cref="ShuttleDockResult"/>.
  600. /// </summary>
  601. public enum ShuttleDockResultType : byte
  602. {
  603. // This enum is ordered from "best" to "worst". This is used to sort the results.
  604. /// <summary>
  605. /// The shuttle was docked at a priority dock, which is the intended destination.
  606. /// </summary>
  607. PriorityDock,
  608. /// <summary>
  609. /// The shuttle docked at another dock on the station then the intended priority dock.
  610. /// </summary>
  611. OtherDock,
  612. /// <summary>
  613. /// The shuttle couldn't find any suitable dock on the station at all, it did not dock.
  614. /// </summary>
  615. NoDock,
  616. /// <summary>
  617. /// No station grid was found at all, shuttle did not get moved.
  618. /// </summary>
  619. GoodLuck,
  620. }
  621. }