ShuttleSystem.FasterThanLight.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Server.Shuttles.Components;
  5. using Content.Server.Shuttles.Events;
  6. using Content.Server.Station.Events;
  7. using Content.Shared.Body.Components;
  8. using Content.Shared.Buckle.Components;
  9. using Content.Shared.CCVar;
  10. using Content.Shared.Database;
  11. using Content.Shared.Ghost;
  12. using Content.Shared.Maps;
  13. using Content.Shared.Parallax;
  14. using Content.Shared.Shuttles.Components;
  15. using Content.Shared.Shuttles.Systems;
  16. using Content.Shared.StatusEffect;
  17. using Content.Shared.Timing;
  18. using Content.Shared.Whitelist;
  19. using JetBrains.Annotations;
  20. using Robust.Shared.Audio;
  21. using Robust.Shared.Audio.Components;
  22. using Robust.Shared.Collections;
  23. using Robust.Shared.Map;
  24. using Robust.Shared.Map.Components;
  25. using Robust.Shared.Physics;
  26. using Robust.Shared.Physics.Components;
  27. using Robust.Shared.Player;
  28. using Robust.Shared.Utility;
  29. using FTLMapComponent = Content.Shared.Shuttles.Components.FTLMapComponent;
  30. namespace Content.Server.Shuttles.Systems;
  31. public sealed partial class ShuttleSystem
  32. {
  33. /*
  34. * This is a way to move a shuttle from one location to another, via an intermediate map for fanciness.
  35. */
  36. private readonly SoundSpecifier _startupSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_begin.ogg")
  37. {
  38. Params = AudioParams.Default.WithVolume(-5f),
  39. };
  40. private readonly SoundSpecifier _arrivalSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_end.ogg")
  41. {
  42. Params = AudioParams.Default.WithVolume(-5f),
  43. };
  44. public float DefaultStartupTime;
  45. public float DefaultTravelTime;
  46. public float DefaultArrivalTime;
  47. private float FTLCooldown;
  48. public float FTLMassLimit;
  49. private TimeSpan _hyperspaceKnockdownTime = TimeSpan.FromSeconds(5);
  50. /// <summary>
  51. /// Left-side of the station we're allowed to use
  52. /// </summary>
  53. private float _index;
  54. /// <summary>
  55. /// Space between grids within hyperspace.
  56. /// </summary>
  57. private const float Buffer = 5f;
  58. /// <summary>
  59. /// How many times we try to proximity warp close to something before falling back to map-wideAABB.
  60. /// </summary>
  61. private const int FTLProximityIterations = 5;
  62. private readonly HashSet<EntityUid> _lookupEnts = new();
  63. private readonly HashSet<EntityUid> _immuneEnts = new();
  64. private readonly HashSet<Entity<NoFTLComponent>> _noFtls = new();
  65. private EntityQuery<BodyComponent> _bodyQuery;
  66. private EntityQuery<BuckleComponent> _buckleQuery;
  67. private EntityQuery<FTLSmashImmuneComponent> _immuneQuery;
  68. private EntityQuery<PhysicsComponent> _physicsQuery;
  69. private EntityQuery<StatusEffectsComponent> _statusQuery;
  70. private EntityQuery<TransformComponent> _xformQuery;
  71. private void InitializeFTL()
  72. {
  73. SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit);
  74. SubscribeLocalEvent<FTLComponent, ComponentShutdown>(OnFtlShutdown);
  75. _bodyQuery = GetEntityQuery<BodyComponent>();
  76. _buckleQuery = GetEntityQuery<BuckleComponent>();
  77. _immuneQuery = GetEntityQuery<FTLSmashImmuneComponent>();
  78. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  79. _statusQuery = GetEntityQuery<StatusEffectsComponent>();
  80. _xformQuery = GetEntityQuery<TransformComponent>();
  81. _cfg.OnValueChanged(CCVars.FTLStartupTime, time => DefaultStartupTime = time, true);
  82. _cfg.OnValueChanged(CCVars.FTLTravelTime, time => DefaultTravelTime = time, true);
  83. _cfg.OnValueChanged(CCVars.FTLArrivalTime, time => DefaultArrivalTime = time, true);
  84. _cfg.OnValueChanged(CCVars.FTLCooldown, time => FTLCooldown = time, true);
  85. _cfg.OnValueChanged(CCVars.FTLMassLimit, time => FTLMassLimit = time, true);
  86. _cfg.OnValueChanged(CCVars.HyperspaceKnockdownTime, time => _hyperspaceKnockdownTime = TimeSpan.FromSeconds(time), true);
  87. }
  88. private void OnFtlShutdown(Entity<FTLComponent> ent, ref ComponentShutdown args)
  89. {
  90. QueueDel(ent.Comp.VisualizerEntity);
  91. ent.Comp.VisualizerEntity = null;
  92. }
  93. private void OnStationPostInit(ref StationPostInitEvent ev)
  94. {
  95. // Add all grid maps as ftl destinations that anyone can FTL to.
  96. foreach (var gridUid in ev.Station.Comp.Grids)
  97. {
  98. var gridXform = _xformQuery.GetComponent(gridUid);
  99. if (gridXform.MapUid == null)
  100. {
  101. continue;
  102. }
  103. TryAddFTLDestination(gridXform.MapID, true, false, false, out _);
  104. }
  105. }
  106. /// <summary>
  107. /// Ensures the FTL map exists and returns it.
  108. /// </summary>
  109. private EntityUid EnsureFTLMap()
  110. {
  111. var query = AllEntityQuery<FTLMapComponent>();
  112. while (query.MoveNext(out var uid, out _))
  113. {
  114. return uid;
  115. }
  116. var mapUid = _mapSystem.CreateMap(out var mapId);
  117. var ftlMap = AddComp<FTLMapComponent>(mapUid);
  118. _metadata.SetEntityName(mapUid, "FTL");
  119. Log.Debug($"Setup hyperspace map at {mapUid}");
  120. DebugTools.Assert(!_mapSystem.IsPaused(mapId));
  121. var parallax = EnsureComp<ParallaxComponent>(mapUid);
  122. parallax.Parallax = ftlMap.Parallax;
  123. return mapUid;
  124. }
  125. public StartEndTime GetStateTime(FTLComponent component)
  126. {
  127. var state = component.State;
  128. switch (state)
  129. {
  130. case FTLState.Starting:
  131. case FTLState.Travelling:
  132. case FTLState.Arriving:
  133. case FTLState.Cooldown:
  134. return component.StateTime;
  135. case FTLState.Available:
  136. return default;
  137. default:
  138. throw new NotImplementedException();
  139. }
  140. }
  141. /// <summary>
  142. /// Updates the whitelist for this FTL destination.
  143. /// </summary>
  144. /// <param name="entity"></param>
  145. /// <param name="whitelist"></param>
  146. public void SetFTLWhitelist(Entity<FTLDestinationComponent?> entity, EntityWhitelist? whitelist)
  147. {
  148. if (!Resolve(entity, ref entity.Comp))
  149. return;
  150. if (entity.Comp.Whitelist == whitelist)
  151. return;
  152. entity.Comp.Whitelist = whitelist;
  153. _console.RefreshShuttleConsoles();
  154. Dirty(entity);
  155. }
  156. /// <summary>
  157. /// Adds the target map as available for FTL.
  158. /// </summary>
  159. public bool TryAddFTLDestination(MapId mapId, bool enabled, [NotNullWhen(true)] out FTLDestinationComponent? component)
  160. {
  161. return TryAddFTLDestination(mapId, enabled, true, false, out component);
  162. }
  163. public bool TryAddFTLDestination(MapId mapId, bool enabled, bool requireDisk, bool beaconsOnly, [NotNullWhen(true)] out FTLDestinationComponent? component)
  164. {
  165. var mapUid = _mapSystem.GetMapOrInvalid(mapId);
  166. component = null;
  167. if (!Exists(mapUid))
  168. return false;
  169. component = EnsureComp<FTLDestinationComponent>(mapUid);
  170. if (component.Enabled == enabled && component.RequireCoordinateDisk == requireDisk && component.BeaconsOnly == beaconsOnly)
  171. return true;
  172. component.Enabled = enabled;
  173. component.RequireCoordinateDisk = requireDisk;
  174. component.BeaconsOnly = beaconsOnly;
  175. _console.RefreshShuttleConsoles();
  176. Dirty(mapUid, component);
  177. return true;
  178. }
  179. [PublicAPI]
  180. public void RemoveFTLDestination(EntityUid uid)
  181. {
  182. if (!RemComp<FTLDestinationComponent>(uid))
  183. return;
  184. _console.RefreshShuttleConsoles();
  185. }
  186. /// <summary>
  187. /// Returns true if the grid can FTL. Used to block protected shuttles like the emergency shuttle.
  188. /// </summary>
  189. public bool CanFTL(EntityUid shuttleUid, [NotNullWhen(false)] out string? reason)
  190. {
  191. // Currently in FTL already
  192. if (HasComp<FTLComponent>(shuttleUid))
  193. {
  194. reason = Loc.GetString("shuttle-console-in-ftl");
  195. return false;
  196. }
  197. if (TryComp<PhysicsComponent>(shuttleUid, out var shuttlePhysics))
  198. {
  199. // Too large to FTL
  200. if (FTLMassLimit > 0 && shuttlePhysics.Mass > FTLMassLimit)
  201. {
  202. reason = Loc.GetString("shuttle-console-mass");
  203. return false;
  204. }
  205. }
  206. if (HasComp<PreventPilotComponent>(shuttleUid))
  207. {
  208. reason = Loc.GetString("shuttle-console-prevent");
  209. return false;
  210. }
  211. var ev = new ConsoleFTLAttemptEvent(shuttleUid, false, string.Empty);
  212. RaiseLocalEvent(shuttleUid, ref ev, true);
  213. if (ev.Cancelled)
  214. {
  215. reason = ev.Reason;
  216. return false;
  217. }
  218. reason = null;
  219. return true;
  220. }
  221. /// <summary>
  222. /// Moves a shuttle from its current position to the target one without any checks. Goes through the hyperspace map while the timer is running.
  223. /// </summary>
  224. public void FTLToCoordinates(
  225. EntityUid shuttleUid,
  226. ShuttleComponent component,
  227. EntityCoordinates coordinates,
  228. Angle angle,
  229. float? startupTime = null,
  230. float? hyperspaceTime = null,
  231. string? priorityTag = null)
  232. {
  233. if (!TrySetupFTL(shuttleUid, component, out var hyperspace))
  234. return;
  235. startupTime ??= DefaultStartupTime;
  236. hyperspaceTime ??= DefaultTravelTime;
  237. hyperspace.StartupTime = startupTime.Value;
  238. hyperspace.TravelTime = hyperspaceTime.Value;
  239. hyperspace.StateTime = StartEndTime.FromStartDuration(
  240. _gameTiming.CurTime,
  241. TimeSpan.FromSeconds(hyperspace.StartupTime));
  242. hyperspace.TargetCoordinates = coordinates;
  243. hyperspace.TargetAngle = angle;
  244. hyperspace.PriorityTag = priorityTag;
  245. _console.RefreshShuttleConsoles(shuttleUid);
  246. var mapId = _transform.GetMapId(coordinates);
  247. var mapUid = _mapSystem.GetMap(mapId);
  248. var ev = new FTLRequestEvent(mapUid);
  249. RaiseLocalEvent(shuttleUid, ref ev, true);
  250. }
  251. /// <summary>
  252. /// Moves a shuttle from its current position to docked on the target one.
  253. /// If no docks are free when FTLing it will arrive in proximity
  254. /// </summary>
  255. public void FTLToDock(
  256. EntityUid shuttleUid,
  257. ShuttleComponent component,
  258. EntityUid target,
  259. float? startupTime = null,
  260. float? hyperspaceTime = null,
  261. string? priorityTag = null)
  262. {
  263. if (!TrySetupFTL(shuttleUid, component, out var hyperspace))
  264. return;
  265. startupTime ??= DefaultStartupTime;
  266. hyperspaceTime ??= DefaultTravelTime;
  267. var config = _dockSystem.GetDockingConfig(shuttleUid, target, priorityTag);
  268. hyperspace.StartupTime = startupTime.Value;
  269. hyperspace.TravelTime = hyperspaceTime.Value;
  270. hyperspace.StateTime = StartEndTime.FromStartDuration(
  271. _gameTiming.CurTime,
  272. TimeSpan.FromSeconds(hyperspace.StartupTime));
  273. hyperspace.PriorityTag = priorityTag;
  274. _console.RefreshShuttleConsoles(shuttleUid);
  275. // Valid dock for now time so just use that as the target.
  276. if (config != null)
  277. {
  278. hyperspace.TargetCoordinates = config.Coordinates;
  279. hyperspace.TargetAngle = config.Angle;
  280. }
  281. else if (TryGetFTLProximity(shuttleUid, new EntityCoordinates(target, Vector2.Zero), out var coords, out var targAngle))
  282. {
  283. hyperspace.TargetCoordinates = coords;
  284. hyperspace.TargetAngle = targAngle;
  285. }
  286. else
  287. {
  288. // FTL back to its own position.
  289. hyperspace.TargetCoordinates = Transform(shuttleUid).Coordinates;
  290. Log.Error($"Unable to FTL grid {ToPrettyString(shuttleUid)} to target properly?");
  291. }
  292. }
  293. private bool TrySetupFTL(EntityUid uid, ShuttleComponent shuttle, [NotNullWhen(true)] out FTLComponent? component)
  294. {
  295. component = null;
  296. if (HasComp<FTLComponent>(uid))
  297. {
  298. Log.Warning($"Tried queuing {ToPrettyString(uid)} which already has {nameof(FTLComponent)}?");
  299. return false;
  300. }
  301. _thruster.DisableLinearThrusters(shuttle);
  302. _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
  303. _thruster.SetAngularThrust(shuttle, false);
  304. _dockSystem.UndockDocks(uid);
  305. component = AddComp<FTLComponent>(uid);
  306. component.State = FTLState.Starting;
  307. var audio = _audio.PlayPvs(_startupSound, uid);
  308. _audio.SetGridAudio(audio);
  309. component.StartupStream = audio?.Entity;
  310. // TODO: Play previs here for docking arrival.
  311. // Make sure the map is setup before we leave to avoid pop-in (e.g. parallax).
  312. EnsureFTLMap();
  313. return true;
  314. }
  315. /// <summary>
  316. /// Transitions shuttle to FTL map.
  317. /// </summary>
  318. private void UpdateFTLStarting(Entity<FTLComponent, ShuttleComponent> entity)
  319. {
  320. var uid = entity.Owner;
  321. var comp = entity.Comp1;
  322. var xform = _xformQuery.GetComponent(entity);
  323. DoTheDinosaur(xform);
  324. comp.State = FTLState.Travelling;
  325. var fromMapUid = xform.MapUid;
  326. var fromMatrix = _transform.GetWorldMatrix(xform);
  327. var fromRotation = _transform.GetWorldRotation(xform);
  328. var grid = Comp<MapGridComponent>(uid);
  329. var width = grid.LocalAABB.Width;
  330. var ftlMap = EnsureFTLMap();
  331. var body = _physicsQuery.GetComponent(entity);
  332. var shuttleCenter = grid.LocalAABB.Center;
  333. // Leave audio at the old spot
  334. // Just so we don't clip
  335. if (fromMapUid != null && TryComp(comp.StartupStream, out AudioComponent? startupAudio))
  336. {
  337. var clippedAudio = _audio.PlayStatic(_startupSound, Filter.Broadcast(),
  338. new EntityCoordinates(fromMapUid.Value, _mapSystem.GetGridPosition(entity.Owner)), true, startupAudio.Params);
  339. _audio.SetPlaybackPosition(clippedAudio, entity.Comp1.StartupTime);
  340. if (clippedAudio != null)
  341. clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
  342. }
  343. // Offset the start by buffer range just to avoid overlap.
  344. var ftlStart = new EntityCoordinates(ftlMap, new Vector2(_index + width / 2f, 0f) - shuttleCenter);
  345. // Store the matrix for the grid prior to movement. This means any entities we need to leave behind we can make sure their positions are updated.
  346. // Setting the entity to map directly may run grid traversal (at least at time of writing this).
  347. var oldMapUid = xform.MapUid;
  348. var oldGridMatrix = _transform.GetWorldMatrix(xform);
  349. _transform.SetCoordinates(entity.Owner, ftlStart);
  350. LeaveNoFTLBehind((entity.Owner, xform), oldGridMatrix, oldMapUid);
  351. // Reset rotation so they always face the same direction.
  352. xform.LocalRotation = Angle.Zero;
  353. _index += width + Buffer;
  354. comp.StateTime = StartEndTime.FromCurTime(_gameTiming, comp.TravelTime - DefaultArrivalTime);
  355. Enable(uid, component: body);
  356. _physics.SetLinearVelocity(uid, new Vector2(0f, 20f), body: body);
  357. _physics.SetAngularVelocity(uid, 0f, body: body);
  358. _physics.SetLinearDamping(uid, body, 0f);
  359. _physics.SetAngularDamping(uid, body, 0f);
  360. _dockSystem.SetDockBolts(uid, true);
  361. _console.RefreshShuttleConsoles(uid);
  362. var ev = new FTLStartedEvent(uid, comp.TargetCoordinates, fromMapUid, fromMatrix, fromRotation);
  363. RaiseLocalEvent(uid, ref ev, true);
  364. // Audio
  365. var wowdio = _audio.PlayPvs(comp.TravelSound, uid);
  366. comp.TravelStream = wowdio?.Entity;
  367. _audio.SetGridAudio(wowdio);
  368. }
  369. /// <summary>
  370. /// Shuttle arriving.
  371. /// </summary>
  372. private void UpdateFTLTravelling(Entity<FTLComponent, ShuttleComponent> entity)
  373. {
  374. var shuttle = entity.Comp2;
  375. var comp = entity.Comp1;
  376. comp.StateTime = StartEndTime.FromCurTime(_gameTiming, DefaultArrivalTime);
  377. comp.State = FTLState.Arriving;
  378. if (entity.Comp1.VisualizerProto != null)
  379. {
  380. comp.VisualizerEntity = SpawnAtPosition(entity.Comp1.VisualizerProto, entity.Comp1.TargetCoordinates);
  381. var visuals = Comp<FtlVisualizerComponent>(comp.VisualizerEntity.Value);
  382. visuals.Grid = entity.Owner;
  383. Dirty(comp.VisualizerEntity.Value, visuals);
  384. _transform.SetLocalRotation(comp.VisualizerEntity.Value, entity.Comp1.TargetAngle);
  385. _pvs.AddGlobalOverride(comp.VisualizerEntity.Value);
  386. }
  387. _thruster.DisableLinearThrusters(shuttle);
  388. _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
  389. _console.RefreshShuttleConsoles(entity.Owner);
  390. }
  391. /// <summary>
  392. /// Shuttle arrived.
  393. /// </summary>
  394. private void UpdateFTLArriving(Entity<FTLComponent, ShuttleComponent> entity)
  395. {
  396. var uid = entity.Owner;
  397. var xform = _xformQuery.GetComponent(uid);
  398. var body = _physicsQuery.GetComponent(uid);
  399. var comp = entity.Comp1;
  400. DoTheDinosaur(xform);
  401. _dockSystem.SetDockBolts(entity, false);
  402. _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
  403. _physics.SetAngularVelocity(uid, 0f, body: body);
  404. _physics.SetLinearDamping(uid, body, entity.Comp2.LinearDamping);
  405. _physics.SetAngularDamping(uid, body, entity.Comp2.AngularDamping);
  406. var target = entity.Comp1.TargetCoordinates;
  407. MapId mapId;
  408. QueueDel(entity.Comp1.VisualizerEntity);
  409. entity.Comp1.VisualizerEntity = null;
  410. if (!Exists(entity.Comp1.TargetCoordinates.EntityId))
  411. {
  412. // Uhh good luck
  413. // Pick earliest map?
  414. var maps = EntityQuery<MapComponent>().Select(o => o.MapId).ToList();
  415. var map = maps.Min(o => o.GetHashCode());
  416. mapId = new MapId(map);
  417. TryFTLProximity(uid, _mapSystem.GetMap(mapId));
  418. }
  419. // Docking FTL
  420. else if (HasComp<MapGridComponent>(target.EntityId) &&
  421. !HasComp<MapComponent>(target.EntityId))
  422. {
  423. var config = _dockSystem.GetDockingConfigAt(uid, target.EntityId, target, entity.Comp1.TargetAngle);
  424. var mapCoordinates = _transform.ToMapCoordinates(target);
  425. // Couldn't dock somehow so just fallback to regular position FTL.
  426. if (config == null)
  427. {
  428. TryFTLProximity(uid, target.EntityId);
  429. }
  430. else
  431. {
  432. FTLDock((uid, xform), config);
  433. }
  434. mapId = mapCoordinates.MapId;
  435. }
  436. // Position ftl
  437. else
  438. {
  439. // TODO: This should now use tryftlproximity
  440. mapId = _transform.GetMapId(target);
  441. _transform.SetCoordinates(uid, xform, target, rotation: entity.Comp1.TargetAngle);
  442. }
  443. if (_physicsQuery.TryGetComponent(uid, out body))
  444. {
  445. _physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
  446. _physics.SetAngularVelocity(uid, 0f, body: body);
  447. // Disable shuttle if it's on a planet; unfortunately can't do this in parent change messages due
  448. // to event ordering and awake body shenanigans (at least for now).
  449. if (HasComp<MapGridComponent>(xform.MapUid))
  450. {
  451. Disable(uid, component: body);
  452. }
  453. else
  454. {
  455. Enable(uid, component: body, shuttle: entity.Comp2);
  456. }
  457. }
  458. _thruster.DisableLinearThrusters(entity.Comp2);
  459. comp.TravelStream = _audio.Stop(comp.TravelStream);
  460. var audio = _audio.PlayPvs(_arrivalSound, uid);
  461. _audio.SetGridAudio(audio);
  462. if (TryComp<FTLDestinationComponent>(uid, out var dest))
  463. {
  464. dest.Enabled = true;
  465. }
  466. comp.State = FTLState.Cooldown;
  467. comp.StateTime = StartEndTime.FromCurTime(_gameTiming, FTLCooldown);
  468. _console.RefreshShuttleConsoles(uid);
  469. _mapManager.SetMapPaused(mapId, false);
  470. Smimsh(uid, xform: xform);
  471. var ftlEvent = new FTLCompletedEvent(uid, _mapSystem.GetMap(mapId));
  472. RaiseLocalEvent(uid, ref ftlEvent, true);
  473. }
  474. private void UpdateFTLCooldown(Entity<FTLComponent, ShuttleComponent> entity)
  475. {
  476. RemCompDeferred<FTLComponent>(entity);
  477. _console.RefreshShuttleConsoles(entity);
  478. }
  479. private void UpdateHyperspace()
  480. {
  481. var curTime = _gameTiming.CurTime;
  482. var query = EntityQueryEnumerator<FTLComponent, ShuttleComponent>();
  483. while (query.MoveNext(out var uid, out var comp, out var shuttle))
  484. {
  485. if (curTime < comp.StateTime.End)
  486. continue;
  487. var entity = (uid, comp, shuttle);
  488. switch (comp.State)
  489. {
  490. // Startup time has elapsed and in hyperspace.
  491. case FTLState.Starting:
  492. UpdateFTLStarting(entity);
  493. break;
  494. // Arriving, play effects
  495. case FTLState.Travelling:
  496. UpdateFTLTravelling(entity);
  497. break;
  498. // Arrived
  499. case FTLState.Arriving:
  500. UpdateFTLArriving(entity);
  501. break;
  502. case FTLState.Cooldown:
  503. UpdateFTLCooldown(entity);
  504. break;
  505. default:
  506. Log.Error($"Found invalid FTL state {comp.State} for {uid}");
  507. RemCompDeferred<FTLComponent>(uid);
  508. break;
  509. }
  510. }
  511. }
  512. private float GetSoundRange(EntityUid uid)
  513. {
  514. if (!TryComp<MapGridComponent>(uid, out var grid))
  515. return 4f;
  516. return MathF.Max(grid.LocalAABB.Width, grid.LocalAABB.Height) + 12.5f;
  517. }
  518. /// <summary>
  519. /// Puts everyone unbuckled on the floor, paralyzed.
  520. /// </summary>
  521. private void DoTheDinosaur(TransformComponent xform)
  522. {
  523. // Get enumeration exceptions from people dropping things if we just paralyze as we go
  524. var toKnock = new ValueList<EntityUid>();
  525. KnockOverKids(xform, ref toKnock);
  526. TryComp<MapGridComponent>(xform.GridUid, out var grid);
  527. if (TryComp<PhysicsComponent>(xform.GridUid, out var shuttleBody))
  528. {
  529. foreach (var child in toKnock)
  530. {
  531. if (!_statusQuery.TryGetComponent(child, out var status))
  532. continue;
  533. _stuns.TryParalyze(child, _hyperspaceKnockdownTime, true, status);
  534. // If the guy we knocked down is on a spaced tile, throw them too
  535. if (grid != null)
  536. TossIfSpaced((xform.GridUid.Value, grid, shuttleBody), child);
  537. }
  538. }
  539. }
  540. private void LeaveNoFTLBehind(Entity<TransformComponent> grid, Matrix3x2 oldGridMatrix, EntityUid? oldMapUid)
  541. {
  542. if (oldMapUid == null)
  543. return;
  544. _noFtls.Clear();
  545. var oldGridRotation = oldGridMatrix.Rotation();
  546. _lookup.GetGridEntities(grid.Owner, _noFtls);
  547. foreach (var childUid in _noFtls)
  548. {
  549. if (!_xformQuery.TryComp(childUid, out var childXform))
  550. continue;
  551. // If we're not parented directly to the grid the matrix may be wrong.
  552. var relative = _physics.GetRelativePhysicsTransform(childUid.Owner, (grid.Owner, grid.Comp));
  553. _transform.SetCoordinates(
  554. childUid,
  555. childXform,
  556. new EntityCoordinates(oldMapUid.Value,
  557. Vector2.Transform(relative.Position, oldGridMatrix)), rotation: relative.Quaternion2D.Angle + oldGridRotation);
  558. }
  559. }
  560. private void KnockOverKids(TransformComponent xform, ref ValueList<EntityUid> toKnock)
  561. {
  562. // Not recursive because probably not necessary? If we need it to be that's why this method is separate.
  563. var childEnumerator = xform.ChildEnumerator;
  564. while (childEnumerator.MoveNext(out var child))
  565. {
  566. if (!_buckleQuery.TryGetComponent(child, out var buckle) || buckle.Buckled)
  567. continue;
  568. toKnock.Add(child);
  569. }
  570. }
  571. /// <summary>
  572. /// Throws people who are standing on a spaced tile, tries to throw them towards a neighbouring space tile
  573. /// </summary>
  574. private void TossIfSpaced(Entity<MapGridComponent, PhysicsComponent> shuttleEntity, EntityUid tossed)
  575. {
  576. var shuttleGrid = shuttleEntity.Comp1;
  577. var shuttleBody = shuttleEntity.Comp2;
  578. if (!_xformQuery.TryGetComponent(tossed, out var childXform))
  579. return;
  580. // only toss if its on lattice/space
  581. var tile = _mapSystem.GetTileRef(shuttleEntity, shuttleGrid, childXform.Coordinates);
  582. if (!tile.IsSpace(_tileDefManager))
  583. return;
  584. var throwDirection = childXform.LocalPosition - shuttleBody.LocalCenter;
  585. if (throwDirection == Vector2.Zero)
  586. return;
  587. _throwing.TryThrow(tossed, throwDirection.Normalized() * 10.0f, 50.0f);
  588. }
  589. /// <summary>
  590. /// Tries to dock with the target grid, otherwise falls back to proximity.
  591. /// This bypasses FTL travel time.
  592. /// </summary>
  593. public bool TryFTLDock(
  594. EntityUid shuttleUid,
  595. ShuttleComponent component,
  596. EntityUid targetUid,
  597. string? priorityTag = null)
  598. {
  599. return TryFTLDock(shuttleUid, component, targetUid, out _, priorityTag);
  600. }
  601. /// <summary>
  602. /// Tries to dock with the target grid, otherwise falls back to proximity.
  603. /// This bypasses FTL travel time.
  604. /// </summary>
  605. public bool TryFTLDock(
  606. EntityUid shuttleUid,
  607. ShuttleComponent component,
  608. EntityUid targetUid,
  609. [NotNullWhen(true)] out DockingConfig? config,
  610. string? priorityTag = null)
  611. {
  612. config = null;
  613. if (!_xformQuery.TryGetComponent(shuttleUid, out var shuttleXform) ||
  614. !_xformQuery.TryGetComponent(targetUid, out var targetXform) ||
  615. targetXform.MapUid == null ||
  616. !targetXform.MapUid.Value.IsValid())
  617. {
  618. return false;
  619. }
  620. config = _dockSystem.GetDockingConfig(shuttleUid, targetUid, priorityTag);
  621. if (config != null)
  622. {
  623. FTLDock((shuttleUid, shuttleXform), config);
  624. return true;
  625. }
  626. TryFTLProximity(shuttleUid, targetUid, shuttleXform, targetXform);
  627. return false;
  628. }
  629. /// <summary>
  630. /// Forces an FTL dock.
  631. /// </summary>
  632. public void FTLDock(Entity<TransformComponent> shuttle, DockingConfig config)
  633. {
  634. // Set position
  635. var mapCoordinates = _transform.ToMapCoordinates(config.Coordinates);
  636. var mapUid = _mapSystem.GetMap(mapCoordinates.MapId);
  637. _transform.SetCoordinates(shuttle.Owner, shuttle.Comp, new EntityCoordinates(mapUid, mapCoordinates.Position), rotation: config.Angle);
  638. // Connect everything
  639. foreach (var (dockAUid, dockBUid, dockA, dockB) in config.Docks)
  640. {
  641. _dockSystem.Dock((dockAUid, dockA), (dockBUid, dockB));
  642. }
  643. }
  644. /// <summary>
  645. /// Tries to get the target position to FTL near the target coordinates.
  646. /// If the target coordinates have a mapgrid then will try to offset the AABB.
  647. /// </summary>
  648. /// <param name="minOffset">Min offset for the final FTL.</param>
  649. /// <param name="maxOffset">Max offset for the final FTL from the box we spawn.</param>
  650. private bool TryGetFTLProximity(
  651. EntityUid shuttleUid,
  652. EntityCoordinates targetCoordinates,
  653. out EntityCoordinates coordinates, out Angle angle,
  654. float minOffset = 0f, float maxOffset = 64f,
  655. TransformComponent? xform = null, TransformComponent? targetXform = null)
  656. {
  657. DebugTools.Assert(minOffset < maxOffset);
  658. coordinates = EntityCoordinates.Invalid;
  659. angle = Angle.Zero;
  660. if (!Resolve(targetCoordinates.EntityId, ref targetXform) ||
  661. targetXform.MapUid == null ||
  662. !targetXform.MapUid.Value.IsValid() ||
  663. !Resolve(shuttleUid, ref xform))
  664. {
  665. return false;
  666. }
  667. // We essentially expand the Box2 of the target area until nothing else is added then we know it's valid.
  668. // Can't just get an AABB of every grid as we may spawn very far away.
  669. var nearbyGrids = new HashSet<EntityUid>();
  670. var shuttleAABB = Comp<MapGridComponent>(shuttleUid).LocalAABB;
  671. // Start with small point.
  672. // If our target pos is offset we mot even intersect our target's AABB so we don't include it.
  673. var targetLocalAABB = Box2.CenteredAround(targetCoordinates.Position, Vector2.One);
  674. // How much we expand the target AABB be.
  675. // We half it because we only need the width / height in each direction if it's placed at a particular spot.
  676. var expansionAmount = MathF.Max(shuttleAABB.Width / 2f, shuttleAABB.Height / 2f);
  677. // Expand the starter AABB so we have something to query to start with.
  678. var targetAABB = _transform.GetWorldMatrix(targetXform)
  679. .TransformBox(targetLocalAABB)
  680. .Enlarged(expansionAmount);
  681. var iteration = 0;
  682. var lastCount = nearbyGrids.Count;
  683. var mapId = targetXform.MapID;
  684. var grids = new List<Entity<MapGridComponent>>();
  685. while (iteration < FTLProximityIterations)
  686. {
  687. grids.Clear();
  688. // We pass in an expanded offset here so we can safely do a random offset later.
  689. // We don't include this in the actual targetAABB because then we would be double-expanding it.
  690. // Once in this loop, then again when placing the shuttle later.
  691. // Note that targetAABB already has expansionAmount factored in already.
  692. _mapManager.FindGridsIntersecting(mapId, targetAABB.Enlarged(maxOffset), ref grids);
  693. foreach (var grid in grids)
  694. {
  695. if (!nearbyGrids.Add(grid))
  696. continue;
  697. // Include the other grid's AABB (expanded by ours) as well.
  698. targetAABB = targetAABB.Union(
  699. _transform.GetWorldMatrix(grid)
  700. .TransformBox(Comp<MapGridComponent>(grid).LocalAABB.Enlarged(expansionAmount)));
  701. }
  702. // Can do proximity
  703. if (nearbyGrids.Count == lastCount)
  704. {
  705. break;
  706. }
  707. iteration++;
  708. lastCount = nearbyGrids.Count;
  709. // Mishap moment, dense asteroid field or whatever
  710. if (iteration != FTLProximityIterations)
  711. continue;
  712. var query = AllEntityQuery<MapGridComponent>();
  713. while (query.MoveNext(out var uid, out var grid))
  714. {
  715. // Don't add anymore as it is irrelevant, but that doesn't mean we need to re-do existing work.
  716. if (nearbyGrids.Contains(uid))
  717. continue;
  718. targetAABB = targetAABB.Union(
  719. _transform.GetWorldMatrix(uid)
  720. .TransformBox(Comp<MapGridComponent>(uid).LocalAABB.Enlarged(expansionAmount)));
  721. }
  722. break;
  723. }
  724. // Now we have a targetAABB. This has already been expanded to account for our fat ass.
  725. Vector2 spawnPos;
  726. if (TryComp<PhysicsComponent>(shuttleUid, out var shuttleBody))
  727. {
  728. _physics.SetLinearVelocity(shuttleUid, Vector2.Zero, body: shuttleBody);
  729. _physics.SetAngularVelocity(shuttleUid, 0f, body: shuttleBody);
  730. }
  731. // TODO: This should prefer the position's angle instead.
  732. // TODO: This is pretty crude for multiple landings.
  733. if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid))
  734. {
  735. // Pick a random angle
  736. var offsetAngle = _random.NextAngle();
  737. // Our valid spawn positions are <targetAABB width / height + offset> away.
  738. var minRadius = MathF.Max(targetAABB.Width / 2f, targetAABB.Height / 2f);
  739. spawnPos = targetAABB.Center + offsetAngle.RotateVec(new Vector2(_random.NextFloat(minRadius + minOffset, minRadius + maxOffset), 0f));
  740. }
  741. else if (shuttleBody != null)
  742. {
  743. (spawnPos, angle) = _transform.GetWorldPositionRotation(targetXform);
  744. }
  745. else
  746. {
  747. spawnPos = _transform.GetWorldPosition(targetXform);
  748. }
  749. var offset = Vector2.Zero;
  750. // Offset it because transform does not correspond to AABB position.
  751. if (TryComp(shuttleUid, out MapGridComponent? shuttleGrid))
  752. {
  753. offset = -shuttleGrid.LocalAABB.Center;
  754. }
  755. if (!HasComp<MapComponent>(targetXform.GridUid))
  756. {
  757. angle = _random.NextAngle();
  758. }
  759. else
  760. {
  761. angle = Angle.Zero;
  762. }
  763. // Rotate our localcenter around so we spawn exactly where we "think" we should (center of grid on the dot).
  764. var transform = new Transform(spawnPos, angle);
  765. spawnPos = Robust.Shared.Physics.Transform.Mul(transform, offset);
  766. coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos - offset);
  767. return true;
  768. }
  769. /// <summary>
  770. /// Tries to arrive nearby without overlapping with other grids.
  771. /// </summary>
  772. public bool TryFTLProximity(EntityUid shuttleUid, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
  773. {
  774. if (!Resolve(targetUid, ref targetXform) ||
  775. targetXform.MapUid == null ||
  776. !targetXform.MapUid.Value.IsValid() ||
  777. !Resolve(shuttleUid, ref xform))
  778. {
  779. return false;
  780. }
  781. if (!TryGetFTLProximity(shuttleUid, new EntityCoordinates(targetUid, Vector2.Zero), out var coords, out var angle, xform: xform, targetXform: targetXform))
  782. return false;
  783. _transform.SetCoordinates(shuttleUid, xform, coords, rotation: angle);
  784. return true;
  785. }
  786. /// <summary>
  787. /// Tries to FTL to the target coordinates; will move nearby if not possible.
  788. /// </summary>
  789. public bool TryFTLProximity(Entity<TransformComponent?> shuttle, EntityCoordinates targetCoordinates)
  790. {
  791. if (!Resolve(shuttle.Owner, ref shuttle.Comp) ||
  792. _transform.GetMap(targetCoordinates)?.IsValid() != true)
  793. {
  794. return false;
  795. }
  796. if (!TryGetFTLProximity(shuttle, targetCoordinates, out var coords, out var angle))
  797. return false;
  798. _transform.SetCoordinates(shuttle, shuttle.Comp, coords, rotation: angle);
  799. return true;
  800. }
  801. /// <summary>
  802. /// Flattens / deletes everything under the grid upon FTL.
  803. /// </summary>
  804. private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridComponent? grid = null, TransformComponent? xform = null)
  805. {
  806. if (!Resolve(uid, ref manager, ref grid, ref xform) || xform.MapUid == null)
  807. return;
  808. if (!TryComp(xform.MapUid, out BroadphaseComponent? lookup))
  809. return;
  810. // Flatten anything not parented to a grid.
  811. var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value);
  812. var aabbs = new List<Box2>(manager.Fixtures.Count);
  813. var tileSet = new List<(Vector2i, Tile)>();
  814. foreach (var fixture in manager.Fixtures.Values)
  815. {
  816. if (!fixture.Hard)
  817. continue;
  818. var aabb = fixture.Shape.ComputeAABB(transform, 0);
  819. // Shift it slightly
  820. // Create a small border around it.
  821. aabb = aabb.Enlarged(0.2f);
  822. aabbs.Add(aabb);
  823. // Handle clearing biome stuff as relevant.
  824. tileSet.Clear();
  825. _biomes.ReserveTiles(xform.MapUid.Value, aabb, tileSet);
  826. _lookupEnts.Clear();
  827. _immuneEnts.Clear();
  828. // TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above.
  829. _lookup.GetLocalEntitiesIntersecting(xform.MapUid.Value, fixture.Shape, transform, _lookupEnts, flags: LookupFlags.Uncontained, lookup: lookup);
  830. foreach (var ent in _lookupEnts)
  831. {
  832. if (ent == uid || _immuneEnts.Contains(ent))
  833. {
  834. continue;
  835. }
  836. // If it's on our grid ignore it.
  837. if (!_xformQuery.TryComp(ent, out var childXform) || childXform.GridUid == uid)
  838. {
  839. continue;
  840. }
  841. // If it has the FTLSmashImmuneComponent ignore it.
  842. if (_immuneQuery.HasComponent(ent))
  843. {
  844. continue;
  845. }
  846. if (_bodyQuery.TryGetComponent(ent, out var mob))
  847. {
  848. _logger.Add(LogType.Gib, LogImpact.Extreme, $"{ToPrettyString(ent):player} got gibbed by the shuttle" +
  849. $" {ToPrettyString(uid)} arriving from FTL at {xform.Coordinates:coordinates}");
  850. var gibs = _bobby.GibBody(ent, body: mob);
  851. _immuneEnts.UnionWith(gibs);
  852. continue;
  853. }
  854. QueueDel(ent);
  855. }
  856. }
  857. var ev = new ShuttleFlattenEvent(xform.MapUid.Value, aabbs);
  858. RaiseLocalEvent(ref ev);
  859. }
  860. }