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

504 lines
21 KiB
Java

package de.themoep.randomteleport;
/*
* RandomTeleport
* 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.HashBasedTable;
import com.google.common.collect.Table;
import de.themoep.minedown.MineDown;
import de.themoep.randomteleport.api.RandomTeleportAPI;
import de.themoep.randomteleport.hook.HookManager;
import de.themoep.randomteleport.listeners.SignListener;
import de.themoep.randomteleport.searcher.RandomSearcher;
import de.themoep.randomteleport.searcher.options.AdditionalOptionParser;
import de.themoep.randomteleport.searcher.options.NotFoundException;
import de.themoep.randomteleport.searcher.options.OptionParser;
import de.themoep.randomteleport.searcher.options.PlayerNotFoundException;
import de.themoep.randomteleport.searcher.options.SimpleOptionParser;
import de.themoep.randomteleport.searcher.options.WorldNotFoundException;
import de.themoep.randomteleport.searcher.validators.BiomeValidator;
import de.themoep.randomteleport.searcher.validators.BlockValidator;
import de.themoep.randomteleport.searcher.validators.HeightValidator;
import de.themoep.randomteleport.searcher.validators.LocationValidator;
import de.themoep.randomteleport.searcher.validators.ProtectionValidator;
import de.themoep.randomteleport.searcher.validators.WorldborderValidator;
import de.themoep.utils.lang.bukkit.LanguageManager;
import io.papermc.lib.PaperLib;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class RandomTeleport extends JavaPlugin implements RandomTeleportAPI {
public static final Random RANDOM = new Random();
private HookManager hookManager;
private LanguageManager lang;
private Table<String, UUID, Map.Entry<Long, Integer>> cooldowns = HashBasedTable.create();
private Map<UUID, RandomSearcher> runningSearchers = new HashMap<>();
private ValidatorRegistry locationValidators = new ValidatorRegistry();
private List<OptionParser> optionParsers = new ArrayList<>();
private Material[] safeBlocks;
private Material[] unsafeBlocks;
private Set<String> signVariables;
private boolean hasMinHeight = true;
public void onEnable() {
hookManager = new HookManager(this);
loadConfig();
initOptionParsers();
initValidators();
getCommand("randomteleport").setExecutor(new RandomTeleportCommand(this));
getServer().getPluginManager().registerEvents(new SignListener(this), this);
}
public void loadConfig() {
saveDefaultConfig();
reloadConfig();
List<String> safeBlocksList;
if (getConfig().contains("save-blocks")) {
safeBlocksList = getConfig().getStringList("save-blocks");
} else {
safeBlocksList = getConfig().getStringList("safe-blocks");
}
safeBlocks = safeBlocksList.stream()
.map(s -> {
Material mat = Material.matchMaterial(s);
if (mat == null) {
getLogger().log(Level.WARNING, "Error in save-blocks config! No material found with name " + s);
}
return mat;
})
.filter(Objects::nonNull)
.toArray(Material[]::new);
List<String> unsafeBlocksList;
if (getConfig().contains("unsave-blocks")) {
unsafeBlocksList = getConfig().getStringList("unsave-blocks");
} else {
unsafeBlocksList = getConfig().getStringList("unsafe-blocks");
}
unsafeBlocks = unsafeBlocksList.stream()
.map(s -> {
Material mat = Material.matchMaterial(s);
if (mat == null) {
getLogger().log(Level.WARNING, "Error in unsave-blocks config! No material found with name " + s);
}
return mat;
})
.filter(Objects::nonNull)
.toArray(Material[]::new);
signVariables = getConfig().getStringList("sign-variables").stream().map(String::toLowerCase).collect(Collectors.toSet());
lang = new LanguageManager(this, getConfig().getString("lang"));
}
private void initOptionParsers() {
addOptionParser(new SimpleOptionParser(array("p", "player"), (searcher, args) -> {
if (args.length > 0 && searcher.getInitiator().hasPermission("randomteleport.tpothers")) {
List<Player> players = new ArrayList<>();
for (String arg : args) {
for (String s : arg.split(",")) {
Player player = getServer().getPlayer(s);
if (player == null) {
throw new PlayerNotFoundException(s);
}
players.add(player);
}
}
searcher.getTargets().addAll(players);
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("x", "xpos"), 1, (searcher, args) -> {
if (args.length > 0) {
searcher.getCenter().setX(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("z", "zpos"), 1, (searcher, args) -> {
if (args.length > 0) {
searcher.getCenter().setZ(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("miny"), (searcher, args) -> {
if (args.length > 0) {
searcher.setMinY(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("maxy"), (searcher, args) -> {
if (args.length > 0) {
searcher.setMaxY(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("w", "world"), (searcher, args) -> {
if (args.length > 0) {
World world = getServer().getWorld(args[0]);
if (world == null) {
throw new WorldNotFoundException(args[0]);
}
searcher.getCenter().setWorld(world);
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("c", "cooldown"), (searcher, args) -> {
if (args.length > 0) {
searcher.setCooldown(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("f", "force"), (searcher, args) -> {
if (args.length > 0) {
if ("regions".equalsIgnoreCase(args[0])) {
searcher.getValidators().remove("protection");
} else if ("blocks".equalsIgnoreCase(args[0])) {
searcher.getValidators().add(new BlockValidator(false, unsafeBlocks));
} else {
throw new NotFoundException(args[0]);
}
} else {
searcher.getValidators().remove("protection");
searcher.getValidators().add(new BlockValidator(false, unsafeBlocks));
}
return true;
}));
addOptionParser(new SimpleOptionParser(array("b", "biome"), (searcher, args) -> {
if (args.length > 0) {
List<Biome> biomes = new ArrayList<>();
for (String arg : args) {
biomes.add(Biome.valueOf(arg.toUpperCase()));
}
if (!biomes.isEmpty()) {
searcher.getValidators().add(new BiomeValidator(biomes.toArray(new Biome[0])));
return true;
}
return false;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("l", "loaded"), (searcher, args) -> {
searcher.searchInLoadedOnly(true);
return true;
}));
addOptionParser(new SimpleOptionParser(array("g", "generated"), (searcher, args) -> {
searcher.searchInGeneratedOnly(true);
return true;
}));
addOptionParser(new SimpleOptionParser(array("id"), (searcher, args) -> {
if (args.length > 0) {
searcher.setId(args[0]);
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser(array("t", "tries"), (searcher, args) -> {
if (args.length > 0) {
searcher.setMaxTries(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new SimpleOptionParser("checkdelay", (searcher, args) -> {
if (args.length > 0) {
searcher.setCheckDelay(Integer.parseInt(args[0]));
return true;
}
return false;
}));
addOptionParser(new AdditionalOptionParser("spawnpoint", "sp"));
}
private void initValidators() {
locationValidators.add(new WorldborderValidator());
locationValidators.add(new HeightValidator(unsafeBlocks));
locationValidators.add(new ProtectionValidator());
locationValidators.add(new BlockValidator(safeBlocks));
}
/**
* Get the map of all running searchers
* @return The map of running searchers
*/
public Map<UUID, RandomSearcher> getRunningSearchers() {
return runningSearchers;
}
/**
* Utility method to create arrays with a nicer syntax. Seriously, why does Java not just accept {"string"} as parameters?!?
* @param array The array values
* @return The same array
*/
private static <T> T[] array(T... array) {
return array;
}
public boolean sendMessage(Collection<? extends CommandSender> senders, String key, String... replacements) {
boolean r = false;
for (CommandSender sender : senders) {
r |= sendMessage(sender, key, replacements);
}
return r;
}
public boolean sendMessage(CommandSender sender, String key, String... replacements) {
BaseComponent[] message = getComponentMessage(sender, key, replacements);
if (message != null && message.length != 0) {
sender.spigot().sendMessage(message);
return true;
}
return false;
}
public BaseComponent[] getComponentMessage(CommandSender sender, String key, String... replacements) {
return new MineDown(getLang(sender, key))
.placeholderPrefix("{")
.placeholderSuffix("}")
.replace(replacements)
.toComponent();
}
public String getTextMessage(CommandSender sender, String key, String... replacements) {
return TextComponent.toLegacyText(getComponentMessage(sender, key, replacements));
}
private String getLang(CommandSender sender, String key) {
return lang.getConfig(sender).get(key);
}
public HookManager getHookManager() {
return hookManager;
}
public ValidatorRegistry getLocationValidators() {
return locationValidators;
}
/**
*
* @return
*/
public List<OptionParser> getOptionParsers() {
return optionParsers;
}
/**
* Add an option parser to this plugin
* @param parser The parser to add
*/
public void addOptionParser(OptionParser parser) {
optionParsers.add(parser);
if (parser instanceof SimpleOptionParser) {
Permission parent = getServer().getPluginManager().getPermission("randomteleport.manual.option.*");
for (String alias : ((SimpleOptionParser) parser).getAliases()) {
Permission perm = new Permission("randomteleport.manual.option." + alias, PermissionDefault.OP);
perm.addParent(parent, true);
try {
getServer().getPluginManager().addPermission(perm);
} catch (IllegalArgumentException ignored) {} // duplicate
}
}
}
/**
* Check whether or not a sign line matches the configured variables
* @param line The line to match
* @return <tt>true</tt> if it matches; <tt>false</tt> if not
*/
public boolean matchesSignVariable(String line) {
return signVariables.contains(line.toLowerCase());
}
/**
* Create and run a searcher using specified args the same way the command does
* @param sender The sender of the command
* @param center The center location for the searcher
* @param args The arguments to parse
* @return Returns the searcher that is running or null if it was stopped due to a cooldown
* @throws IllegalArgumentException Thrown when arguments couldn't be handled properly
*/
public RandomSearcher parseAndRun(CommandSender sender, Location center, String[] args) {
RandomSearcher searcher = new RandomSearcher(this, sender, center, Integer.parseInt(args[0]), Integer.parseInt(args[1]));
String[] optionArgs = Arrays.copyOfRange(args, 2, args.length);
for (OptionParser parser : getOptionParsers()) {
parser.parse(searcher, optionArgs);
}
int cooldown = 0;
for (Entity target : searcher.getTargets()) {
Map.Entry<Long, Integer> lastUse = cooldowns.get(searcher.getId(), target.getUniqueId());
if (lastUse != null) {
int targetCooldown = (int) ((System.currentTimeMillis() - lastUse.getKey()) / 1000);
if (targetCooldown > cooldown && !target.hasPermission("randomteleport.cooldownexempt")) {
cooldown = targetCooldown;
}
}
}
if (cooldown > 0 && cooldown < searcher.getCooldown()) {
sendMessage(searcher.getTargets(), "error.cooldown", "cooldown_text", (searcher.getCooldown() - cooldown) + "s");
return null;
}
sendMessage(searcher.getTargets(), "search", "worldname", searcher.getCenter().getWorld().getName());
searcher.search().thenApply(targetLoc -> {
searcher.getTargets().forEach(e -> {
if (e instanceof Player) {
Location belowLoc = targetLoc.clone().subtract(0, 1, 0);
Block belowBlock = belowLoc.getBlock();
((Player) e).sendBlockChange(belowLoc, belowBlock.getBlockData());
}
targetLoc.setX(targetLoc.getBlockX() + 0.5);
targetLoc.setY(targetLoc.getY() + 0.1);
targetLoc.setZ(targetLoc.getBlockZ() + 0.5);
PaperLib.teleportAsync(e, targetLoc).thenAccept(success -> {
if (success) {
cooldowns.put(searcher.getId(), e.getUniqueId(), new AbstractMap.SimpleImmutableEntry<>(System.currentTimeMillis(), searcher.getCooldown()));
sendMessage(e, "teleport",
"worldname", targetLoc.getWorld().getName(),
"x", String.valueOf(targetLoc.getBlockX()),
"y", String.valueOf(targetLoc.getBlockY()),
"z", String.valueOf(targetLoc.getBlockZ())
);
if (searcher.getOptions().containsKey("spawnpoint") && e instanceof Player) {
if (((Player) e).getBedSpawnLocation() == null || "force".equalsIgnoreCase(searcher.getOptions().get("spawnpoint"))) {
((Player) e).setBedSpawnLocation(targetLoc, true);
sendMessage(e, "setspawnpoint",
"worldname", targetLoc.getWorld().getName(),
"x", String.valueOf(targetLoc.getBlockX()),
"y", String.valueOf(targetLoc.getBlockY()),
"z", String.valueOf(targetLoc.getBlockZ())
);
}
}
} else {
sendMessage(e, "error.teleport",
"worldname", targetLoc.getWorld().getName(),
"x", String.valueOf(targetLoc.getBlockX()),
"y", String.valueOf(targetLoc.getBlockY()),
"z", String.valueOf(targetLoc.getBlockZ())
);
}
});
});
return true;
}).exceptionally(ex -> {
sendMessage(sender, "error.location");
if (!(ex.getCause() instanceof NotFoundException)) {
getLogger().log(Level.SEVERE, "Error while trying to find a location!", ex);
}
searcher.getTargets().forEach(e -> {
if (e != sender) {
sendMessage(e, "error.location");
}
});
return true;
});
return searcher;
}
/**
* Run a preset
* @param sender The sender that executed the preset
* @param preset The preset ID to run
* @param target The player targeted by the teleporter
* @param center The center for the search
* @return The RandomSearcher instance that is searching
*/
public RandomSearcher runPreset(CommandSender sender, String preset, Player target, Location center) {
String cmd = getConfig().getString("presets." + preset) + " -p " + target.getName();
if (!cmd.contains("-id")) {
cmd += " -id " + preset;
}
if (cmd.startsWith("/")) {
cmd = cmd.substring(1);
}
if (cmd.startsWith("rtp ")) {
cmd = cmd.substring(4);
}
return parseAndRun(sender, center, cmd.split(" "));
}
@Override
public CompletableFuture<Location> getRandomLocation(Player player, Location origin, int minRange, int maxRange, LocationValidator... validators) {
return getRandomSearcher(player, origin, minRange, maxRange, validators).search();
}
@Override
public CompletableFuture<Boolean> teleportToRandomLocation(Player player, Location origin, int minRange, int maxRange, LocationValidator... validators) {
return getRandomLocation(player, origin, minRange, maxRange, validators).thenApply(player::teleport);
}
@Override
public RandomSearcher getRandomSearcher(Player player, Location origin, int minRange, int maxRange, LocationValidator... validators) {
return new RandomSearcher(this, player, origin, minRange, maxRange, validators);
}
/**
* Utility method to get the min height of a world as old versions didn't have support for this.
* @param world The world
* @return The min height or 0 if querying that isn't supported
*/
public int getMinHeight(World world) {
if (hasMinHeight) {
try {
return world.getMinHeight();
} catch (NoSuchMethodError ignored) {
// getMinHeight is only available starting in 1.16.5
}
hasMinHeight = false;
}
return 0;
}
}