From 6f01310f92ec5faa08e8cc00adf176cef299898b Mon Sep 17 00:00:00 2001 From: Huynh Tien Date: Thu, 16 Dec 2021 08:58:41 +0700 Subject: [PATCH 1/5] better safe spot finder --- .../util/teleport/SafeSpotTeleport.java | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index fedad9a52..ae99ba035 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -230,17 +230,48 @@ public class SafeSpotTeleport { * @return true if a safe spot was found */ private boolean scanChunk(ChunkSnapshot chunk) { - // Run through the chunk - for (int x = 0; x< 16; x++) { + int startY = location.getBlockY(); + int minY = location.getWorld().getMinHeight() + 1; + int maxY = maxHeight; + + // Check the safe spot at the current height + for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - // Work down from the entry point up - for (int y = Math.min(chunk.getHighestBlockYAt(x, z), maxHeight); y >= 0; y--) { - if (checkBlock(chunk, x,y,z)) { + if (minY >= startY && startY <= maxY && checkBlock(chunk, x, startY ,z)) { + return true; + } + } + } + + // Expand the height up and down until a safe spot is found + int upperY = startY + 1; + int lowerY = startY - 1; + boolean checkUpper = upperY <= maxY; + boolean checkLower = lowerY >= minY; + while (checkUpper || checkLower) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (checkUpper && upperY <= maxY && checkBlock(chunk, x, upperY, z)) { return true; } - } // end y - } //end z - } // end x + if (checkLower && lowerY >= minY && checkBlock(chunk, x, lowerY, z)) { + return true; + } + } + } + if (upperY > maxY) { + checkUpper = false; + } else { + upperY++; + } + if (lowerY < minY) { + checkLower = false; + } else { + lowerY--; + } + } + + // We can't find a safe spot return false; } From f91ed4705a1efff66c02a0535073518c890756b8 Mon Sep 17 00:00:00 2001 From: Huynh Tien Date: Thu, 16 Dec 2021 17:18:09 +0700 Subject: [PATCH 2/5] this should be better --- .../util/teleport/SafeSpotTeleport.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index ae99ba035..8c041104c 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -1,15 +1,6 @@ package world.bentobox.bentobox.util.teleport; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.bukkit.Bukkit; -import org.bukkit.ChunkSnapshot; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; +import org.bukkit.*; import org.bukkit.World.Environment; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; @@ -17,13 +8,17 @@ import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.Nullable; - import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Util; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + /** * A class that calculates finds a safe spot asynchronously and then teleports the player there. * @author tastybento @@ -231,17 +226,19 @@ public class SafeSpotTeleport { */ private boolean scanChunk(ChunkSnapshot chunk) { int startY = location.getBlockY(); - int minY = location.getWorld().getMinHeight() + 1; - int maxY = maxHeight; + int minY = location.getWorld().getMinHeight(); + int maxY = 60; // Just a dummy value // Check the safe spot at the current height for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - if (minY >= startY && startY <= maxY && checkBlock(chunk, x, startY ,z)) { + if (minY >= startY && checkBlock(chunk, x, startY, z)) { return true; } + maxY = Math.max(chunk.getHighestBlockYAt(x, z), maxY); } } + maxY = Math.min(maxY, maxHeight); // Expand the height up and down until a safe spot is found int upperY = startY + 1; @@ -251,23 +248,25 @@ public class SafeSpotTeleport { while (checkUpper || checkLower) { for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - if (checkUpper && upperY <= maxY && checkBlock(chunk, x, upperY, z)) { + if (checkUpper && checkBlock(chunk, x, upperY, z)) { return true; } - if (checkLower && lowerY >= minY && checkBlock(chunk, x, lowerY, z)) { + if (checkLower && checkBlock(chunk, x, lowerY, z)) { return true; } } } - if (upperY > maxY) { - checkUpper = false; - } else { + if (checkUpper) { upperY++; + if (upperY > maxY) { + checkUpper = false; + } } - if (lowerY < minY) { - checkLower = false; - } else { + if (checkLower) { lowerY--; + if (lowerY < minY) { + checkLower = false; + } } } From 168de622ea17d16ea7d0ed6b864378de1fe9159c Mon Sep 17 00:00:00 2001 From: Huynh Tien Date: Sun, 19 Dec 2021 20:40:01 +0700 Subject: [PATCH 3/5] limit how far the y-coordinate will be expanded --- .../java/world/bentobox/bentobox/Settings.java | 15 +++++++++++++++ .../bentobox/util/teleport/SafeSpotTeleport.java | 4 +++- src/main/resources/config.yml | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 2d6c77c9d..fb238994a 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -298,6 +298,13 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "island.deletion.keep-previous-island-on-reset", since = "1.13.0") private boolean keepPreviousIslandOnReset = false; + @ConfigComment("By default, If the destination is not safe, the plugin will try to search for a safe spot around the destination,") + @ConfigComment("then it will try to expand the y-coordinate up and down from the destination.") + @ConfigComment("This setting limits how far the y-coordinate will be expanded.") + @ConfigComment("If set to 0 or lower, the plugin will not expand the y-coordinate.") + @ConfigEntry(path = "island.safe-spot-search-vertical-range", since = "1.19.1") + private int safeSpotSearchVerticalRange = 400; + /* WEB */ @ConfigComment("Toggle whether BentoBox can connect to GitHub to get data about updates and addons.") @ConfigComment("Disabling this will result in the deactivation of the update checker and of some other") @@ -890,4 +897,12 @@ public class Settings implements ConfigObject { public void setMinPortalSearchRadius(int minPortalSearchRadius) { this.minPortalSearchRadius = minPortalSearchRadius; } + + public int getSafeSpotSearchVerticalRange() { + return safeSpotSearchVerticalRange; + } + + public void setSafeSpotSearchVerticalRange(int safeSpotSearchVerticalRange) { + this.safeSpotSearchVerticalRange = safeSpotSearchVerticalRange; + } } diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index 8c041104c..ed68ea525 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -245,7 +245,8 @@ public class SafeSpotTeleport { int lowerY = startY - 1; boolean checkUpper = upperY <= maxY; boolean checkLower = lowerY >= minY; - while (checkUpper || checkLower) { + int limitRange = plugin.getSettings().getSafeSpotSearchVerticalRange(); // Limit the y-coordinate range + while (limitRange > 0 && (checkUpper || checkLower)) { for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { if (checkUpper && checkBlock(chunk, x, upperY, z)) { @@ -268,6 +269,7 @@ public class SafeSpotTeleport { checkLower = false; } } + limitRange--; } // We can't find a safe spot diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ec4d50f76..f224674c6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -195,6 +195,12 @@ island: # This is the default behaviour. # Added since 1.13.0. keep-previous-island-on-reset: false + # By default, If the destination is not safe, the plugin will try to search for a safe spot around the destination, + # then it will try to expand the y-coordinate up and down from the destination. + # This setting limits how far the y-coordinate will be expanded. + # If set to 0 or lower, the plugin will not expand the y-coordinate. + # Added since 1.19.1. + safe-spot-search-vertical-range: 400 web: github: # Toggle whether BentoBox can connect to GitHub to get data about updates and addons. From 333c9a82729e08eec2f319518466e22d91752bfa Mon Sep 17 00:00:00 2001 From: Huynh Tien Date: Mon, 20 Dec 2021 19:37:50 +0700 Subject: [PATCH 4/5] load chunks passively --- .../util/teleport/SafeSpotTeleport.java | 147 +++++++++--------- 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index ed68ea525..a41dcbfd6 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -18,39 +18,40 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; /** * A class that calculates finds a safe spot asynchronously and then teleports the player there. - * @author tastybento * + * @author tastybento */ public class SafeSpotTeleport { private static final int MAX_CHUNKS = 6; private static final long SPEED = 1; private static final int MAX_RADIUS = 50; - private boolean notChecking; - private BukkitTask task; - // Parameters private final Entity entity; private final Location location; - private boolean portal; private final int homeNumber; - - // Locations - private Location bestSpot; - private final BentoBox plugin; - private List> chunksToScan; private final Runnable runnable; private final Runnable failRunnable; private final CompletableFuture result; private final String homeName; private final int maxHeight; + private final World world; + private final AtomicBoolean checking = new AtomicBoolean(); + private BukkitTask task; + private boolean portal; + // Locations + private Location bestSpot; + private Iterator> chunksToScanIterator; + private int checkedChunks = 0; /** * Teleports and entity to a safe spot on island + * * @param builder - safe spot teleport builder */ SafeSpotTeleport(Builder builder) { @@ -63,9 +64,11 @@ public class SafeSpotTeleport { this.runnable = builder.getRunnable(); this.failRunnable = builder.getFailRunnable(); this.result = builder.getResult(); - this.maxHeight = location.getWorld().getMaxHeight() - 20; + this.world = location.getWorld(); + assert world != null; + this.maxHeight = world.getMaxHeight() - 20; // Try to go - Util.getChunkAtAsync(location).thenRun(()-> tryToGo(builder.getFailureMessage())); + Util.getChunkAtAsync(location).thenRun(() -> tryToGo(builder.getFailureMessage())); } private void tryToGo(String failureMessage) { @@ -83,41 +86,42 @@ public class SafeSpotTeleport { } } // Get chunks to scan - chunksToScan = getChunksToScan(); - - // Start checking - notChecking = true; + chunksToScanIterator = getChunksToScan().iterator(); // Start a recurring task until done or cancelled task = Bukkit.getScheduler().runTaskTimer(plugin, () -> gatherChunks(failureMessage), 0L, SPEED); } private void gatherChunks(String failureMessage) { - if (!notChecking) { + if (checking.get()) { return; } - notChecking = false; - List chunkSnapshot = new ArrayList<>(); - Iterator> it = chunksToScan.iterator(); - if (!it.hasNext()) { + checking.set(true); + if (checkedChunks > MAX_CHUNKS || !chunksToScanIterator.hasNext()) { // Nothing left tidyUp(entity, failureMessage); return; } - // Add chunk snapshots to the list - while (it.hasNext() && chunkSnapshot.size() < MAX_CHUNKS) { - Pair pair = it.next(); - if (location.getWorld() != null) { - boolean isLoaded = location.getWorld().getChunkAt(pair.x, pair.z).isLoaded(); - chunkSnapshot.add(location.getWorld().getChunkAt(pair.x, pair.z).getChunkSnapshot()); - if (!isLoaded) { - location.getWorld().getChunkAt(pair.x, pair.z).unload(); - } - } - it.remove(); + + // Get the chunk + Pair chunkPair = chunksToScanIterator.next(); + chunksToScanIterator.remove(); + checkedChunks++; + if (checkedChunks >= MAX_CHUNKS) { + checking.set(false); + return; } - // Move to next step - checkChunks(chunkSnapshot); + + // Get the chunk snapshot and scan it + Util.getChunkAtAsync(world, chunkPair.x, chunkPair.z) + .thenApply(Chunk::getChunkSnapshot) + .whenCompleteAsync((snapshot, e) -> { + if (snapshot != null && scanChunk(snapshot)) { + task.cancel(); + } else { + checking.set(false); + } + }); } private void tidyUp(Entity entity, String failureMessage) { @@ -128,7 +132,7 @@ public class SafeSpotTeleport { if (portal && bestSpot != null) { // Portals found, teleport to the best spot we found teleportEntity(bestSpot); - } else if (entity instanceof Player) { + } else if (entity instanceof Player player) { // Return to main thread and teleport the player Bukkit.getScheduler().runTask(plugin, () -> { // Failed, no safe spot @@ -137,15 +141,15 @@ public class SafeSpotTeleport { } if (!plugin.getIWM().inWorld(entity.getLocation())) { // Last resort - ((Player)entity).performCommand("spawn"); + player.performCommand("spawn"); } else { // Create a spot for the player to be - if (location.getWorld().getEnvironment().equals(Environment.NETHER)) { - makeAndTelport(Material.NETHERRACK); - } else if (location.getWorld().getEnvironment().equals(Environment.THE_END)) { - makeAndTelport(Material.END_STONE); + if (world.getEnvironment().equals(Environment.NETHER)) { + makeAndTeleport(Material.NETHERRACK); + } else if (world.getEnvironment().equals(Environment.THE_END)) { + makeAndTeleport(Material.END_STONE); } else { - makeAndTelport(Material.COBBLESTONE); + makeAndTeleport(Material.COBBLESTONE); } } if (failRunnable != null) { @@ -161,7 +165,7 @@ public class SafeSpotTeleport { } } - private void makeAndTelport(Material m) { + private void makeAndTeleport(Material m) { location.getBlock().getRelative(BlockFace.DOWN).setType(m, false); location.getBlock().setType(Material.AIR, false); location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false); @@ -174,20 +178,21 @@ public class SafeSpotTeleport { /** * Gets a set of chunk coords that will be scanned. + * * @return - list of chunk coords to be scanned */ private List> getChunksToScan() { List> chunksToScan = new ArrayList<>(); - int maxRadius = plugin.getIslands().getIslandAt(location).map(Island::getProtectionRange).orElseGet(() -> plugin.getIWM().getIslandProtectionRange(location.getWorld())); + int maxRadius = plugin.getIslands().getIslandAt(location).map(Island::getProtectionRange).orElseGet(() -> plugin.getIWM().getIslandProtectionRange(world)); maxRadius = Math.min(MAX_RADIUS, maxRadius); int x = location.getBlockX(); int z = location.getBlockZ(); // Create ever increasing squares around the target location int radius = 0; do { - for (int i = x - radius; i <= x + radius; i+=16) { - for (int j = z - radius; j <= z + radius; j+=16) { - addChunk(chunksToScan, new Pair<>(i,j), new Pair<>(i >> 4, j >> 4)); + for (int i = x - radius; i <= x + radius; i += 16) { + for (int j = z - radius; j <= z + radius; j += 16) { + addChunk(chunksToScan, new Pair<>(i, j), new Pair<>(i >> 4, j >> 4)); } } radius++; @@ -201,32 +206,13 @@ public class SafeSpotTeleport { } } - /** - * Loops through the chunks and if a safe spot is found, fires off the teleportation - * @param chunkSnapshot - list of chunk snapshots to check - */ - private void checkChunks(final List chunkSnapshot) { - // Run async task to scan chunks - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - for (ChunkSnapshot chunk: chunkSnapshot) { - if (scanChunk(chunk)) { - task.cancel(); - return; - } - } - // Nothing happened, change state - notChecking = true; - }); - } - - /** * @param chunk - chunk snapshot * @return true if a safe spot was found */ private boolean scanChunk(ChunkSnapshot chunk) { int startY = location.getBlockY(); - int minY = location.getWorld().getMinHeight(); + int minY = world.getMinHeight(); int maxY = 60; // Just a dummy value // Check the safe spot at the current height @@ -296,14 +282,14 @@ public class SafeSpotTeleport { /** * Returns true if the location is a safe one. + * * @param chunk - chunk snapshot - * @param x - x coordinate - * @param y - y coordinate - * @param z - z coordinate + * @param x - x coordinate + * @param y - y coordinate + * @param z - z coordinate * @return true if this is a safe spot, false if this is a portal scan */ boolean checkBlock(ChunkSnapshot chunk, int x, int y, int z) { - World world = location.getWorld(); Material type = chunk.getBlockType(x, y, z); Material space1 = chunk.getBlockType(x, Math.min(y + 1, maxHeight), z); Material space2 = chunk.getBlockType(x, Math.min(y + 2, maxHeight), z); @@ -333,6 +319,7 @@ public class SafeSpotTeleport { public static class Builder { private final BentoBox plugin; + private final CompletableFuture result = new CompletableFuture<>(); private Entity entity; private int homeNumber = 0; private String homeName = ""; @@ -341,7 +328,6 @@ public class SafeSpotTeleport { private Location location; private Runnable runnable; private Runnable failRunnable; - private final CompletableFuture result = new CompletableFuture<>(); public Builder(BentoBox plugin) { this.plugin = plugin; @@ -349,6 +335,7 @@ public class SafeSpotTeleport { /** * Set who or what is going to teleport + * * @param entity entity to teleport * @return Builder */ @@ -359,6 +346,7 @@ public class SafeSpotTeleport { /** * Set the island to teleport to + * * @param island island destination * @return Builder */ @@ -369,6 +357,7 @@ public class SafeSpotTeleport { /** * Set the home number to this number + * * @param homeNumber home number * @return Builder * @deprecated use {@link #homeName} @@ -381,6 +370,7 @@ public class SafeSpotTeleport { /** * Set the home name + * * @param homeName - home name * @return Builder * @since 1.16.0 @@ -392,6 +382,7 @@ public class SafeSpotTeleport { /** * This is a portal teleportation + * * @return Builder */ public Builder portal() { @@ -401,6 +392,7 @@ public class SafeSpotTeleport { /** * Set the failure message if this teleport cannot happen + * * @param failureMessage failure message to report to user * @return Builder */ @@ -411,6 +403,7 @@ public class SafeSpotTeleport { /** * Set the desired location + * * @param location the location * @return Builder */ @@ -421,6 +414,7 @@ public class SafeSpotTeleport { /** * Try to teleport the player + * * @return CompletableFuture that will become true if successful and false if not * @since 1.14.0 */ @@ -432,6 +426,7 @@ public class SafeSpotTeleport { /** * Try to teleport the player + * * @return SafeSpotTeleport */ @Nullable @@ -447,6 +442,11 @@ public class SafeSpotTeleport { result.complete(null); return null; } + if (location.getWorld() == null) { + plugin.logError("Attempt to safe teleport to a null world!"); + result.complete(null); + return null; + } if (failureMessage.isEmpty() && entity instanceof Player) { failureMessage = "general.errors.no-safe-location-found"; } @@ -455,6 +455,7 @@ public class SafeSpotTeleport { /** * The task to run after the player is safely teleported. + * * @param runnable - task * @return Builder * @since 1.13.0 @@ -466,14 +467,16 @@ public class SafeSpotTeleport { /** * The task to run if the player is not safely teleported + * * @param runnable - task * @return Builder * @since 1.18.0 */ - public Builder ifFail(Runnable rannable) { + public Builder ifFail(Runnable runnable) { this.failRunnable = runnable; return this; } + /** * @return the plugin */ From f049fc6941ec10925a246ecaca014c3ff02b09b5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 20 Dec 2021 09:46:04 -0800 Subject: [PATCH 5/5] Added SafeSpotTepeortTest --- .../util/teleport/SafeSpotTeleport.java | 40 +-- .../util/teleport/SafeSpotTeleportTest.java | 281 ++++++++++++++++++ 2 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 src/test/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleportTest.java diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index a41dcbfd6..3963b8b4c 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -71,7 +71,7 @@ public class SafeSpotTeleport { Util.getChunkAtAsync(location).thenRun(() -> tryToGo(builder.getFailureMessage())); } - private void tryToGo(String failureMessage) { + void tryToGo(String failureMessage) { if (plugin.getIslands().isSafeLocation(location)) { if (portal) { // If the desired location is safe, then that's where you'll go if there's no portal @@ -92,15 +92,16 @@ public class SafeSpotTeleport { task = Bukkit.getScheduler().runTaskTimer(plugin, () -> gatherChunks(failureMessage), 0L, SPEED); } - private void gatherChunks(String failureMessage) { + boolean gatherChunks(String failureMessage) { + // Set a flag so this is only run if it's not already in progress if (checking.get()) { - return; + return false; } checking.set(true); if (checkedChunks > MAX_CHUNKS || !chunksToScanIterator.hasNext()) { // Nothing left tidyUp(entity, failureMessage); - return; + return false; } // Get the chunk @@ -109,22 +110,23 @@ public class SafeSpotTeleport { checkedChunks++; if (checkedChunks >= MAX_CHUNKS) { checking.set(false); - return; + return false; } // Get the chunk snapshot and scan it Util.getChunkAtAsync(world, chunkPair.x, chunkPair.z) - .thenApply(Chunk::getChunkSnapshot) - .whenCompleteAsync((snapshot, e) -> { - if (snapshot != null && scanChunk(snapshot)) { - task.cancel(); - } else { - checking.set(false); - } - }); + .thenApply(Chunk::getChunkSnapshot) + .whenCompleteAsync((snapshot, e) -> { + if (snapshot != null && scanChunk(snapshot)) { + task.cancel(); + } else { + checking.set(false); + } + }); + return true; } - private void tidyUp(Entity entity, String failureMessage) { + void tidyUp(Entity entity, String failureMessage) { // Still Async! // Nothing left to check and still not canceled task.cancel(); @@ -165,7 +167,7 @@ public class SafeSpotTeleport { } } - private void makeAndTeleport(Material m) { + void makeAndTeleport(Material m) { location.getBlock().getRelative(BlockFace.DOWN).setType(m, false); location.getBlock().setType(Material.AIR, false); location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false); @@ -181,7 +183,7 @@ public class SafeSpotTeleport { * * @return - list of chunk coords to be scanned */ - private List> getChunksToScan() { + List> getChunksToScan() { List> chunksToScan = new ArrayList<>(); int maxRadius = plugin.getIslands().getIslandAt(location).map(Island::getProtectionRange).orElseGet(() -> plugin.getIWM().getIslandProtectionRange(world)); maxRadius = Math.min(MAX_RADIUS, maxRadius); @@ -210,7 +212,7 @@ public class SafeSpotTeleport { * @param chunk - chunk snapshot * @return true if a safe spot was found */ - private boolean scanChunk(ChunkSnapshot chunk) { + boolean scanChunk(ChunkSnapshot chunk) { int startY = location.getBlockY(); int minY = world.getMinHeight(); int maxY = 60; // Just a dummy value @@ -265,7 +267,7 @@ public class SafeSpotTeleport { /** * Teleports entity to the safe spot */ - private void teleportEntity(final Location loc) { + void teleportEntity(final Location loc) { task.cancel(); // Return to main thread and teleport the player Bukkit.getScheduler().runTask(plugin, () -> { @@ -303,7 +305,7 @@ public class SafeSpotTeleport { return false; } - private boolean safe(ChunkSnapshot chunk, int x, int y, int z, World world) { + boolean safe(ChunkSnapshot chunk, int x, int y, int z, World world) { Vector newSpot = new Vector((chunk.getX() << 4) + x + 0.5D, y + 1.0D, (chunk.getZ() << 4) + z + 0.5D); if (portal) { if (bestSpot == null) { diff --git a/src/test/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleportTest.java b/src/test/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleportTest.java new file mode 100644 index 000000000..412dab93d --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleportTest.java @@ -0,0 +1,281 @@ +package world.bentobox.bentobox.util.teleport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.eclipse.jdt.annotation.NonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.util.Pair; +import world.bentobox.bentobox.util.Util; + +/** + * Test class for safe teleporting + * @author tastybento + * + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({Util.class, Bukkit.class}) +public class SafeSpotTeleportTest { + + // Class under test + private SafeSpotTeleport sst; + + @Mock + private SafeSpotTeleport.Builder builder; + @Mock + private BentoBox plugin; + @Mock + private Location location; + @Mock + private World world; + @Mock + private Entity entity; + + private boolean portal; + + private int num; + + private String name; + @Mock + private Runnable runnable; + @Mock + private Runnable failRunnable; + @Mock + private CompletableFuture result; + @Mock + private @NonNull CompletableFuture cfChunk; + @Mock + private IslandsManager im; + @Mock + private BukkitScheduler scheduler; + + private Island island; + @Mock + private IslandWorldManager iwm; + + @Mock + private BukkitTask task; + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + // Setup instance + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + // IWM + when(iwm.getIslandProtectionRange(any())).thenReturn(100); + when(iwm.getIslandDistance(any())).thenReturn(400); + when(plugin.getIWM()).thenReturn(iwm); + + // Mock static Util + PowerMockito.mockStatic(Util.class, Mockito.RETURNS_MOCKS); + when(Util.getChunkAtAsync(any(Location.class))).thenReturn(cfChunk); + // Same world + when(Util.sameWorld(any(), any())).thenReturn(true); + // Set up a mock builder + when(builder.getPlugin()).thenReturn(plugin); + when(builder.getEntity()).thenReturn(entity); + when(builder.getLocation()).thenReturn(location); + when(builder.isPortal()).thenReturn(portal); + when(builder.getHomeNumber()).thenReturn(num); + when(builder.getHomeName()).thenReturn(name); + when(builder.getRunnable()).thenReturn(runnable); + when(builder.getFailRunnable()).thenReturn(failRunnable); + when(builder.getResult()).thenReturn(result); + // Set the default world + when(location.getWorld()).thenReturn(world); + + // Island + island = new Island(location, UUID.randomUUID(), 50); + + // Plugin Island Manager + // Default that locations are safe + when(im.isSafeLocation(any(Location.class))).thenReturn(true); + // Provide an island + when(im.getIslandAt(any(Location.class))).thenReturn(Optional.of(island)); + + + when(plugin.getIslands()).thenReturn(im); + + // Bukkit scheduler + when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getScheduler()).thenReturn(scheduler); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#SafeSpotTeleport(world.bentobox.bentobox.util.teleport.SafeSpotTeleport.Builder)}. + */ + @Test(expected = AssertionError.class) + public void testSafeSpotTeleportNullWorld() { + when(location.getWorld()).thenReturn(null); + sst = new SafeSpotTeleport(builder); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#SafeSpotTeleport(world.bentobox.bentobox.util.teleport.SafeSpotTeleport.Builder)}. + */ + @Test + public void testSafeSpotTeleport() { + sst = new SafeSpotTeleport(builder); + verify(cfChunk).thenRun(any(Runnable.class)); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#tryToGo(java.lang.String)}. + */ + @Test + public void testTryToGoSafeNotPortal() { + portal = false; + testSafeSpotTeleport(); + sst.tryToGo("failure message"); + PowerMockito.verifyStatic(Util.class); + // Verify that the teleport is done immediately + Util.teleportAsync(entity, location); + + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#tryToGo(java.lang.String)}. + */ + @Test + public void testTryToGoUnsafe() { + when(im.isSafeLocation(any(Location.class))).thenReturn(false); + // Set up fields + testSafeSpotTeleport(); + sst.tryToGo("failure message"); + verify(scheduler).runTaskTimer(eq(plugin), any(Runnable.class), eq(0L), eq(1L)); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#gatherChunks(java.lang.String)}. + */ + @Test + public void testGatherChunks() { + // Setup fields + testTryToGoUnsafe(); + // run test + assertTrue(sst.gatherChunks("failure message")); + PowerMockito.verifyStatic(Util.class); + Util.getChunkAtAsync(eq(world), anyInt(), anyInt()); + // run test again - should be blocked because of atomic boolean + assertFalse(sst.gatherChunks("failure message")); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#tidyUp(org.bukkit.entity.Entity, java.lang.String)}. + */ + @Test + public void testTidyUpNoPlayerFailRunnable() { + when(im.isSafeLocation(any(Location.class))).thenReturn(false); + sst = new SafeSpotTeleport(builder); + sst.tryToGo("failure message"); + sst.tidyUp(entity, "failure note"); + verify(task).cancel(); + verify(scheduler).runTask(plugin, failRunnable); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#tidyUp(org.bukkit.entity.Entity, java.lang.String)}. + */ + @Test + public void testTidyUpPlayer() { + when(im.isSafeLocation(any(Location.class))).thenReturn(false); + sst = new SafeSpotTeleport(builder); + sst.tryToGo("failure message"); + sst.tidyUp(entity, "failure note"); + verify(task).cancel(); + verify(scheduler).runTask(plugin, failRunnable); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#makeAndTeleport(org.bukkit.Material)}. + */ + @Test + public void testMakeAndTeleport() { + //fail("Not yet implemented"); // TODO + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#getChunksToScan()}. + */ + @Test + public void testGetChunksToScan() { + testSafeSpotTeleport(); + List> pairs = sst.getChunksToScan(); + assertEquals(62, pairs.size()); + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#scanChunk(org.bukkit.ChunkSnapshot)}. + */ + @Test + public void testScanChunk() { + //fail("Not yet implemented"); // TODO + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#teleportEntity(org.bukkit.Location)}. + */ + @Test + public void testTeleportEntity() { + //fail("Not yet implemented"); // TODO + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#checkBlock(org.bukkit.ChunkSnapshot, int, int, int)}. + */ + @Test + public void testCheckBlock() { + //fail("Not yet implemented"); // TODO + } + + /** + * Test method for {@link world.bentobox.bentobox.util.teleport.SafeSpotTeleport#safe(org.bukkit.ChunkSnapshot, int, int, int, org.bukkit.World)}. + */ + @Test + public void testSafe() { + //fail("Not yet implemented"); // TODO + } + +}