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);