diff --git a/pom.xml b/pom.xml index e5c02fa..95e9dae 100644 --- a/pom.xml +++ b/pom.xml @@ -31,16 +31,22 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.3.0 + + ${project.build.directory}/dependency-reduced-pom.xml + + + io.papermc.lib + dev.michaud.greenpanda.randomspawnpoint.paperlib + + + package shade - - false - @@ -58,6 +64,10 @@ papermc-repo https://repo.papermc.io/repository/maven-public/ + + papermc + https://papermc.io/repo/repository/maven-public/ + sonatype https://oss.sonatype.org/content/groups/public/ @@ -71,5 +81,11 @@ 1.19-R0.1-SNAPSHOT provided + + io.papermc + paperlib + 1.0.7 + compile + - + \ No newline at end of file diff --git a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/RandomSpawnpoint.java b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/RandomSpawnpoint.java index 16ea61b..a463d9d 100644 --- a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/RandomSpawnpoint.java +++ b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/RandomSpawnpoint.java @@ -1,9 +1,12 @@ package dev.michaud.greenpanda.randomspawnpoint; +import dev.michaud.greenpanda.randomspawnpoint.async.LocationCache; +import dev.michaud.greenpanda.randomspawnpoint.commands.AsyncTest; import dev.michaud.greenpanda.randomspawnpoint.commands.SetRandomSpawnpoint; import dev.michaud.greenpanda.randomspawnpoint.events.PlayerJoin; import dev.michaud.greenpanda.randomspawnpoint.events.PlayerRespawn; import dev.michaud.greenpanda.randomspawnpoint.util.ServerProperties; +import io.papermc.lib.PaperLib; import java.util.HashSet; import org.bukkit.Material; import org.bukkit.World; @@ -42,9 +45,14 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new PlayerJoin(), this); getCommand("setrandomspawnpoint").setExecutor(new SetRandomSpawnpoint()); + getCommand("asynctest").setExecutor(new AsyncTest()); loadConfig(); + PaperLib.suggestPaper(plugin); + + LocationCache.generateLocations(10); + } private void loadConfig() { diff --git a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/GenerateLocationAsync.java b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/GenerateLocationAsync.java new file mode 100644 index 0000000..578bb71 --- /dev/null +++ b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/GenerateLocationAsync.java @@ -0,0 +1,97 @@ +package dev.michaud.greenpanda.randomspawnpoint.async; + +import dev.michaud.greenpanda.randomspawnpoint.RandomSpawnpoint; +import io.papermc.lib.PaperLib; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class GenerateLocationAsync { + + private static void log(String msg) { + String prefix = "[Thread " + Thread.currentThread().getId() + "] "; + RandomSpawnpoint.getPlugin().getServer().getConsoleSender().sendMessage(prefix + msg); + } + + @Contract(" -> new") + public static @NotNull CompletableFuture getLocation() { + return CompletableFuture.supplyAsync(GenerateLocationAsync::generateSafeLocation); + } + + private static @NotNull Location generateSafeLocation() { + + log("Generating location..."); + + Location location; + + do { + location = randomLocationInCircle(RandomSpawnpoint.spawnRadius, 0, 0); + } + while (!isSafeLocation(location)); + + return location; + + } + + @Contract("_, _, _ -> new") + private static @NotNull Location randomLocationInCircle(int radius, int centerX, int centerY) { + + log("Getting random location in a circle"); + + ThreadLocalRandom random = ThreadLocalRandom.current(); + double rand1 = random.nextDouble(); + double rand2 = random.nextDouble(); + + double r = radius * Math.sqrt(rand1); + double theta = rand2 * 2 * Math.PI; + + int x = (int) (centerX + r * Math.cos(theta)); + int z = (int) (centerY + r * Math.sin(theta)); + + return new Location(RandomSpawnpoint.defaultWorld, x, 0, z); + + } + + @Contract("null -> false") + private static boolean isSafeLocation(@Nullable Location location) { + + if (location == null) { + return false; + } + + World world = location.getWorld(); + int x = location.getBlockX() >> 4; + int z = location.getBlockZ() >> 4; + + ChunkSnapshot chunk = PaperLib.getChunkAtAsync(world, x, z, true, true) + .join() + .getChunkSnapshot(); + + int chunkX = location.getBlockX() - (chunk.getX() << 4); + int chunkZ = location.getBlockZ() - (chunk.getZ() << 4); + int chunkY = chunk.getHighestBlockYAt(chunkX, chunkZ); + + location.setY(chunkY + 1); + + Material ground = chunk.getBlockType(chunkX, chunkY, chunkZ); + Material feet = chunk.getBlockType(chunkX, chunkY + 1, chunkZ); + Material head = chunk.getBlockType(chunkX, chunkY + 2, chunkZ); + + HashSet blacklist = RandomSpawnpoint.blockBlacklist; + + return ground.isSolid() + && feet.isAir() && head.isAir() + && !blacklist.contains(ground) + && !blacklist.contains(feet) + && !blacklist.contains(head); + + } + +} \ No newline at end of file diff --git a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/LocationCache.java b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/LocationCache.java new file mode 100644 index 0000000..f99221f --- /dev/null +++ b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/async/LocationCache.java @@ -0,0 +1,76 @@ +package dev.michaud.greenpanda.randomspawnpoint.async; + +import dev.michaud.greenpanda.randomspawnpoint.RandomSpawnpoint; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.bukkit.Location; + +public class LocationCache { + + public static final int TIMEOUT_SECONDS = 120; + public static final int MAX_CACHE_SIZE = 10; + + private static final Queue locations = new LinkedBlockingQueue<>(MAX_CACHE_SIZE); + private static final Logger logger = RandomSpawnpoint.getPlugin().getLogger(); + + public static Location getLocation() { + + Location location = locations.poll(); + + generateLocations(1); + + return location; + + } + + public static void generateLocations(int num) { + + for (int i = 0; i < num; i++) { + generateLocation(); + } + + } + + public static void generateLocation() { + + GenerateLocationAsync.getLocation() + .thenAccept(LocationCache::addIfNotNull) + .orTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .whenComplete(LocationCache::onComplete); + + } + + private static void onComplete(Void value, Throwable error) { + + if (error instanceof TimeoutException) { + logger.log(Level.SEVERE, "Thread timed out" + error); + } else if (error != null) { + logger.log(Level.SEVERE, "Thread ran into an error: " + error); + } + + } + + private static void addIfNotNull(Location location) { + + if (location != null) { + locations.offer(location); + logger.log(Level.INFO, "Adding location {0}", location); + } else { + generateLocations(1); + logger.log(Level.INFO, "Location was null"); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/commands/AsyncTest.java b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/commands/AsyncTest.java new file mode 100644 index 0000000..4b07cad --- /dev/null +++ b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/commands/AsyncTest.java @@ -0,0 +1,45 @@ +package dev.michaud.greenpanda.randomspawnpoint.commands; + +import dev.michaud.greenpanda.randomspawnpoint.async.LocationCache; +import io.papermc.lib.PaperLib; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.jetbrains.annotations.NotNull; + +public class AsyncTest implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + + if (!(sender instanceof Player player)) { + sender.sendMessage("You must be a player to run that command"); + return true; + } + + Location location = LocationCache.getLocation(); + + if (location == null) { + player.sendMessage(Component.text("Unable to find a suitable spawn location") + .color(NamedTextColor.RED)); + return true; + } + + int x = location.getBlockX(); + int y = location.getBlockY(); + int z = location.getBlockZ(); + + player.sendMessage(Component.text("Set your spawn point to " + x + ", " + y + ", " + z)); + PaperLib.teleportAsync(player, location, TeleportCause.COMMAND); + + return true; + + } + +} \ No newline at end of file diff --git a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/util/SetSpawn.java b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/util/SetSpawn.java index 5f24af0..0c54147 100644 --- a/src/main/java/dev/michaud/greenpanda/randomspawnpoint/util/SetSpawn.java +++ b/src/main/java/dev/michaud/greenpanda/randomspawnpoint/util/SetSpawn.java @@ -20,7 +20,7 @@ public class SetSpawn { /** * Sets the player's spawn point to a random position. The location must be safe as defined by - * {@link SetSpawn#getSafeLocation(World)}. If after 100 tries no suitable location is found, the + * {@link SetSpawn#getSafeLocation(World)}. * * @param world the world to set the player's spawnpoint in. * @param player the player to set the spawnpoint for. diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1ce864f..018b023 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -8,7 +8,13 @@ commands: setrandomspawnpoint: description: Sets your spawnpoint to a random location. permission: randomspawnpoint.setspawn + asynctest: + description: Sets your spawnpoint to a random location asynchronously. + permission: randomspawnpoint.asynctest permissions: randomspawnpoint.setspawn: description: Lets you set your spawnpoint to a random location. + default: op + randomspawnpoint.asynctest: + description: Lets you spawnpoint to a random location asynchronously. default: op \ No newline at end of file