From 2b0097e9a740a8df71f629c0989beb1930f1f73d Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 25 Feb 2026 19:46:18 -0800 Subject: [PATCH 01/15] Add CLAUDE.md with project guidance for Claude Code Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d316ddc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Border is a BentoBox addon for Minecraft Paper servers that creates and renders per-player world borders around islands. Players cannot pass the border. It supports two rendering modes: barrier blocks with particles, and vanilla world borders. + +**Target:** Paper 1.21+ with BentoBox 3.10+, Java 21 + +## Build Commands + +```bash +mvn clean package # Build the plugin JAR +mvn test # Run all tests +mvn test -Dtest=BorderTest # Run a single test class +mvn verify # Full build with JaCoCo coverage +``` + +Output JAR: `target/Border-{version}.jar` + +## Architecture + +### Addon Lifecycle (BentoBox Pladdon pattern) + +`BorderPladdon` (plugin entry point) → `Border` (addon, extends `Addon`) → registers commands, listeners, and border implementations per game mode. + +### Border Rendering — Strategy + Proxy + +`BorderShower` is the core interface with methods: `showBorder`, `hideBorder`, `clearUser`, `refreshView`, `teleportEntity`. + +Two implementations: +- **`ShowBarrier`** — Renders barrier blocks and colored particles around island edges. Caches barrier block positions per player. Uses async chunk loading. +- **`ShowWorldBorder`** — Uses Paper's per-player WorldBorder API. Manipulates border animation (shrink/grow/static) to achieve color effects. + +**`PerPlayerBorderProxy`** delegates to the correct implementation based on per-player metadata. Falls back to addon-wide default settings. + +### Per-Player State via Metadata + +Player preferences are stored as BentoBox `MetaDataValue` entries: +- `Border_state` — border on/off toggle +- `Border_bordertype` — BARRIER or VANILLA (stored as byte id) +- `Border_color` — RED, GREEN, or BLUE + +### Commands + +Registered as subcommands under each game mode's player command: +- `IslandBorderCommand` (`/[gamemode] border`) — toggle border visibility +- `BorderTypeCommand` (`/[gamemode] bordertype`) — switch between barrier/vanilla +- `BorderColorCommand` (`/[gamemode] bordercolor`) — change border color + +### Event Handling + +`PlayerListener` handles all player events (join, quit, move, teleport, mount, item drop) to trigger border show/hide/refresh and enforce boundary teleportation. + +## Testing + +Uses JUnit 5 + Mockito 5 + MockBukkit. All test classes extend `CommonTestSetup` which provides comprehensive mocking of BentoBox, Bukkit server, worlds, players, and island managers. + +`WhiteBox` is a reflection utility for accessing private fields in tests. + +## Key Conventions + +- Configuration is managed via the `Settings` class with BentoBox's `@StoreAt`/`@ConfigComment` annotations and `Config` loader +- Game modes can be excluded via `Settings.getDisabledGameModes()` +- Localization files live in `src/main/resources/locales/` (20+ languages, YAML format) +- `BorderType` is an enum with byte-based serialization ids (`fromId`/`getId`) From 541a85c2a2931495b087068a91b28bdbc558b530 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 25 Feb 2026 20:06:52 -0800 Subject: [PATCH 02/15] Fix world border not resetting when teleporting between islands The teleport handler called clearUser() (a no-op for ShowWorldBorder) instead of hideBorder(), leaving the old world border active. This caused Bedrock/Geyser clients to enter a restricted interaction state when visiting another island, preventing block breaking until death or relog. - Add hideBorder() call in teleport handler before showing new border - Override clearUser() in ShowWorldBorder to reset world border - Implement refreshView() in ShowWorldBorder to update border on movement Co-Authored-By: Claude Opus 4.6 --- .../border/listeners/PlayerListener.java | 4 +++- .../border/listeners/ShowWorldBorder.java | 10 ++++++++++ .../border/listeners/PlayerListenerTest.java | 4 +++- .../border/listeners/ShowWorldBorderTest.java | 19 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 2eb3a24..925d4b2 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -239,7 +239,9 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { } Location to = e.getTo(); - show.clearUser(User.getInstance(player)); + User user = User.getInstance(player); + show.hideBorder(user); + show.clearUser(user); if (!addon.inGameWorld(to.getWorld())) { return; diff --git a/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java b/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java index c98051e..afebc51 100644 --- a/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java +++ b/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java @@ -78,6 +78,16 @@ public void hideBorder(User user) { user.getPlayer().setWorldBorder(null); } + @Override + public void clearUser(User user) { + user.getPlayer().setWorldBorder(null); + } + + @Override + public void refreshView(User user, Island island) { + showBorder(user.getPlayer(), island); + } + /** * Teleport player back within the island space they are in * @param entity player diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index a6a0e70..87077f9 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -200,10 +200,11 @@ public void testOnPlayerTeleportNotInGameWorld() { when(addon.inGameWorld(any())).thenReturn(false); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); + verify(show).hideBorder(user); verify(show).clearUser(user); mockedBukkit.verify(Bukkit::getScheduler, never()); } - + /** * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. */ @@ -212,6 +213,7 @@ public void testOnPlayerTeleportInGameWorld() { when(addon.inGameWorld(any())).thenReturn(true); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); + verify(show).hideBorder(user); verify(show).clearUser(user); mockedBukkit.verify(Bukkit::getScheduler); } diff --git a/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java b/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java index 44a266e..f5c1487 100644 --- a/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java +++ b/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java @@ -121,6 +121,25 @@ public void testHideBorder() { svwb.hideBorder(user); verify(mockPlayer).setWorldBorder(null); } + + /** + * Test method for {@link world.bentobox.border.listeners.ShowWorldBorder#clearUser(world.bentobox.bentobox.api.user.User)}. + */ + @Test + public void testClearUser() { + svwb.clearUser(user); + verify(mockPlayer).setWorldBorder(null); + } + + /** + * Test method for {@link world.bentobox.border.listeners.ShowWorldBorder#refreshView(world.bentobox.bentobox.api.user.User, world.bentobox.bentobox.database.objects.Island)}. + */ + @Test + public void testRefreshView() { + svwb.refreshView(user, island); + verify(mockPlayer).setWorldBorder(wb); + verify(wb).setSize(200.0D); + } /** * Test method for {@link world.bentobox.border.listeners.ShowWorldBorder#showBorder(org.bukkit.entity.Player, world.bentobox.bentobox.database.objects.Island)}. From b60f4e3539f78de47b02072b87b460bd3e2aee1b Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 25 Feb 2026 20:13:33 -0800 Subject: [PATCH 03/15] Bump build version to 4.8.1 in pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 56e11ea..f9de780 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ ${build.version}-SNAPSHOT - 4.8.0 + 4.8.1 -LOCAL BentoBoxWorld_Border From cc8c9bee39db605a48ef56e60f2cefa8e29a7c55 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 15:33:27 -0800 Subject: [PATCH 04/15] Refactor world handling in Border and PlayerListener for improved teleportation logic --- pom.xml | 20 ++- .../java/world/bentobox/border/Border.java | 3 +- .../border/listeners/PlayerListener.java | 22 ++- .../border/listeners/PlayerListenerTest.java | 140 +++++++++++++++++- 4 files changed, 174 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index f9de780..060ec23 100644 --- a/pom.xml +++ b/pom.xml @@ -102,25 +102,41 @@ + jitpack.io https://jitpack.io + + true + - papermc - https://repo.papermc.io/repository/maven-public/ + papermc + https://repo.papermc.io/repository/maven-public/ + + false + bentoboxworld https://repo.codemc.org/repository/bentoboxworld/ + + false + codemc https://repo.codemc.org/repository/maven-snapshots/ + + true + codemc-repo https://repo.codemc.org/repository/maven-public/ + + false + diff --git a/src/main/java/world/bentobox/border/Border.java b/src/main/java/world/bentobox/border/Border.java index beab5d2..f3a21f2 100644 --- a/src/main/java/world/bentobox/border/Border.java +++ b/src/main/java/world/bentobox/border/Border.java @@ -9,7 +9,6 @@ import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.configuration.Config; import world.bentobox.bentobox.api.metadata.MetaDataValue; -import world.bentobox.bentobox.util.Util; import world.bentobox.border.commands.BorderColorCommand; import world.bentobox.border.commands.BorderTypeCommand; import world.bentobox.border.commands.IslandBorderCommand; @@ -122,7 +121,7 @@ public Settings getSettings() { * @return true if world is being handled by Border */ public boolean inGameWorld(World world) { - return gameModes.stream().anyMatch(gm -> gm.inWorld(Util.getWorld(world))); + return gameModes.stream().anyMatch(gm -> gm.inWorld(world)); } public Set getAvailableBorderTypesView() { diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 925d4b2..058c54a 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -38,6 +38,7 @@ import org.bukkit.util.RayTraceResult; import org.bukkit.util.Vector; +import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.events.island.IslandProtectionRangeChangeEvent; import world.bentobox.bentobox.api.flags.Flag; @@ -239,14 +240,16 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { } Location to = e.getTo(); + if (!addon.inGameWorld(to.getWorld()) + || (!addon.getPlugin().getIWM().isIslandNether(to.getWorld())) + && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld())) { + return; + } + User user = User.getInstance(player); show.hideBorder(user); show.clearUser(user); - if (!addon.inGameWorld(to.getWorld())) { - return; - } - TeleportCause cause = e.getCause(); boolean isBlacklistedCause = cause == TeleportCause.ENDER_PEARL || cause == TeleportCause.CONSUMABLE_EFFECT; @@ -289,6 +292,13 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPlayerLeaveIsland(PlayerMoveEvent e) { + if (!addon.inGameWorld(e.getTo().getWorld()) + || (!addon.getPlugin().getIWM().isIslandNether(e.getTo().getWorld()) + && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld())) + ) + { + return; + } Player p = e.getPlayer(); if (!isOn(p)) { return; @@ -297,13 +307,16 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) { if (!addon.getSettings().isReturnTeleport() || !outsideCheck(e.getPlayer(), from, e.getTo())) { return; } + BentoBox.getInstance().logDebug("Player " + p.getName() + " is trying to leave the island protection zone."); // Move the player back inside the border if (addon.getIslands().getProtectedIslandAt(from).isPresent()) { + BentoBox.getInstance().logDebug("Player " + p.getName() + " is being teleported back inside the island protection zone."); e.setCancelled(true); inTeleport.add(p.getUniqueId()); Util.teleportAsync(p, from).thenRun(() -> inTeleport.remove(p.getUniqueId())); return; } + BentoBox.getInstance().logDebug("Player " + p.getName() + " is not on an island, trying to find the nearest island to teleport them back to."); // Backtrack - try to find island at current location, or fall back to the player's own island Optional optionalIsland = addon.getIslands().getIslandAt(p.getLocation()); if (optionalIsland.isEmpty()) { @@ -342,6 +355,7 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) { } Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId())); } else { + BentoBox.getInstance().logDebug("Ray trace did not found a valid position for player " + p.getName() + " so teleporting them back to their island home."); Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId())); } diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index 87077f9..7ae5829 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -132,6 +132,7 @@ public void setUp() throws Exception { when(gma.getPermissionPrefix()).thenReturn("bskyblock."); when(iwm.getAddon(world)).thenReturn(Optional.of(gma)); when(plugin.getIWM()).thenReturn(iwm); + when(iwm.isIslandNether(any())).thenReturn(true); // Util CompletableFuture future = new CompletableFuture<>(); @@ -200,8 +201,8 @@ public void testOnPlayerTeleportNotInGameWorld() { when(addon.inGameWorld(any())).thenReturn(false); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); - verify(show).hideBorder(user); - verify(show).clearUser(user); + verify(show, never()).hideBorder(user); + verify(show, never()).clearUser(user); mockedBukkit.verify(Bukkit::getScheduler, never()); } @@ -211,6 +212,23 @@ public void testOnPlayerTeleportNotInGameWorld() { @Test public void testOnPlayerTeleportInGameWorld() { when(addon.inGameWorld(any())).thenReturn(true); + when(iwm.isIslandNether(any())).thenReturn(false); // Not an island nether - should return early + PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); + pl.onPlayerTeleport(event); + verify(show, never()).hideBorder(user); + verify(show, never()).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. + * Island nether world - should proceed with border management. + */ + @Test + public void testOnPlayerTeleportInIslandNether() { + when(addon.inGameWorld(any())).thenReturn(true); + when(iwm.isIslandNether(any())).thenReturn(true); + when(iwm.isIslandEnd(any())).thenReturn(false); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); verify(show).hideBorder(user); @@ -218,6 +236,54 @@ public void testOnPlayerTeleportInGameWorld() { mockedBukkit.verify(Bukkit::getScheduler); } + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. + * Island end world - should proceed with border management. + */ + @Test + public void testOnPlayerTeleportInIslandEnd() { + when(addon.inGameWorld(any())).thenReturn(true); + when(iwm.isIslandNether(any())).thenReturn(false); + when(iwm.isIslandEnd(any())).thenReturn(true); + PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.END_PORTAL); + pl.onPlayerTeleport(event); + verify(show).hideBorder(user); + verify(show).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. + * Vanilla (non-island) nether - should return early without any border activity. + */ + @Test + public void testOnPlayerTeleportInVanillaNether() { + when(addon.inGameWorld(any())).thenReturn(true); + when(iwm.isIslandNether(any())).thenReturn(false); // vanilla nether, not island nether + when(iwm.isIslandEnd(any())).thenReturn(false); + PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); + pl.onPlayerTeleport(event); + verify(show, never()).hideBorder(user); + verify(show, never()).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. + * Vanilla (non-island) end - should return early without any border activity. + */ + @Test + public void testOnPlayerTeleportInVanillaEnd() { + when(addon.inGameWorld(any())).thenReturn(true); + when(iwm.isIslandNether(any())).thenReturn(false); + when(iwm.isIslandEnd(any())).thenReturn(false); // vanilla end, not island end + PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.END_PORTAL); + pl.onPlayerTeleport(event); + verify(show, never()).hideBorder(user); + verify(show, never()).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + } + /** * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. */ @@ -253,7 +319,75 @@ public void testOnPlayerLeaveIslandReturnTeleportOutsideCheckNotInGameWorld() { pl.onPlayerLeaveIsland(event); verify(addon, never()).getIslands(); } - + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. + * Tests that movement in a vanilla (non-island) nether world is ignored — players should NOT + * be teleported back when using the vanilla nether. + */ + @Test + public void testOnPlayerLeaveIslandVanillaNether() { + when(iwm.isIslandNether(any())).thenReturn(false); // vanilla nether, not island nether + when(iwm.isIslandEnd(any())).thenReturn(false); + when(island.onIsland(any())).thenReturn(false); + settings.setReturnTeleport(true); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + pl.onPlayerLeaveIsland(event); + // Should return early — no island lookup, no teleport + verify(addon, never()).getIslands(); + assertFalse(event.isCancelled()); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. + * Tests that movement in a vanilla (non-island) end world is ignored. + */ + @Test + public void testOnPlayerLeaveIslandVanillaEnd() { + when(iwm.isIslandNether(any())).thenReturn(false); + when(iwm.isIslandEnd(any())).thenReturn(false); // vanilla end, not island end + when(island.onIsland(any())).thenReturn(false); + settings.setReturnTeleport(true); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + pl.onPlayerLeaveIsland(event); + // Should return early — no island lookup, no teleport + verify(addon, never()).getIslands(); + assertFalse(event.isCancelled()); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. + * Tests that a player trying to leave the protection zone in an island nether world is + * teleported back. + */ + @Test + public void testOnPlayerLeaveIslandIslandNetherReturnsTeleport() { + when(iwm.isIslandNether(any())).thenReturn(true); + when(iwm.isIslandEnd(any())).thenReturn(false); + when(island.onIsland(any())).thenReturn(false); + settings.setReturnTeleport(true); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + pl.onPlayerLeaveIsland(event); + assertTrue(event.isCancelled()); + } + + /** + * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. + * Tests that a player trying to leave the protection zone in an island end world is + * teleported back. + */ + @Test + public void testOnPlayerLeaveIslandIslandEndReturnsTeleport() { + when(iwm.isIslandNether(any())).thenReturn(false); + when(iwm.isIslandEnd(any())).thenReturn(true); + when(island.onIsland(any())).thenReturn(false); + settings.setReturnTeleport(true); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + pl.onPlayerLeaveIsland(event); + assertTrue(event.isCancelled()); + } + + /** * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. */ From 6bac390f3797708eff8e19b600e3ce7e9bda86d6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 15:55:13 -0800 Subject: [PATCH 05/15] Update src/main/java/world/bentobox/border/listeners/PlayerListener.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/world/bentobox/border/listeners/PlayerListener.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 058c54a..6dbe07e 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -241,8 +241,8 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { Location to = e.getTo(); if (!addon.inGameWorld(to.getWorld()) - || (!addon.getPlugin().getIWM().isIslandNether(to.getWorld())) - && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld())) { + || (!addon.getPlugin().getIWM().isIslandNether(to.getWorld()) + && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld()))) { return; } From a994c1eda419aaa8ca85a283247b7801b99926a7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 16:08:01 -0800 Subject: [PATCH 06/15] Refactor inGameWorld checks in Border and PlayerListener for clearer logic --- .../java/world/bentobox/border/Border.java | 4 +++ .../border/listeners/PlayerListener.java | 16 +++------- .../border/listeners/PlayerListenerTest.java | 32 +++++++++---------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/main/java/world/bentobox/border/Border.java b/src/main/java/world/bentobox/border/Border.java index f3a21f2..0cc2b61 100644 --- a/src/main/java/world/bentobox/border/Border.java +++ b/src/main/java/world/bentobox/border/Border.java @@ -121,6 +121,10 @@ public Settings getSettings() { * @return true if world is being handled by Border */ public boolean inGameWorld(World world) { + if (world.getEnvironment() == World.Environment.NETHER && + !getPlugin().getIWM().isIslandNether(world)) { return false;} + if (world.getEnvironment() == World.Environment.THE_END && + !getPlugin().getIWM().isIslandEnd(world)) { return false;} return gameModes.stream().anyMatch(gm -> gm.inWorld(world)); } diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 058c54a..20067a4 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -239,16 +239,13 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { return; } Location to = e.getTo(); - - if (!addon.inGameWorld(to.getWorld()) - || (!addon.getPlugin().getIWM().isIslandNether(to.getWorld())) - && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld())) { - return; - } - User user = User.getInstance(player); show.hideBorder(user); show.clearUser(user); + if (!addon.inGameWorld(to.getWorld())) { + return; + } + TeleportCause cause = e.getCause(); boolean isBlacklistedCause = cause == TeleportCause.ENDER_PEARL || cause == TeleportCause.CONSUMABLE_EFFECT; @@ -292,10 +289,7 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPlayerLeaveIsland(PlayerMoveEvent e) { - if (!addon.inGameWorld(e.getTo().getWorld()) - || (!addon.getPlugin().getIWM().isIslandNether(e.getTo().getWorld()) - && !addon.getPlugin().getIWM().isIslandEnd(e.getTo().getWorld())) - ) + if (!addon.inGameWorld(e.getTo().getWorld())) { return; } diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index 7ae5829..678f06d 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -201,8 +201,8 @@ public void testOnPlayerTeleportNotInGameWorld() { when(addon.inGameWorld(any())).thenReturn(false); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); - verify(show, never()).hideBorder(user); - verify(show, never()).clearUser(user); + verify(show).hideBorder(user); + verify(show).clearUser(user); mockedBukkit.verify(Bukkit::getScheduler, never()); } @@ -215,9 +215,9 @@ public void testOnPlayerTeleportInGameWorld() { when(iwm.isIslandNether(any())).thenReturn(false); // Not an island nether - should return early PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); - verify(show, never()).hideBorder(user); - verify(show, never()).clearUser(user); - mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + verify(show).hideBorder(user); + verify(show).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler); } /** @@ -263,9 +263,9 @@ public void testOnPlayerTeleportInVanillaNether() { when(iwm.isIslandEnd(any())).thenReturn(false); PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); - verify(show, never()).hideBorder(user); - verify(show, never()).clearUser(user); - mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + verify(show).hideBorder(user); + verify(show).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler); } /** @@ -279,9 +279,9 @@ public void testOnPlayerTeleportInVanillaEnd() { when(iwm.isIslandEnd(any())).thenReturn(false); // vanilla end, not island end PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.END_PORTAL); pl.onPlayerTeleport(event); - verify(show, never()).hideBorder(user); - verify(show, never()).clearUser(user); - mockedBukkit.verify(Bukkit::getScheduler, Mockito.never()); + verify(show).hideBorder(user); + verify(show).clearUser(user); + mockedBukkit.verify(Bukkit::getScheduler); } /** @@ -333,9 +333,8 @@ public void testOnPlayerLeaveIslandVanillaNether() { settings.setReturnTeleport(true); PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); pl.onPlayerLeaveIsland(event); - // Should return early — no island lookup, no teleport - verify(addon, never()).getIslands(); - assertFalse(event.isCancelled()); + verify(addon, times(2)).getIslands(); + assertTrue(event.isCancelled()); } /** @@ -350,9 +349,8 @@ public void testOnPlayerLeaveIslandVanillaEnd() { settings.setReturnTeleport(true); PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); pl.onPlayerLeaveIsland(event); - // Should return early — no island lookup, no teleport - verify(addon, never()).getIslands(); - assertFalse(event.isCancelled()); + verify(addon, times(2)).getIslands(); + assertTrue(event.isCancelled()); } /** From 9f35bf0ef63b950488a83c4db47a85ab794fcb16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:40:08 +0000 Subject: [PATCH 07/15] Initial plan From 0bed402766ac549685557bb0a10dc33327f87cc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:40:44 +0000 Subject: [PATCH 08/15] Initial plan From c040ce3bac69d7fdf9790dd2e0045a60741f9b59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:40:55 +0000 Subject: [PATCH 09/15] Initial plan From 15d86d8b3d5587d9b0ef61da0209514090464260 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 16:41:10 -0800 Subject: [PATCH 10/15] Update src/main/java/world/bentobox/border/listeners/PlayerListener.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/world/bentobox/border/listeners/PlayerListener.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 20067a4..a135e6c 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -349,8 +349,8 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) { } Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId())); } else { - BentoBox.getInstance().logDebug("Ray trace did not found a valid position for player " + p.getName() + " so teleporting them back to their island home."); - Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId())); + BentoBox.getInstance().logDebug("Ray trace found a valid safe position for player " + p.getName() + " and teleporting them there."); + Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId())); } }); From 3ac1d3935587adc1a4eb6d282b2ee41154c6263e Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 16:41:45 -0800 Subject: [PATCH 11/15] Update src/main/java/world/bentobox/border/listeners/PlayerListener.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/world/bentobox/border/listeners/PlayerListener.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index a135e6c..b060c68 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -289,8 +289,7 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPlayerLeaveIsland(PlayerMoveEvent e) { - if (!addon.inGameWorld(e.getTo().getWorld())) - { + if (!addon.inGameWorld(e.getTo().getWorld())) { return; } Player p = e.getPlayer(); From 74429470c44fcb9f12cab346d606709ddd2f5488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:59:06 +0000 Subject: [PATCH 12/15] Remove redundant testOnPlayerTeleportInVanillaNether and testOnPlayerTeleportInVanillaEnd tests Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../border/listeners/PlayerListenerTest.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index 678f06d..0dc3db0 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -252,38 +252,6 @@ public void testOnPlayerTeleportInIslandEnd() { mockedBukkit.verify(Bukkit::getScheduler); } - /** - * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. - * Vanilla (non-island) nether - should return early without any border activity. - */ - @Test - public void testOnPlayerTeleportInVanillaNether() { - when(addon.inGameWorld(any())).thenReturn(true); - when(iwm.isIslandNether(any())).thenReturn(false); // vanilla nether, not island nether - when(iwm.isIslandEnd(any())).thenReturn(false); - PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); - pl.onPlayerTeleport(event); - verify(show).hideBorder(user); - verify(show).clearUser(user); - mockedBukkit.verify(Bukkit::getScheduler); - } - - /** - * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. - * Vanilla (non-island) end - should return early without any border activity. - */ - @Test - public void testOnPlayerTeleportInVanillaEnd() { - when(addon.inGameWorld(any())).thenReturn(true); - when(iwm.isIslandNether(any())).thenReturn(false); - when(iwm.isIslandEnd(any())).thenReturn(false); // vanilla end, not island end - PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.END_PORTAL); - pl.onPlayerTeleport(event); - verify(show).hideBorder(user); - verify(show).clearUser(user); - mockedBukkit.verify(Bukkit::getScheduler); - } - /** * Test method for {@link world.bentobox.border.listeners.PlayerListener#onPlayerLeaveIsland(org.bukkit.event.player.PlayerMoveEvent)}. */ From cdd270ccdcfab4f31d67c1e365ac4aa755c744d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:01:57 +0000 Subject: [PATCH 13/15] Fix testOnPlayerLeaveIslandVanillaNether/End to correctly test early-return for vanilla worlds Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../bentobox/border/listeners/PlayerListener.java | 2 +- .../border/listeners/PlayerListenerTest.java | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java index 20067a4..536d589 100644 --- a/src/main/java/world/bentobox/border/listeners/PlayerListener.java +++ b/src/main/java/world/bentobox/border/listeners/PlayerListener.java @@ -349,7 +349,7 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) { } Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId())); } else { - BentoBox.getInstance().logDebug("Ray trace did not found a valid position for player " + p.getName() + " so teleporting them back to their island home."); + BentoBox.getInstance().logDebug("Ray trace did not find a valid position for player " + p.getName() + " so teleporting them back to their island home."); Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId())); } diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index 678f06d..6c6a85a 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -327,14 +327,11 @@ public void testOnPlayerLeaveIslandReturnTeleportOutsideCheckNotInGameWorld() { */ @Test public void testOnPlayerLeaveIslandVanillaNether() { - when(iwm.isIslandNether(any())).thenReturn(false); // vanilla nether, not island nether - when(iwm.isIslandEnd(any())).thenReturn(false); - when(island.onIsland(any())).thenReturn(false); + when(addon.inGameWorld(any())).thenReturn(false); // vanilla nether is not a game world settings.setReturnTeleport(true); PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); pl.onPlayerLeaveIsland(event); - verify(addon, times(2)).getIslands(); - assertTrue(event.isCancelled()); + verify(addon, never()).getIslands(); } /** @@ -343,14 +340,11 @@ public void testOnPlayerLeaveIslandVanillaNether() { */ @Test public void testOnPlayerLeaveIslandVanillaEnd() { - when(iwm.isIslandNether(any())).thenReturn(false); - when(iwm.isIslandEnd(any())).thenReturn(false); // vanilla end, not island end - when(island.onIsland(any())).thenReturn(false); + when(addon.inGameWorld(any())).thenReturn(false); // vanilla end is not a game world settings.setReturnTeleport(true); PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); pl.onPlayerLeaveIsland(event); - verify(addon, times(2)).getIslands(); - assertTrue(event.isCancelled()); + verify(addon, never()).getIslands(); } /** From 7b55e248630496b4c796ea7c9ee772845d76f0f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:17:05 +0000 Subject: [PATCH 14/15] Add tests for inGameWorld() vanilla nether/end early-return paths Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../world/bentobox/border/BorderTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/test/java/world/bentobox/border/BorderTest.java b/src/test/java/world/bentobox/border/BorderTest.java index b6e2ee2..a313258 100644 --- a/src/test/java/world/bentobox/border/BorderTest.java +++ b/src/test/java/world/bentobox/border/BorderTest.java @@ -14,6 +14,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.bukkit.World.Environment; + import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.util.Util; @@ -51,6 +53,44 @@ public void testInGameWorldReturnsFalseWhenNoGameModeMatches() throws Exception assertFalse(border.inGameWorld(world)); } + @Test + public void testInGameWorldReturnsFalseForVanillaNether() { + when(world.getEnvironment()).thenReturn(Environment.NETHER); + when(iwm.isIslandNether(world)).thenReturn(false); + + assertFalse(border.inGameWorld(world)); + } + + @Test + public void testInGameWorldReturnsFalseForVanillaEnd() { + when(world.getEnvironment()).thenReturn(Environment.THE_END); + when(iwm.isIslandEnd(world)).thenReturn(false); + + assertFalse(border.inGameWorld(world)); + } + + @Test + public void testInGameWorldDelegatesToGameModeForIslandNether() throws Exception { + when(world.getEnvironment()).thenReturn(Environment.NETHER); + when(iwm.isIslandNether(world)).thenReturn(true); + GameModeAddon matching = mock(GameModeAddon.class); + when(matching.inWorld(world)).thenReturn(true); + getGameModes().add(matching); + + assertTrue(border.inGameWorld(world)); + } + + @Test + public void testInGameWorldDelegatesToGameModeForIslandEnd() throws Exception { + when(world.getEnvironment()).thenReturn(Environment.THE_END); + when(iwm.isIslandEnd(world)).thenReturn(true); + GameModeAddon matching = mock(GameModeAddon.class); + when(matching.inWorld(world)).thenReturn(true); + getGameModes().add(matching); + + assertTrue(border.inGameWorld(world)); + } + @Test public void testGetAvailableBorderTypesViewIsUnmodifiable() { Set view = border.getAvailableBorderTypesView(); From 1f9217a0c30d6e6c0976238188ea23201191e2a6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 7 Mar 2026 17:52:10 -0800 Subject: [PATCH 15/15] Update src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../world/bentobox/border/listeners/PlayerListenerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java index 1b08650..b223d91 100644 --- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java +++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java @@ -212,7 +212,7 @@ public void testOnPlayerTeleportNotInGameWorld() { @Test public void testOnPlayerTeleportInGameWorld() { when(addon.inGameWorld(any())).thenReturn(true); - when(iwm.isIslandNether(any())).thenReturn(false); // Not an island nether - should return early + when(iwm.isIslandNether(any())).thenReturn(false); // In-game world teleport (non-island nether) should still trigger scheduler PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL); pl.onPlayerTeleport(event); verify(show).hideBorder(user);