SharedPortalSystem.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. using System.Linq;
  2. using Content.Shared.Ghost;
  3. using Content.Shared.Movement.Pulling.Components;
  4. using Content.Shared.Movement.Pulling.Systems;
  5. using Content.Shared.Popups;
  6. using Content.Shared.Projectiles;
  7. using Content.Shared.Teleportation.Components;
  8. using Content.Shared.Verbs;
  9. using Robust.Shared.Audio;
  10. using Robust.Shared.Audio.Systems;
  11. using Robust.Shared.Map;
  12. using Robust.Shared.Network;
  13. using Robust.Shared.Physics.Dynamics;
  14. using Robust.Shared.Physics.Events;
  15. using Robust.Shared.Player;
  16. using Robust.Shared.Random;
  17. using Robust.Shared.Utility;
  18. namespace Content.Shared.Teleportation.Systems;
  19. /// <summary>
  20. /// This handles teleporting entities through portals, and creating new linked portals.
  21. /// </summary>
  22. public abstract class SharedPortalSystem : EntitySystem
  23. {
  24. [Dependency] private readonly IRobustRandom _random = default!;
  25. [Dependency] private readonly INetManager _netMan = default!;
  26. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  27. [Dependency] private readonly SharedAudioSystem _audio = default!;
  28. [Dependency] private readonly SharedTransformSystem _transform = default!;
  29. [Dependency] private readonly PullingSystem _pulling = default!;
  30. [Dependency] private readonly SharedPopupSystem _popup = default!;
  31. private const string PortalFixture = "portalFixture";
  32. private const string ProjectileFixture = "projectile";
  33. private const int MaxRandomTeleportAttempts = 20;
  34. /// <inheritdoc/>
  35. public override void Initialize()
  36. {
  37. SubscribeLocalEvent<PortalComponent, StartCollideEvent>(OnCollide);
  38. SubscribeLocalEvent<PortalComponent, EndCollideEvent>(OnEndCollide);
  39. SubscribeLocalEvent<PortalComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
  40. }
  41. private void OnGetVerbs(EntityUid uid, PortalComponent component, GetVerbsEvent<AlternativeVerb> args)
  42. {
  43. // Traversal altverb for ghosts to use that bypasses normal functionality
  44. if (!args.CanAccess || !HasComp<GhostComponent>(args.User))
  45. return;
  46. // Don't use the verb with unlinked or with multi-output portals
  47. // (this is only intended to be useful for ghosts to see where a linked portal leads)
  48. var disabled = !TryComp<LinkedEntityComponent>(uid, out var link) || link.LinkedEntities.Count != 1;
  49. args.Verbs.Add(new AlternativeVerb
  50. {
  51. Priority = 11,
  52. Act = () =>
  53. {
  54. if (link == null || disabled)
  55. return;
  56. var ent = link.LinkedEntities.First();
  57. TeleportEntity(uid, args.User, Transform(ent).Coordinates, ent, false);
  58. },
  59. Disabled = disabled,
  60. Text = Loc.GetString("portal-component-ghost-traverse"),
  61. Message = disabled
  62. ? Loc.GetString("portal-component-no-linked-entities")
  63. : Loc.GetString("portal-component-can-ghost-traverse"),
  64. Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"))
  65. });
  66. }
  67. private bool ShouldCollide(string ourId, string otherId, Fixture our, Fixture other)
  68. {
  69. // most non-hard fixtures shouldn't pass through portals, but projectiles are non-hard as well
  70. // and they should still pass through
  71. return ourId == PortalFixture && (other.Hard || otherId == ProjectileFixture);
  72. }
  73. private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollideEvent args)
  74. {
  75. if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId, args.OurFixture, args.OtherFixture))
  76. return;
  77. var subject = args.OtherEntity;
  78. // best not.
  79. if (Transform(subject).Anchored)
  80. return;
  81. // break pulls before portal enter so we dont break shit
  82. if (TryComp<PullableComponent>(subject, out var pullable) && pullable.BeingPulled)
  83. {
  84. _pulling.TryStopPull(subject, pullable);
  85. }
  86. if (TryComp<PullerComponent>(subject, out var pullerComp)
  87. && TryComp<PullableComponent>(pullerComp.Pulling, out var subjectPulling))
  88. {
  89. _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling);
  90. }
  91. // if they came from another portal, just return and wait for them to exit the portal
  92. if (HasComp<PortalTimeoutComponent>(subject))
  93. {
  94. return;
  95. }
  96. if (TryComp<LinkedEntityComponent>(uid, out var link))
  97. {
  98. if (!link.LinkedEntities.Any())
  99. return;
  100. // client can't predict outside of simple portal-to-portal interactions due to randomness involved
  101. // --also can't predict if the target doesn't exist on the client / is outside of PVS
  102. if (_netMan.IsClient)
  103. {
  104. var first = link.LinkedEntities.First();
  105. var exists = Exists(first);
  106. if (link.LinkedEntities.Count != 1 || !exists || (exists && Transform(first).MapID == MapId.Nullspace))
  107. return;
  108. }
  109. // pick a target and teleport there
  110. var target = _random.Pick(link.LinkedEntities);
  111. if (HasComp<PortalComponent>(target))
  112. {
  113. // if target is a portal, signal that they shouldn't be immediately portaled back
  114. var timeout = EnsureComp<PortalTimeoutComponent>(subject);
  115. timeout.EnteredPortal = uid;
  116. Dirty(subject, timeout);
  117. }
  118. TeleportEntity(uid, subject, Transform(target).Coordinates, target);
  119. return;
  120. }
  121. if (_netMan.IsClient)
  122. return;
  123. // no linked entity--teleport randomly
  124. if (component.RandomTeleport)
  125. TeleportRandomly(uid, subject, component);
  126. }
  127. private void OnEndCollide(EntityUid uid, PortalComponent component, ref EndCollideEvent args)
  128. {
  129. if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId,args.OurFixture, args.OtherFixture))
  130. return;
  131. var subject = args.OtherEntity;
  132. // if they came from (not us), remove the timeout
  133. if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != uid)
  134. {
  135. RemCompDeferred<PortalTimeoutComponent>(subject);
  136. }
  137. }
  138. private void TeleportEntity(EntityUid portal, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity=null, bool playSound=true,
  139. PortalComponent? portalComponent = null)
  140. {
  141. if (!Resolve(portal, ref portalComponent))
  142. return;
  143. var ourCoords = Transform(portal).Coordinates;
  144. var onSameMap = ourCoords.GetMapId(EntityManager) == target.GetMapId(EntityManager);
  145. var distanceInvalid = portalComponent.MaxTeleportRadius != null
  146. && ourCoords.TryDistance(EntityManager, target, out var distance)
  147. && distance > portalComponent.MaxTeleportRadius;
  148. if (!onSameMap && !portalComponent.CanTeleportToOtherMaps || distanceInvalid)
  149. {
  150. if (!_netMan.IsServer)
  151. return;
  152. // Early out if this is an invalid configuration
  153. _popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
  154. ourCoords, Filter.Pvs(ourCoords, entityMan: EntityManager), true);
  155. _popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
  156. target, Filter.Pvs(target, entityMan: EntityManager), true);
  157. QueueDel(portal);
  158. if (targetEntity != null)
  159. QueueDel(targetEntity.Value);
  160. return;
  161. }
  162. var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? portalComponent.ArrivalSound;
  163. var departureSound = portalComponent.DepartureSound;
  164. // Some special cased stuff: projectiles should stop ignoring shooter when they enter a portal, to avoid
  165. // stacking 500 bullets in between 2 portals and instakilling people--you'll just hit yourself instead
  166. // (as expected)
  167. if (TryComp<ProjectileComponent>(subject, out var projectile))
  168. {
  169. projectile.IgnoreShooter = false;
  170. }
  171. LogTeleport(portal, subject, Transform(subject).Coordinates, target);
  172. _transform.SetCoordinates(subject, target);
  173. if (!playSound)
  174. return;
  175. _audio.PlayPredicted(departureSound, portal, subject);
  176. _audio.PlayPredicted(arrivalSound, subject, subject);
  177. }
  178. private void TeleportRandomly(EntityUid portal, EntityUid subject, PortalComponent? component = null)
  179. {
  180. if (!Resolve(portal, ref component))
  181. return;
  182. var xform = Transform(portal);
  183. var coords = xform.Coordinates;
  184. var newCoords = coords.Offset(_random.NextVector2(component.MaxRandomRadius));
  185. for (var i = 0; i < MaxRandomTeleportAttempts; i++)
  186. {
  187. var randVector = _random.NextVector2(component.MaxRandomRadius);
  188. newCoords = coords.Offset(randVector);
  189. if (!_lookup.GetEntitiesIntersecting(newCoords.ToMap(EntityManager, _transform), LookupFlags.Static).Any())
  190. {
  191. break;
  192. }
  193. }
  194. TeleportEntity(portal, subject, newCoords);
  195. }
  196. protected virtual void LogTeleport(EntityUid portal, EntityUid subject, EntityCoordinates source,
  197. EntityCoordinates target)
  198. {
  199. }
  200. }