From e3b99195cadeee9db7f5bb41333584a9ef475fb4 Mon Sep 17 00:00:00 2001 From: Huynh Tien Date: Tue, 21 Dec 2021 00:44:34 +0700 Subject: [PATCH] Optimize safe location searching with configurable range limit (#1892) * better safe spot finder * this should be better * limit how far the y-coordinate will be expanded * load chunks passively --- .../world/bentobox/bentobox/Settings.java | 15 ++ .../util/teleport/SafeSpotTeleport.java | 215 ++++++++++-------- src/main/resources/config.yml | 6 + 3 files changed, 146 insertions(+), 90 deletions(-) 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 fedad9a52..a41dcbfd6 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,45 +8,50 @@ 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; +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) { @@ -68,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) { @@ -88,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) { @@ -133,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 @@ -142,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) { @@ -166,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); @@ -179,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++; @@ -206,41 +206,59 @@ 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) { - // Run through the chunk - for (int x = 0; x< 16; x++) { + int startY = location.getBlockY(); + int minY = world.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++) { - // 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 && 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; + int lowerY = startY - 1; + boolean checkUpper = upperY <= maxY; + boolean checkLower = lowerY >= minY; + 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)) { return true; } - } // end y - } //end z - } // end x + if (checkLower && checkBlock(chunk, x, lowerY, z)) { + return true; + } + } + } + if (checkUpper) { + upperY++; + if (upperY > maxY) { + checkUpper = false; + } + } + if (checkLower) { + lowerY--; + if (lowerY < minY) { + checkLower = false; + } + } + limitRange--; + } + + // We can't find a safe spot return false; } @@ -264,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); @@ -301,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 = ""; @@ -309,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; @@ -317,6 +335,7 @@ public class SafeSpotTeleport { /** * Set who or what is going to teleport + * * @param entity entity to teleport * @return Builder */ @@ -327,6 +346,7 @@ public class SafeSpotTeleport { /** * Set the island to teleport to + * * @param island island destination * @return Builder */ @@ -337,6 +357,7 @@ public class SafeSpotTeleport { /** * Set the home number to this number + * * @param homeNumber home number * @return Builder * @deprecated use {@link #homeName} @@ -349,6 +370,7 @@ public class SafeSpotTeleport { /** * Set the home name + * * @param homeName - home name * @return Builder * @since 1.16.0 @@ -360,6 +382,7 @@ public class SafeSpotTeleport { /** * This is a portal teleportation + * * @return Builder */ public Builder portal() { @@ -369,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 */ @@ -379,6 +403,7 @@ public class SafeSpotTeleport { /** * Set the desired location + * * @param location the location * @return Builder */ @@ -389,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 */ @@ -400,6 +426,7 @@ public class SafeSpotTeleport { /** * Try to teleport the player + * * @return SafeSpotTeleport */ @Nullable @@ -415,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"; } @@ -423,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 @@ -434,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 */ 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.