/** * This file is part of Skript. * * Skript 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. * * Skript 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 Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2017 Peter Güttinger and contributors */ package ch.njol.skript.util; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Creature; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.Messenger; import org.bukkit.plugin.messaging.PluginMessageListener; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.Nullable; import com.google.common.collect.Iterables; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import ch.njol.skript.Skript; import ch.njol.skript.effects.EffTeleport; import ch.njol.skript.entity.EntityData; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.LanguageChangeListener; import ch.njol.skript.registrations.Classes; import ch.njol.util.Callback; import ch.njol.util.NonNullPair; import ch.njol.util.Pair; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; /** * Utility class. * * @author Peter Güttinger */ public abstract class Utils { private Utils() {} public final static Random random = new Random(); public static String join(final Object[] objects) { assert objects != null; final StringBuilder b = new StringBuilder(); for (int i = 0; i < objects.length; i++) { if (i != 0) b.append(", "); b.append(Classes.toString(objects[i])); } return "" + b.toString(); } public static String join(final Iterable<?> objects) { assert objects != null; final StringBuilder b = new StringBuilder(); boolean first = true; for (final Object o : objects) { if (!first) b.append(", "); else first = false; b.append(Classes.toString(o)); } return "" + b.toString(); } public static <T> boolean isEither(@Nullable T compared, @Nullable T... types) { return CollectionUtils.contains(types, compared); } /** * Gets an entity's target. * * @param entity The entity to get the target of * @param type Can be null for any entity * @return The entity's target */ @SuppressWarnings("unchecked") @Nullable public static <T extends Entity> T getTarget(final LivingEntity entity, @Nullable final EntityData<T> type) { if (entity instanceof Creature) { return ((Creature) entity).getTarget() == null || type != null && !type.isInstance(((Creature) entity).getTarget()) ? null : (T) ((Creature) entity).getTarget(); } T target = null; double targetDistanceSquared = 0; final double radiusSquared = 1; final Vector l = entity.getEyeLocation().toVector(), n = entity.getLocation().getDirection().normalize(); final double cos45 = Math.cos(Math.PI / 4); for (final T other : type == null ? (List<T>) entity.getWorld().getEntities() : entity.getWorld().getEntitiesByClass(type.getType())) { if (other == null || other == entity || type != null && !type.isInstance(other)) continue; if (target == null || targetDistanceSquared > other.getLocation().distanceSquared(entity.getLocation())) { final Vector t = other.getLocation().add(0, 1, 0).toVector().subtract(l); if (n.clone().crossProduct(t).lengthSquared() < radiusSquared && t.normalize().dot(n) >= cos45) { target = other; targetDistanceSquared = target.getLocation().distanceSquared(entity.getLocation()); } } } return target; } public static Pair<String, Integer> getAmount(final String s) { if (s.matches("\\d+ of .+")) { return new Pair<>(s.split(" ", 3)[2], Utils.parseInt("" + s.split(" ", 2)[0])); } else if (s.matches("\\d+ .+")) { return new Pair<>(s.split(" ", 2)[1], Utils.parseInt("" + s.split(" ", 2)[0])); } else if (s.matches("an? .+")) { return new Pair<>(s.split(" ", 2)[1], 1); } return new Pair<>(s, Integer.valueOf(-1)); } // public final static class AmountResponse { // public final String s; // public final int amount; // public final boolean every; // // public AmountResponse(final String s, final int amount, final boolean every) { // this.s = s; // this.amount = amount; // this.every = every; // } // // public AmountResponse(final String s, final boolean every) { // this.s = s; // amount = -1; // this.every = every; // } // // public AmountResponse(final String s, final int amount) { // this.s = s; // this.amount = amount; // every = false; // } // // public AmountResponse(final String s) { // this.s = s; // amount = -1; // every = false; // } // } // // public final static AmountResponse getAmountWithEvery(final String s) { // if (s.matches("\\d+ of (all|every) .+")) { // return new AmountResponse("" + s.split(" ", 4)[3], Utils.parseInt("" + s.split(" ", 2)[0]), true); // } else if (s.matches("\\d+ of .+")) { // return new AmountResponse("" + s.split(" ", 3)[2], Utils.parseInt("" + s.split(" ", 2)[0])); // } else if (s.matches("\\d+ .+")) { // return new AmountResponse("" + s.split(" ", 2)[1], Utils.parseInt("" + s.split(" ", 2)[0])); // } else if (s.matches("an? .+")) { // return new AmountResponse("" + s.split(" ", 2)[1], 1); // } else if (s.matches("(all|every) .+")) { // return new AmountResponse("" + s.split(" ", 2)[1], true); // } // return new AmountResponse(s); // } private final static String[][] plurals = { {"fe", "ves"},// most -f words' plurals can end in -fs as well as -ves {"axe", "axes"}, {"x", "xes"}, {"ay", "ays"}, {"ey", "eys"}, {"iy", "iys"}, {"oy", "oys"}, {"uy", "uys"}, {"kie", "kies"}, {"zombie", "zombies"}, {"y", "ies"}, {"h", "hes"}, {"man", "men"}, {"us", "i"}, {"hoe", "hoes"}, {"toe", "toes"}, {"o", "oes"}, {"alias", "aliases"}, {"gas", "gases"}, {"child", "children"}, {"sheep", "sheep"}, // general ending {"", "s"}, }; /** * @param s trimmed string * @return Pair of singular string + boolean whether it was plural */ @SuppressWarnings("null") public static NonNullPair<String, Boolean> getEnglishPlural(final String s) { assert s != null; if (s.isEmpty()) return new NonNullPair<>("", Boolean.FALSE); for (final String[] p : plurals) { if (s.endsWith(p[1])) return new NonNullPair<>(s.substring(0, s.length() - p[1].length()) + p[0], Boolean.TRUE); if (s.endsWith(p[1].toUpperCase())) return new NonNullPair<>(s.substring(0, s.length() - p[1].length()) + p[0].toUpperCase(), Boolean.TRUE); } return new NonNullPair<>(s, Boolean.FALSE); } /** * Gets the english plural of a word. * * @param s * @return The english plural of the given word */ public static String toEnglishPlural(final String s) { assert s != null && s.length() != 0; for (final String[] p : plurals) { if (s.endsWith(p[0])) return s.substring(0, s.length() - p[0].length()) + p[1]; } assert false; return s + "s"; } /** * Gets the plural of a word (or not if p is false) * * @param s * @param p * @return The english plural of the given word, or the word itself if p is false. */ public static String toEnglishPlural(final String s, final boolean p) { if (p) return toEnglishPlural(s); return s; } /** * Adds 'a' or 'an' to the given string, depending on the first character of the string. * * @param s The string to add the article to * @return The given string with an appended a/an and a space at the beginning * @see #A(String) * @see #a(String, boolean) */ public static String a(final String s) { return a(s, false); } /** * Adds 'A' or 'An' to the given string, depending on the first character of the string. * * @param s The string to add the article to * @return The given string with an appended A/An and a space at the beginning * @see #a(String) * @see #a(String, boolean) */ public static String A(final String s) { return a(s, true); } /** * Adds 'a' or 'an' to the given string, depending on the first character of the string. * * @param s The string to add the article to * @param capA Whether to use a capital a or not * @return The given string with an appended a/an (or A/An if capA is true) and a space at the beginning * @see #a(String) */ public static String a(final String s, final boolean capA) { assert s != null && s.length() != 0; if ("aeiouAEIOU".indexOf(s.charAt(0)) != -1) { if (capA) return "An " + s; return "an " + s; } else { if (capA) return "A " + s; return "a " + s; } } /** * Gets the collision height of solid or partially-solid blocks at the center of the block. * This is mostly for use in the {@link EffTeleport teleport effect}. * <p> * This version operates on numeric ids, thus only working on * Minecraft 1.12 or older. * * @param type * @return The block's height at the center */ public static double getBlockHeight(final int type, final byte data) { switch (type) { case 26: // bed return 9. / 16; case 44: // slabs case 126: return (data & 0x8) == 0 ? 0.5 : 1; case 78: // snow layer return data == 0 ? 1 : (data % 8) * 2. / 16; case 85: // fences & gates case 107: case 113: case 139: // cobblestone wall return 1.5; case 88: // soul sand return 14. / 16; case 92: // cake return 7. / 16; case 93: // redstone repeater case 94: case 149: // redstone comparator case 150: return 2. / 16; case 96: // trapdoor return (data & 0x4) == 0 ? ((data & 0x8) == 0 ? 3. / 16 : 1) : 0; case 116: // enchantment table return 12. / 16; case 117: // brewing stand return 14. / 16; case 118: // cauldron return 5. / 16; case 120: // end portal frame return (data & 0x4) == 0 ? 13. / 16 : 1; case 127: // cocoa plant return 12. / 16; case 140: // flower pot return 6. / 16; case 144: // mob head return 0.5; case 151: // daylight sensor return 6. / 16; case 154: // hopper return 10. / 16; default: return 1; } } /** * Sends a plugin message using the first player from {@link Bukkit#getOnlinePlayers()}. * * The next plugin message to be received through {@code channel} will be assumed to be * the response. * * @param channel the channel for this plugin message * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if no players are online. */ public static CompletableFuture<ByteArrayDataInput> sendPluginMessage(String channel, String... data) { return sendPluginMessage(channel, r -> true, data); } /** * Sends a plugin message using the from {@code player}. * * The next plugin message to be received through {@code channel} will be assumed to be * the response. * * @param player the player to send the plugin message through * @param channel the channel for this plugin message * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if no players are online. */ public static CompletableFuture<ByteArrayDataInput> sendPluginMessage(Player player, String channel, String... data) { return sendPluginMessage(player, channel, r -> true, data); } /** * Sends a plugin message using the first player from {@link Bukkit#getOnlinePlayers()}. * * @param channel the channel for this plugin message * @param messageVerifier verifies that a plugin message is the response to the sent message * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if the player is null. */ public static CompletableFuture<ByteArrayDataInput> sendPluginMessage(String channel, Predicate<ByteArrayDataInput> messageVerifier, String... data) { Player firstPlayer = Iterables.getFirst(Bukkit.getOnlinePlayers(), null); return sendPluginMessage(firstPlayer, channel, messageVerifier, data); } /** * Sends a plugin message. * * Example usage using the "GetServers" bungee plugin message channel via an overload: * <code> * Utils.sendPluginMessage("BungeeCord", r -> "GetServers".equals(r.readUTF()), "GetServers") * .thenAccept(response -> Bukkit.broadcastMessage(response.readUTF()) // comma delimited server broadcast * .exceptionally(ex -> { * Skript.warning("Failed to get servers because there are no players online"); * return null; * }); * </code> * * @param player the player to send the plugin message through * @param channel the channel for this plugin message * @param messageVerifier verifies that a plugin message is the response to the sent message * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if the player is null. */ public static CompletableFuture<ByteArrayDataInput> sendPluginMessage(Player player, String channel, Predicate<ByteArrayDataInput> messageVerifier, String... data) { CompletableFuture<ByteArrayDataInput> completableFuture = new CompletableFuture<>(); if (player == null) { completableFuture.completeExceptionally(new IllegalStateException("Can't send plugin messages from a null player")); return completableFuture; } Skript skript = Skript.getInstance(); Messenger messenger = Bukkit.getMessenger(); messenger.registerOutgoingPluginChannel(skript, channel); PluginMessageListener listener = (sendingChannel, sendingPlayer, message) -> { ByteArrayDataInput input = ByteStreams.newDataInput(message); if (channel.equals(sendingChannel) && sendingPlayer == player && !completableFuture.isDone() && !completableFuture.isCancelled() && messageVerifier.test(input)) { completableFuture.complete(input); } }; messenger.registerIncomingPluginChannel(skript, channel, listener); completableFuture.whenComplete((r, ex) -> messenger.unregisterIncomingPluginChannel(skript, channel, listener)); // if we haven't gotten a response after a minute, let's just assume there wil never be one Bukkit.getScheduler().scheduleSyncDelayedTask(skript, () -> { if (!completableFuture.isDone()) completableFuture.cancel(true); }, 60 * 20); ByteArrayDataOutput out = ByteStreams.newDataOutput(); Stream.of(data).forEach(out::writeUTF); player.sendPluginMessage(Skript.getInstance(), channel, out.toByteArray()); return completableFuture; } final static ChatColor[] styles = {ChatColor.BOLD, ChatColor.ITALIC, ChatColor.STRIKETHROUGH, ChatColor.UNDERLINE, ChatColor.MAGIC, ChatColor.RESET}; final static Map<String, String> chat = new HashMap<>(); final static Map<String, String> englishChat = new HashMap<>(); static { Language.addListener(new LanguageChangeListener() { @Override public void onLanguageChange() { final boolean english = englishChat.isEmpty(); chat.clear(); for (final ChatColor style : styles) { for (final String s : Language.getList("chat styles." + style.name())) { chat.put(s.toLowerCase(), style.toString()); if (english) englishChat.put(s.toLowerCase(), style.toString()); } } } }); } @Nullable public static String getChatStyle(final String s) { SkriptColor color = SkriptColor.fromName(s); if (color != null) return color.getFormattedChat(); return chat.get(s); } private final static Pattern stylePattern = Pattern.compile("<([^<>]+)>"); /** * Replaces <chat styles> in the message * * @param message * @return message with localised chat styles converted to Minecraft's format */ public static String replaceChatStyles(final String message) { if (message.isEmpty()) return message; String m = StringUtils.replaceAll("" + message.replace("<<none>>", ""), stylePattern, new Callback<String, Matcher>() { @Override public String run(final Matcher m) { SkriptColor color = SkriptColor.fromName("" + m.group(1)); if (color != null) return color.getFormattedChat(); final String f = chat.get(m.group(1).toLowerCase()); if (f != null) return f; return "" + m.group(); } }); assert m != null; m = ChatColor.translateAlternateColorCodes('&', "" + m); return "" + m; } /** * Replaces english <chat styles> in the message. This is used for messages in the language file as the language of colour codes is not well defined while the language is * changing, and for some hardcoded messages. * * @param message * @return message with english chat styles converted to Minecraft's format */ public static String replaceEnglishChatStyles(final String message) { if (message.isEmpty()) return message; String m = StringUtils.replaceAll(message, stylePattern, new Callback<String, Matcher>() { @Override public String run(final Matcher m) { SkriptColor color = SkriptColor.fromName("" + m.group(1)); if (color != null) return color.getFormattedChat(); final String f = englishChat.get(m.group(1).toLowerCase()); if (f != null) return f; return "" + m.group(); } }); assert m != null; m = ChatColor.translateAlternateColorCodes('&', "" + m); return "" + m; } /** * Gets a random value between <tt>start</tt> (inclusive) and <tt>end</tt> (exclusive) * * @param start * @param end * @return <tt>start + random.nextInt(end - start)</tt> */ public static int random(final int start, final int end) { if (end <= start) throw new IllegalArgumentException("end (" + end + ") must be > start (" + start + ")"); return start + random.nextInt(end - start); } // TODO improve public static Class<?> getSuperType(final Class<?>... cs) { assert cs.length > 0; Class<?> r = cs[0]; assert r != null; outer: for (final Class<?> c : cs) { assert c != null && !c.isArray() && !c.isPrimitive() : c; if (c.isAssignableFrom(r)) { r = c; continue; } if (!r.isAssignableFrom(c)) { Class<?> s = c; while ((s = s.getSuperclass()) != null) { if (s != Object.class && s.isAssignableFrom(r)) { r = s; continue outer; } } for (final Class<?> i : c.getInterfaces()) { s = getSuperType(i, r); if (s != Object.class) { r = s; continue outer; } } return Object.class; } } // Cloneable is about as useful as object as super type // However, it lacks special handling used for Object supertype // See #1747 to learn how it broke returning items from functions return r.equals(Cloneable.class) ? Object.class : r; } /** * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} when parsed with {@link Integer#parseInt(String)} due to * overflow. * This method will return {@link Integer#MIN_VALUE} or {@link Integer#MAX_VALUE} respectively if that happens. * * @param s * @return The parsed integer, {@link Integer#MIN_VALUE} or {@link Integer#MAX_VALUE} respectively */ public static int parseInt(final String s) { assert s.matches("-?\\d+"); try { return Integer.parseInt(s); } catch (final NumberFormatException e) { return s.startsWith("-") ? Integer.MIN_VALUE : Integer.MAX_VALUE; } } /** * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} when parsed with {@link Long#parseLong(String)} due to * overflow. * This method will return {@link Long#MIN_VALUE} or {@link Long#MAX_VALUE} respectively if that happens. * * @param s * @return The parsed long, {@link Long#MIN_VALUE} or {@link Long#MAX_VALUE} respectively */ public static long parseLong(final String s) { assert s.matches("-?\\d+"); try { return Long.parseLong(s); } catch (final NumberFormatException e) { return s.startsWith("-") ? Long.MIN_VALUE : Long.MAX_VALUE; } } /** * Gets class for name. Throws RuntimeException instead of checked one. * Use this only when absolutely necessary. * @param name Class name. * @return The class. */ public static Class<?> classForName(String name) { Class<?> c; try { c = Class.forName(name); return c; } catch (ClassNotFoundException e) { throw new RuntimeException("Class not found!"); } } }