diff --git a/pom.xml b/pom.xml
index 1b7e25d..56e11ea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
jenkins
- http://ci.codemc.org/job/BentoBoxWorld/job/Border
+ https://ci.codemc.org/job/BentoBoxWorld/job/Border
@@ -51,7 +51,7 @@
${build.version}-SNAPSHOT
- 4.7.0
+ 4.8.0
-LOCAL
BentoBoxWorld_Border
@@ -224,6 +224,7 @@
${argLine}
+ -XX:+EnableDynamicAgentLoading
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
diff --git a/src/main/java/world/bentobox/border/Border.java b/src/main/java/world/bentobox/border/Border.java
index 7c0e7d5..f678012 100644
--- a/src/main/java/world/bentobox/border/Border.java
+++ b/src/main/java/world/bentobox/border/Border.java
@@ -14,6 +14,7 @@
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;
import world.bentobox.border.listeners.BorderShower;
@@ -21,15 +22,26 @@
import world.bentobox.border.listeners.ShowBarrier;
import world.bentobox.border.listeners.ShowWorldBorder;
+/**
+ * Border add-on entry point that wires commands, listeners, and per-player border
+ * rendering for supported game modes.
+ *
+ * Lifecycle:
+ *
+ * - {@link #onLoad()} loads and persists configuration.
+ * - {@link #onEnable()} hooks into available game modes, registers commands and listeners,
+ * and builds the border display implementation.
+ *
+ */
public class Border extends Addon {
private BorderShower borderShower;
private Settings settings;
- private Config config = new Config<>(this, Settings.class);
+ private final Config config = new Config<>(this, Settings.class);
- private @NonNull List gameModes = new ArrayList<>();
+ private final @NonNull List gameModes = new ArrayList<>();
private final Set availableBorderTypes = EnumSet.of(BorderType.VANILLA, BorderType.BARRIER);
@@ -54,6 +66,7 @@ public void onEnable() {
log("Border hooking into " + gameModeAddon.getDescription().getName());
gameModeAddon.getPlayerCommand().ifPresent(c -> new IslandBorderCommand(this, c, "border"));
gameModeAddon.getPlayerCommand().ifPresent(c -> new BorderTypeCommand(this, c, "bordertype"));
+ gameModeAddon.getPlayerCommand().ifPresent(c -> new BorderColorCommand(this, c, "bordercolor"));
}
});
@@ -69,12 +82,20 @@ public void onDisable() {
// Nothing to do here
}
+ /**
+ * Creates the border shower implementation used for per-player rendering.
+ *
+ * @return proxy that delegates to the configured border implementations
+ */
private BorderShower createBorder() {
BorderShower customBorder = new ShowBarrier(this);
BorderShower wbapiBorder = new ShowWorldBorder(this);
return new PerPlayerBorderProxy(this, customBorder, wbapiBorder);
}
+ /**
+ * @return the active border shower, or {@code null} if the addon is not enabled
+ */
public BorderShower getBorderShower() {
return borderShower;
}
@@ -101,7 +122,7 @@ public Settings getSettings() {
}
/**
- * @param world
+ * @param world to check
* @return true if world is being handled by Border
*/
public boolean inGameWorld(World world) {
@@ -130,6 +151,10 @@ private void registerPlaceholders()
orElse(new MetaDataValue(getSettings().getType().getId())).asByte()).
orElse(getSettings().getType()).
getCommandLabel());
+ this.getPlugin().getPlaceholdersManager().registerPlaceholder(this,
+ "color",
+ user -> user.getMetaData(PerPlayerBorderProxy.BORDER_COLOR_META_DATA)
+ .map(MetaDataValue::asString)
+ .orElse(getSettings().getColor().name().toLowerCase()));
}
-
}
diff --git a/src/main/java/world/bentobox/border/PerPlayerBorderProxy.java b/src/main/java/world/bentobox/border/PerPlayerBorderProxy.java
index b53bfee..5f22eda 100644
--- a/src/main/java/world/bentobox/border/PerPlayerBorderProxy.java
+++ b/src/main/java/world/bentobox/border/PerPlayerBorderProxy.java
@@ -10,14 +10,37 @@
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.border.listeners.BorderShower;
+/**
+ * Delegates border rendering to either the custom or vanilla implementation
+ * based on the per-player border type metadata.
+ *
+ * Selection rules:
+ *
+ * - If the player has no border type metadata, the add-on default is used.
+ * - If the metadata id is unknown or not enabled, the add-on default is used.
+ * - Otherwise, the stored border type is honored.
+ *
+ */
public final class PerPlayerBorderProxy implements BorderShower {
+ /**
+ * Metadata key for a player's preferred border type id.
+ */
public static final String BORDER_BORDERTYPE_META_DATA = "Border_bordertype";
+ /**
+ * Metadata key for a player's preferred border color id.
+ */
+ public static final String BORDER_COLOR_META_DATA = "Border_color";
private final Border addon;
private final BorderShower customBorder;
private final BorderShower vanillaBorder;
+ /**
+ * @param addon owning add-on providing settings and available types
+ * @param customBorder custom border renderer (barrier-based)
+ * @param vanillaBorder vanilla world border renderer
+ */
public PerPlayerBorderProxy(Border addon, BorderShower customBorder, BorderShower vanillaBorder) {
this.addon = addon;
this.customBorder = customBorder;
diff --git a/src/main/java/world/bentobox/border/commands/BorderColorCommand.java b/src/main/java/world/bentobox/border/commands/BorderColorCommand.java
new file mode 100644
index 0000000..103ae77
--- /dev/null
+++ b/src/main/java/world/bentobox/border/commands/BorderColorCommand.java
@@ -0,0 +1,87 @@
+package world.bentobox.border.commands;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import world.bentobox.bentobox.api.commands.CompositeCommand;
+import world.bentobox.bentobox.api.metadata.MetaDataValue;
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.database.objects.Island;
+import world.bentobox.bentobox.util.Util;
+import world.bentobox.border.Border;
+import world.bentobox.border.PerPlayerBorderProxy;
+import world.bentobox.border.Settings.BorderColor;
+
+/**
+ * Command to allow players with the appropriate permission to set their own border color.
+ * Permission required: [gamemode].border.color.[color] e.g. bskyblock.border.color.red
+ */
+public final class BorderColorCommand extends CompositeCommand {
+
+ private static final List COLOR_NAMES = Arrays.stream(BorderColor.values())
+ .map(c -> c.name().toLowerCase())
+ .toList();
+
+ private final Border addon;
+ private Island island;
+
+ public BorderColorCommand(Border addon, CompositeCommand parent, String commandLabel) {
+ super(addon, parent, commandLabel);
+ this.addon = addon;
+ }
+
+ @Override
+ public void setup() {
+ this.setPermission("border.color");
+ this.setDescription("border.set-color.description");
+ this.setOnlyPlayer(true);
+ }
+
+ @Override
+ public boolean canExecute(User user, String label, List args) {
+ if (!this.getWorld().equals(Util.getWorld(user.getWorld()))) {
+ user.sendMessage("general.errors.wrong-world");
+ return false;
+ }
+ island = getIslands().getIsland(getWorld(), user);
+ return island != null;
+ }
+
+ @Override
+ public boolean execute(User user, String label, List args) {
+ if (args.size() != 1) {
+ this.showHelp(this, user);
+ return false;
+ }
+
+ String colorArg = args.getFirst().toLowerCase();
+
+ if (!COLOR_NAMES.contains(colorArg)) {
+ user.sendMessage("border.set-color.error-invalid-color");
+ return false;
+ }
+
+ String permPrefix = getPlugin().getIWM().getPermissionPrefix(getWorld());
+ String colorPerm = permPrefix + "border.color." + colorArg;
+ if (!user.hasPermission(colorPerm)) {
+ user.sendMessage("general.errors.no-permission", "[permission]", colorPerm);
+ return false;
+ }
+
+ BorderColor color = BorderColor.valueOf(colorArg.toUpperCase());
+ addon.getBorderShower().hideBorder(user);
+ user.putMetaData(PerPlayerBorderProxy.BORDER_COLOR_META_DATA, new MetaDataValue(color.name()));
+ addon.getBorderShower().showBorder(user.getPlayer(), island);
+ user.sendMessage("border.set-color.changed", "[color]", colorArg);
+ return true;
+ }
+
+ @Override
+ public Optional> tabComplete(User user, String alias, List args) {
+ String permPrefix = getPlugin().getIWM().getPermissionPrefix(getWorld());
+ return Optional.of(COLOR_NAMES.stream()
+ .filter(c -> user.hasPermission(permPrefix + "border.color." + c))
+ .toList());
+ }
+}
diff --git a/src/main/java/world/bentobox/border/commands/BorderTypeCommand.java b/src/main/java/world/bentobox/border/commands/BorderTypeCommand.java
index e4f183b..7467b44 100644
--- a/src/main/java/world/bentobox/border/commands/BorderTypeCommand.java
+++ b/src/main/java/world/bentobox/border/commands/BorderTypeCommand.java
@@ -63,8 +63,8 @@ public boolean execute(User user, String label, List args) {
return false;
}
- if (availableTypes.stream().anyMatch(args.get(0)::equalsIgnoreCase)) {
- changeBorderTypeTo(user, args.get(0));
+ if (availableTypes.stream().anyMatch(args.getFirst()::equalsIgnoreCase)) {
+ changeBorderTypeTo(user, args.getFirst());
return true;
}
@@ -88,7 +88,7 @@ private void toggleBorderType(User user)
if (index + 1 >= borderTypes.size())
{
- this.changeBorderTypeTo(user, borderTypes.get(0).getCommandLabel());
+ this.changeBorderTypeTo(user, borderTypes.getFirst().getCommandLabel());
}
else
{
diff --git a/src/main/java/world/bentobox/border/commands/IslandBorderCommand.java b/src/main/java/world/bentobox/border/commands/IslandBorderCommand.java
index 098ad54..c8815ff 100644
--- a/src/main/java/world/bentobox/border/commands/IslandBorderCommand.java
+++ b/src/main/java/world/bentobox/border/commands/IslandBorderCommand.java
@@ -13,7 +13,7 @@
public class IslandBorderCommand extends CompositeCommand {
public static final String BORDER_COMMAND_PERM = "border.toggle";
- private Border addon;
+ private final Border addon;
private Island island;
public IslandBorderCommand(Border addon, CompositeCommand parent, String label) {
@@ -29,6 +29,7 @@ public void setup() {
setConfigurableRankCommand();
new BorderTypeCommand(this.getAddon(), this, "type");
+ new BorderColorCommand(this.getAddon(), this, "color");
}
@Override
diff --git a/src/main/java/world/bentobox/border/listeners/BorderShower.java b/src/main/java/world/bentobox/border/listeners/BorderShower.java
index 6c25d47..eb50147 100644
--- a/src/main/java/world/bentobox/border/listeners/BorderShower.java
+++ b/src/main/java/world/bentobox/border/listeners/BorderShower.java
@@ -17,26 +17,26 @@
*
*/
public interface BorderShower {
- public static final String BORDER_STATE_META_DATA = "Border_state";
+ String BORDER_STATE_META_DATA = "Border_state";
/**
* Show the barrier to the player on an island
* @param player - player to show
* @param island - island
*/
- public void showBorder(Player player, Island island);
+ void showBorder(Player player, Island island);
/**
* Hide the barrier
* @param user - user
*/
- public void hideBorder(User user);
+ void hideBorder(User user);
/**
* Removes any cache
* @param user - user
*/
- public default void clearUser(User user) {
+ default void clearUser(User user) {
// Do nothing
}
@@ -45,7 +45,7 @@ public default void clearUser(User user) {
* @param user user
* @param island island
*/
- public default void refreshView(User user, Island island){
+ default void refreshView(User user, Island island){
// Do nothing
}
@@ -53,7 +53,7 @@ public default void refreshView(User user, Island island){
* Teleports an entity, typically a player back within the island space they are in
* @param entity entity
*/
- public default void teleportEntity(Border addon, Entity entity) {
+ default void teleportEntity(Border addon, Entity entity) {
addon.getIslands().getIslandAt(entity.getLocation()).ifPresent(i -> {
Vector unitVector = i.getCenter().toVector().subtract(entity.getLocation().toVector()).normalize()
.multiply(new Vector(1, 0, 1));
diff --git a/src/main/java/world/bentobox/border/listeners/PlayerListener.java b/src/main/java/world/bentobox/border/listeners/PlayerListener.java
index 8b4f3d1..2eb3a24 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.api.addons.Addon;
import world.bentobox.bentobox.api.events.island.IslandProtectionRangeChangeEvent;
import world.bentobox.bentobox.api.flags.Flag;
import world.bentobox.bentobox.api.metadata.MetaDataValue;
@@ -50,22 +51,50 @@
import world.bentobox.border.commands.IslandBorderCommand;
/**
+ * Listener for player-related events in the Border addon.
+ *
+ * This listener handles various player events including joining, teleporting, moving, mounting,
+ * dropping items, and dying. It manages the display of island borders for players and handles
+ * teleportation logic to keep players within island protection zones.
+ *
+ * Key responsibilities:
+ * - Display/hide island borders based on player settings and permissions
+ * - Manage border visibility state using player metadata
+ * - Handle barrier-based protection with fall damage detection
+ * - Prevent players from leaving island protection zones
+ * - Track mounted players and manage vehicle movement
+ * - Handle item bouncing at island barriers
+ * - React to protection range changes
+ *
* @author tastybento
*/
public class PlayerListener implements Listener {
- private static final Vector XZ = new Vector(1,0,1);
+ private static final Vector XZ = new Vector(1, 0, 1);
private final Border addon;
- private Set inTeleport;
+ private final Set inTeleport;
private final BorderShower show;
- private Map mountedPlayers = new HashMap<>();
+ private final Map mountedPlayers = new HashMap<>();
+ /**
+ * Constructs a new PlayerListener.
+ *
+ * @param addon the Border addon instance
+ */
public PlayerListener(Border addon) {
this.addon = addon;
inTeleport = new HashSet<>();
this.show = addon.getBorderShower();
}
+ /**
+ * Handles player join events.
+ *
+ * When a player joins the game, if the border is enabled for them, this method
+ * schedules processing on the next tick to initialize their border settings.
+ *
+ * @param e the player join event
+ */
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent e) {
Player player = e.getPlayer();
@@ -75,6 +104,17 @@ public void onPlayerJoin(PlayerJoinEvent e) {
}
}
+ /**
+ * Processes the player join event on the next game tick.
+ *
+ * This method initializes border settings for the joining player:
+ * - Hides any existing borders and clears the world border
+ * - Resets border visibility state to default if player lacks border command permissions
+ * - Resets border type to default if player lacks border type permissions
+ * - Shows the border for the player's current island
+ *
+ * @param e the player join event
+ */
protected void processEvent(PlayerJoinEvent e) {
Player player = e.getPlayer();
if (!isOn(player)) {
@@ -88,24 +128,36 @@ protected void processEvent(PlayerJoinEvent e) {
user.getPlayer().setWorldBorder(null);
// Get the game mode that this player is in
- addon.getPlugin().getIWM().getAddon(e.getPlayer().getWorld()).map(gma -> gma.getPermissionPrefix()).filter(
- permPrefix -> !e.getPlayer().hasPermission(permPrefix + IslandBorderCommand.BORDER_COMMAND_PERM))
- .ifPresent(permPrefix -> {
- // Restore barrier on/off to default
- user.putMetaData(BorderShower.BORDER_STATE_META_DATA,
- new MetaDataValue(addon.getSettings().isShowByDefault()));
- if (!e.getPlayer().hasPermission(permPrefix + "border.type") && !e.getPlayer().hasPermission(permPrefix + "border.bordertype")) {
- // Restore default barrier type to player
- MetaDataValue metaDataValue = new MetaDataValue(addon.getSettings().getType().getId());
- user.putMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA, metaDataValue);
- }
- });
+ addon.getPlugin().getIWM().getAddon(e.getPlayer().getWorld()).map(Addon::getPermissionPrefix).filter(
+ permPrefix -> !e.getPlayer().hasPermission(permPrefix + IslandBorderCommand.BORDER_COMMAND_PERM))
+ .ifPresent(permPrefix -> {
+ // Restore barrier on/off to default
+ user.putMetaData(BorderShower.BORDER_STATE_META_DATA,
+ new MetaDataValue(addon.getSettings().isShowByDefault()));
+ if (!e.getPlayer().hasPermission(permPrefix + "border.type") && !e.getPlayer().hasPermission(permPrefix + "border.bordertype")) {
+ // Restore default barrier type to player
+ MetaDataValue metaDataValue = new MetaDataValue(addon.getSettings().getType().getId());
+ user.putMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA, metaDataValue);
+ }
+ });
// Show the border if required one tick after
- Bukkit.getScheduler().runTask(addon.getPlugin(), () -> addon.getIslands().getIslandAt(e.getPlayer().getLocation()).ifPresent(i ->
- show.showBorder(e.getPlayer(), i)));
+ Bukkit.getScheduler().runTask(addon.getPlugin(), () -> addon.getIslands().getIslandAt(e.getPlayer().getLocation()).ifPresent(i ->
+ show.showBorder(e.getPlayer(), i)));
}
+ /**
+ * Handles player fall damage when using barrier-type borders.
+ *
+ * Detects when a player takes fall damage while standing on air (indicating they've
+ * crossed the barrier border) and teleports them back inside the island's protection zone.
+ * Only applies when:
+ * - The border type is set to BARRIER
+ * - The player has borders enabled
+ * - The event is a fall damage event
+ *
+ * @param e the entity damage event
+ */
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerDamage(EntityDamageEvent e) {
// Only deal with fall damage in the right world if the barrier is on
@@ -115,16 +167,32 @@ public void onPlayerDamage(EntityDamageEvent e) {
}
Material type = p.getLocation().getBlock().getRelative(BlockFace.DOWN).getType();
if (type == Material.AIR) {
- ((BorderShower) show).teleportEntity(addon, p);
+ show.teleportEntity(addon, p);
e.setCancelled(true);
}
}
+ /**
+ * Handles player quit events.
+ *
+ * Clears any active border display for the leaving player, cleaning up resources and
+ * removing any visual indicators.
+ *
+ * @param e the player quit event
+ */
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent e) {
show.clearUser(User.getInstance(e.getPlayer()));
}
+ /**
+ * Handles player respawn events.
+ *
+ * Clears the previous border display and shows the border again for the island where the
+ * player respawned. This ensures the border is properly updated after resurrection.
+ *
+ * @param e the player respawn event
+ */
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerRespawn(PlayerRespawnEvent e) {
Player player = e.getPlayer();
@@ -136,9 +204,13 @@ public void onPlayerRespawn(PlayerRespawnEvent e) {
}
/**
- * Check if the border is on or off
- * @param player player
- * @return true if the border is on, false if not
+ * Check if the border is on or off for a player.
+ *
+ * Checks the player's metadata to determine if they have borders enabled, falling back to
+ * the addon's default setting if no metadata is found.
+ *
+ * @param player the player to check
+ * @return true if the border is enabled for this player, false if not
*/
private boolean isOn(Player player) {
// Check if border is off
@@ -148,6 +220,17 @@ private boolean isOn(Player player) {
}
+ /**
+ * Handles player teleport events.
+ *
+ * Manages teleportation by:
+ * - Clearing existing border displays before teleporting
+ * - Cancelling certain teleport types (Ender Pearl, Chorus Fruit) if they would take the player
+ * outside their island's protection zone (unless the ALLOW_MOVE_BOX flag is set)
+ * - Showing the border for the destination island
+ *
+ * @param e the player teleport event
+ */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent e) {
Player player = e.getPlayer();
@@ -158,39 +241,50 @@ public void onPlayerTeleport(PlayerTeleportEvent e) {
show.clearUser(User.getInstance(player));
- if (to == null || !addon.inGameWorld(to.getWorld())) {
+ if (!addon.inGameWorld(to.getWorld())) {
return;
}
TeleportCause cause = e.getCause();
- boolean isBlacklistedCause = cause == TeleportCause.ENDER_PEARL || cause == TeleportCause.CHORUS_FRUIT;
+ boolean isBlacklistedCause = cause == TeleportCause.ENDER_PEARL || cause == TeleportCause.CONSUMABLE_EFFECT;
Bukkit.getScheduler().runTask(addon.getPlugin(), () ->
- addon.getIslands().getIslandAt(to).ifPresentOrElse(i -> {
- Optional boxedEnderPearlFlag = i.getPlugin().getFlagsManager().getFlag("ALLOW_MOVE_BOX");
+ addon.getIslands().getIslandAt(to).ifPresentOrElse(i -> {
+ Optional boxedEnderPearlFlag = i.getPlugin().getFlagsManager().getFlag("ALLOW_MOVE_BOX");
- if (isBlacklistedCause
- && (!i.getProtectionBoundingBox().contains(to.toVector())
+ if (isBlacklistedCause
+ && (!i.getProtectionBoundingBox().contains(to.toVector())
|| !i.onIsland(player.getLocation()))) {
- e.setCancelled(true);
- }
+ e.setCancelled(true);
+ }
- if (boxedEnderPearlFlag.isPresent()
- && boxedEnderPearlFlag.get().isSetForWorld(to.getWorld())
- && cause == TeleportCause.ENDER_PEARL) {
- e.setCancelled(false);
- }
+ if (boxedEnderPearlFlag.isPresent()
+ && boxedEnderPearlFlag.get().isSetForWorld(to.getWorld())
+ && cause == TeleportCause.ENDER_PEARL) {
+ e.setCancelled(false);
+ }
- show.showBorder(player, i);
- }, () -> {
- if (isBlacklistedCause) {
- e.setCancelled(true);
- return;
- }
- })
- );
+ show.showBorder(player, i);
+ }, () -> {
+ if (isBlacklistedCause) {
+ e.setCancelled(true);
+ }
+ })
+ );
}
+ /**
+ * Handles player movement and prevents them from leaving their island's protection zone.
+ *
+ * When a player attempts to leave their island's protection range (if return teleport is enabled),
+ * this method:
+ * - Detects the crossing of the island boundary
+ * - Teleports the player back inside the protection zone
+ * - Uses ray tracing to find a safe position on the boundary
+ * - Creates necessary blocks (Netherrack, End Stone, or Stone) for safe landing
+ *
+ * @param e the player move event
+ */
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerLeaveIsland(PlayerMoveEvent e) {
Player p = e.getPlayer();
@@ -216,19 +310,24 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) {
}
optionalIsland.ifPresent(i -> {
Vector unitVector = i.getProtectionCenter().toVector().subtract(p.getLocation().toVector()).normalize()
- .multiply(new Vector(1,0,1));
+ .multiply(new Vector(1, 0, 1));
if (unitVector.lengthSquared() <= 0D) {
// Direction is zero, so nothing to do; cannot move.
return;
}
RayTraceResult r = i.getProtectionBoundingBox().rayTrace(p.getLocation().toVector(), unitVector, i.getRange());
- if (r != null && checkFinite(r.getHitPosition())) {
+ if (r == null || !checkFinite(r.getHitPosition())) {
+ // Ray trace failed, so just teleport the player back to that island
inTeleport.add(p.getUniqueId());
- Location targetPos = r.getHitPosition().toLocation(p.getWorld(), p.getLocation().getYaw(), p.getLocation().getPitch());
+ Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
+ return;
+ }
- if (!e.getPlayer().isFlying() && addon.getSettings().isReturnTeleportBlock()
- && !addon.getIslands().isSafeLocation(targetPos)) {
- switch (targetPos.getWorld().getEnvironment()) {
+ inTeleport.add(p.getUniqueId());
+ Location targetPos = r.getHitPosition().toLocation(p.getWorld(), p.getLocation().getYaw(), p.getLocation().getPitch());
+
+ if (!addon.getIslands().isSafeLocation(targetPos)) {
+ switch (targetPos.getWorld().getEnvironment()) {
case NETHER:
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(Material.NETHERRACK);
break;
@@ -238,13 +337,21 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) {
default:
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(Material.STONE);
break;
- }
}
Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId()));
+ } else {
+ Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
}
+
});
}
+ /**
+ * Validates that all coordinates of a vector are finite (not NaN or Infinite).
+ *
+ * @param toCheck the vector to check
+ * @return true if all X, Y, Z coordinates are finite, false otherwise
+ */
public boolean checkFinite(Vector toCheck) {
return NumberConversions.isFinite(toCheck.getX()) && NumberConversions.isFinite(toCheck.getY())
&& NumberConversions.isFinite(toCheck.getZ());
@@ -252,9 +359,10 @@ public boolean checkFinite(Vector toCheck) {
/**
* Check if the player is outside the island protection zone that they are supposed to be in.
+ *
* @param player - player moving
- * @param from - from location
- * @param to - to location
+ * @param from - from location
+ * @param to - to location
* @return true if outside the island protection zone
*/
private boolean outsideCheck(Player player, Location from, Location to) {
@@ -277,6 +385,7 @@ private boolean outsideCheck(Player player, Location from, Location to) {
/**
* Runs a task while the player is mounting an entity and eject
* if the entity went outside the protection range
+ *
* @param event - event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@@ -305,6 +414,7 @@ public void onEntityMount(EntityMountEvent event) {
/**
* Cancel the running task if the player was mounting an entity
+ *
* @param event - event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@@ -328,6 +438,7 @@ public void onEntityDismount(EntityDismountEvent event) {
/**
* Refreshes the barrier view when the player moves (more than just moving their head)
+ *
* @param e event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@@ -336,13 +447,14 @@ public void onPlayerMove(PlayerMoveEvent e) {
// Remove head movement
if (isOn(player) && !e.getFrom().toVector().equals(e.getTo().toVector())) {
addon.getIslands()
- .getIslandAt(e.getPlayer().getLocation())
- .ifPresent(i -> show.refreshView(User.getInstance(e.getPlayer()), i));
+ .getIslandAt(e.getPlayer().getLocation())
+ .ifPresent(i -> show.refreshView(User.getInstance(e.getPlayer()), i));
}
}
/**
* Refresh the view when riding in a vehicle
+ *
* @param e event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@@ -350,14 +462,15 @@ public void onVehicleMove(VehicleMoveEvent e) {
// Remove head movement
if (!e.getFrom().toVector().equals(e.getTo().toVector())) {
e.getVehicle().getPassengers().stream().filter(Player.class::isInstance).map(Player.class::cast)
- .filter(this::isOn).forEach(p -> addon.getIslands().getIslandAt(p.getLocation())
- .ifPresent(i -> show.refreshView(User.getInstance(p), i)));
+ .filter(this::isOn).forEach(p -> addon.getIslands().getIslandAt(p.getLocation())
+ .ifPresent(i -> show.refreshView(User.getInstance(p), i)));
}
}
/**
* Hide and then show the border to react to the change in protection area
- * @param e
+ *
+ * @param e event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onProtectionRangeChange(IslandProtectionRangeChangeEvent e) {
@@ -372,36 +485,48 @@ public void onProtectionRangeChange(IslandProtectionRangeChangeEvent e) {
/**
* Bounces items back to inside the barrier if thrown by a player
+ *
* @param event event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onItemDrop(PlayerDropItemEvent event) {
if (addon.getSettings().isBounceBack()
- && addon.inGameWorld(event.getPlayer().getWorld())
+ && addon.inGameWorld(event.getPlayer().getWorld())
&& isOn(event.getPlayer())
- ) {
+ ) {
// Get this island
- addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> trackItem(event.getItemDrop(), is));
+ addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> trackItem(event.getItemDrop(), is));
}
}
/**
* Bounces items back to inside the barrier if dropped when a player dies
+ *
* @param event event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerDeath(PlayerDeathEvent event) {
if (addon.getSettings().isBounceBack()
- && addon.inGameWorld(event.getPlayer().getWorld())
+ && addon.inGameWorld(event.getPlayer().getWorld())
&& isOn(event.getPlayer())) {
// Get this island
- addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> {
+ addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> {
event.getDrops().forEach(item -> trackItem(event.getPlayer().getWorld().dropItemNaturally(event.getPlayer().getLocation(), item), is));
event.getDrops().clear(); // We handled them
});
}
}
+ /**
+ * Tracks an item to prevent it from leaving the island's protection zone.
+ *
+ * This method monitors dropped items and bounces them back if they try to leave
+ * the protection range by reversing and reducing their velocity. Tracking stops after
+ * 20 seconds or when the item is picked up/despawned.
+ *
+ * @param item the item to track
+ * @param island the island whose protection zone should contain the item
+ */
private void trackItem(Item item, Island island) {
new BukkitRunnable() {
int ticksActive = 0;
@@ -418,12 +543,12 @@ public void run() {
// Check if the item is going outside the border
if (!island.onIsland(loc)) {
// Reverse the direction
- item.setVelocity(item.getVelocity().multiply(-0.5));
+ item.setVelocity(item.getVelocity().multiply(-0.5));
this.cancel();
}
ticksActive++;
}
- }.runTaskTimer(addon.getPlugin(), 1L, 2L); // Check every 2 ticks (0.1 seconds)
+ }.runTaskTimer(addon.getPlugin(), 1L, 1L); // Check every 2 ticks (0.1 seconds)
}
}
diff --git a/src/main/java/world/bentobox/border/listeners/ShowBarrier.java b/src/main/java/world/bentobox/border/listeners/ShowBarrier.java
index 05673fb..f21b75b 100644
--- a/src/main/java/world/bentobox/border/listeners/ShowBarrier.java
+++ b/src/main/java/world/bentobox/border/listeners/ShowBarrier.java
@@ -126,10 +126,10 @@ private void showWalls(Player player, Location loc, int xMin, int xMax, int zMin
/**
* @param player player
- * @param i
- * @param j
- * @param k
- * @param max
+ * @param i - x
+ * @param j - y
+ * @param k - z
+ * @param max - whether this is the max border or not
*/
private void showPlayer(Player player, int i, int j, int k, boolean max) {
// Get if on or in border
@@ -200,7 +200,7 @@ public void refreshView(User user, Island island) {
this.showBorder(user.getPlayer(), island);
}
- private class BarrierBlock {
+ private static class BarrierBlock {
Location l;
BlockData oldBlockData;
public BarrierBlock(Location l, BlockData oldBlockData) {
diff --git a/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java b/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java
index 4ac93e9..c98051e 100644
--- a/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java
+++ b/src/main/java/world/bentobox/border/listeners/ShowWorldBorder.java
@@ -16,6 +16,8 @@
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
import world.bentobox.border.Border;
+import world.bentobox.border.PerPlayerBorderProxy;
+import world.bentobox.border.Settings.BorderColor;
/**
* Show a border using Paper's WorldBorder API
@@ -33,11 +35,12 @@ public ShowWorldBorder(Border addon) {
@Override
public void showBorder(Player player, Island island) {
+ User user = Objects.requireNonNull(User.getInstance(player));
if (addon.getSettings().getDisabledGameModes().contains(island.getGameMode())
- || !Objects.requireNonNull(User.getInstance(player)).getMetaData(BORDER_STATE_META_DATA).map(MetaDataValue::asBoolean).orElse(addon.getSettings().isShowByDefault())) {
+ || !user.getMetaData(BORDER_STATE_META_DATA).map(MetaDataValue::asBoolean).orElse(addon.getSettings().isShowByDefault())) {
return;
}
-
+
if (player.getWorld().getEnvironment() == Environment.NETHER && !addon.getPlugin().getIWM().isIslandNether(player.getWorld())) {
return;
}
@@ -47,7 +50,17 @@ public void showBorder(Player player, Island island) {
double size = Math.min(island.getRange() * 2D, (island.getProtectionRange() + addon.getSettings().getBarrierOffset()) * 2D);
wb.setSize(size);
wb.setWarningDistance(0);
- switch(addon.getSettings().getColor()) {
+ BorderColor borderColor = user.getMetaData(PerPlayerBorderProxy.BORDER_COLOR_META_DATA)
+ .map(MetaDataValue::asString)
+ .map(name -> {
+ try {
+ return BorderColor.valueOf(name);
+ } catch (IllegalArgumentException e) {
+ return addon.getSettings().getColor();
+ }
+ })
+ .orElseGet(() -> addon.getSettings().getColor());
+ switch(borderColor) {
case RED:
wb.changeSize(wb.getSize() - 0.1, MAX_TICKS);
break;
diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml
index 6f727cf..7bca784 100644
--- a/src/main/resources/locales/en-US.yml
+++ b/src/main/resources/locales/en-US.yml
@@ -6,4 +6,8 @@ border:
set-type:
description: "changes the type of the border"
changed: "&a Border type changed to &b[type]&a."
- error-unavailable-type: "&c This type is unavailable or does not exist."
\ No newline at end of file
+ error-unavailable-type: "&c This type is unavailable or does not exist."
+ set-color:
+ description: "changes the color of the border"
+ changed: "&a Border color changed to &b[color]&a."
+ error-invalid-color: "&c That color is invalid. Valid colors are: red, green, blue."
\ No newline at end of file
diff --git a/src/test/java/world/bentobox/border/BorderTest.java b/src/test/java/world/bentobox/border/BorderTest.java
new file mode 100644
index 0000000..b6e2ee2
--- /dev/null
+++ b/src/test/java/world/bentobox/border/BorderTest.java
@@ -0,0 +1,86 @@
+package world.bentobox.border;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import world.bentobox.bentobox.api.addons.GameModeAddon;
+import world.bentobox.bentobox.util.Util;
+
+/**
+ * Tests for {@link Border} behavior that does not require a full Bukkit runtime.
+ */
+public class BorderTest extends CommonTestSetup {
+
+ private Border border;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ border = new Border();
+ mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world);
+ setField(border, "settings", new Settings());
+ }
+
+ @Test
+ public void testInGameWorldReturnsTrueWhenAnyGameModeMatches() throws Exception {
+ GameModeAddon matching = mock(GameModeAddon.class);
+ when(matching.inWorld(world)).thenReturn(true);
+ getGameModes().add(matching);
+
+ assertTrue(border.inGameWorld(world));
+ }
+
+ @Test
+ public void testInGameWorldReturnsFalseWhenNoGameModeMatches() throws Exception {
+ GameModeAddon nonMatching = mock(GameModeAddon.class);
+ when(nonMatching.inWorld(world)).thenReturn(false);
+ getGameModes().add(nonMatching);
+
+ assertFalse(border.inGameWorld(world));
+ }
+
+ @Test
+ public void testGetAvailableBorderTypesViewIsUnmodifiable() {
+ Set view = border.getAvailableBorderTypesView();
+ assertTrue(view.contains(BorderType.VANILLA));
+ assertTrue(view.contains(BorderType.BARRIER));
+ assertThrows(UnsupportedOperationException.class, () -> view.add(BorderType.VANILLA));
+ }
+
+ @Test
+ public void testGetSettingsReturnsConfiguredInstance() {
+ Settings settings = new Settings();
+ setField(border, "settings", settings);
+ assertSame(settings, border.getSettings());
+ }
+
+ @SuppressWarnings("unchecked")
+ private List getGameModes() throws Exception {
+ Field field = Border.class.getDeclaredField("gameModes");
+ field.setAccessible(true);
+ return (List) field.get(border);
+ }
+
+ private static void setField(Object target, String fieldName, Object value) {
+ try {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException("Failed to set field '" + fieldName + "'", e);
+ }
+ }
+}
+
diff --git a/src/test/java/world/bentobox/border/CommonTestSetup.java b/src/test/java/world/bentobox/border/CommonTestSetup.java
index ea03ace..092a607 100644
--- a/src/test/java/world/bentobox/border/CommonTestSetup.java
+++ b/src/test/java/world/bentobox/border/CommonTestSetup.java
@@ -289,10 +289,10 @@ public void checkSpigotMessage(String expectedMessage, int expectedOccurrences)
/**
* Get the exploded event
- * @param entity
- * @param l
- * @param list
- * @return
+ * @param entity - entity that exploded
+ * @param l - location of explosion
+ * @param list - list of blocks that exploded
+ * @return the event
*/
public EntityExplodeEvent getExplodeEvent(Entity entity, Location l, List list) {
return new EntityExplodeEvent(entity, l, list, 0, null);
diff --git a/src/test/java/world/bentobox/border/PerPlayerBorderProxyTest.java b/src/test/java/world/bentobox/border/PerPlayerBorderProxyTest.java
new file mode 100644
index 0000000..28569fa
--- /dev/null
+++ b/src/test/java/world/bentobox/border/PerPlayerBorderProxyTest.java
@@ -0,0 +1,120 @@
+package world.bentobox.border;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.EnumSet;
+import java.util.Optional;
+
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import world.bentobox.bentobox.api.metadata.MetaDataValue;
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.database.objects.Island;
+import world.bentobox.border.listeners.BorderShower;
+
+/**
+ * Tests for {@link PerPlayerBorderProxy} border selection logic.
+ */
+public class PerPlayerBorderProxyTest {
+
+ @Mock
+ private Border addon;
+ @Mock
+ private BorderShower customBorder;
+ @Mock
+ private BorderShower vanillaBorder;
+ @Mock
+ private User user;
+ @Mock
+ private Player player;
+ @Mock
+ private Entity entity;
+ @Mock
+ private Island island;
+
+ private Settings settings;
+ private MockedStatic mockedUser;
+ private AutoCloseable closeable;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ closeable = MockitoAnnotations.openMocks(this);
+ settings = new Settings();
+ when(addon.getSettings()).thenReturn(settings);
+ when(addon.getAvailableBorderTypesView()).thenReturn(EnumSet.of(BorderType.VANILLA, BorderType.BARRIER));
+
+ mockedUser = Mockito.mockStatic(User.class, Mockito.RETURNS_MOCKS);
+ mockedUser.when(() -> User.getInstance(any(Player.class))).thenReturn(user);
+ mockedUser.when(() -> User.getInstance(any(Entity.class))).thenReturn(user);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ mockedUser.closeOnDemand();
+ closeable.close();
+ }
+
+ @Test
+ public void testShowBorderUsesDefaultWhenNoMetadata() {
+ when(user.getMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA)).thenReturn(Optional.empty());
+ PerPlayerBorderProxy proxy = new PerPlayerBorderProxy(addon, customBorder, vanillaBorder);
+
+ proxy.showBorder(player, island);
+
+ verify(vanillaBorder).showBorder(player, island);
+ verify(customBorder, never()).showBorder(player, island);
+ }
+
+ @Test
+ public void testShowBorderUsesCustomWhenMetadataBarrier() {
+ MetaDataValue metaDataValue = mock(MetaDataValue.class);
+ when(metaDataValue.asByte()).thenReturn(BorderType.BARRIER.getId());
+ when(user.getMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA)).thenReturn(Optional.of(metaDataValue));
+ PerPlayerBorderProxy proxy = new PerPlayerBorderProxy(addon, customBorder, vanillaBorder);
+
+ proxy.showBorder(player, island);
+
+ verify(customBorder).showBorder(player, island);
+ verify(vanillaBorder, never()).showBorder(player, island);
+ }
+
+ @Test
+ public void testHideBorderFallsBackWhenTypeUnavailable() {
+ MetaDataValue metaDataValue = mock(MetaDataValue.class);
+ when(metaDataValue.asByte()).thenReturn(BorderType.BARRIER.getId());
+ when(user.getMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA)).thenReturn(Optional.of(metaDataValue));
+ when(addon.getAvailableBorderTypesView()).thenReturn(EnumSet.of(BorderType.VANILLA));
+ PerPlayerBorderProxy proxy = new PerPlayerBorderProxy(addon, customBorder, vanillaBorder);
+
+ proxy.hideBorder(user);
+
+ verify(vanillaBorder).hideBorder(user);
+ verify(customBorder, never()).hideBorder(user);
+ }
+
+ @Test
+ public void testTeleportEntityUsesCustomWhenBarrier() {
+ MetaDataValue metaDataValue = mock(MetaDataValue.class);
+ when(metaDataValue.asByte()).thenReturn(BorderType.BARRIER.getId());
+ when(user.getMetaData(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA)).thenReturn(Optional.of(metaDataValue));
+ PerPlayerBorderProxy proxy = new PerPlayerBorderProxy(addon, customBorder, vanillaBorder);
+
+ proxy.teleportEntity(addon, entity);
+
+ verify(customBorder).teleportEntity(addon, entity);
+ verify(vanillaBorder, never()).teleportEntity(addon, entity);
+ }
+}
+
diff --git a/src/test/java/world/bentobox/border/SettingsTest.java b/src/test/java/world/bentobox/border/SettingsTest.java
index 6f655e2..701a291 100644
--- a/src/test/java/world/bentobox/border/SettingsTest.java
+++ b/src/test/java/world/bentobox/border/SettingsTest.java
@@ -18,10 +18,9 @@ public class SettingsTest {
private Settings settings;
/**
- * @throws java.lang.Exception
*/
@BeforeEach
- public void setUp() throws Exception {
+ public void setUp() {
settings = new Settings();
}
diff --git a/src/test/java/world/bentobox/border/commands/BorderColorCommandTest.java b/src/test/java/world/bentobox/border/commands/BorderColorCommandTest.java
new file mode 100644
index 0000000..882358f
--- /dev/null
+++ b/src/test/java/world/bentobox/border/commands/BorderColorCommandTest.java
@@ -0,0 +1,275 @@
+package world.bentobox.border.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import world.bentobox.bentobox.api.commands.CompositeCommand;
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.managers.CommandsManager;
+import world.bentobox.bentobox.util.Util;
+import world.bentobox.border.Border;
+import world.bentobox.border.CommonTestSetup;
+import world.bentobox.border.PerPlayerBorderProxy;
+import world.bentobox.border.Settings;
+import world.bentobox.border.listeners.BorderShower;
+
+/**
+ * Tests for {@link BorderColorCommand}
+ */
+public class BorderColorCommandTest extends CommonTestSetup {
+
+ @Mock
+ private CompositeCommand ac;
+ @Mock
+ private User user;
+ @Mock
+ private Border addon;
+ @Mock
+ private BorderShower bs;
+
+ private BorderColorCommand ic;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Command manager
+ CommandsManager cm = mock(CommandsManager.class);
+ when(plugin.getCommandsManager()).thenReturn(cm);
+
+ // Player
+ Player p = mock(Player.class);
+ when(user.isOp()).thenReturn(false);
+ when(user.getPermissionValue(anyString(), anyInt())).thenReturn(4);
+ when(user.getWorld()).thenReturn(world);
+ uuid = UUID.randomUUID();
+ when(user.getUniqueId()).thenReturn(uuid);
+ when(user.getPlayer()).thenReturn(p);
+ when(user.getName()).thenReturn("tastybento");
+ when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class));
+ User.setPlugin(plugin);
+
+ // Parent command has no aliases
+ when(ac.getSubCommandAliases()).thenReturn(new HashMap<>());
+ when(ac.getWorld()).thenReturn(world);
+
+ // Util - return what was put into the method
+ mockedUtil.when(() -> Util.getWorld(any())).thenAnswer(invocation -> invocation.getArgument(0, World.class));
+
+ // Islands
+ when(im.getIsland(world, user)).thenReturn(island);
+
+ // IWM
+ when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock.");
+
+ // Shower
+ when(addon.getBorderShower()).thenReturn(bs);
+
+ // Settings
+ Settings settings = new Settings();
+ when(addon.getSettings()).thenReturn(settings);
+
+ ic = new BorderColorCommand(addon, ac, "color");
+ // Pre-populate the island field (canExecute sets it; execute depends on it)
+ ic.canExecute(user, "", Collections.emptyList());
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#setup()}.
+ */
+ @Test
+ public void testSetup() {
+ assertEquals("border.color", ic.getPermission());
+ assertEquals("border.set-color.description", ic.getDescription());
+ assertTrue(ic.isOnlyPlayer());
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#canExecute(User, String, List)}.
+ */
+ @Test
+ public void testCanExecuteWrongWorld() {
+ when(user.getWorld()).thenReturn(mock(World.class));
+ assertFalse(ic.canExecute(user, "", Collections.emptyList()));
+ verify(user).sendMessage("general.errors.wrong-world");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#canExecute(User, String, List)}.
+ */
+ @Test
+ public void testCanExecuteNoIsland() {
+ when(im.getIsland(world, user)).thenReturn(null);
+ assertFalse(ic.canExecute(user, "", Collections.emptyList()));
+ verify(user, never()).sendMessage("general.errors.wrong-world");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#canExecute(User, String, List)}.
+ */
+ @Test
+ public void testCanExecuteOk() {
+ assertTrue(ic.canExecute(user, "", Collections.emptyList()));
+ verify(user, never()).sendMessage("general.errors.wrong-world");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * No arguments should show help.
+ */
+ @Test
+ public void testExecuteNoArgs() {
+ assertFalse(ic.execute(user, "", Collections.emptyList()));
+ verify(user).sendMessage("commands.help.header", "[label]", "BSkyBlock");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Too many arguments should show help.
+ */
+ @Test
+ public void testExecuteTooManyArgs() {
+ assertFalse(ic.execute(user, "", List.of("red", "extra")));
+ verify(user).sendMessage("commands.help.header", "[label]", "BSkyBlock");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Unknown color should send error message.
+ */
+ @Test
+ public void testExecuteInvalidColor() {
+ assertFalse(ic.execute(user, "", List.of("purple")));
+ verify(user).sendMessage("border.set-color.error-invalid-color");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * No permission for the specific color should deny.
+ */
+ @Test
+ public void testExecuteNoPermission() {
+ when(user.hasPermission("bskyblock.border.color.red")).thenReturn(false);
+ assertFalse(ic.execute(user, "", List.of("red")));
+ verify(user).sendMessage(eq("general.errors.no-permission"), anyString(), anyString());
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Successfully set red.
+ */
+ @Test
+ public void testExecuteSetRed() {
+ when(user.hasPermission("bskyblock.border.color.red")).thenReturn(true);
+ assertTrue(ic.execute(user, "", List.of("red")));
+ verify(user).putMetaData(eq(PerPlayerBorderProxy.BORDER_COLOR_META_DATA), any());
+ verify(bs).hideBorder(user);
+ verify(bs).showBorder(any(Player.class), eq(island));
+ verify(user).sendMessage("border.set-color.changed", "[color]", "red");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Successfully set green.
+ */
+ @Test
+ public void testExecuteSetGreen() {
+ when(user.hasPermission("bskyblock.border.color.green")).thenReturn(true);
+ assertTrue(ic.execute(user, "", List.of("green")));
+ verify(user).putMetaData(eq(PerPlayerBorderProxy.BORDER_COLOR_META_DATA), any());
+ verify(user).sendMessage("border.set-color.changed", "[color]", "green");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Successfully set blue.
+ */
+ @Test
+ public void testExecuteSetBlue() {
+ when(user.hasPermission("bskyblock.border.color.blue")).thenReturn(true);
+ assertTrue(ic.execute(user, "", List.of("blue")));
+ verify(user).putMetaData(eq(PerPlayerBorderProxy.BORDER_COLOR_META_DATA), any());
+ verify(user).sendMessage("border.set-color.changed", "[color]", "blue");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#execute(User, String, List)}.
+ * Color argument should be case-insensitive.
+ */
+ @Test
+ public void testExecuteCaseInsensitive() {
+ when(user.hasPermission("bskyblock.border.color.red")).thenReturn(true);
+ assertTrue(ic.execute(user, "", List.of("RED")));
+ verify(user).sendMessage("border.set-color.changed", "[color]", "red");
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#tabComplete(User, String, List)}.
+ * All colors returned when user has all permissions.
+ */
+ @Test
+ public void testTabCompleteAllPermissions() {
+ when(user.hasPermission(anyString())).thenReturn(true);
+ Optional> result = ic.tabComplete(user, "", Collections.emptyList());
+ assertTrue(result.isPresent());
+ List colors = result.get();
+ assertTrue(colors.contains("red"));
+ assertTrue(colors.contains("green"));
+ assertTrue(colors.contains("blue"));
+ assertEquals(3, colors.size());
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#tabComplete(User, String, List)}.
+ * No colors returned when user has no permissions.
+ */
+ @Test
+ public void testTabCompleteNoPermissions() {
+ when(user.hasPermission(anyString())).thenReturn(false);
+ Optional> result = ic.tabComplete(user, "", Collections.emptyList());
+ assertTrue(result.isPresent());
+ assertTrue(result.get().isEmpty());
+ }
+
+ /**
+ * Test method for {@link BorderColorCommand#tabComplete(User, String, List)}.
+ * Only permitted colors are returned.
+ */
+ @Test
+ public void testTabCompletePartialPermissions() {
+ when(user.hasPermission(anyString())).thenReturn(false);
+ when(user.hasPermission("bskyblock.border.color.red")).thenReturn(true);
+ Optional> result = ic.tabComplete(user, "", Collections.emptyList());
+ assertTrue(result.isPresent());
+ assertEquals(List.of("red"), result.get());
+ }
+}
diff --git a/src/test/java/world/bentobox/border/commands/BorderTypeCommandTest.java b/src/test/java/world/bentobox/border/commands/BorderTypeCommandTest.java
index 8344a6e..e7a7e44 100644
--- a/src/test/java/world/bentobox/border/commands/BorderTypeCommandTest.java
+++ b/src/test/java/world/bentobox/border/commands/BorderTypeCommandTest.java
@@ -58,7 +58,7 @@ public class BorderTypeCommandTest extends CommonTestSetup {
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@BeforeEach
diff --git a/src/test/java/world/bentobox/border/commands/IslandBorderCommandTest.java b/src/test/java/world/bentobox/border/commands/IslandBorderCommandTest.java
index ee3eb13..96d0378 100644
--- a/src/test/java/world/bentobox/border/commands/IslandBorderCommandTest.java
+++ b/src/test/java/world/bentobox/border/commands/IslandBorderCommandTest.java
@@ -60,7 +60,7 @@ public class IslandBorderCommandTest extends CommonTestSetup {
private PlayersManager pm;
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@BeforeEach
@@ -116,7 +116,7 @@ public void setUp() throws Exception {
}
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@AfterEach
@@ -133,8 +133,8 @@ public void testSetup() {
assertEquals("border.toggle.description", ic.getDescription());
assertTrue(ic.isOnlyPlayer());
assertTrue(ic.isConfigurableRankCommand());
- // Help and the type command
- assertEquals(2,ic.getSubCommands().size());
+ // Help, type and color commands
+ assertEquals(3,ic.getSubCommands().size());
}
/**
diff --git a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java
index e505e1f..a6a0e70 100644
--- a/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java
+++ b/src/test/java/world/bentobox/border/listeners/PlayerListenerTest.java
@@ -77,7 +77,7 @@ public class PlayerListenerTest extends CommonTestSetup {
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@BeforeEach
@@ -165,7 +165,7 @@ public void testOnPlayerJoinNoPerms() {
pl.processEvent(event);
verify(user).putMetaData(eq(BorderShower.BORDER_STATE_META_DATA), any());
verify(user).putMetaData(eq(PerPlayerBorderProxy.BORDER_BORDERTYPE_META_DATA), any());
- mockedBukkit.verify(() -> Bukkit.getScheduler());
+ mockedBukkit.verify(Bukkit::getScheduler);
verify(show).hideBorder(user);
verify(player).setWorldBorder(null);
@@ -188,7 +188,7 @@ public void testOnPlayerQuit() {
public void testOnPlayerRespawn() {
PlayerRespawnEvent event = new PlayerRespawnEvent(player, from, false, false, false, RespawnReason.DEATH);
pl.onPlayerRespawn(event);
- mockedBukkit.verify(() -> Bukkit.getScheduler());
+ mockedBukkit.verify(Bukkit::getScheduler);
verify(show).clearUser(user);
}
@@ -201,7 +201,7 @@ public void testOnPlayerTeleportNotInGameWorld() {
PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL);
pl.onPlayerTeleport(event);
verify(show).clearUser(user);
- mockedBukkit.verify(() -> Bukkit.getScheduler(), never());
+ mockedBukkit.verify(Bukkit::getScheduler, never());
}
/**
@@ -213,7 +213,7 @@ public void testOnPlayerTeleportInGameWorld() {
PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, TeleportCause.NETHER_PORTAL);
pl.onPlayerTeleport(event);
verify(show).clearUser(user);
- mockedBukkit.verify(() -> Bukkit.getScheduler());
+ mockedBukkit.verify(Bukkit::getScheduler);
}
/**
diff --git a/src/test/java/world/bentobox/border/listeners/ShowBarrierTest.java b/src/test/java/world/bentobox/border/listeners/ShowBarrierTest.java
index 814284e..8396cd1 100644
--- a/src/test/java/world/bentobox/border/listeners/ShowBarrierTest.java
+++ b/src/test/java/world/bentobox/border/listeners/ShowBarrierTest.java
@@ -50,7 +50,7 @@ public class ShowBarrierTest extends CommonTestSetup {
private MockedStatic mockedUser;
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@BeforeEach
diff --git a/src/test/java/world/bentobox/border/listeners/ShowVirtualWorldBorderTest.java b/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java
similarity index 94%
rename from src/test/java/world/bentobox/border/listeners/ShowVirtualWorldBorderTest.java
rename to src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java
index 57b5d63..44a266e 100644
--- a/src/test/java/world/bentobox/border/listeners/ShowVirtualWorldBorderTest.java
+++ b/src/test/java/world/bentobox/border/listeners/ShowWorldBorderTest.java
@@ -28,7 +28,7 @@
* @author tastybento
*
*/
-public class ShowVirtualWorldBorderTest extends CommonTestSetup {
+public class ShowWorldBorderTest extends CommonTestSetup {
@Mock
private Border addon;
private Settings settings;
@@ -41,7 +41,7 @@ public class ShowVirtualWorldBorderTest extends CommonTestSetup {
/**
- * @throws java.lang.Exception
+ * @throws java.lang.Exception - exception
*/
@Override
@BeforeEach
@@ -64,7 +64,7 @@ public void setUp() throws Exception {
when(mockPlayer.getWorld()).thenReturn(world);
// Bukkit
- mockedBukkit.when(() -> Bukkit.createWorldBorder()).thenReturn(wb);
+ mockedBukkit.when(Bukkit::createWorldBorder).thenReturn(wb);
svwb = new ShowWorldBorder(addon);
}
@@ -75,9 +75,6 @@ public void tearDown() throws Exception {
super.tearDown();
}
- /**
- * Test method for {@link world.bentobox.border.listeners.ShowWorldBorder#ShowVirtualWorldBorder(world.bentobox.border.Border)}.
- */
@Test
public void testShowVirtualWorldBorder() {
assertNotNull(svwb);