From 8a9640d992cc77703a258f8f99b77b3acbebd385 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 21 Aug 2026 22:36:10 -0400 Subject: [PATCH] fix(teleport): require same dimension for visits --- .../us-008-unlock-visitor-access.md | 1 + .../dmg/spigotbase/BaseTeleportManager.java | 9 ++- .../spigotbase/BaseTeleportManagerTest.java | 64 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/test/java/games/dmg/spigotbase/BaseTeleportManagerTest.java diff --git a/design/user-stories/us-008-unlock-visitor-access.md b/design/user-stories/us-008-unlock-visitor-access.md index 138b965..9938a34 100644 --- a/design/user-stories/us-008-unlock-visitor-access.md +++ b/design/user-stories/us-008-unlock-visitor-access.md @@ -21,6 +21,7 @@ As a **player with Base III**, I want to open my base to visitors so that other - [x] `/gotobase ` autocompletes bases that the requesting player is currently eligible to visit. - [x] `/visit ` aliases `/gotobase ` with identical autocomplete, including eligible bases whose owners are offline. - [x] Enabled bases remain visitable while their owners are offline. +- [x] `/visit` and `/gotobase` require the visitor to be in the destination base's recorded world; cross-dimension requests are rejected before warm-up with a clear message and without consuming cooldown. - [x] A visitor teleport uses the destination owner's current warm-up tier, but always requires at least a one-second stationary warm-up; `/visit` and `/gotobase` never teleport instantly. - [x] Looking around is permitted, while movement between block coordinates, damage, teleportation, world change, death, logout, or a conflicting teleport cancels the visitor warm-up. - [x] Cancellation or destination failure does not consume a visitor cooldown. diff --git a/src/main/java/games/dmg/spigotbase/BaseTeleportManager.java b/src/main/java/games/dmg/spigotbase/BaseTeleportManager.java index 669b40b..0f4e517 100644 --- a/src/main/java/games/dmg/spigotbase/BaseTeleportManager.java +++ b/src/main/java/games/dmg/spigotbase/BaseTeleportManager.java @@ -83,6 +83,13 @@ final class BaseTeleportManager implements Listener { visitor.sendMessage(ChatColor.RED + "That base is not accepting visitors."); return; } + BaseLocation destination = owner.base().orElseThrow(); + if (!visitor.getWorld().getUID().equals(destination.worldId())) { + visitor.sendMessage( + ChatColor.RED + "You must be in the same dimension as that base to visit it." + ); + return; + } PlayerState visitorState = stateManager.player(visitor.getUniqueId(), visitor.getName()); Optional remaining = visitorPolicy.remaining( visitorState, owner.playerId(), clock.instant() @@ -94,7 +101,7 @@ final class BaseTeleportManager implements Listener { } begin( visitor, - owner.base().orElseThrow(), + destination, policy.visitorWarmup(owner), owner.playerId(), policy.cooldown(owner), diff --git a/src/test/java/games/dmg/spigotbase/BaseTeleportManagerTest.java b/src/test/java/games/dmg/spigotbase/BaseTeleportManagerTest.java new file mode 100644 index 0000000..f76fd18 --- /dev/null +++ b/src/test/java/games/dmg/spigotbase/BaseTeleportManagerTest.java @@ -0,0 +1,64 @@ +package games.dmg.spigotbase; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.ChatColor; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.Test; + +final class BaseTeleportManagerTest { + @Test + void visitFromAnotherWorldIsRejectedBeforeWarmupAndCooldownChecks() { + UUID visitorId = UUID.randomUUID(); + UUID ownerId = UUID.randomUUID(); + UUID baseWorldId = UUID.randomUUID(); + BaseStateManager stateManager = mock(BaseStateManager.class); + TeleportPolicy teleportPolicy = mock(TeleportPolicy.class); + VisitorPolicy visitorPolicy = mock(VisitorPolicy.class); + Player visitor = mock(Player.class); + World visitorWorld = mock(World.class); + PlayerState owner = PlayerState.newPlayer(ownerId, "Owner") + .withGrassAndDirtProgress(500, 4) + .withBase( + new BaseLocation(baseWorldId, "world", 0, 64, 0, 0, 0), + Instant.EPOCH + ) + .withVisitorsEnabled(true); + PlayerState visitorState = PlayerState.newPlayer(visitorId, "Visitor"); + BaseTeleportManager manager = new BaseTeleportManager( + mock(Plugin.class), + stateManager, + teleportPolicy, + visitorPolicy, + mock(SafeBaseDestination.class), + Clock.systemUTC() + ); + + when(visitor.getUniqueId()).thenReturn(visitorId); + when(visitor.getName()).thenReturn("Visitor"); + when(visitor.getWorld()).thenReturn(visitorWorld); + when(visitorWorld.getUID()).thenReturn(UUID.randomUUID()); + when(stateManager.player(visitorId, "Visitor")).thenReturn(visitorState); + when(visitorPolicy.remaining(eq(visitorState), eq(ownerId), any())) + .thenReturn(Optional.of(Duration.ofSeconds(1))); + + manager.startVisit(visitor, owner); + + verify(visitor).sendMessage( + ChatColor.RED + "You must be in the same dimension as that base to visit it." + ); + verifyNoInteractions(stateManager, teleportPolicy, visitorPolicy); + } +}