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..a6231b2
--- /dev/null
+++ b/src/main/java/de/btegermany/terraplusminus/commands/CommandHelper.java
@@ -0,0 +1,80 @@
+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.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Comparator.comparing;
+
+/**
+ * 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();
+
+ }
+
+ public static class InvalidTargetSelectorException extends Exception {
+
+ }
+
+ private CommandHelper() {
+ throw new IllegalStateException();
+ }
+
+}
diff --git a/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java b/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
index 42d4e0a..ea0c913 100644
--- a/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
+++ b/src/main/java/de/btegermany/terraplusminus/commands/TpllCommand.java
@@ -3,235 +3,347 @@
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.Location;
+import org.bukkit.World;
import org.bukkit.command.Command;
+import org.bukkit.command.CommandException;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
-import static org.bukkit.ChatColor.RED;
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static de.btegermany.terraplusminus.commands.CommandHelper.InvalidTargetSelectorException;
+import static de.btegermany.terraplusminus.commands.CommandHelper.parseTargetSelector;
+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.Objects.requireNonNullElseGet;
+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 CommandExecutor {
+ 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 sender is not a player cancel the command.
- if (!(commandSender instanceof Player)) {
- commandSender.sendMessage("This command can only be used by players!");
+ 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;
}
- 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;
- }
+ final String prefix = Terraplusminus.config.getString("prefix"); // Used in feedback messages
- 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;
- }
+ // 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;
}
- //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");
+ 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
+ }
+ }
+ if (targets == null) {
+ if (sender instanceof Player) {
+ targets = Collections.singleton((Player) sender);
} else {
- player.chat("/" + passthroughTpll + ":tpll " + String.join(" ", args));
+ return false; // Invalid command, non player senders must specify a valid target
}
+ } else if ((targets.size() > 1 || !targets.contains(sender)) && !sender.hasPermission("t+-.forcetpll")){
+ // Sender specified a target which is not themselves
+ sender.sendMessage(
+ prefix
+ + RED + "You do not have permission to use TPLL on others"
+ );
return true;
}
- // -
- if (args.length >= 2) {
- if (player.hasPermission("t+-.tpll")) {
+ // 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;
+ }
- 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");
+ Map worldMap = new HashMap<>();
+ Map> targetMap = new HashMap<>();
- double[] coordinates = new double[2];
- coordinates[1] = Double.parseDouble(args[0].replace(",", "").replace("°", ""));
- coordinates[0] = Double.parseDouble(args[1].replace("°", ""));
+ // 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);
+ }
- ChunkGenerator generator = player.getWorld().getGenerator();
- if (!(generator instanceof RealWorldGenerator)) {
- commandSender.sendMessage(RED + "Must be in a Terra 1 to 1 world!");
- return true;
- }
- RealWorldGenerator terraGenerator = (RealWorldGenerator) generator;
- EarthGeneratorSettings generatorSettings = terraGenerator.getSettings();
- GeographicProjection projection = generatorSettings.projection();
- int yOffset = terraGenerator.getYOffset();
-
- double[] mcCoordinates;
- try {
- mcCoordinates = projection.fromGeo(coordinates[0], coordinates[1]);
- } catch (OutOfProjectionBoundsException e) {
- commandSender.sendMessage(RED + "Location is not within projection bounds");
- return true;
- }
+ // 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());
+ }
+ }
- 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") + RED + "You cannot tpll to these coordinates, because this area is being worked on by another build team.");
- return true;
- }
- }
+ return true;
+ }
- TerraConnector terraConnector = new TerraConnector();
+ private void dispatchTargetsInWorld(CommandSender sender, Collection targets, World world, final LatLng geolocation, double altitude) throws CommandException {
- 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;
- }
-
- 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 high enough at the moment.");
- return true;
- }
- } else if (height <= player.getWorld().getMinHeight()) {
- 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.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;
- }
-
- 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], height, mcCoordinates[1], player.getLocation().getYaw(), player.getLocation().getPitch());
-
- if (PaperLib.isChunkGenerated(location)) {
- if (args.length >= 3) {
- location = new Location(player.getWorld(), mcCoordinates[0], height, mcCoordinates[1], player.getLocation().getYaw(), player.getLocation().getPitch());
- } else {
- location = new Location(player.getWorld(), mcCoordinates[0], player.getWorld().getHighestBlockYAt((int) mcCoordinates[0], (int) mcCoordinates[1]) + 1, mcCoordinates[1], player.getLocation().getYaw(), player.getLocation().getPitch());
- }
- } else {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Location is generating. Please wait a moment...");
- }
- PaperLib.teleportAsync(player, location);
+ // Get X and Z
+ Location destination = this.projectGeolocation(world, geolocation, altitude);
+ double y = destination.getY();
+ boolean tooLow = y < world.getMinHeight();
+ boolean tooHigh = y >= world.getMaxHeight();
- 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] + ".");
- }
+ String prefix = Terraplusminus.config.getString("prefix");
- return true;
+ 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") + "§7No permission for /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 {
- player.sendMessage(Terraplusminus.config.getString("prefix") + "§7Usage: /tpll ");
- return true;
+ 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 §9" + this.formatTargetList(success) + RED + " to " + this.formatDestination(geolocation));
+ }
+ if (!success.isEmpty()) {
+ sender.sendMessage(prefix + "§7Teleported §9" + this.formatTargetList(success) + " §7to " + this.formatDestination(geolocation));
+ }
+ });
}
}
- 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 + this.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 + this.formatTargetName(target) +
+ GRAY + " to server " + DARK_GRAY + server + GRAY + "."
+ );
+ player.sendMessage(prefix +
+ "§cSending to another server..."
+ );
+
+ }
+
+ private String formatTargetName(Entity target) {
+ return requireNonNullElseGet(target.getCustomName(), target::getName);
+ }
+
+ private String formatTargetList(Collection targets) {
+ if (targets.isEmpty()) {
+ return "";
+ }
+ List names = new ArrayList<>();
+ targets.stream()
+ .map(this::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();
+ }
+
}