Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2b0097e
Add CLAUDE.md with project guidance for Claude Code
tastybento Feb 26, 2026
541a85c
Fix world border not resetting when teleporting between islands
tastybento Feb 26, 2026
b60f4e3
Bump build version to 4.8.1 in pom.xml
tastybento Feb 26, 2026
cc8c9be
Refactor world handling in Border and PlayerListener for improved tel…
tastybento Mar 7, 2026
6bac390
Update src/main/java/world/bentobox/border/listeners/PlayerListener.java
tastybento Mar 7, 2026
a994c1e
Refactor inGameWorld checks in Border and PlayerListener for clearer …
tastybento Mar 8, 2026
534108d
Merge remote-tracking branch 'origin/develop' into develop
tastybento Mar 8, 2026
9f35bf0
Initial plan
Copilot Mar 8, 2026
0bed402
Initial plan
Copilot Mar 8, 2026
c040ce3
Initial plan
Copilot Mar 8, 2026
15d86d8
Update src/main/java/world/bentobox/border/listeners/PlayerListener.java
tastybento Mar 8, 2026
3ac1d39
Update src/main/java/world/bentobox/border/listeners/PlayerListener.java
tastybento Mar 8, 2026
7442947
Remove redundant testOnPlayerTeleportInVanillaNether and testOnPlayer…
Copilot Mar 8, 2026
cdd270c
Fix testOnPlayerLeaveIslandVanillaNether/End to correctly test early-…
Copilot Mar 8, 2026
683c1c4
Merge pull request #167 from BentoBoxWorld/copilot/sub-pr-164-another…
tastybento Mar 8, 2026
cf303f1
Merge branch 'develop' into copilot/sub-pr-164-again
tastybento Mar 8, 2026
7b55e24
Add tests for inGameWorld() vanilla nether/end early-return paths
Copilot Mar 8, 2026
5d0e9d5
Merge pull request #166 from BentoBoxWorld/copilot/sub-pr-164-again
tastybento Mar 8, 2026
f67665d
Merge pull request #165 from BentoBoxWorld/copilot/sub-pr-164
tastybento Mar 8, 2026
1f9217a
Update src/test/java/world/bentobox/border/listeners/PlayerListenerTe…
tastybento Mar 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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<Settings>` 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`)
22 changes: 19 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>4.8.0</build.version>
<build.version>4.8.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_Border</sonar.projectKey>
Expand Down Expand Up @@ -102,25 +102,41 @@
</profiles>

<repositories>
<!-- jitpack first so MockBukkit snapshots resolve without hitting other repos -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>bentoboxworld</id>
<url>https://repo.codemc.org/repository/bentoboxworld/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>codemc</id>
<url>https://repo.codemc.org/repository/maven-snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

Expand Down
7 changes: 5 additions & 2 deletions src/main/java/world/bentobox/border/Border.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -122,7 +121,11 @@ 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)));
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));
}

public Set<BorderType> getAvailableBorderTypesView() {
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/world/bentobox/border/listeners/PlayerListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -238,13 +239,14 @@ public void onPlayerTeleport(PlayerTeleportEvent e) {
return;
}
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;
}


TeleportCause cause = e.getCause();
boolean isBlacklistedCause = cause == TeleportCause.ENDER_PEARL || cause == TeleportCause.CONSUMABLE_EFFECT;

Expand Down Expand Up @@ -287,6 +289,9 @@ public void onPlayerTeleport(PlayerTeleportEvent e) {
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerLeaveIsland(PlayerMoveEvent e) {
if (!addon.inGameWorld(e.getTo().getWorld())) {
return;
}
Player p = e.getPlayer();
if (!isOn(p)) {
return;
Expand All @@ -295,13 +300,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<Island> optionalIsland = addon.getIslands().getIslandAt(p.getLocation());
if (optionalIsland.isEmpty()) {
Expand Down Expand Up @@ -340,6 +348,7 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) {
}
Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId()));
} else {
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()));
}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/test/java/world/bentobox/border/BorderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<BorderType> view = border.getAvailableBorderTypesView();
Expand Down
Loading
Loading