diff --git a/Content.Server/Tabletop/TabletopSystem.Map.cs b/Content.Server/Tabletop/TabletopSystem.Map.cs index 89563ec6bf..70e44de7ec 100644 --- a/Content.Server/Tabletop/TabletopSystem.Map.cs +++ b/Content.Server/Tabletop/TabletopSystem.Map.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Shared.GameTicking; +using Content.Shared.Maths; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -37,7 +38,7 @@ namespace Content.Server.Tabletop /// private Vector2 GetNextTabletopPosition() { - return UlamSpiral(++_tabletops) * TabletopSeparation; + return UlamSpiral.Point(++_tabletops) * TabletopSeparation; } /// @@ -59,34 +60,6 @@ namespace Content.Server.Tabletop Dirty(mapUid, mapComp); } - /// - /// Algorithm for mapping scalars to 2D positions in the same pattern as an Ulam Spiral. - /// - /// Scalar to map to a 2D position. Must be greater than or equal to 1. - /// The mapped 2D position for the scalar. - private Vector2i UlamSpiral(int n) - { - var k = (int)MathF.Ceiling((MathF.Sqrt(n) - 1) / 2); - var t = 2 * k + 1; - var m = (int)MathF.Pow(t, 2); - t--; - - if (n >= m - t) - return new Vector2i(k - (m - n), -k); - - m -= t; - - if (n >= m - t) - return new Vector2i(-k, -k + (m - n)); - - m -= t; - - if (n >= m - t) - return new Vector2i(-k + (m - n), k); - - return new Vector2i(k, k - (m - n - t)); - } - private void OnRoundRestart(RoundRestartCleanupEvent _) { if (TabletopMap == MapId.Nullspace || !_map.MapExists(TabletopMap)) diff --git a/Content.Server/Teleportation/TeleportLocationsSystem.cs b/Content.Server/Teleportation/TeleportLocationsSystem.cs index 9e0c219e04..00390e3de6 100644 --- a/Content.Server/Teleportation/TeleportLocationsSystem.cs +++ b/Content.Server/Teleportation/TeleportLocationsSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Chat; +using Content.Shared.Popups; using Content.Shared.Teleportation; using Content.Shared.Teleportation.Components; using Content.Shared.Teleportation.Systems; @@ -15,6 +16,7 @@ namespace Content.Server.Teleportation; public sealed partial class TeleportLocationsSystem : SharedTeleportLocationsSystem { [Dependency] private ChatSystem _chat = default!; + [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private EntityWhitelistSystem _whitelist = default!; public override void Initialize() @@ -37,7 +39,7 @@ public sealed partial class TeleportLocationsSystem : SharedTeleportLocationsSys protected override void OnTeleportToLocationRequest(Entity ent, ref TeleportLocationDestinationMessage args) { - if (Delay.IsDelayed(ent.Owner, TeleportDelay)) + if (IsDelayed(ent)) return; if (!string.IsNullOrWhiteSpace(ent.Comp.Speech)) @@ -49,6 +51,13 @@ public sealed partial class TeleportLocationsSystem : SharedTeleportLocationsSys base.OnTeleportToLocationRequest(ent, ref args); } + protected override void HandleTeleportDestinationObstructed(EntityUid userUid) + { + // Client can't know if the destination is obstructed if it's not loaded. + var msg = Loc.GetString("teleportation-menu-destination-obstructed"); + _popup.PopupEntity(msg, userUid, userUid, PopupType.MediumCaution); + } + // If it's in shared this doesn't populate the points on the UI /// /// Gets the teleport points to send to the BUI diff --git a/Content.Shared/Maths/UlamSpiral.cs b/Content.Shared/Maths/UlamSpiral.cs new file mode 100644 index 0000000000..1226d6022b --- /dev/null +++ b/Content.Shared/Maths/UlamSpiral.cs @@ -0,0 +1,44 @@ +namespace Content.Shared.Maths; + +public static class UlamSpiral +{ + /// + /// Algorithm for mapping scalars to 2D positions in the same pattern as an Ulam Spiral. + /// + /// Scalar to map to a 2D position. Returns a zero vector for values smaller than 1. + /// The mapped 2D position for the scalar. + public static Vector2i Point(int n) + { + if (n <= 0) + return new Vector2i(0, 0); + + var k = (int)MathF.Ceiling((MathF.Sqrt(n) - 1) / 2); + var t = 2 * k + 1; + var m = (int)MathF.Pow(t, 2); + t--; + + if (n >= m - t) + return new Vector2i(k - (m - n), -k); + + m -= t; + + if (n >= m - t) + return new Vector2i(-k, -k + (m - n)); + + m -= t; + + if (n >= m - t) + return new Vector2i(-k + (m - n), k); + + return new Vector2i(k, k - (m - n - t)); + } + + /// + /// Returns the largest value for which will generate a point within a Chebyshev distance from origin. + /// + public static int PointsForMaxDistance(int maxDistance) + { + var x = maxDistance * 2 + 1; + return x * x; + } +} diff --git a/Content.Shared/Teleportation/Systems/SharedTeleportLocationsSystem.cs b/Content.Shared/Teleportation/Systems/SharedTeleportLocationsSystem.cs index 596d1626d5..b66e8d1ce8 100644 --- a/Content.Shared/Teleportation/Systems/SharedTeleportLocationsSystem.cs +++ b/Content.Shared/Teleportation/Systems/SharedTeleportLocationsSystem.cs @@ -1,7 +1,10 @@ -using Content.Shared.Teleportation.Components; +using Content.Shared.Maths; +using Content.Shared.Teleportation.Components; using Content.Shared.Timing; using Content.Shared.UserInterface; using Content.Shared.Warps; +using Robust.Shared.Map; +using System.Numerics; namespace Content.Shared.Teleportation.Systems; @@ -10,12 +13,14 @@ namespace Content.Shared.Teleportation.Systems; /// public abstract partial class SharedTeleportLocationsSystem : EntitySystem { - [Dependency] protected UseDelaySystem Delay = default!; + [Dependency] private UseDelaySystem _delay = default!; + [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedUserInterfaceSystem _ui = default!; [Dependency] private SharedTransformSystem _xform = default!; protected const string TeleportDelay = "TeleportDelay"; + protected const string TeleportFailedDelay = "TeleportFailedDelay"; public override void Initialize() { @@ -25,9 +30,14 @@ public abstract partial class SharedTeleportLocationsSystem : EntitySystem SubscribeLocalEvent(OnTeleportToLocationRequest); } + protected bool IsDelayed(EntityUid entityUid) + { + return _delay.IsDelayed(entityUid, TeleportDelay) || _delay.IsDelayed(entityUid, TeleportFailedDelay); + } + private void OnUiOpenAttempt(Entity ent, ref ActivatableUIOpenAttemptEvent args) { - if (!Delay.IsDelayed(ent.Owner, TeleportDelay)) + if (!IsDelayed(ent)) return; args.Cancel(); @@ -35,20 +45,31 @@ public abstract partial class SharedTeleportLocationsSystem : EntitySystem protected virtual void OnTeleportToLocationRequest(Entity ent, ref TeleportLocationDestinationMessage args) { - if (!TryGetEntity(args.NetEnt, out var telePointEnt) || TerminatingOrDeleted(telePointEnt) || !HasComp(telePointEnt) || Delay.IsDelayed(ent.Owner, TeleportDelay)) + if (!TryGetEntity(args.NetEnt, out var telePointEnt) || TerminatingOrDeleted(telePointEnt) || !HasComp(telePointEnt) || IsDelayed(ent)) return; var comp = ent.Comp; var originEnt = args.Actor; var telePointXForm = Transform(telePointEnt.Value); - SpawnAtPosition(comp.TeleportEffect, Transform(originEnt).Coordinates); + // Spawn effect even if the target is unsafe - the failure is funny. + var originEntXForm = Transform(originEnt); + SpawnAtPosition(comp.TeleportEffect, originEntXForm.Coordinates); - _xform.SetMapCoordinates(originEnt, _xform.GetMapCoordinates(telePointEnt.Value, telePointXForm)); + if (ChooseSafeLocation((telePointEnt.Value, telePointXForm), maxDistance: 3) is not { } safeTargetMapCoords) + { + // Prevent spamming effects if the target is obstructed. + _delay.TryResetDelay(ent.Owner, true, id: TeleportFailedDelay); - SpawnAtPosition(comp.TeleportEffect, telePointXForm.Coordinates); + HandleTeleportDestinationObstructed(originEnt); - Delay.TryResetDelay(ent.Owner, true, id: TeleportDelay); + return; + } + + _xform.SetMapCoordinates(originEnt, safeTargetMapCoords); + SpawnAtPosition(comp.TeleportEffect, originEntXForm.Coordinates); + + _delay.TryResetDelay(ent.Owner, true, id: TeleportDelay); if (!ent.Comp.CloseAfterTeleport) return; @@ -56,4 +77,51 @@ public abstract partial class SharedTeleportLocationsSystem : EntitySystem // Teleport's done, now tell the BUI to close if needed. _ui.CloseUi(ent.Owner, TeleportLocationUiKey.Key); } + + /// + /// Called when the destination was obstructed and the user wasn't teleported. + /// + protected virtual void HandleTeleportDestinationObstructed(EntityUid userUid) + { + } + + /// + /// The result of this overload might be different between the client and server due to PVS. + /// + private MapCoordinates? ChooseSafeLocation(Entity targetEntity, int maxDistance) + { + // If the target point is on a grid, use that grid's rotation. + var gridTransform = targetEntity.Comp.GridUid is { } grid + ? Matrix3Helpers.CreateTransform(Vector2.Zero, _xform.GetWorldRotation(Transform(grid))) + : Matrix3x2.Identity; + + var targetCoords = _xform.GetMapCoordinates(targetEntity); + // The entity might've left PVS on the client. + if (targetCoords.MapId == MapId.Nullspace) + return null; + + return ChooseSafeLocation(targetCoords, maxDistance, gridTransform); + } + + private MapCoordinates? ChooseSafeLocation(MapCoordinates targetCoords, int maxDistance, Matrix3x2 gridTransform) + { + var maxAttempts = UlamSpiral.PointsForMaxDistance(maxDistance); + // Transforms an offset from the target entity into the final world space position. + var worldToGridSpacePlusTargetPos = gridTransform * Matrix3x2.CreateTranslation(targetCoords.Position); + + for (var attempt = 0; attempt <= maxAttempts; attempt++) + { + var offset = UlamSpiral.Point(attempt); + var offsetWorldPos = Vector2.Transform(new Vector2(offset.X, offset.Y), worldToGridSpacePlusTargetPos); + var offsetCoords = new MapCoordinates(offsetWorldPos, targetCoords.MapId); + + if (!_lookup.AnyEntitiesIntersecting(offsetCoords, LookupFlags.Static)) + { + // Selected location is not inside a wall. + return offsetCoords; + } + } + + return null; + } } diff --git a/Resources/Locale/en-US/teleportation/teleportation-menu-gui.ftl b/Resources/Locale/en-US/teleportation/teleportation-menu-gui.ftl index 847f2cbc54..74e5a1804b 100644 --- a/Resources/Locale/en-US/teleportation/teleportation-menu-gui.ftl +++ b/Resources/Locale/en-US/teleportation/teleportation-menu-gui.ftl @@ -1,5 +1,6 @@ ## Default teleportation-menu-default-window-title = Teleportation Menu +teleportation-menu-destination-obstructed = You don't feel like you went anywhere... ## Wizard teleportation-scroll-window-title = Teleportation Scroll diff --git a/Resources/Prototypes/Magic/teleport_scroll.yml b/Resources/Prototypes/Magic/teleport_scroll.yml index 2174b4d21e..fbef3f74ed 100644 --- a/Resources/Prototypes/Magic/teleport_scroll.yml +++ b/Resources/Prototypes/Magic/teleport_scroll.yml @@ -30,3 +30,5 @@ delays: TeleportDelay: !type:UseDelayInfo length: 300 + TeleportFailedDelay: !type:UseDelayInfo + length: 10