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
This commit is contained in:
Huynh Tien 2021-12-21 00:44:34 +07:00 committed by GitHub
parent ed50765b98
commit e3b99195ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 90 deletions

View File

@ -298,6 +298,13 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "island.deletion.keep-previous-island-on-reset", since = "1.13.0") @ConfigEntry(path = "island.deletion.keep-previous-island-on-reset", since = "1.13.0")
private boolean keepPreviousIslandOnReset = false; 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 */ /* WEB */
@ConfigComment("Toggle whether BentoBox can connect to GitHub to get data about updates and addons.") @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") @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) { public void setMinPortalSearchRadius(int minPortalSearchRadius) {
this.minPortalSearchRadius = minPortalSearchRadius; this.minPortalSearchRadius = minPortalSearchRadius;
} }
public int getSafeSpotSearchVerticalRange() {
return safeSpotSearchVerticalRange;
}
public void setSafeSpotSearchVerticalRange(int safeSpotSearchVerticalRange) {
this.safeSpotSearchVerticalRange = safeSpotSearchVerticalRange;
}
} }

View File

@ -1,15 +1,6 @@
package world.bentobox.bentobox.util.teleport; package world.bentobox.bentobox.util.teleport;
import java.util.ArrayList; import org.bukkit.*;
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.World.Environment; import org.bukkit.World.Environment;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@ -17,45 +8,50 @@ import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Pair;
import world.bentobox.bentobox.util.Util; 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. * A class that calculates finds a safe spot asynchronously and then teleports the player there.
* @author tastybento
* *
* @author tastybento
*/ */
public class SafeSpotTeleport { public class SafeSpotTeleport {
private static final int MAX_CHUNKS = 6; private static final int MAX_CHUNKS = 6;
private static final long SPEED = 1; private static final long SPEED = 1;
private static final int MAX_RADIUS = 50; private static final int MAX_RADIUS = 50;
private boolean notChecking;
private BukkitTask task;
// Parameters // Parameters
private final Entity entity; private final Entity entity;
private final Location location; private final Location location;
private boolean portal;
private final int homeNumber; private final int homeNumber;
// Locations
private Location bestSpot;
private final BentoBox plugin; private final BentoBox plugin;
private List<Pair<Integer, Integer>> chunksToScan;
private final Runnable runnable; private final Runnable runnable;
private final Runnable failRunnable; private final Runnable failRunnable;
private final CompletableFuture<Boolean> result; private final CompletableFuture<Boolean> result;
private final String homeName; private final String homeName;
private final int maxHeight; 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<Pair<Integer, Integer>> chunksToScanIterator;
private int checkedChunks = 0;
/** /**
* Teleports and entity to a safe spot on island * Teleports and entity to a safe spot on island
*
* @param builder - safe spot teleport builder * @param builder - safe spot teleport builder
*/ */
SafeSpotTeleport(Builder builder) { SafeSpotTeleport(Builder builder) {
@ -68,9 +64,11 @@ public class SafeSpotTeleport {
this.runnable = builder.getRunnable(); this.runnable = builder.getRunnable();
this.failRunnable = builder.getFailRunnable(); this.failRunnable = builder.getFailRunnable();
this.result = builder.getResult(); 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 // Try to go
Util.getChunkAtAsync(location).thenRun(()-> tryToGo(builder.getFailureMessage())); Util.getChunkAtAsync(location).thenRun(() -> tryToGo(builder.getFailureMessage()));
} }
private void tryToGo(String failureMessage) { private void tryToGo(String failureMessage) {
@ -88,41 +86,42 @@ public class SafeSpotTeleport {
} }
} }
// Get chunks to scan // Get chunks to scan
chunksToScan = getChunksToScan(); chunksToScanIterator = getChunksToScan().iterator();
// Start checking
notChecking = true;
// Start a recurring task until done or cancelled // Start a recurring task until done or cancelled
task = Bukkit.getScheduler().runTaskTimer(plugin, () -> gatherChunks(failureMessage), 0L, SPEED); task = Bukkit.getScheduler().runTaskTimer(plugin, () -> gatherChunks(failureMessage), 0L, SPEED);
} }
private void gatherChunks(String failureMessage) { private void gatherChunks(String failureMessage) {
if (!notChecking) { if (checking.get()) {
return; return;
} }
notChecking = false; checking.set(true);
List<ChunkSnapshot> chunkSnapshot = new ArrayList<>(); if (checkedChunks > MAX_CHUNKS || !chunksToScanIterator.hasNext()) {
Iterator<Pair<Integer, Integer>> it = chunksToScan.iterator();
if (!it.hasNext()) {
// Nothing left // Nothing left
tidyUp(entity, failureMessage); tidyUp(entity, failureMessage);
return; return;
} }
// Add chunk snapshots to the list
while (it.hasNext() && chunkSnapshot.size() < MAX_CHUNKS) { // Get the chunk
Pair<Integer, Integer> pair = it.next(); Pair<Integer, Integer> chunkPair = chunksToScanIterator.next();
if (location.getWorld() != null) { chunksToScanIterator.remove();
boolean isLoaded = location.getWorld().getChunkAt(pair.x, pair.z).isLoaded(); checkedChunks++;
chunkSnapshot.add(location.getWorld().getChunkAt(pair.x, pair.z).getChunkSnapshot()); if (checkedChunks >= MAX_CHUNKS) {
if (!isLoaded) { checking.set(false);
location.getWorld().getChunkAt(pair.x, pair.z).unload(); return;
}
}
it.remove();
} }
// 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) { private void tidyUp(Entity entity, String failureMessage) {
@ -133,7 +132,7 @@ public class SafeSpotTeleport {
if (portal && bestSpot != null) { if (portal && bestSpot != null) {
// Portals found, teleport to the best spot we found // Portals found, teleport to the best spot we found
teleportEntity(bestSpot); teleportEntity(bestSpot);
} else if (entity instanceof Player) { } else if (entity instanceof Player player) {
// Return to main thread and teleport the player // Return to main thread and teleport the player
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
// Failed, no safe spot // Failed, no safe spot
@ -142,15 +141,15 @@ public class SafeSpotTeleport {
} }
if (!plugin.getIWM().inWorld(entity.getLocation())) { if (!plugin.getIWM().inWorld(entity.getLocation())) {
// Last resort // Last resort
((Player)entity).performCommand("spawn"); player.performCommand("spawn");
} else { } else {
// Create a spot for the player to be // Create a spot for the player to be
if (location.getWorld().getEnvironment().equals(Environment.NETHER)) { if (world.getEnvironment().equals(Environment.NETHER)) {
makeAndTelport(Material.NETHERRACK); makeAndTeleport(Material.NETHERRACK);
} else if (location.getWorld().getEnvironment().equals(Environment.THE_END)) { } else if (world.getEnvironment().equals(Environment.THE_END)) {
makeAndTelport(Material.END_STONE); makeAndTeleport(Material.END_STONE);
} else { } else {
makeAndTelport(Material.COBBLESTONE); makeAndTeleport(Material.COBBLESTONE);
} }
} }
if (failRunnable != null) { 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().getRelative(BlockFace.DOWN).setType(m, false);
location.getBlock().setType(Material.AIR, false); location.getBlock().setType(Material.AIR, false);
location.getBlock().getRelative(BlockFace.UP).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. * Gets a set of chunk coords that will be scanned.
*
* @return - list of chunk coords to be scanned * @return - list of chunk coords to be scanned
*/ */
private List<Pair<Integer, Integer>> getChunksToScan() { private List<Pair<Integer, Integer>> getChunksToScan() {
List<Pair<Integer, Integer>> chunksToScan = new ArrayList<>(); List<Pair<Integer, Integer>> 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); maxRadius = Math.min(MAX_RADIUS, maxRadius);
int x = location.getBlockX(); int x = location.getBlockX();
int z = location.getBlockZ(); int z = location.getBlockZ();
// Create ever increasing squares around the target location // Create ever increasing squares around the target location
int radius = 0; int radius = 0;
do { do {
for (int i = x - radius; i <= x + radius; i+=16) { for (int i = x - radius; i <= x + radius; i += 16) {
for (int j = z - radius; j <= z + radius; j+=16) { for (int j = z - radius; j <= z + radius; j += 16) {
addChunk(chunksToScan, new Pair<>(i,j), new Pair<>(i >> 4, j >> 4)); addChunk(chunksToScan, new Pair<>(i, j), new Pair<>(i >> 4, j >> 4));
} }
} }
radius++; 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> 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 * @param chunk - chunk snapshot
* @return true if a safe spot was found * @return true if a safe spot was found
*/ */
private boolean scanChunk(ChunkSnapshot chunk) { private boolean scanChunk(ChunkSnapshot chunk) {
// Run through the chunk int startY = location.getBlockY();
for (int x = 0; x< 16; x++) { 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++) { for (int z = 0; z < 16; z++) {
// Work down from the entry point up if (minY >= startY && checkBlock(chunk, x, startY, z)) {
for (int y = Math.min(chunk.getHighestBlockYAt(x, z), maxHeight); y >= 0; y--) { return true;
if (checkBlock(chunk, x,y,z)) { }
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; return true;
} }
} // end y if (checkLower && checkBlock(chunk, x, lowerY, z)) {
} //end z return true;
} // end x }
}
}
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; return false;
} }
@ -264,14 +282,14 @@ public class SafeSpotTeleport {
/** /**
* Returns true if the location is a safe one. * Returns true if the location is a safe one.
*
* @param chunk - chunk snapshot * @param chunk - chunk snapshot
* @param x - x coordinate * @param x - x coordinate
* @param y - y coordinate * @param y - y coordinate
* @param z - z coordinate * @param z - z coordinate
* @return true if this is a safe spot, false if this is a portal scan * @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) { boolean checkBlock(ChunkSnapshot chunk, int x, int y, int z) {
World world = location.getWorld();
Material type = chunk.getBlockType(x, y, z); Material type = chunk.getBlockType(x, y, z);
Material space1 = chunk.getBlockType(x, Math.min(y + 1, maxHeight), z); Material space1 = chunk.getBlockType(x, Math.min(y + 1, maxHeight), z);
Material space2 = chunk.getBlockType(x, Math.min(y + 2, maxHeight), z); Material space2 = chunk.getBlockType(x, Math.min(y + 2, maxHeight), z);
@ -301,6 +319,7 @@ public class SafeSpotTeleport {
public static class Builder { public static class Builder {
private final BentoBox plugin; private final BentoBox plugin;
private final CompletableFuture<Boolean> result = new CompletableFuture<>();
private Entity entity; private Entity entity;
private int homeNumber = 0; private int homeNumber = 0;
private String homeName = ""; private String homeName = "";
@ -309,7 +328,6 @@ public class SafeSpotTeleport {
private Location location; private Location location;
private Runnable runnable; private Runnable runnable;
private Runnable failRunnable; private Runnable failRunnable;
private final CompletableFuture<Boolean> result = new CompletableFuture<>();
public Builder(BentoBox plugin) { public Builder(BentoBox plugin) {
this.plugin = plugin; this.plugin = plugin;
@ -317,6 +335,7 @@ public class SafeSpotTeleport {
/** /**
* Set who or what is going to teleport * Set who or what is going to teleport
*
* @param entity entity to teleport * @param entity entity to teleport
* @return Builder * @return Builder
*/ */
@ -327,6 +346,7 @@ public class SafeSpotTeleport {
/** /**
* Set the island to teleport to * Set the island to teleport to
*
* @param island island destination * @param island island destination
* @return Builder * @return Builder
*/ */
@ -337,6 +357,7 @@ public class SafeSpotTeleport {
/** /**
* Set the home number to this number * Set the home number to this number
*
* @param homeNumber home number * @param homeNumber home number
* @return Builder * @return Builder
* @deprecated use {@link #homeName} * @deprecated use {@link #homeName}
@ -349,6 +370,7 @@ public class SafeSpotTeleport {
/** /**
* Set the home name * Set the home name
*
* @param homeName - home name * @param homeName - home name
* @return Builder * @return Builder
* @since 1.16.0 * @since 1.16.0
@ -360,6 +382,7 @@ public class SafeSpotTeleport {
/** /**
* This is a portal teleportation * This is a portal teleportation
*
* @return Builder * @return Builder
*/ */
public Builder portal() { public Builder portal() {
@ -369,6 +392,7 @@ public class SafeSpotTeleport {
/** /**
* Set the failure message if this teleport cannot happen * Set the failure message if this teleport cannot happen
*
* @param failureMessage failure message to report to user * @param failureMessage failure message to report to user
* @return Builder * @return Builder
*/ */
@ -379,6 +403,7 @@ public class SafeSpotTeleport {
/** /**
* Set the desired location * Set the desired location
*
* @param location the location * @param location the location
* @return Builder * @return Builder
*/ */
@ -389,6 +414,7 @@ public class SafeSpotTeleport {
/** /**
* Try to teleport the player * Try to teleport the player
*
* @return CompletableFuture that will become true if successful and false if not * @return CompletableFuture that will become true if successful and false if not
* @since 1.14.0 * @since 1.14.0
*/ */
@ -400,6 +426,7 @@ public class SafeSpotTeleport {
/** /**
* Try to teleport the player * Try to teleport the player
*
* @return SafeSpotTeleport * @return SafeSpotTeleport
*/ */
@Nullable @Nullable
@ -415,6 +442,11 @@ public class SafeSpotTeleport {
result.complete(null); result.complete(null);
return 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) { if (failureMessage.isEmpty() && entity instanceof Player) {
failureMessage = "general.errors.no-safe-location-found"; failureMessage = "general.errors.no-safe-location-found";
} }
@ -423,6 +455,7 @@ public class SafeSpotTeleport {
/** /**
* The task to run after the player is safely teleported. * The task to run after the player is safely teleported.
*
* @param runnable - task * @param runnable - task
* @return Builder * @return Builder
* @since 1.13.0 * @since 1.13.0
@ -434,14 +467,16 @@ public class SafeSpotTeleport {
/** /**
* The task to run if the player is not safely teleported * The task to run if the player is not safely teleported
*
* @param runnable - task * @param runnable - task
* @return Builder * @return Builder
* @since 1.18.0 * @since 1.18.0
*/ */
public Builder ifFail(Runnable rannable) { public Builder ifFail(Runnable runnable) {
this.failRunnable = runnable; this.failRunnable = runnable;
return this; return this;
} }
/** /**
* @return the plugin * @return the plugin
*/ */

View File

@ -195,6 +195,12 @@ island:
# This is the default behaviour. # This is the default behaviour.
# Added since 1.13.0. # Added since 1.13.0.
keep-previous-island-on-reset: false 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: web:
github: github:
# Toggle whether BentoBox can connect to GitHub to get data about updates and addons. # Toggle whether BentoBox can connect to GitHub to get data about updates and addons.