package de.themoep.randomteleport.searcher; /* * RandomTeleport - randomteleport-plugin - $project.description * Copyright (c) 2019 Max Lee aka Phoenix616 (mail@moep.tv) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; import de.themoep.randomteleport.RandomTeleport; import de.themoep.randomteleport.ValidatorRegistry; import de.themoep.randomteleport.searcher.options.NotFoundException; import de.themoep.randomteleport.searcher.validators.LocationValidator; import io.papermc.lib.PaperLib; import org.apache.commons.lang.Validate; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class RandomSearcher { private final RandomTeleport plugin; private final CommandSender initiator; private final UUID uniqueId = UUID.randomUUID(); private final ValidatorRegistry validators = new ValidatorRegistry(); private static final List RANDOM_LIST = new ArrayList<>(); static { for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { RANDOM_LIST.add(new int[]{x, z}); } } } private Random random = RandomTeleport.RANDOM; private Set targets = Collections.newSetFromMap(new LinkedHashMap<>()); private String id = null; private long seed = -1; private Location center; private int minRadius = 0; private int maxRadius = Integer.MAX_VALUE; private int checkDelay = 1; private int minY; private int maxY; private boolean loadedOnly = false; private boolean generatedOnly = false; private int maxTries = 100; private int cooldown; private Map options = new LinkedHashMap<>(); private long lastCheck; private int checks = 0; private final Multimap checked = MultimapBuilder.hashKeys().hashSetValues().build(); private CompletableFuture future = null; public RandomSearcher(RandomTeleport plugin, CommandSender initiator, Location center, int minRadius, int maxRadius, LocationValidator... validators) { this.plugin = plugin; this.initiator = initiator; setCenter(center); setMinRadius(minRadius); setMaxRadius(maxRadius); minY = plugin.getMinHeight(center.getWorld()); if (center.getWorld().getEnvironment() == World.Environment.NETHER) { maxY = 126; } else { maxY = center.getWorld().getMaxHeight(); } this.validators.getRaw().putAll(plugin.getLocationValidators().getRaw()); Arrays.asList(validators).forEach(this.validators::add); } /** * Get all entities targeted by this searcher * @return The entitiy to target */ public Set getTargets() { return targets; } public ValidatorRegistry getValidators() { return validators; } /** * Get a ID unique to each searcher * @return The searcher's version 4 UUID */ public UUID getUniqueId() { return uniqueId; } /** * Set the ID of this searcher used for cooldowns. Set to null to use an automatically generated one! * @param id The ID of the searcher */ public void setId(String id) { this.id = id; } /** * Get the ID of the searcher used for cooldowns. If no specific one is set then one generated by the settings will be returned * @return The ID of the searcher */ public String getId() { if (id == null) { return toString(); } return id; } /** * Set the seed that should be used when selecting locations. See {@link Random#setSeed(long)}. * @param seed The seed. */ public void setSeed(long seed) { this.seed = seed; if (random == RandomTeleport.RANDOM) { random = new Random(seed); } else { random.setSeed(seed); } } /** * Get the seed of this random searcher. Returns -1 if none was set. * @return The seed or -1 */ public long getSeed() { return seed; } /** * Directly set the Random instance used for selecting coordinates * @param random The random instance */ public void setRandom(Random random) { this.random = random; } /** * Get the random instance that is used for finding locations * @return The random instance; {@link RandomTeleport#RANDOM} by default */ public Random getRandom() { return random; } /** * Get the center for this searcher * @return The center location */ public Location getCenter() { return center; } /** * Set the center of this searcher * @param center The center location; never null */ public void setCenter(Location center) { Validate.notNull(center, "Center cannot be null!"); Validate.notNull(center.getWorld(), "Center world cannot be null!"); this.center = center; } /** * Get the minimum radius * @return The minimum radius, always positive and less than the max radius! */ public int getMinRadius() { return minRadius; } /** * Set the minimum search radius * @param minRadius The min radius; has to be positive and less than the max radius! */ public void setMinRadius(int minRadius) { Validate.isTrue(minRadius >= 0 && minRadius < maxRadius, "Min radius has to be positive and less than the max radius!"); this.minRadius = minRadius; } /** * Get the maximum radius * @return The maximum radius, always greater than the minimum radius */ public int getMaxRadius() { return maxRadius; } /** * Set the maximum search radius * @param maxRadius The max radius; has to be greater than the min radius! */ public void setMaxRadius(int maxRadius) { Validate.isTrue(maxRadius > minRadius, "Max radius has to be greater than the min radius!"); this.maxRadius = maxRadius; } /** * Get the delay in ticks between checking chunks when searching * @return The delay in ticks */ public int getCheckDelay() { return checkDelay; } /** * Set the delay in ticks between checking chunks when searching * @param checkDelay The delay in ticks */ public void setCheckDelay(int checkDelay) { this.checkDelay = checkDelay; } /** * Get the minimum Y * @return The minimum Y, always positive and less than the max Y! */ public int getMinY() { return minY; } /** * Set the minimum search Y * @param minY The min Y; has to be positive and less than the max Y! */ public void setMinY(int minY) { Validate.isTrue(minY >= plugin.getMinHeight(center.getWorld()), "Min Y has to be at least the world's minimum height!"); Validate.isTrue(minY < maxY, "Min Y has to be less than the max Y!"); this.minY = minY; } /** * Get the maximum Y * @return The maximum Y, always greater than the minimum Y */ public int getMaxY() { return maxY; } /** * Set the maximum search Y * @param maxY The max Y; has to be greater than the min Y! */ public void setMaxY(int maxY) { Validate.isTrue(maxY <= center.getWorld().getMaxHeight() && maxY > minY, "Max Y has to be greater than the min Y and at most the world's max height!"); this.maxY = maxY; } /** * By default it will search for coordinates in any chunk, even unloaded ones prompting the server to load new * chunks which might result in some performance impact if the server doesn't support async loading. This disables * that and only searches in already loaded chunks. (But might fail more often) * @param loadedOnly Whether or not to search in loaded chunks only */ public void searchInLoadedOnly(boolean loadedOnly) { this.loadedOnly = loadedOnly; } /** * By default it will search for coordinates in any chunk, even ungenerated ones prompting the world to get * generated at the point which might result in some performance impact. This disables that and only searches * in already generated chunks. * @param generatedOnly Whether or not to search in generated chunks only */ public void searchInGeneratedOnly(boolean generatedOnly) { this.generatedOnly = generatedOnly; } public int getMaxTries() { return maxTries; } public void setMaxTries(int maxTries) { this.maxTries = maxTries; } /** * Set the cooldown that a player has to wait before using a searcher with similar settings again * @param cooldown The cooldown in seconds */ public void setCooldown(int cooldown) { Validate.isTrue(cooldown >= 0, "Cooldown can't be negative!"); this.cooldown = cooldown; } /** * Get the cooldown that a player has to wait before using a searcher with similar settings again * @return The cooldown in seconds */ public int getCooldown() { return cooldown; } /** * Get additional options * @return A map of additional options */ public Map getOptions() { return options; } /** * Search for a valid location * @return A CompletableFuture for when the search task is complete * @throws IllegalStateException when the searcher is already running */ public CompletableFuture search() { if (plugin.getRunningSearchers().containsKey(uniqueId)) { throw new IllegalStateException("Searcher " + uniqueId + " is already running!"); } plugin.getRunningSearchers().put(uniqueId, this); if (targets.isEmpty() && initiator instanceof Entity) { targets.add((Entity) initiator); } future = new CompletableFuture<>(); checks = 0; checked.clear(); plugin.getServer().getScheduler().runTask(plugin, () -> checkRandom(future)); future.whenComplete((l, e) -> plugin.getRunningSearchers().remove(uniqueId)); return future; } private void checkRandom(CompletableFuture future) { if (checks >= maxTries) { future.completeExceptionally(new NotFoundException("location")); return; } if (future.isCancelled() || future.isDone() || future.isCompletedExceptionally()) { return; } lastCheck = center.getWorld().getTime(); Location randomLoc = center.clone(); randomLoc.setY(plugin.getMinHeight(center.getWorld())); int minChunk = minRadius >> 4; int maxChunk = maxRadius >> 4; int randChunkX; int randChunkZ; Chunk[] loadedChunks = new Chunk[0]; if (loadedOnly) { loadedChunks = randomLoc.getWorld().getLoadedChunks(); if (loadedChunks.length == 0) { future.completeExceptionally(new NotFoundException("loaded chunk")); return; } } do { checks++; if (checks >= maxTries) { future.completeExceptionally(new NotFoundException("location")); return; } if (loadedOnly) { Chunk chunk = loadedChunks[random.nextInt(loadedChunks.length)]; randChunkX = chunk.getX(); randChunkZ = chunk.getZ(); } else { randChunkX = (random.nextBoolean() ? 1 : -1) * random.nextInt(maxChunk + 1); randChunkZ = (random.nextBoolean() ? 1 : -1) * random.nextInt(maxChunk + 1); } } while (!checked.put(randChunkX, randChunkZ) || !inRadius(Math.abs(randChunkX), Math.abs(randChunkZ), minChunk, maxChunk) || (generatedOnly && !PaperLib.isChunkGenerated(randomLoc.getWorld(), randChunkX, randChunkZ))); randomLoc.setX(((center.getBlockX() >> 4) + randChunkX) * 16); randomLoc.setZ(((center.getBlockZ() >> 4) + randChunkZ) * 16); PaperLib.getChunkAtAsync(randomLoc).thenApply(c -> { checks++; if (c == null) { // Chunk not generated, test another one checkRandom(future); return false; } int indexOffset = random.nextInt(RANDOM_LIST.size()); Location foundLoc = null; for (int i = 0; i < RANDOM_LIST.size(); i++) { int index = (i + indexOffset) % RANDOM_LIST.size(); boolean validated = true; Location loc = randomLoc.clone().add(RANDOM_LIST.get(index)[0], 0, RANDOM_LIST.get(index)[1]); if (!inRadius(loc)) { continue; } for (LocationValidator validator : getValidators().getAll()) { if (!validator.validate(this, loc)) { validated = false; break; } } if (validated) { foundLoc = loc; break; } } if (foundLoc != null) { // all checks are for the top block, put we want a location above that so add 1 to y future.complete(foundLoc.add(0, 1, 0)); return true; } long diff = center.getWorld().getTime() - lastCheck; if (diff < checkDelay) { plugin.getServer().getScheduler().runTaskLater(plugin, () -> checkRandom(future), checkDelay - diff); } else { checkRandom(future); } return false; }).exceptionally(future::completeExceptionally); } private boolean inRadius(Location location) { int diffX = Math.abs(location.getBlockX() - center.getBlockX()); int diffZ = Math.abs(location.getBlockZ() - center.getBlockZ()); return inRadius(diffX, diffZ, minRadius, maxRadius); } private boolean inRadius(int diffX, int diffZ, int minRadius, int maxRadius) { return diffX >= minRadius && diffX <= maxRadius && diffZ <= maxRadius || diffZ >= minRadius && diffZ <= maxRadius && diffX <= maxRadius; } public RandomTeleport getPlugin() { return plugin; } /** * The sender who initiated this search * @return The initiator */ public CommandSender getInitiator() { return initiator; } /** * Get the currently running search future * @return The currently running search future or null if none is running */ public CompletableFuture getFuture() { return future; } @Override public String toString() { return "RandomSearcher{" + "id='" + id + '\'' + ", seed=" + seed + ", center=" + center + ", minRadius=" + minRadius + ", maxRadius=" + maxRadius + ", loadedOnly=" + loadedOnly + ", generatedOnly=" + generatedOnly + ", maxTries=" + maxTries + ", cooldown=" + cooldown + '}'; } }