diff --git a/src/main/java/de/btegermany/terraplusminus/Terraplusminus.java b/src/main/java/de/btegermany/terraplusminus/Terraplusminus.java
index fa80c51..d9a4b86 100644
--- a/src/main/java/de/btegermany/terraplusminus/Terraplusminus.java
+++ b/src/main/java/de/btegermany/terraplusminus/Terraplusminus.java
@@ -11,7 +11,10 @@
import de.btegermany.terraplusminus.utils.FileBuilder;
import de.btegermany.terraplusminus.utils.PlayerHashMapManagement;
import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.command.*;
import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldInitEvent;
@@ -21,6 +24,9 @@
import java.io.*;
import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static java.lang.String.format;
public final class Terraplusminus extends JavaPlugin implements Listener {
public static FileConfiguration config;
@@ -76,9 +82,9 @@ public void onEnable() {
// --------------------------
// Registering commands
- getCommand("tpll").setExecutor(new TpllCommand());
- getCommand("where").setExecutor(new WhereCommand());
- getCommand("offset").setExecutor(new OffsetCommand());
+ this.registerCommand("tpll", new TpllCommand());
+ this.registerCommand("where", new WhereCommand());
+ this.registerCommand("offset", new OffsetCommand());
// --------------------------
Bukkit.getLogger().log(Level.INFO, "[T+-] Terraplusminus successfully enabled");
@@ -190,4 +196,27 @@ private void updateConfig() {
}
}
+ private void registerCommand(String commandName, CommandExecutor executor) {
+ Logger logger = this.getLogger();
+ PluginCommand command = this.getCommand(commandName);
+ if (command == null) {
+ logger.warning(format("Could not register command %s. Command is unknown to bukkit, has it been properly registered in plugin.yml?", commandName));
+ return;
+ }
+ command.setExecutor(executor);
+ logger.fine(format("Registered command executor for /%s command", commandName));
+ if (executor instanceof TabCompleter) {
+ command.setTabCompleter((TabCompleter) executor);
+ logger.fine(format("Registered tab completer for /%s command", commandName));
+ }
+ }
+
+ public static boolean isTerraWorld(World world) {
+ return world.getGenerator() instanceof RealWorldGenerator;
+ }
+
+ public static boolean isInTerraWorld(Entity entity) {
+ return isTerraWorld(entity.getWorld());
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/de/btegermany/terraplusminus/commands/CommandHelper.java b/src/main/java/de/btegermany/terraplusminus/commands/CommandHelper.java
new file mode 100644
index 0000000..251540e
--- /dev/null
+++ b/src/main/java/de/btegermany/terraplusminus/commands/CommandHelper.java
@@ -0,0 +1,97 @@
+package de.btegermany.terraplusminus.commands;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNullElseGet;
+
+/**
+ * Utility class to help with commands.
+ *
+ * @author Smyler
+ */
+public final class CommandHelper {
+
+ /**
+ * Parses a command target selector like '@a'.
+ * Currently supported selectors:
+ *
+ * @a
+ * @p
(doesn't select the sender)
+ *
+ * Target selector arguments are not supported.
+ * It's a shame that Bukkit does not expose the vanilla command system...
+ *
+ * @param sender the {@link CommandSender command sender}
+ * @param selector the selector string
+ * @return the collection of {@link Entity entities} that match the selector
+ *
+ * @throws InvalidTargetSelectorException if the selector is invalid, either syntactically or in the specific context
+ */
+ public static Collection parseTargetSelector(@NotNull CommandSender sender, String selector) throws InvalidTargetSelectorException {
+ if (selector.startsWith("@") && selector.length() >= 2) {
+ char selectorChar = selector.charAt(1);
+ if (selectorChar == 'a') {
+ return Bukkit.getOnlinePlayers().stream()
+ .map(p -> (Entity)p)
+ .collect(Collectors.toList());
+ } else if (selectorChar == 'p' && sender instanceof Entity) {
+ Entity entitySender = (Entity) sender;
+ Location senderLocation = entitySender.getLocation();
+ return Collections.singleton(
+ Bukkit.getOnlinePlayers().stream()
+ .filter(p -> p != sender)
+ .min(comparing(p -> p.getLocation().distanceSquared(senderLocation)))
+ .orElseThrow(InvalidTargetSelectorException::new)
+ );
+ }
+ } else {
+ Player player = Bukkit.getPlayerExact(selector);
+ if (player == null || !player.isOnline()) {
+ throw new InvalidTargetSelectorException();
+ } else {
+ return List.of(player);
+ }
+ }
+
+ throw new InvalidTargetSelectorException();
+
+ }
+
+ /**
+ * Checks if a collection of entities is comprised solely of a single command sender (which may appear multiple times).
+ * This is useful for permission checks where one might allow a sender to execute a command on themselves,
+ * but not on other entities.
+ *
+ * @param sender the {@link CommandSender sender} executing the command
+ * @param targets the {@link Collection collection} of targets
+ *
+ * @return whether a {@link CommandSender sender} is the only entity in a {@link Collection collection} of targets
+ */
+ public static boolean senderIsSoleTarget(CommandSender sender, @NotNull Collection targets) {
+ return !targets.stream().allMatch(target -> target == sender);
+ }
+
+ public static String formatTargetName(Entity target) {
+ return requireNonNullElseGet(target.getCustomName(), target::getName);
+ }
+
+ public static class InvalidTargetSelectorException extends Exception {
+
+ }
+
+ private CommandHelper() {
+ throw new IllegalStateException();
+ }
+
+}
diff --git a/src/main/java/de/btegermany/terraplusminus/commands/OffsetCommand.java b/src/main/java/de/btegermany/terraplusminus/commands/OffsetCommand.java
index 64e7a4d..3077421 100644
--- a/src/main/java/de/btegermany/terraplusminus/commands/OffsetCommand.java
+++ b/src/main/java/de/btegermany/terraplusminus/commands/OffsetCommand.java
@@ -1,27 +1,92 @@
package de.btegermany.terraplusminus.commands;
import de.btegermany.terraplusminus.Terraplusminus;
+import de.btegermany.terraplusminus.gen.RealWorldGenerator;
+import net.buildtheearth.terraminusminus.projection.GeographicProjection;
+import net.buildtheearth.terraminusminus.projection.transform.OffsetProjectionTransform;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
+import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static java.lang.Math.round;
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.toList;
+import static org.bukkit.ChatColor.*;
+
+public class OffsetCommand implements TabExecutor {
-public class OffsetCommand implements CommandExecutor {
@Override
- public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
- if (command.getName().equalsIgnoreCase("offset")) {
- Player player = (Player) commandSender;
- if (player.hasPermission("t+-.offset")) {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Offsets:");
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7 | X: §8" + Terraplusminus.config.getInt("terrain_offset.x"));
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7 | Y: §8" + Terraplusminus.config.getInt("terrain_offset.y"));
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7 | Z: §8" + Terraplusminus.config.getInt("terrain_offset.z"));
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7No permission for /offset");
- return true;
- }
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String... arguments) {
+
+ String prefix = Terraplusminus.config.getString("prefix");
+
+ if (!sender.hasPermission("t+-.offset")) {
+ sender.sendMessage(Terraplusminus.config.getString("prefix") + GRAY + "No permission for /offset");
+ return true;
+ }
+
+ World world;
+ if (arguments.length == 1) {
+ String worldName = arguments[0];
+ world = Bukkit.getWorld(worldName);
+ } else if (arguments.length == 0 && sender instanceof Player) {
+ Player player = (Player) sender;
+ world = player.getWorld();
+ } else {
+ return false;
+ }
+
+ if (world == null) {
+ sender.sendMessage(prefix + RED + "No such world");
+ return true;
+ }
+
+ ChunkGenerator generator = world.getGenerator();
+ if (!(generator instanceof RealWorldGenerator)) {
+ sender.sendMessage(prefix + GRAY + world.getName() + RED + " is not a Terra+- world");
+ return true;
+ }
+ RealWorldGenerator realWorldGenerator = (RealWorldGenerator) generator;
+ GeographicProjection projection = realWorldGenerator.getSettings().projection();
+
+ int offsetY = realWorldGenerator.getYOffset();
+ int offsetX, offsetZ;
+ if (projection instanceof OffsetProjectionTransform) {
+ OffsetProjectionTransform transform = (OffsetProjectionTransform) projection;
+ // We assume there is ony one offset transform, or that it is the only one the user cares about
+ // We can safely round to int as we are only dealing with blocks at that scale
+ offsetX = (int) round(transform.dx());
+ offsetZ = (int) round(transform.dy());
+ } else {
+ offsetX = offsetZ = 0;
}
+
+ sender.sendMessage(prefix + GRAY + "Offsets for world \"" + DARK_GRAY + world.getName() + GRAY + "\":");
+ sender.sendMessage(prefix + GRAY + " | X: " + DARK_GRAY + offsetX);
+ sender.sendMessage(prefix + GRAY + " | Y: " + DARK_GRAY + offsetY);
+ sender.sendMessage(prefix + GRAY + " | Z: " + DARK_GRAY + offsetZ);
return true;
}
+
+ @Nullable
+ @Override
+ public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
+ if (args.length > 1 || !sender.hasPermission("t+-.offset")) {
+ return emptyList();
+ }
+
+ return Bukkit.getWorlds().stream()
+ .filter(Terraplusminus::isTerraWorld)
+ .map(World::getName)
+ .collect(toList());
+ }
+
}
diff --git a/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java b/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
index e585019..e496301 100644
--- a/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
+++ b/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
@@ -3,226 +3,367 @@
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import de.btegermany.terraplusminus.Terraplusminus;
-import de.btegermany.terraplusminus.data.TerraConnector;
+import de.btegermany.terraplusminus.gen.RealWorldGenerator;
import de.btegermany.terraplusminus.utils.PluginMessageUtil;
-import io.papermc.lib.PaperLib;
-import net.buildtheearth.terraminusminus.generator.EarthGeneratorSettings;
+import net.buildtheearth.terraminusminus.projection.GeographicProjection;
import net.buildtheearth.terraminusminus.projection.OutOfProjectionBoundsException;
+import net.buildtheearth.terraminusminus.util.geo.LatLng;
+import org.bukkit.Bukkit;
import org.bukkit.Location;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
-import org.bukkit.command.CommandSender;
+import org.bukkit.World;
+import org.bukkit.command.*;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
+import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
-public class TpllCommand implements CommandExecutor {
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
- private final EarthGeneratorSettings bteGeneratorSettings = EarthGeneratorSettings.parse(EarthGeneratorSettings.BTE_DEFAULT_SETTINGS);
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static de.btegermany.terraplusminus.Terraplusminus.isInTerraWorld;
+import static de.btegermany.terraplusminus.commands.CommandHelper.*;
+import static io.papermc.lib.PaperLib.isChunkGenerated;
+import static io.papermc.lib.PaperLib.teleportAsync;
+import static java.lang.Double.isNaN;
+import static java.lang.Double.parseDouble;
+import static java.lang.String.join;
+import static java.util.Arrays.copyOfRange;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static net.buildtheearth.terraminusminus.util.geo.CoordinateParseUtils.parseVerbatimCoordinates;
+import static org.bukkit.ChatColor.*;
+
+
+public class TpllCommand implements TabExecutor {
+
+ private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("##.#####");
@Override
- public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) {
- if (command.getName().equalsIgnoreCase("tpll")) {
- //If sender is not a player cancel the command.
- if (!(commandSender instanceof Player)) {
- commandSender.sendMessage("This command can only be used by players!");
- return true;
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] arguments) {
+
+ if (!sender.hasPermission("t+-.tpll")) {
+ sender.sendMessage(RED + "You do not have permission to use that command");
+ return true;
+ }
+
+ final String prefix = Terraplusminus.config.getString("prefix"); // Used in feedback messages
+
+ // Option to pass through tpll to other bukkit plugins.
+ String passthroughTpll = Terraplusminus.config.getString("passthrough_tpll");
+ if (!isNullOrEmpty(passthroughTpll)) {
+ Terraplusminus.instance.getServer().dispatchCommand(
+ sender,
+ passthroughTpll + ":tpll " + join(" ", arguments)
+ );
+ return true;
+ }
+
+ int consumedArguments = 0; // Number of consumed arguments, starting left
+
+ // Parse targets first.
+ // They were either explicitly provided in the first argument, or it's just the sender
+ Collection targets = null;
+ if (arguments.length > 1) {
+ try {
+ targets = parseTargetSelector(sender, arguments[consumedArguments]);
+ consumedArguments++;
+ } catch (InvalidTargetSelectorException ignored) {
+ // Not a valid selector, probably coordinates
}
- Player player = (Player) commandSender;
-
- // Entity selector
-
- // detect if command starts with @ or with a player name
-
- if ((args[0].startsWith("@") || !isDouble(args[0].replace(",", ""))) && player.hasPermission("t+-.forcetpll")) {
- if (args[0].equals("@a")) {
- StringBuilder playerList = new StringBuilder();
- Terraplusminus.instance.getServer().getOnlinePlayers().forEach(p -> {
- p.chat("/tpll " + String.join(" ", args).substring(2));
- if (Terraplusminus.instance.getServer().getOnlinePlayers().size() > 1) {
- playerList.append(p.getName()).append(", ");
- } else {
- playerList.append(p.getName()).append(" ");
- }
- });
- // delete last comma if no player follows
- if (playerList.length() > 0 && playerList.charAt(playerList.length() - 2) == ',') {
- playerList.deleteCharAt(playerList.length() - 2);
- }
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Teleported §9" + playerList + "§7to" + String.join(" ", args).substring(2));
- return true;
- } else if (args[0].equals("@p")) {
- // find nearest player but not the player itself
- Player nearestPlayer = null;
- double nearestDistance = Double.MAX_VALUE;
- for (Player p : Terraplusminus.instance.getServer().getOnlinePlayers()) {
- if (p.getLocation().distanceSquared(player.getLocation()) < nearestDistance && (!p.equals(player) || Terraplusminus.instance.getServer().getOnlinePlayers().size() == 1)) {
- nearestPlayer = p;
- nearestDistance = p.getLocation().distanceSquared(player.getLocation());
- }
- }
- if (nearestPlayer != null) {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Teleported §9" + nearestPlayer.getName() + " §7to" + String.join(" ", args).substring(2));
- nearestPlayer.chat("/tpll " + String.join(" ", args).substring(2));
- }
- return true;
- } else {
- Player target = null;
- //check if target player is online
- for (Player p : Terraplusminus.instance.getServer().getOnlinePlayers()) {
- if (p.getName().equals(args[0])) {
- target = p;
- }
- }
-
- if (target == null) {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cNo player found with name §9" + args[0]);
- return true;
- }
-
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Teleported §9" + target.getName() + " §7to " + args[1] + " " + args[2]);
- target.chat("/tpll " + String.join(" ", args).replace(target.getName(), ""));
- return true;
- }
+ }
+ if (targets == null) {
+ if (sender instanceof Player) {
+ targets = Collections.singleton((Player) sender);
+ } else {
+ return false; // Invalid command, non player senders must specify a valid target
}
+ } else if (!sender.hasPermission("t+-.forcetpll") && !senderIsSoleTarget(sender, targets)){
+ // Sender specified a target which is not themselves
+ sender.sendMessage(
+ prefix
+ + RED + "You do not have permission to use TPLL on others"
+ );
+ return true;
+ }
- //Option to passthrough tpll to other bukkit plugins.
- String passthroughTpll = Terraplusminus.config.getString("passthrough_tpll");
- if (!passthroughTpll.isEmpty()) {
- //Check if any args are parsed.
- if (args.length == 0) {
- player.chat("/" + passthroughTpll + ":tpll");
- } else {
- player.chat("/" + passthroughTpll + ":tpll " + String.join(" ", args));
- }
- return true;
+ // Now, parse destination latitude and longitude.
+ // This is trickier, as they may span multiple arguments as we delegate parsing to Terra--.
+ // We start left and arguments, stopping as soon as we found the first valid set of coordinates.
+ LatLng searchingLocation = null;
+ final LatLng geoLocation; // We are passing it to a lambda later, so it needs to be final
+ int i = consumedArguments;
+ for (; i < arguments.length && searchingLocation == null; i++) {
+ String rawArgumentString = join(" ", copyOfRange(arguments, consumedArguments, i + 1));
+ searchingLocation = parseVerbatimCoordinates(rawArgumentString);
+ }
+ if (searchingLocation == null) {
+ // No valid position, command is invalid
+ return false;
+ } else {
+ geoLocation = searchingLocation;
+ }
+ consumedArguments = i;
+
+ // Parse optional destination altitude
+ // If arguments remain after that, the command is invalid
+ double altitude;
+ if (consumedArguments == arguments.length - 1) {
+ // An altitude was passed in
+ try {
+ altitude = parseDouble(arguments[arguments.length - 1]);
+ } catch (NumberFormatException e) {
+ // Altitude is not valid
+ return false;
}
+ } else if (consumedArguments == arguments.length){
+ // We will calculate the world height for each target latter (they may be in different worlds)
+ altitude = Double.NaN;
+ } else {
+ // Arguments would remain, syntax is wrong
+ return false;
+ }
- // -
- if (args.length >= 2) {
- if (player.hasPermission("t+-.tpll")) {
-
- int xOffset = Terraplusminus.config.getInt("terrain_offset.x");
- int yOffset = Terraplusminus.config.getInt("terrain_offset.y");
- int zOffset = Terraplusminus.config.getInt("terrain_offset.z");
- Double minLat = Terraplusminus.config.getDouble("min_latitude");
- Double maxLat = Terraplusminus.config.getDouble("max_latitude");
- Double minLon = Terraplusminus.config.getDouble("min_longitude");
- Double maxLon = Terraplusminus.config.getDouble("max_longitude");
-
- double[] coordinates = new double[2];
- coordinates[1] = Double.parseDouble(args[0].replace(",", "").replace("°", ""));
- coordinates[0] = Double.parseDouble(args[1].replace("°", ""));
-
- double[] mcCoordinates = new double[0];
- try {
- mcCoordinates = bteGeneratorSettings.projection().fromGeo(coordinates[0], coordinates[1]);
- } catch (OutOfProjectionBoundsException e) {
- e.printStackTrace();
- }
-
- if (minLat != 0 && maxLat != 0 && minLon != 0 && maxLon != 0 && !player.hasPermission("t+-.admin")) {
- if (coordinates[1] < minLat || coordinates[0] < minLon || coordinates[1] > maxLat || coordinates[0] > maxLon) {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cYou cannot tpll to these coordinates, because this area is being worked on by another build team.");
- return true;
- }
- }
-
- TerraConnector terraConnector = new TerraConnector();
-
- double height;
- if (args.length >= 3) {
- height = Double.parseDouble(args[2]) + yOffset;
- } else {
- height = terraConnector.getHeight((int) mcCoordinates[0], (int) mcCoordinates[1]).join() + yOffset;
- }
- if (height > player.getWorld().getMaxHeight()) {
- if (Terraplusminus.config.getBoolean("linked_servers.enabled")) {
-
- //send player uuid and coordinates to bungee
-
- ByteArrayDataOutput out = ByteStreams.newDataOutput();
- out.writeUTF(player.getUniqueId().toString());
-
- if (PluginMessageUtil.getNextServerName() != null) {
- out.writeUTF(PluginMessageUtil.getNextServerName());
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cPlease contact server administrator. Your config is not set up correctly.");
- return true;
- }
+ Map worldMap = new HashMap<>();
+ Map> targetMap = new HashMap<>();
- out.writeUTF(coordinates[1] + ", " + coordinates[0]);
- player.sendPluginMessage(Terraplusminus.instance, "bungeecord:terraplusminus", out.toByteArray());
+ // Group targets by world
+ for (Entity target: targets) {
+ World world = target.getWorld();
+ UUID worldId = world.getUID();
+ worldMap.put(worldId, world);
+ targetMap.computeIfAbsent(worldId, u -> new ArrayList<>()).add(target);
+ }
+
+ // Dispatch targets
+ for (Map.Entry> entry: targetMap.entrySet()) {
+ UUID worldId = entry.getKey();
+ World world = worldMap.get(worldId);
+ Collection worldTargets = entry.getValue();
+ try {
+ this.dispatchTargetsInWorld(sender, worldTargets, world, geoLocation, altitude);
+ } catch (CommandException e) {
+ sender.sendMessage(prefix + RED + "Could not teleport " + this.formatTargetList(targets) + ", " + e.getMessage());
+ }
+ }
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cSending to another server...");
- return true;
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cYou cannot tpll to these coordinates, because the world is not high enough at the moment.");
- return true;
- }
- } else if (height <= player.getWorld().getMinHeight()) {
- if (Terraplusminus.config.getBoolean("linked_servers.enabled")) {
+ return true;
+ }
- //send player uuid and coordinates to bungee
+ private void dispatchTargetsInWorld(CommandSender sender, Collection targets, World world, final LatLng geolocation, double altitude) throws CommandException {
- ByteArrayDataOutput out = ByteStreams.newDataOutput();
- out.writeUTF(player.getUniqueId().toString());
+ // Get X and Z
+ Location destination = this.projectGeolocation(world, geolocation, altitude);
- if (PluginMessageUtil.getLastServerName() != null) {
- out.writeUTF(PluginMessageUtil.getLastServerName());
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cPlease contact server administrator. Your config is not set up correctly.");
- return true;
- }
+ double y = destination.getY();
+ boolean tooLow = y < world.getMinHeight();
+ boolean tooHigh = y >= world.getMaxHeight();
- out.writeUTF(coordinates[1] + ", " + coordinates[0]);
- player.sendPluginMessage(Terraplusminus.instance, "bungeecord:terraplusminus", out.toByteArray());
-
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cSending to another server...");
- return true;
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§cYou cannot tpll to these coordinates, because the world is not low enough at the moment.");
- return true;
- }
- }
- Location location = new Location(player.getWorld(), mcCoordinates[0] + xOffset, height, mcCoordinates[1] + zOffset, player.getLocation().getYaw(), player.getLocation().getPitch());
-
- if (PaperLib.isChunkGenerated(location)) {
- if (args.length >= 3) {
- location = new Location(player.getWorld(), mcCoordinates[0] + xOffset, height, mcCoordinates[1] + zOffset, player.getLocation().getYaw(), player.getLocation().getPitch());
- } else {
- location = new Location(player.getWorld(), mcCoordinates[0] + xOffset, player.getWorld().getHighestBlockYAt((int) mcCoordinates[0] + xOffset, (int) mcCoordinates[1] + zOffset) + 1, mcCoordinates[1] + zOffset, player.getLocation().getYaw(), player.getLocation().getPitch());
- }
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Location is generating. Please wait a moment...");
- }
- PaperLib.teleportAsync(player, location);
-
-
- if (args.length >= 3) {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Teleported to " + coordinates[1] + ", " + coordinates[0] + ", " + height + ".");
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Teleported to " + coordinates[1] + ", " + coordinates[0] + ".");
- }
-
- return true;
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7No permission for /tpll");
- return true;
- }
+ String prefix = Terraplusminus.config.getString("prefix");
+
+ if (tooLow || tooHigh) {
+ // Not within world bounds, we either send to a different server or fail
+ if (!Terraplusminus.config.getBoolean("linked_servers.enabled")) {
+ throw new CommandException("the world is not " + (tooLow ? "low" : "high") + " enough at the moment");
+ }
+ final String server;
+ if (tooLow) {
+ server = PluginMessageUtil.getLastServerName();
} else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Usage: /tpll ");
- return true;
+ server = PluginMessageUtil.getNextServerName();
+ }
+ if (server == null) {
+ throw new CommandException("the server configuration is not set up properly, please contact the server administrator");
}
+ targets.forEach(t -> this.dispatchToOtherServer(sender, t, server, geolocation));
+ } else if (!sender.hasPermission("t+-.admin") && !this.isWithinTeleportationBounds(world, geolocation)) {
+ throw new CommandException("you cannot tpll to these coordinates, because this area is being worked on by another build team");
+ } else {
+ if (!isChunkGenerated(destination)) {
+ sender.sendMessage(prefix + "Generating destination in world " + world.getName() + "...");
+ }
+ final Map> futures = new HashMap<>();
+ targets.forEach(
+ target -> futures.put(target, teleportAsync(target, destination))
+ );
+
+ // Wait for all teleportations to complete
+ CompletableFuture.allOf(futures.values().toArray(CompletableFuture[]::new)).thenAccept(unused -> {
+ List success = futures.entrySet().stream()
+ .filter(e -> {
+ try {
+ return e.getValue().get();
+ } catch (InterruptedException | ExecutionException ex) {
+ throw new IllegalStateException();
+ }
+ })
+ .map(Map.Entry::getKey)
+ .collect(toList());
+ List failures = futures.entrySet().stream()
+ .filter(e -> {
+ try {
+ return !e.getValue().get();
+ } catch (InterruptedException | ExecutionException ex) {
+ throw new IllegalStateException();
+ }
+ })
+ .map(Map.Entry::getKey)
+ .collect(toList());
+ if (!failures.isEmpty()) {
+ sender.sendMessage(prefix + RED + "Failed to teleport " + BLUE + this.formatTargetList(success) + RED + " to " + this.formatDestination(geolocation));
+ }
+ if (!success.isEmpty()) {
+ sender.sendMessage(prefix + GRAY + "Teleported " + BLUE + this.formatTargetList(success) + GRAY + " to " + this.formatDestination(geolocation));
+ }
+ });
}
- return true;
}
- public boolean isDouble(String str) {
+ private Location projectGeolocation(World world, LatLng geolocation, double altitude) throws CommandException {
+
+ ChunkGenerator chunkGenerator = world.getGenerator();
+ if (!(chunkGenerator instanceof RealWorldGenerator)) {
+ throw new CommandException("world " + world.getName() + " is not a Terra+- world");
+ }
+
+ RealWorldGenerator generator = (RealWorldGenerator) chunkGenerator;
+
+ // We update this later
+ final Location destination = new Location(world, 0, 0, 0);
+
+ // Set destination X and Z
+ GeographicProjection projection = generator.getSettings().projection();
try {
- Double.parseDouble(str);
- return true;
- } catch (NumberFormatException e) {
- return false;
+ double[] xz = projection.fromGeo(geolocation.getLng(), geolocation.getLat());
+ destination.setX(xz[0]);
+ destination.setZ(xz[1]);
+ } catch (OutOfProjectionBoundsException e) {
+ throw new CommandException("destination is out of projection bounds");
}
+
+ // Set destination Y
+ if (isNaN(altitude)) { // Is set to NaN when not explicitly provided by the command sender
+ destination.setY(world.getHighestBlockYAt(destination));
+ } else {
+ destination.setY(altitude + generator.getYOffset());
+ }
+
+ return destination;
+ }
+
+ @SuppressWarnings("unused") // Passing world as an argument because ideally we could set boundaries per-world
+ private boolean isWithinTeleportationBounds(World world, LatLng geolocation) {
+
+ // Read the configuration
+ double minLat = Terraplusminus.config.getDouble("min_latitude");
+ double maxLat = Terraplusminus.config.getDouble("max_latitude");
+ double minLon = Terraplusminus.config.getDouble("min_longitude");
+ double maxLon = Terraplusminus.config.getDouble("max_longitude");
+
+ // Keeping this for backward compatibility
+ // This is a bit abusive, there are legitimate use cases where the bounds could be 0.
+ // The best approach would probably be to simply make it optional in the config
+ if (minLat == 0d || maxLat == 0d || minLon == 0d || maxLon == 0d) {
+ return true; // No bounds configured
+ }
+
+ // Actual boundary check (bounds inclusive)
+ double latitude = geolocation.getLat();
+ double longitude = geolocation.getLng();
+ return latitude >= minLat && longitude >= minLon & latitude >= maxLat && longitude >= maxLon;
+ }
+
+ private void dispatchToOtherServer(CommandSender sender, Entity target, String server, LatLng geolocation) {
+
+ // Make sure target is a player
+ String prefix = Terraplusminus.config.getString("prefix");
+ if (!(target instanceof Player)) {
+ sender.sendMessage(prefix +
+ RED + "Cannot teleport " + GRAY + formatTargetName(target) +
+ RED + ": destination is outside of range and only players may be sent to linked servers."
+ );
+ return;
+ }
+ Player player = (Player) target;
+
+
+ // Send the message to the proxy using the player's connexion
+ byte[] message = this.encodeTerraLinkDispatchMessage(player, server, geolocation);
+ player.sendPluginMessage(Terraplusminus.instance, "bungeecord:terraplusminus", message);
+
+ // Send feedback to command sender and target
+ sender.sendMessage(prefix +
+ GRAY + "Sending " + DARK_GRAY + formatTargetName(target) +
+ GRAY + " to server " + DARK_GRAY + server + GRAY + "."
+ );
+ player.sendMessage(prefix +
+ RED + "Sending to another server..."
+ );
+
+ }
+
+
+ private String formatTargetList(Collection targets) {
+ if (targets.isEmpty()) {
+ return "";
+ }
+ List names = new ArrayList<>();
+ targets.stream()
+ .map(CommandHelper::formatTargetName)
+ .sorted()
+ .forEach(names::add);
+ if (names.size() == 1) {
+ return names.get(0);
+ }
+ String last = names.remove(names.size() - 1);
+ return join(", ", names) + " and " + last;
+ }
+
+ private String formatDestination(LatLng location) {
+ return
+ BLUE + DECIMAL_FORMATTER.format(location.getLat())
+ + GRAY + ", "
+ + BLUE + DECIMAL_FORMATTER.format(location.getLng());
+ }
+
+ private byte[] encodeTerraLinkDispatchMessage(Player player, String serverName, LatLng destination) {
+ // Keeping it as is for backward compatibility,
+ // but we could cut down message length by more than half by sending doubles and longs directly
+ // That would also eliminate the cost of encoding numbers to UTF-8 and having to send string length information
+ // Also, relying on a beta functionality is not ideal when there are viable alternatives in the JDK
+ ByteArrayDataOutput out = ByteStreams.newDataOutput();
+ out.writeUTF(player.getUniqueId().toString());
+ out.writeUTF(serverName);
+ String coordinateString = destination.getLat() + ", " + destination.getLng();
+ out.writeUTF(coordinateString);
+ return out.toByteArray();
+ }
+
+ @Nullable
+ @Override
+ public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
+
+ if (!sender.hasPermission("t+-.tpll")) {
+ return emptyList();
+ }
+
+ // First argument: target
+ if (args.length == 1) {
+ if (sender.hasPermission("t+-.forcetpll")) {
+ return Bukkit.getOnlinePlayers().stream()
+ .filter(Terraplusminus::isInTerraWorld)
+ .map(Player::getName)
+ .collect(toList());
+ } else if (sender instanceof Player && isInTerraWorld((Player) sender)) {
+ return singletonList(sender.getName());
+ }
+ }
+
+ // We can't tab-complete the location or altitude, so we are done here
+ return emptyList();
}
}
diff --git a/src/main/java/de/btegermany/terraplusminus/commands/WhereCommand.java b/src/main/java/de/btegermany/terraplusminus/commands/WhereCommand.java
index 6b25cef..b89d75a 100644
--- a/src/main/java/de/btegermany/terraplusminus/commands/WhereCommand.java
+++ b/src/main/java/de/btegermany/terraplusminus/commands/WhereCommand.java
@@ -1,54 +1,116 @@
package de.btegermany.terraplusminus.commands;
import de.btegermany.terraplusminus.Terraplusminus;
-import net.buildtheearth.terraminusminus.generator.EarthGeneratorSettings;
+import de.btegermany.terraplusminus.gen.RealWorldGenerator;
+import net.buildtheearth.terraminusminus.projection.GeographicProjection;
import net.buildtheearth.terraminusminus.projection.OutOfProjectionBoundsException;
+import net.buildtheearth.terraminusminus.util.geo.LatLng;
import net.md_5.bungee.api.chat.ClickEvent;
-import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
+import net.md_5.bungee.api.chat.hover.content.Text;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
+import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
-public class WhereCommand implements CommandExecutor {
+import java.text.DecimalFormat;
+import java.util.Collection;
+import java.util.List;
- private final EarthGeneratorSettings bteGeneratorSettings = EarthGeneratorSettings.parse(EarthGeneratorSettings.BTE_DEFAULT_SETTINGS);
+import static de.btegermany.terraplusminus.commands.CommandHelper.*;
+import static java.util.Collections.*;
+import static java.util.Objects.requireNonNullElse;
+import static java.util.stream.Collectors.toList;
+import static org.bukkit.ChatColor.*;
+
+public class WhereCommand implements TabExecutor {
+
+ private static final DecimalFormat DECIMAL_FORMATTER = new DecimalFormat("##.#####");
@Override
- public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) {
- if (command.getName().equalsIgnoreCase("where")) {
- if (!(commandSender instanceof Player)) {
- commandSender.sendMessage("This command can only be used by players!");
- return true;
- }
- Player player = (Player) commandSender;
- if (player.hasPermission("t+-.where")) {
- int xOffset = Terraplusminus.config.getInt("terrain_offset.x");
- int zOffset = Terraplusminus.config.getInt("terrain_offset.z");
-
- double[] mcCoordinates = new double[2];
- mcCoordinates[0] = player.getLocation().getX() - xOffset;
- mcCoordinates[1] = player.getLocation().getZ() - zOffset;
- System.out.println(mcCoordinates[0] + ", " + mcCoordinates[1]);
- double[] coordinates = new double[0];
- try {
- coordinates = bteGeneratorSettings.projection().toGeo(mcCoordinates[0], mcCoordinates[1]);
- } catch (OutOfProjectionBoundsException e) {
- e.printStackTrace();
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s, @NotNull String[] args) {
+ String prefix = Terraplusminus.config.getString("prefix");
+
+ if (!sender.hasPermission("t+-.where")) {
+ sender.sendMessage(prefix + GRAY + "No permission for /where");
+ return true;
+ }
+
+ // Parse targets
+ final Collection targets;
+ if (args.length == 0 && sender instanceof Player) {
+ targets = singleton((Player) sender);
+ } else if (args.length == 1) {
+ try {
+ targets = parseTargetSelector(sender, args[0]);
+ if (!sender.hasPermission("t+-.admin") && !senderIsSoleTarget(sender, targets)) {
+ String message = requireNonNullElse(command.getPermissionMessage(), RED + "Missing permission");
+ sender.sendMessage(message);
+ return true;
}
- TextComponent message = new TextComponent(Terraplusminus.config.getString("prefix") + "§7Your coordinates are:");
- message.addExtra("\n§8" + coordinates[1] + ", " + coordinates[0] + "§7.");
- message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://maps.google.com/maps?t=k&q=loc:" + coordinates[1] + "+" + coordinates[0]));
- message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Click here to view in Google Maps.").create()));
- player.spigot().sendMessage(message);
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7No permission for /where");
- return true;
+ } catch (CommandHelper.InvalidTargetSelectorException ignored) {
+ return false;
}
+ } else {
+ return false;
+ }
+
+ // Do the magic
+ for (Entity target: targets) {
+ final TextComponent message = new TextComponent(prefix);
+ try {
+ LatLng geolocation = this.getGeolocation(target);
+ String googleMapsUrl = "https://www.google.com/maps/@" + geolocation.getLat() + "," + geolocation.getLng();
+ String targetVerb = target == sender ? "Your": formatTargetName(target) + "'s";
+ message.addExtra(GRAY + targetVerb + " coordinates are:\n" + DARK_GRAY + DECIMAL_FORMATTER.format(geolocation.getLat()) + ", " + DECIMAL_FORMATTER.format(geolocation.getLng()) + GRAY + ".");
+ message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, googleMapsUrl));
+ message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(GRAY + "Click here to view in Google Maps.")));
+ } catch (OutOfProjectionBoundsException e) {
+ String targetVerb = target == sender? "You are": formatTargetName(target) + " is";
+ message.addExtra(RED + targetVerb + " not in a Terra+- world or outside projection bounds.");
+ }
+ sender.spigot().sendMessage(message);
}
return true;
}
+
+ private LatLng getGeolocation(Entity target) throws OutOfProjectionBoundsException {
+ World world = target.getWorld();
+ ChunkGenerator chunkGenerator = world.getGenerator();
+ if (!(chunkGenerator instanceof RealWorldGenerator)) {
+ throw OutOfProjectionBoundsException.get();
+ }
+ RealWorldGenerator generator = (RealWorldGenerator) chunkGenerator;
+ GeographicProjection projection = generator.getSettings().projection();
+ Location location = target.getLocation();
+ double[] coordinates = projection.toGeo(location.getX(), location.getZ());
+ return new LatLng(coordinates[1], coordinates[0]);
+ }
+
+ @Nullable
+ @Override
+ public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
+
+ if (args.length > 1 || !sender.hasPermission("t+-.where")) {
+ return emptyList();
+ }
+
+ if (sender.hasPermission("t+-.admin")) {
+ return Bukkit.getOnlinePlayers().stream()
+ .map(Player::getName)
+ .collect(toList());
+ } else if (sender instanceof Player) {
+ return singletonList(sender.getName());
+ }
+
+ return emptyList();
+ }
}
diff --git a/src/main/java/de/btegermany/terraplusminus/events/PlayerMoveEvent.java b/src/main/java/de/btegermany/terraplusminus/events/PlayerMoveEvent.java
index d6f343a..5da9345 100644
--- a/src/main/java/de/btegermany/terraplusminus/events/PlayerMoveEvent.java
+++ b/src/main/java/de/btegermany/terraplusminus/events/PlayerMoveEvent.java
@@ -4,6 +4,7 @@
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -12,6 +13,9 @@
import java.util.ArrayList;
+import static java.lang.String.valueOf;
+import static org.bukkit.ChatColor.BOLD;
+
public class PlayerMoveEvent implements Listener {
@@ -33,7 +37,7 @@ void onPlayerMove(org.bukkit.event.player.PlayerMoveEvent event) {
@Override
public void run() {
int height = p.getLocation().getBlockY() - yOffset;
- p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent("§l" + height + "m"));
+ p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(BOLD + valueOf(height) + "m"));
}
};
runnable.runTaskTimer(plugin, 0, 20);
diff --git a/src/main/java/de/btegermany/terraplusminus/gen/RealWorldGenerator.java b/src/main/java/de/btegermany/terraplusminus/gen/RealWorldGenerator.java
index 0ab19dd..dc37f6c 100644
--- a/src/main/java/de/btegermany/terraplusminus/gen/RealWorldGenerator.java
+++ b/src/main/java/de/btegermany/terraplusminus/gen/RealWorldGenerator.java
@@ -5,6 +5,7 @@
import de.btegermany.terraplusminus.Terraplusminus;
import de.btegermany.terraplusminus.gen.tree.TreePopulator;
import de.btegermany.terraplusminus.utils.ConfigurationHelper;
+import lombok.Getter;
import net.buildtheearth.terraminusminus.generator.CachedChunkData;
import net.buildtheearth.terraminusminus.generator.ChunkDataLoader;
import net.buildtheearth.terraminusminus.generator.EarthGeneratorSettings;
@@ -37,12 +38,16 @@
public class RealWorldGenerator extends ChunkGenerator {
+
+ @Getter
+ private final EarthGeneratorSettings settings;
+ @Getter
+ private final int yOffset;
private Location spawnLocation = null;
- public LoadingCache> cache;
+ private final LoadingCache> cache;
private final CustomBiomeProvider customBiomeProvider;
- private final int yOffset;
private final Material surfaceMaterial;
private final Map materialMapping;
@@ -66,13 +71,13 @@ public RealWorldGenerator() {
);
this.yOffset = Terraplusminus.config.getInt("terrain_offset.y");
- settings = settings.withProjection(projection);
+ this.settings = settings.withProjection(projection);
this.customBiomeProvider = new CustomBiomeProvider();
this.cache = CacheBuilder.newBuilder()
.expireAfterAccess(5L, TimeUnit.MINUTES)
.softValues()
- .build(new ChunkDataLoader(settings));
+ .build(new ChunkDataLoader(this.settings));
this.surfaceMaterial = ConfigurationHelper.getMaterial(Terraplusminus.config, "surface_material", GRASS_BLOCK);
this.materialMapping = Map.of(
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 4c496ed..c87db84 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -11,12 +11,12 @@ load: STARTUP
commands:
tpll:
description: Teleports you to longitude and latitude
- usage: /tpll
+ usage: /tpll [target]
aliases: [ tpc ]
where:
description: Gives you the longitude and latitude of your minecraft coordinates
- usage: /where
+ usage: /where [target]
offset:
description: Displays the x,y and z offset of your world
- usage: /offset
+ usage: /offset [world]