RandomTeleport/randomteleport-plugin/src/main/java/de/themoep/randomteleport/searcher/RandomSearcher.java

491 lines
16 KiB
Java

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 <http://www.gnu.org/licenses/>.
*/
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<int[]> 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<Entity> 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<String, String> options = new LinkedHashMap<>();
private long lastCheck;
private int checks = 0;
private final Multimap<Integer, Integer> checked = MultimapBuilder.hashKeys().hashSetValues().build();
private CompletableFuture<Location> 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<Entity> 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<String, String> 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<Location> 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<Location> 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<Location> 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 +
'}';
}
}