diff --git a/jitpack.yml b/jitpack.yml index 160a92e81..ff0936c49 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ before_install: - wget https://github.com/sormuras/bach/raw/master/install-jdk.sh - - source ./install-jdk.sh --feature 18 + - source ./install-jdk.sh --feature 19 jdk: - - openjdk18 \ No newline at end of file + - openjdk19 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 498be205b..4088f791a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,16 +1,14 @@ - - + 4.0.0 org.mineacademy Foundation - 6.1.0 + 6.2.1 jar Foundation @@ -33,6 +31,7 @@ + org.projectlombok @@ -49,13 +48,13 @@ provided - + org.spigotmc spigot-api 1.19.2-R0.1-SNAPSHOT - + org.mineacademy.plugin @@ -66,11 +65,11 @@ org.mineacademy.plugin BentoBox 1.20.1 - + org.mineacademy.plugin CitizensAPI - 2.0.29-b839 + 2.0.30-b879 org.mineacademy.plugin @@ -155,12 +154,12 @@ org.mineacademy.plugin WorldEdit - 7.2.10 + 7.2.12 org.mineacademy.plugin WorldGuard - 7.0.7 + 7.0.5 @@ -179,6 +178,24 @@ ${java.version} + + org.projectlombok + lombok-maven-plugin + 1.18.20.0 + + ${project.basedir}/src/main/java + ${delombok.output} + false + + + + generate-sources + + delombok + + + + org.apache.maven.plugins maven-javadoc-plugin diff --git a/src/main/java/org/mineacademy/fo/BungeeUtil.java b/src/main/java/org/mineacademy/fo/BungeeUtil.java index 4a7958ecb..7a1b1c1bb 100644 --- a/src/main/java/org/mineacademy/fo/BungeeUtil.java +++ b/src/main/java/org/mineacademy/fo/BungeeUtil.java @@ -18,8 +18,6 @@ import org.mineacademy.fo.plugin.SimplePlugin; import org.mineacademy.fo.remain.Remain; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.util.UUID; /** @@ -73,9 +71,10 @@ public static void sendPluginMessage(String channel, BungeeMessageType actio * * OBS! The data written always start with: * - * 1. The recipient UUID - * 2. {@link Remain#getServerName()} - * 3. The action parameter + * 1. The channel name (to avoid "Unknown custom packed identifier: plugin:chcred" console spam we use "BungeeCord") + * 2. The recipient UUID + * 3. {@link Remain#getServerName()} + * 4. The action parameter * * * @param @@ -104,6 +103,7 @@ public static void sendPluginMessage(Player sender, String channel, BungeeMe final ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF(channel); out.writeUTF(sender.getUniqueId().toString()); out.writeUTF(Remain.getServerName()); out.writeUTF(action.toString()); @@ -197,7 +197,7 @@ public static void sendPluginMessage(Player sender, String channel, BungeeMe final byte[] byteArray = out.toByteArray(); try { - sender.sendPluginMessage(SimplePlugin.getInstance(), channel, byteArray); + sender.sendPluginMessage(SimplePlugin.getInstance(), "BungeeCord", byteArray); } catch (final ChannelNotRegisteredException ex) { Common.log("Cannot send Bungee '" + action + "' message because channel '" + channel + "' is not registered. " @@ -210,6 +210,18 @@ public static void sendPluginMessage(Player sender, String channel, BungeeMe actionHead = 0; } + /** + * Sends a plugin message that will re-connect the player to another server on Bungee + * + * @param player the living non-dead player + * @param serverName the server name as you have in config.yml of your BungeeCord + */ + public static void connect(@NonNull Player player, @NonNull String serverName) { + sendBungeeMessage(player, + "Connect", + serverName); + } + /** * Sends message via a channel to the bungee network (upstreams). You need an * implementation in bungee to handle it, otherwise nothing will happen. @@ -220,7 +232,7 @@ public static void sendPluginMessage(Player sender, String channel, BungeeMe * @param sender the player to send the message as * @param data the data */ - public static void sendMessage(@NonNull Player sender, Object... data) { + public static void sendBungeeMessage(@NonNull Player sender, Object... data) { Valid.checkBoolean(data != null && data.length >= 1, ""); final ByteArrayDataOutput out = ByteStreams.newDataOutput(); @@ -247,47 +259,20 @@ else if (datum instanceof String) sender.sendPluginMessage(SimplePlugin.getInstance(), "BungeeCord", out.toByteArray()); } - /** - * Sends a plugin message that will re-connect the player to another server on Bungee - * - * @param player the living non-dead player - * @param serverName the server name as you have in config.yml of your BungeeCord - */ - public static void connect(@NonNull Player player, @NonNull String serverName) { - final ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); - final DataOutputStream out = new DataOutputStream(byteArray); - - try { - out.writeUTF("Connect"); - out.writeUTF(serverName); - - } catch (final Throwable t) { - Common.error(t, - "Unable to connect " + player.getName() + " to server " + serverName, - "Error: %error"); - } - - player.sendPluginMessage(SimplePlugin.getInstance(), "BungeeCord", byteArray.toByteArray()); - } - - /** + /* * Return either the first online player or the server itself * through which we send the bungee message as - * - * @return */ private static Player findFirstPlayer() { return Remain.getOnlinePlayers().isEmpty() ? null : Remain.getOnlinePlayers().iterator().next(); } - /** + /* * Ensures we are reading in the correct order as the given {@link BungeeMessageType} * specifies in its {@link BungeeMessageType#getContent()} getter. *

* This also ensures we are reading the correct data type (both primitives and wrappers * are supported). - * - * @param typeOf */ private static void moveHead(int actionHead, BungeeMessageType action, Class typeOf, Object[] data) throws Throwable { Valid.checkNotNull(action, "Action not set!"); diff --git a/src/main/java/org/mineacademy/fo/Common.java b/src/main/java/org/mineacademy/fo/Common.java index 8ef54dcae..461f91a08 100644 --- a/src/main/java/org/mineacademy/fo/Common.java +++ b/src/main/java/org/mineacademy/fo/Common.java @@ -1,27 +1,10 @@ package org.mineacademy.fo; -import java.io.IOException; -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; @@ -34,6 +17,8 @@ import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; @@ -44,6 +29,7 @@ import org.mineacademy.fo.collection.SerializedMap; import org.mineacademy.fo.collection.StrictList; import org.mineacademy.fo.collection.StrictMap; +import org.mineacademy.fo.constants.FoConstants; import org.mineacademy.fo.debug.Debugger; import org.mineacademy.fo.exception.FoException; import org.mineacademy.fo.exception.RegexTimeoutException; @@ -52,16 +38,23 @@ import org.mineacademy.fo.model.Replacer; import org.mineacademy.fo.plugin.SimplePlugin; import org.mineacademy.fo.remain.CompChatColor; +import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.remain.Remain; +import org.mineacademy.fo.remain.nbt.NBTItem; import org.mineacademy.fo.settings.ConfigSection; import org.mineacademy.fo.settings.SimpleLocalization; import org.mineacademy.fo.settings.SimpleSettings; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import net.md_5.bungee.api.chat.TextComponent; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * Our main utility class hosting a large variety of different convenience functions @@ -948,7 +941,11 @@ private static String getException(final long count, final String ofWhat) { "datum", "data", "index", "indices", "entry", "entries", - "boss", "bosses"); + "boss", "bosses", + "iron", "iron", + "Iron", "Iron", + "gold", "gold", + "Gold", "Gold"); return exceptions.containsKey(ofWhat) ? count + " " + (count == 0 || count > 1 ? exceptions.getString(ofWhat) : ofWhat) : null; } @@ -999,6 +996,8 @@ public static String fancyBar(final int min, final char minChar, final int max, /** * Formats the vector location to one digit decimal points * + * DO NOT USE FOR SAVING, ONLY INTENDED FOR DEBUGGING + * * @param vec * @return */ @@ -1006,6 +1005,49 @@ public static String shortLocation(final Vector vec) { return " [" + MathUtil.formatOneDigit(vec.getX()) + ", " + MathUtil.formatOneDigit(vec.getY()) + ", " + MathUtil.formatOneDigit(vec.getZ()) + "]"; } + /** + * Formats the item stack into a readable useful console log + * printing only its name, lore and nbt tags + * + * DO NOT USE FOR SAVING, ONLY INTENDED FOR DEBUGGING + * + * @param item + * @return + */ + public static String shortItemStack(ItemStack item) { + if (item == null) + return "null"; + + if (CompMaterial.isAir(item.getType())) + return "Air"; + + String name = ItemUtil.bountifyCapitalized(item.getType()); + + if (Remain.hasItemMeta() && item.hasItemMeta()) { + ItemMeta meta = item.getItemMeta(); + + name += "{"; + + if (meta.hasDisplayName()) + name += "name='" + Common.stripColors(meta.getDisplayName()) + "', "; + + if (meta.hasLore()) + name += "lore=[" + Common.stripColors(String.join(", ", meta.getLore())) + "], "; + + final NBTItem nbt = new NBTItem(item); + + if (nbt.hasTag(FoConstants.NBT.TAG)) + name += "tags=" + nbt.getCompound(FoConstants.NBT.TAG) + ", "; + + if (name.endsWith(", ")) + name = name.substring(0, name.length() - 2); + + name += "}"; + } + + return name; + } + /** * Formats the given location to block points without decimals * diff --git a/src/main/java/org/mineacademy/fo/EntityUtil.java b/src/main/java/org/mineacademy/fo/EntityUtil.java index bda2427f8..ac3e441c0 100644 --- a/src/main/java/org/mineacademy/fo/EntityUtil.java +++ b/src/main/java/org/mineacademy/fo/EntityUtil.java @@ -1,27 +1,10 @@ package org.mineacademy.fo; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.entity.Animals; -import org.bukkit.entity.Creature; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.ExperienceOrb; -import org.bukkit.entity.FallingBlock; -import org.bukkit.entity.Ghast; -import org.bukkit.entity.Item; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.entity.Slime; -import org.bukkit.entity.Wolf; +import org.bukkit.entity.*; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -33,8 +16,12 @@ import org.mineacademy.fo.model.HookManager; import org.mineacademy.fo.remain.Remain; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** * Utility class for managing entities. @@ -60,7 +47,7 @@ public final class EntityUtil { public static T findNearestEntity(Location center, double range3D, Class entityClass) { final List found = new ArrayList<>(); - for (final Entity nearby : center.getWorld().getNearbyEntities(center, range3D, range3D, range3D)) + for (final Entity nearby : Remain.getNearbyEntities(center, range3D)) if (nearby instanceof LivingEntity && entityClass.isAssignableFrom(nearby.getClass())) found.add((T) nearby); diff --git a/src/main/java/org/mineacademy/fo/FileUtil.java b/src/main/java/org/mineacademy/fo/FileUtil.java index 020d6dc15..53e16b5f3 100644 --- a/src/main/java/org/mineacademy/fo/FileUtil.java +++ b/src/main/java/org/mineacademy/fo/FileUtil.java @@ -1,42 +1,25 @@ package org.mineacademy.fo; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.mineacademy.fo.exception.FoException; +import org.mineacademy.fo.plugin.SimplePlugin; +import org.mineacademy.fo.remain.Remain; + +import java.io.*; import java.net.URL; import java.net.URLConnection; import java.nio.channels.ClosedByInterruptException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Enumeration; -import java.util.List; +import java.nio.file.*; +import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.mineacademy.fo.exception.FoException; -import org.mineacademy.fo.plugin.SimplePlugin; -import org.mineacademy.fo.remain.Remain; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.NonNull; - /** * Utility class for managing files. */ @@ -383,7 +366,7 @@ public static File extract(String from, String to) { File file = new File(SimplePlugin.getData(), to); final List lines = getInternalFileContent(from); - Valid.checkNotNull(lines, "Inbuilt " + from + " not found! Did you reload?"); + Valid.checkNotNull(lines, "Inbuilt " + file.getAbsolutePath() + " not found! Did you reload?"); if (file.exists()) return file; diff --git a/src/main/java/org/mineacademy/fo/PlayerUtil.java b/src/main/java/org/mineacademy/fo/PlayerUtil.java index e093ef622..e2565059d 100644 --- a/src/main/java/org/mineacademy/fo/PlayerUtil.java +++ b/src/main/java/org/mineacademy/fo/PlayerUtil.java @@ -4,12 +4,14 @@ import lombok.NoArgsConstructor; import org.bukkit.*; import org.bukkit.Statistic.Type; +import org.bukkit.block.BlockFace; import org.bukkit.entity.EntityType; import org.bukkit.entity.Item; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; +import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.metadata.MetadataValue; import org.bukkit.permissions.Permissible; import org.bukkit.plugin.Plugin; @@ -17,6 +19,7 @@ import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import org.mineacademy.fo.MinecraftVersion.V; +import org.mineacademy.fo.collection.SerializedMap; import org.mineacademy.fo.exception.FoException; import org.mineacademy.fo.jsonsimple.JSONObject; import org.mineacademy.fo.jsonsimple.JSONParser; @@ -31,6 +34,7 @@ import java.io.File; import java.io.FileReader; import java.util.*; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -46,11 +50,22 @@ public final class PlayerUtil { */ public static final int USABLE_PLAYER_INV_SIZE = 36; + /** + * Stores block faces to use for later conversion + */ + private static final BlockFace[] FACE_AXIS = { BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST }; + private static final BlockFace[] FACE_RADIAL = { BlockFace.NORTH, BlockFace.NORTH_EAST, BlockFace.EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH, BlockFace.SOUTH_WEST, BlockFace.WEST, BlockFace.NORTH_WEST }; + /** * Stores a list of currently pending title animation tasks to restore the tile to its original one */ private static final Map titleRestoreTasks = new ConcurrentHashMap<>(); + /** + * Stores temporarily saved player inventories, their health, attributes and other states + */ + private static final Map storedPlayerStates = new HashMap<>(); + // ------------------------------------------------------------------------------------------------------------ // Misc // ------------------------------------------------------------------------------------------------------------ @@ -75,6 +90,91 @@ public static int getPing(final Player player) { return Remain.getPing(player); } + /** + * Converts where the player is looking into a block face + * Source: https://bukkit.org/threads/400099/ + * + * @param player + * @return + */ + public static BlockFace getFacing(Player player) { + return getFacing(player.getLocation().getYaw(), false); + } + + /** + * Converts where the player is looking into a block face + * Source: https://bukkit.org/threads/400099/ + * + * @param player + * @param useSubDirections + * @return + */ + public static BlockFace getFacing(Player player, boolean useSubDirections) { + return getFacing(player.getLocation().getYaw(), useSubDirections); + } + + /** + * Converts the given yaw into a block face + * Source: https://bukkit.org/threads/400099/ + * + * @param yaw + * @param useSubDirections + * @return + */ + public static BlockFace getFacing(float yaw, boolean useSubDirections) { + if (useSubDirections) + return FACE_RADIAL[Math.round(yaw / 45F) & 0x7].getOppositeFace(); + + return FACE_AXIS[Math.round(yaw / 90F) & 0x3].getOppositeFace(); + } + + /** + * Return a yaw from BlockFace + * + * @param face + * @param useSubDirections + * @return + */ + public static int getFacing(BlockFace face, boolean useSubDirections) { + return (useSubDirections ? face.ordinal() * 45 : face.ordinal() * 90) - 180; + } + + /** + * Converts the given yaw into the closest valid blockface and then back to yaw + * + * Used to align entities to look in one of the 4 or 8 directions without you needing + * to stand perfectly straight. + * + * @param yaw + * @param useSubDirections + * @return + */ + public static float alignYaw(float yaw, boolean useSubDirections) { + BlockFace face = getFacing(yaw, useSubDirections); + + return getFacing(face, useSubDirections); + } + + // ------------------------------------------------------------------------------------------------------------ + // Statistics + // ------------------------------------------------------------------------------------------------------------ + + /** + * Return the total amount of time the player has spent on the server. + * This will get reset if you delete the playerdata folder inside your main world folder. + * + * **For Minecraft 1.12 and older this returns a tick value, otherwise this returns the + * amount of minutes!** + * + * @param player + * @return + */ + public static long getPlayTimeTicksOrSeconds(OfflinePlayer player) { + final Statistic playTime = Remain.getPlayTimeStatisticName(); + + return getStatistic(player, playTime); + } + /** * Return statistics of ALL offline players ever played * @@ -127,22 +227,6 @@ public static TreeMap getStatistics(final Statistic statist return statistics; } - /** - * Return the total amount of time the player has spent on the server. - * This will get reset if you delete the playerdata folder inside your main world folder. - * - * **For Minecraft 1.12 and older this returns a tick value, otherwise this returns the - * amount of minutes!** - * - * @param player - * @return - */ - public static long getPlayTimeTicksOrSeconds(OfflinePlayer player) { - final Statistic playTime = Remain.getPlayTimeStatisticName(); - - return getStatistic(player, playTime); - } - /** * Return a statistic of an online player * @@ -315,7 +399,22 @@ public static void normalize(final Player player, final boolean cleanInventory, if (cleanInventory) { cleanInventoryAndFood(player); - player.resetMaxHealth(); + try { + CompAttribute.GENERIC_MAX_HEALTH.set(player, 20); + + } catch (final Throwable t) { + try { + player.setMaxHealth(20); + + } catch (final Throwable tt) { + + try { + player.resetMaxHealth(); + } catch (final Throwable ttt) { + // Minecraft 1.2.5 lol + } + } + } try { player.setHealth(20); @@ -426,6 +525,152 @@ public static boolean hasEmptyInventory(final Player player) { return true; } + // ------------------------------------------------------------------------------------------------------------ + // Player states + // ------------------------------------------------------------------------------------------------------------ + + /** + * Set the players snapshot to be stored locally in the cache + * + * @param player + */ + public static void storeState(final Player player) { + Valid.checkBoolean(!hasStoredState(player), "Player " + player.getName() + " already has a stored state!"); + + final SerializedMap data = SerializedMap.ofArray( + "gameMode", player.getGameMode(), + "content", player.getInventory().getContents(), + "armorContent", player.getInventory().getArmorContents(), + "maxHealth", Remain.getMaxHealth(player), + "health", Remain.getHealth(player), + "healthScaled", player.isHealthScaled(), + "remainingAir", player.getRemainingAir(), + "maximumAir", player.getMaximumAir(), + "fallDistance", player.getFallDistance(), + "fireTicks", player.getFireTicks(), + "totalExp", player.getTotalExperience(), + "level", player.getLevel(), + "exp", player.getExp(), + "foodLevel", player.getFoodLevel(), + "exhaustion", player.getExhaustion(), + "saturation", player.getSaturation(), + "flySpeed", player.getFlySpeed(), + "walkSpeed", player.getWalkSpeed(), + "potionEffects", player.getActivePotionEffects()); + + // Attributes + final Map attributes = new HashMap<>(); + + for (final CompAttribute attribute : CompAttribute.values()) { + final Double value = attribute.get(player); + + if (value != null) + attributes.put(attribute, value); + } + + data.put("attributes", attributes); + + // From now on we have to surround each method with try-catch since + // those are not available in older MC versions + + try { + data.put("extraContent", player.getInventory().getExtraContents()); + } catch (final Throwable t) { + } + + try { + data.put("invulnerable", player.isInvulnerable()); + } catch (final Throwable t) { + } + + try { + data.put("silent", player.isSilent()); + } catch (final Throwable t) { + } + + try { + data.put("glowing", player.isGlowing()); + } catch (final Throwable t) { + } + + storedPlayerStates.put(player.getUniqueId(), data); + } + + /** + * Restores the player inventory and properties + * + * @param player + */ + public static void restoreState(final Player player) { + final SerializedMap data = storedPlayerStates.remove(player.getUniqueId()); + Valid.checkNotNull(data, "Player " + player.getName() + " does not have a stored game state!"); + + player.setGameMode(data.get("gameMode", GameMode.class)); + player.getInventory().setContents((ItemStack[]) data.getObject("content")); + player.getInventory().setArmorContents((ItemStack[]) data.getObject("armorContent")); + player.setMaxHealth(data.getInteger("maxHealth")); + player.setHealth(data.getInteger("health")); + player.setHealthScaled(data.getBoolean("healthScaled")); + player.setRemainingAir(data.getInteger("remainingAir")); + player.setMaximumAir(data.getInteger("maximumAir")); + player.setFallDistance(data.getFloat("fallDistance")); + player.setFireTicks(data.getInteger("fireTicks")); + player.setTotalExperience(data.getInteger("totalExp")); + player.setLevel(data.getInteger("level")); + player.setExp(data.getFloat("exp")); + player.setFoodLevel(data.getInteger("foodLevel")); + player.setExhaustion(data.getFloat("exhaustion")); + player.setSaturation(data.getFloat("saturation")); + player.setFlySpeed(data.getFloat("flySpeed")); + player.setWalkSpeed(data.getFloat("walkSpeed")); + + // Remove old potion effects + for (final PotionEffect effect : player.getActivePotionEffects()) + player.removePotionEffect(effect.getType()); + + // And add news + for (final PotionEffect effect : data.getList("potionEffects", PotionEffect.class)) + player.addPotionEffect(effect); + + // Attributes + final Map attributes = (Map) data.getObject("attributes"); + + for (final Entry entry : attributes.entrySet()) + entry.getKey().set(player, entry.getValue()); + + // From now on we have to surround each method with try-catch since + // those are not available in older MC versions + + try { + player.getInventory().setExtraContents((ItemStack[]) data.getObject("extraContent")); + } catch (final Throwable t) { + } + + try { + player.setInvulnerable(data.getBoolean("invulnerable")); + } catch (final Throwable t) { + } + + try { + player.setSilent(data.getBoolean("silent")); + } catch (final Throwable t) { + } + + try { + player.setGlowing(data.getBoolean("glowing")); + } catch (final Throwable t) { + } + } + + /** + * Return true if the player has a stored snapshot of inventory and properties + * + * @return + */ + public static boolean hasStoredState(Player player) { + return storedPlayerStates.containsKey(player.getUniqueId()); + } + // ------------------------------------------------------------------------------------------------------------ // Vanish // ------------------------------------------------------------------------------------------------------------ @@ -472,20 +717,22 @@ public static boolean isVanished(final Player player) { public static void setVanished(Player player, boolean vanished) { // Hook into other plugins - HookManager.setVanished(player, false); + HookManager.setVanished(player, vanished); - // Remove metadata - final List list = player.getMetadata("vanished"); + // Clear any previous metadata + for (Iterator it = player.getMetadata("vanished").iterator(); it.hasNext();) { + MetadataValue meta = it.next(); - for (final MetadataValue meta : list) - if (meta.asBoolean()) { - player.removeMetadata("vanished", meta.getOwningPlugin()); + if (meta.asBoolean()) + meta.invalidate(); + } - break; - } + // Re-add metadata if vanished + if (vanished) + player.setMetadata("vanished", new FixedMetadataValue(SimplePlugin.getInstance(), true)); // NMS - Remain.setInvisible(player, false); + Remain.setInvisible(player, vanished); } // ------------------------------------------------------------------------------------------------------------ @@ -650,8 +897,32 @@ public static boolean take(Player player, CompMaterial material, int amount) { if (!containsAtLeast(player, amount, material)) return false; - for (int i = 0; i < amount; i++) - takeFirstOnePiece(player, material); + final Inventory inventory = player.getInventory(); + final ItemStack[] content = inventory.getContents(); + + for (int slot = 0; slot < content.length; slot++) { + ItemStack item = content[slot]; + + if (item != null && material.is(item)) { + int itemAmount = item.getAmount(); + int newAmount = itemAmount - amount; + + if (newAmount < 0) { + amount = amount - itemAmount; + + content[slot] = null; + } + + else { + item.setAmount(newAmount); + + content[slot] = item; + break; + } + } + } + + inventory.setContents(content); return true; } diff --git a/src/main/java/org/mineacademy/fo/RandomUtil.java b/src/main/java/org/mineacademy/fo/RandomUtil.java index a39361470..b07a060d8 100644 --- a/src/main/java/org/mineacademy/fo/RandomUtil.java +++ b/src/main/java/org/mineacademy/fo/RandomUtil.java @@ -237,14 +237,31 @@ public static T nextItem(final Iterable items, final Predicate conditi * @return */ public static Location nextLocation(final Location origin, final double radius, final boolean is3D) { - final double randomRadius = random.nextDouble() * radius; - final double theta = Math.toRadians(random.nextDouble() * 360); - final double phi = Math.toRadians(random.nextDouble() * 180 - 90); - - final double x = randomRadius * Math.cos(theta) * Math.sin(phi); - final double z = randomRadius * Math.cos(phi); - - return origin.clone().add(x, is3D ? randomRadius * Math.sin(theta) * Math.cos(phi) : 0, z); + final double rectX = random.nextDouble() * radius; + final double rectZ = random.nextDouble() * radius; + final double offsetX; + final double offsetZ; + double offsetY = 0; + final int transform = random.nextInt(4); + if (is3D) { + final double rectY = random.nextDouble() * radius; + offsetY = getYCords( transform, rectY); + } + if (transform == 0) { + offsetX = rectX; + offsetZ = rectZ; + } else if (transform == 1) { + offsetX = -rectZ; + offsetZ = rectX; + } else if (transform == 2) { + offsetX = -rectX; + offsetZ = -rectZ; + } else { + offsetX = rectZ; + offsetZ = -rectX; + } + + return origin.clone().add(offsetX, offsetY, offsetZ); } /** @@ -261,14 +278,43 @@ public static Location nextLocation(final Location origin, final double minRadiu Valid.checkBoolean(maxRadius > 0 && minRadius > 0, "Max and min radius must be over 0"); Valid.checkBoolean(maxRadius > minRadius, "Max radius must be greater than min radius"); - final double randomRadius = minRadius + (random.nextDouble() * (maxRadius - minRadius)); - final double theta = Math.toRadians(random.nextDouble() * 360); - final double phi = Math.toRadians(random.nextDouble() * 180 - 90); - final double x = randomRadius * Math.cos(theta) * Math.sin(phi); - final double z = randomRadius * Math.cos(phi); + final double rectX = random.nextDouble() * (maxRadius - minRadius) + minRadius; + final double rectZ = random.nextDouble() * (maxRadius + minRadius) - minRadius; + final double offsetX; + final double offsetZ; + double offsetY = 0; + final int transform = random.nextInt(4); + if (is3D) { + final double rectY = random.nextDouble() * (maxRadius + minRadius) - minRadius; + offsetY = getYCords( transform, rectY); + } + if (transform == 0) { + offsetX = rectX; + offsetZ = rectZ; + } else if (transform == 1) { + offsetX = -rectZ; + offsetZ = rectX; + } else if (transform == 2) { + offsetX = -rectX; + offsetZ = -rectZ; + } else { + offsetX = rectZ; + offsetZ = -rectX; + } + + return origin.clone().add(offsetX, offsetY, offsetZ); + } - return origin.clone().add(x, is3D ? randomRadius * Math.sin(theta) * Math.cos(phi) : 0, z); + public static double getYCords( int transform, double rectY) { + double offsetY; + double nextY = random.nextDouble(); + if (transform < 2) { + offsetY = nextY >= 0.5 ? -rectY : rectY; + } else { + offsetY = nextY >= 0.5 ? rectY : -rectY; + } + return offsetY; } /** diff --git a/src/main/java/org/mineacademy/fo/ReflectionUtil.java b/src/main/java/org/mineacademy/fo/ReflectionUtil.java index 36597405e..e63506a10 100644 --- a/src/main/java/org/mineacademy/fo/ReflectionUtil.java +++ b/src/main/java/org/mineacademy/fo/ReflectionUtil.java @@ -1,25 +1,9 @@ package org.mineacademy.fo; -import java.io.File; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import javax.annotation.Nullable; - +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.SneakyThrows; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.entity.EntityType; @@ -33,10 +17,16 @@ import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.remain.Remain; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.SneakyThrows; +import javax.annotation.Nullable; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; /** * Utility class for various reflection methods @@ -165,7 +155,15 @@ public static Constructor getConstructor(@NonNull final Class clazz, final if (reflectionDataCache.containsKey(clazz)) return reflectionDataCache.get(clazz).getConstructor(params); - final Constructor constructor = clazz.getConstructor(params); + Constructor constructor; + + try { + constructor = clazz.getConstructor(params); + + } catch (final NoSuchMethodException err) { + constructor = clazz.getDeclaredConstructor(params); + } + constructor.setAccessible(true); return constructor; @@ -500,9 +498,7 @@ public static T instantiate(final Class clazz) { constructor = ((ReflectionData) reflectionDataCache.get(clazz)).getDeclaredConstructor(); else - constructor = clazz.getDeclaredConstructor(); - - constructor.setAccessible(true); + constructor = (Constructor) ReflectionUtil.getConstructor(clazz); return constructor.newInstance(); @@ -572,6 +568,8 @@ public static T instantiate(final Class clazz, final Object... params) { */ public static T instantiate(final Constructor constructor, final Object... params) { try { + constructor.setAccessible(true); + return constructor.newInstance(params); } catch (final ReflectiveOperationException ex) { diff --git a/src/main/java/org/mineacademy/fo/SerializeUtil.java b/src/main/java/org/mineacademy/fo/SerializeUtil.java index 5be0ef62a..b3ec18622 100644 --- a/src/main/java/org/mineacademy/fo/SerializeUtil.java +++ b/src/main/java/org/mineacademy/fo/SerializeUtil.java @@ -1,21 +1,12 @@ package org.mineacademy.fo; -import java.awt.Color; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.math.BigDecimal; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.Function; -import java.util.regex.Pattern; - +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; @@ -36,32 +27,25 @@ import org.mineacademy.fo.collection.StrictMap; import org.mineacademy.fo.exception.FoException; import org.mineacademy.fo.exception.InvalidWorldException; -import org.mineacademy.fo.jsonsimple.JSONArray; -import org.mineacademy.fo.jsonsimple.JSONObject; -import org.mineacademy.fo.jsonsimple.JSONParseException; -import org.mineacademy.fo.jsonsimple.JSONParser; -import org.mineacademy.fo.jsonsimple.Jsonable; +import org.mineacademy.fo.jsonsimple.*; import org.mineacademy.fo.menu.model.ItemCreator; -import org.mineacademy.fo.model.BoxedMessage; -import org.mineacademy.fo.model.ConfigSerializable; -import org.mineacademy.fo.model.IsInList; -import org.mineacademy.fo.model.RangedSimpleTime; -import org.mineacademy.fo.model.RangedValue; -import org.mineacademy.fo.model.SimpleSound; -import org.mineacademy.fo.model.SimpleTime; +import org.mineacademy.fo.model.*; import org.mineacademy.fo.remain.CompChatColor; import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.remain.JsonItemStack; import org.mineacademy.fo.remain.Remain; import org.mineacademy.fo.settings.ConfigSection; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.HoverEvent; +import java.awt.*; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.nio.file.Path; +import java.util.List; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; /** * Utility class for serializing objects to writeable YAML data and back. @@ -119,18 +103,7 @@ public static Object serialize(Mode mode, Object object) { if (serializers.containsKey(object.getClass())) return serializers.get(object.getClass()).apply(object); - if (object instanceof ConfigurationSerializable) { - - if (isJson) { - if (object instanceof ItemStack) - return JsonItemStack.toJson((ItemStack) object); - - throw new FoException("serializing " + object.getClass().getSimpleName() + " to JSON is not implemented! Please serialize it to string manually first!"); - } - - return object; - - } else if (object instanceof ConfigSerializable) + if (object instanceof ConfigSerializable) return serialize(mode, ((ConfigSerializable) object).serialize().serialize()); else if (object instanceof StrictCollection) @@ -321,6 +294,19 @@ else if (object instanceof BigDecimal) { return big.toPlainString(); } + else if (object instanceof ConfigurationSerializable) { + + if (isJson) { + if (object instanceof ItemStack) + return JsonItemStack.toJson((ItemStack) object); + + throw new FoException("serializing " + object.getClass().getSimpleName() + " to JSON is not implemented! Please serialize it to string manually first!"); + } + + return object; + + } + throw new SerializeFailedException("Does not know how to serialize " + object.getClass().getSimpleName() + "! Does it extends ConfigSerializable? Data: " + object); } diff --git a/src/main/java/org/mineacademy/fo/bungee/BungeeListener.java b/src/main/java/org/mineacademy/fo/bungee/BungeeListener.java index 4ac54bd7b..24086592d 100644 --- a/src/main/java/org/mineacademy/fo/bungee/BungeeListener.java +++ b/src/main/java/org/mineacademy/fo/bungee/BungeeListener.java @@ -1,5 +1,9 @@ package org.mineacademy.fo.bungee; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; +import lombok.Getter; +import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.Listener; @@ -8,8 +12,9 @@ import org.mineacademy.fo.Valid; import org.mineacademy.fo.bungee.message.IncomingMessage; -import lombok.Getter; -import lombok.NonNull; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; /** * Represents a BungeeCord listener using a bungee channel @@ -18,7 +23,14 @@ * This class is also a Listener for Bukkit events for your convenience */ @Getter -public abstract class BungeeListener implements Listener, PluginMessageListener { +public abstract class BungeeListener implements Listener { + + /** + * Holds all registered listeners, all using "BungeeCord" channel and actually writing + * the channel name as UTF in to the byte array to avoid "Unknown custom packed identifier: plugin:chcred" + * bug. + */ + private static final List registeredListeners = new ArrayList<>(); /** * The channel @@ -55,19 +67,6 @@ private static BungeeMessageType[] toActions(@NonNull Class @@ -64,17 +69,30 @@ public IncomingMessage(byte[] data) { * @param data */ public IncomingMessage(BungeeListener listener, byte[] data) { + this(listener, listener.getChannel(), data); + } + + private IncomingMessage(BungeeListener listener, String channel, byte[] data) { super(listener); + this.channel = channel; this.data = data; this.stream = new ByteArrayInputStream(data); - this.input = ByteStreams.newDataInput(this.stream); + try { + this.input = ByteStreams.newDataInput(this.stream); + + } catch (final Throwable t) { + this.input = ByteStreams.newDataInput(data); + } // ----------------------------------------------------------------- // We are automatically reading the first two strings assuming the // first is the senders server name and the second is the action // ----------------------------------------------------------------- + // Read channel name, dispose, set above + this.input.readUTF(); + // Read senders UUID this.setSenderUid(this.input.readUTF()); @@ -200,7 +218,7 @@ public float readFloat() { * * @return */ - public int writeInt() { + public int readInt() { this.moveHead(Integer.class); return this.input.readInt(); @@ -234,6 +252,6 @@ public short readShort() { * @param player */ public void forward(Player player) { - player.sendPluginMessage(SimplePlugin.getInstance(), this.getChannel(), this.data); + player.sendPluginMessage(SimplePlugin.getInstance(), "BungeeCord", this.data); } } \ No newline at end of file diff --git a/src/main/java/org/mineacademy/fo/bungee/message/Message.java b/src/main/java/org/mineacademy/fo/bungee/message/Message.java index 106e5e701..2a55301b8 100644 --- a/src/main/java/org/mineacademy/fo/bungee/message/Message.java +++ b/src/main/java/org/mineacademy/fo/bungee/message/Message.java @@ -1,14 +1,13 @@ package org.mineacademy.fo.bungee.message; -import java.util.UUID; - +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.mineacademy.fo.Valid; import org.mineacademy.fo.bungee.BungeeListener; import org.mineacademy.fo.bungee.BungeeMessageType; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import java.util.UUID; /** * Represents a in/out message with a given action and server name @@ -132,7 +131,7 @@ protected final void moveHead(Class typeOf) { * * @return */ - public final String getChannel() { + public String getChannel() { return this.listener.getChannel(); } } diff --git a/src/main/java/org/mineacademy/fo/bungee/message/OutgoingMessage.java b/src/main/java/org/mineacademy/fo/bungee/message/OutgoingMessage.java index 95507ff80..85d6296f8 100644 --- a/src/main/java/org/mineacademy/fo/bungee/message/OutgoingMessage.java +++ b/src/main/java/org/mineacademy/fo/bungee/message/OutgoingMessage.java @@ -1,9 +1,7 @@ package org.mineacademy.fo.bungee.message; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import org.bukkit.entity.Player; import org.mineacademy.fo.Valid; import org.mineacademy.fo.bungee.BungeeListener; @@ -13,8 +11,9 @@ import org.mineacademy.fo.plugin.SimplePlugin; import org.mineacademy.fo.remain.Remain; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; /** * NB: This uses the standardized Foundation model where the first @@ -57,6 +56,7 @@ public OutgoingMessage(BungeeListener listener, UUID senderUid, BungeeMessageTyp // first is the senders server name and the second is the action // ----------------------------------------------------------------- + this.queue.add(listener.getChannel()); this.queue.add(senderUid); this.queue.add(this.getServerName()); this.queue.add(this.getAction().name()); @@ -176,7 +176,7 @@ private void write(Object object, Class typeOf) { * @param player */ public void send(Player player) { - player.sendPluginMessage(SimplePlugin.getInstance(), this.getChannel(), this.compileData()); + player.sendPluginMessage(SimplePlugin.getInstance(), "BungeeCord", this.compileData()); } /** diff --git a/src/main/java/org/mineacademy/fo/command/DebugCommand.java b/src/main/java/org/mineacademy/fo/command/DebugCommand.java index 7611cf41b..fd3f72342 100644 --- a/src/main/java/org/mineacademy/fo/command/DebugCommand.java +++ b/src/main/java/org/mineacademy/fo/command/DebugCommand.java @@ -1,13 +1,6 @@ package org.mineacademy.fo.command; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - +import lombok.Setter; import org.bukkit.Bukkit; import org.mineacademy.fo.Common; import org.mineacademy.fo.FileUtil; @@ -17,7 +10,12 @@ import org.mineacademy.fo.settings.SimpleLocalization; import org.mineacademy.fo.settings.YamlConfig; -import lombok.Setter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; /** * A sample sub-command that you can automatically add @@ -109,11 +107,11 @@ private void copyFilesToDebug(List files) { final YamlConfig config = YamlConfig.fromFile(file); final YamlConfig copyConfig = YamlConfig.fromFile(copy); - for (final Map.Entry entry : config.getValues(true).entrySet()) { - final String key = entry.getKey(); + for (final String key : config.getKeys(true)) { + final Object value = config.getObject(key); if (!key.contains("MySQL")) - copyConfig.set(key, entry.getValue()); + copyConfig.set(key, value); } copyConfig.save(copy); diff --git a/src/main/java/org/mineacademy/fo/command/SimpleCommand.java b/src/main/java/org/mineacademy/fo/command/SimpleCommand.java index 24d2b6ec3..683237faa 100644 --- a/src/main/java/org/mineacademy/fo/command/SimpleCommand.java +++ b/src/main/java/org/mineacademy/fo/command/SimpleCommand.java @@ -1,33 +1,15 @@ package org.mineacademy.fo.command; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Function; - +import lombok.Getter; +import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.World; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.TabCompleter; +import org.bukkit.command.*; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.Messenger; -import org.mineacademy.fo.PlayerUtil; -import org.mineacademy.fo.ReflectionUtil; -import org.mineacademy.fo.TabUtil; -import org.mineacademy.fo.Valid; +import org.mineacademy.fo.*; import org.mineacademy.fo.collection.StrictList; import org.mineacademy.fo.collection.expiringmap.ExpiringMap; import org.mineacademy.fo.command.SimpleCommandGroup.MainCommand; @@ -35,18 +17,17 @@ import org.mineacademy.fo.exception.CommandException; import org.mineacademy.fo.exception.EventHandledException; import org.mineacademy.fo.exception.InvalidCommandArgException; -import org.mineacademy.fo.model.ChatPaginator; -import org.mineacademy.fo.model.Replacer; -import org.mineacademy.fo.model.SimpleComponent; -import org.mineacademy.fo.model.SimpleTime; -import org.mineacademy.fo.model.Variables; +import org.mineacademy.fo.model.*; import org.mineacademy.fo.plugin.SimplePlugin; import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.remain.Remain; import org.mineacademy.fo.settings.SimpleLocalization; -import lombok.Getter; -import lombok.NonNull; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; /** * A simple command used to replace all Bukkit/Spigot command functionality @@ -353,25 +334,52 @@ public final boolean execute(final CommandSender sender, final String label, fin final String usage = this.getMultilineUsageMessage() != null ? String.join("\n&c", this.getMultilineUsageMessage()) : this.getUsage() != null ? this.getUsage() : null; Valid.checkNotNull(usage, "getUsage() nor getMultilineUsageMessage() not implemented for '/" + this.getLabel() + sublabel + "' command!"); + String description = this.replacePlaceholders("&c" + this.getDescription()); + List usages = new ArrayList<>(); + String singleLineUsage = null; + final ChatPaginator paginator = new ChatPaginator(SimpleLocalization.Commands.HEADER_SECONDARY_COLOR); final List pages = new ArrayList<>(); if (!Common.getOrEmpty(this.getDescription()).isEmpty()) { pages.add(this.replacePlaceholders(SimpleLocalization.Commands.LABEL_DESCRIPTION)); - pages.add(this.replacePlaceholders("&c" + this.getDescription())); + pages.add(description); } if (this.getMultilineUsageMessage() != null) { pages.add(""); pages.add(this.replacePlaceholders(SimpleLocalization.Commands.LABEL_USAGES)); - for (final String usagePart : usage.split("\n")) - pages.add(this.replacePlaceholders("&c" + usagePart)); + for (String usagePart : usage.split("\n")) { + usagePart = this.replacePlaceholders("&c" + usagePart); + + usages.add(usagePart); + pages.add(usagePart); + } } else { + singleLineUsage = "&c" + this.replacePlaceholders("/" + label + sublabel + (!usage.startsWith("/") ? " " + Common.stripColors(usage) : "")); + pages.add(""); pages.add(SimpleLocalization.Commands.LABEL_USAGE); - pages.add("&c" + this.replacePlaceholders("/" + label + sublabel + (!usage.startsWith("/") ? " " + Common.stripColors(usage) : ""))); + pages.add(singleLineUsage); + } + + if (SimpleLocalization.Commands.SIMPLE_HELP_DESIGN) { + Common.tellNoPrefix(sender, addSpaceIfNeeded(SimpleLocalization.Commands.LABEL_DESCRIPTION) + description); + + if (usages.isEmpty()) + Common.tellNoPrefix(sender, addSpaceIfNeeded(SimpleLocalization.Commands.LABEL_USAGE) + singleLineUsage); + + else { + Common.tellNoPrefix(sender, SimpleLocalization.Commands.LABEL_USAGES); + + for (String usagePart : usages) + Common.tellNoPrefix(sender, usagePart); + + } + + return; } paginator @@ -425,6 +433,10 @@ public final boolean execute(final CommandSender sender, final String label, fin return true; } + private String addSpaceIfNeeded(String message) { + return message + (Common.stripColors(message).endsWith(" ") ? "" : " "); + } + /* * If messenger is on, we send the message prefixed with Messenger.getErrorPrefix() * otherwise we just send a normal message @@ -1656,7 +1668,7 @@ public boolean equals(final Object obj) { } @Override - public final String toString() { + public String toString() { return "Command{label=/" + this.label + "}"; } } diff --git a/src/main/java/org/mineacademy/fo/command/SimpleCommandGroup.java b/src/main/java/org/mineacademy/fo/command/SimpleCommandGroup.java index d2b0c41c4..f36fe8698 100644 --- a/src/main/java/org/mineacademy/fo/command/SimpleCommandGroup.java +++ b/src/main/java/org/mineacademy/fo/command/SimpleCommandGroup.java @@ -149,8 +149,6 @@ public final void unregister() { this.mainCommand.unregister(); this.mainCommand = null; - - this.subcommands.clear(); } /** @@ -176,7 +174,10 @@ public final boolean isRegistered() { */ protected final void registerSubcommand(final SimpleSubCommand command) { Valid.checkNotNull(this.mainCommand, "Cannot add subcommands when main command is missing! Call register()"); - Valid.checkBoolean(!this.subcommands.contains(command), "Subcommand /" + this.mainCommand.getLabel() + " " + command.getSublabel() + " already registered when trying to add " + command.getClass()); + + // Fixes reloading issue where all subcommands are cleared + if (this.subcommands.contains(command)) + this.subcommands.remove(command); this.subcommands.add(command); } @@ -493,7 +494,7 @@ protected void tellSubcommandsHelp() { hover.add("&f" + this.replacePlaceholders(this.colorizeUsage(usageLine.replace("{sublabel}", subcommand.getSublabel())))); } else - hover.add(SimpleLocalization.Commands.HELP_TOOLTIP_USAGE + (usage.isEmpty() ? command : usage)); + hover.add(this.replacePlaceholders(SimpleLocalization.Commands.HELP_TOOLTIP_USAGE + (usage.isEmpty() ? command : usage))); for (int i = 0; i < hover.size(); i++) { final String hoverLine = String.join("\n ", Common.split(hover.get(i), 65)); diff --git a/src/main/java/org/mineacademy/fo/command/SimpleSubCommand.java b/src/main/java/org/mineacademy/fo/command/SimpleSubCommand.java index 032897e7c..7e47f5b20 100644 --- a/src/main/java/org/mineacademy/fo/command/SimpleSubCommand.java +++ b/src/main/java/org/mineacademy/fo/command/SimpleSubCommand.java @@ -1,13 +1,12 @@ package org.mineacademy.fo.command; -import java.util.Arrays; - -import org.mineacademy.fo.Valid; -import org.mineacademy.fo.plugin.SimplePlugin; - import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; +import org.mineacademy.fo.Valid; +import org.mineacademy.fo.plugin.SimplePlugin; + +import java.util.Arrays; /** * A simple subcommand belonging to a {@link SimpleCommandGroup} @@ -91,6 +90,11 @@ protected String replacePlaceholders(String message) { return super.replacePlaceholders(message).replace("{sublabel}", this.getSublabel()); } + @Override + public String toString() { + return "SubCommand{parent=/" + this.getLabel() + ", label=" + this.getSublabel() + "}"; + } + @Override public final boolean equals(Object obj) { return obj instanceof SimpleSubCommand ? Arrays.equals(((SimpleSubCommand) obj).sublabels, this.sublabels) : false; diff --git a/src/main/java/org/mineacademy/fo/debug/Debugger.java b/src/main/java/org/mineacademy/fo/debug/Debugger.java index 1c8a5769a..9e74783cc 100644 --- a/src/main/java/org/mineacademy/fo/debug/Debugger.java +++ b/src/main/java/org/mineacademy/fo/debug/Debugger.java @@ -1,13 +1,9 @@ package org.mineacademy.fo.debug; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; - +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; import org.bukkit.Bukkit; import org.mineacademy.fo.Common; import org.mineacademy.fo.FileUtil; @@ -17,10 +13,9 @@ import org.mineacademy.fo.plugin.SimplePlugin; import org.mineacademy.fo.settings.SimpleSettings; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; +import java.io.File; +import java.util.*; +import java.util.logging.Level; /** * Utility class for solving problems and errors @@ -355,6 +350,9 @@ private static boolean canPrint(String message) { // Print a simple console message private static void print(String message) { - System.out.println(message); // our instance may or may not be available yet to log + if (Bukkit.getConsoleSender() != null) + Bukkit.getConsoleSender().sendMessage(Common.colorize(message)); + else + System.out.println(Common.stripColors(message)); // our instance may or may not be available yet to log } } diff --git a/src/main/java/org/mineacademy/fo/menu/Menu.java b/src/main/java/org/mineacademy/fo/menu/Menu.java index e317b124d..b4124bcf8 100644 --- a/src/main/java/org/mineacademy/fo/menu/Menu.java +++ b/src/main/java/org/mineacademy/fo/menu/Menu.java @@ -61,10 +61,11 @@ public abstract class Menu { // -------------------------------------------------------------------------------- /** - * The default sound when switching between menus. + * The default sound when switching between menus. Set to null to disable */ @Getter @Setter + @Nullable private static SimpleSound sound = new SimpleSound(CompSound.NOTE_STICKS.getSound(), .4F); /** @@ -321,9 +322,11 @@ protected final Button getButton(final ItemStack fromItem) { // TODO rewrite to use cache instead of for loop so that two buttons that are the same won't collide for (final Button button : this.registeredButtons.keySet()) { Valid.checkNotNull(button, "Menu button is null at " + this.getClass().getSimpleName()); - Valid.checkNotNull(button.getItem(), "Menu " + this.getTitle() + " contained button " + button + " with empty item!"); - if (ItemUtil.isSimilar(fromItem, button.getItem())) + ItemStack item = button.getItem(); + Valid.checkNotNull(item, "Menu " + this.getTitle() + " contained button " + button.getClass().getSimpleName() + " with empty item!"); + + if (ItemUtil.isSimilar(fromItem, item)) return button; } @@ -417,7 +420,8 @@ public final void displayTo(final Player player) { } // Play the pop sound - sound.play(player); + if (sound != null) + sound.play(player); // Register previous menu if exists { @@ -485,8 +489,11 @@ public final void restartMenu() { */ public final void restartMenu(final String animatedTitle) { - final Inventory inventory = this.getViewer().getOpenInventory().getTopInventory(); - Valid.checkBoolean(inventory.getType() == InventoryType.CHEST, this.getViewer().getName() + "'s inventory closed in the meanwhile (now == " + inventory.getType() + ")."); + final Player player = this.getViewer(); + Valid.checkNotNull(player, "Cannot restartMenu if it was not yet shown to a player! Menu: " + this); + + final Inventory inventory = player.getOpenInventory().getTopInventory(); + Valid.checkBoolean(inventory.getType() == InventoryType.CHEST, player.getName() + "'s inventory closed in the meanwhile (now == " + inventory.getType() + ")."); this.registerButtons(); @@ -499,7 +506,11 @@ public final void restartMenu(final String animatedTitle) { this.animateTitle(animatedTitle); } - void onRestart() {} + /* + * Internal hook before calling getItemAt + */ + void onRestart() { + } /** * Redraws the bottom bar and updates inventory diff --git a/src/main/java/org/mineacademy/fo/menu/MenuPagged.java b/src/main/java/org/mineacademy/fo/menu/MenuPagged.java index b30029706..2ac8689e5 100644 --- a/src/main/java/org/mineacademy/fo/menu/MenuPagged.java +++ b/src/main/java/org/mineacademy/fo/menu/MenuPagged.java @@ -19,11 +19,7 @@ import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.settings.SimpleLocalization; -import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * An advanced menu listing items with automatic page support @@ -52,6 +48,12 @@ public abstract class MenuPagged extends Menu { @Setter private static CompMaterial inactivePageButton = CompMaterial.GRAY_DYE; + /** + * The slots the current page's items will be in + */ + @Getter + private final List slots; + /** * The raw items iterated */ @@ -91,7 +93,7 @@ public abstract class MenuPagged extends Menu { * @param items */ protected MenuPagged(@NonNull final T... items) { - this(null, Arrays.asList(items)); + this(null, null, null, Arrays.asList(items), false); } /** @@ -100,7 +102,7 @@ protected MenuPagged(@NonNull final T... items) { * @param items the pages */ protected MenuPagged(final Iterable items) { - this(null, items); + this(null, null, null, items, false); } /** @@ -110,7 +112,7 @@ protected MenuPagged(final Iterable items) { * @param items the pages the pages */ protected MenuPagged(final Menu parent, @NonNull final T... items) { - this(null, parent, Arrays.asList(items), false); + this(null, parent, null, Arrays.asList(items), false); } /** @@ -120,18 +122,51 @@ protected MenuPagged(final Menu parent, @NonNull final T... items) { * @param items the pages the pages */ protected MenuPagged(final Menu parent, final Iterable items) { - this(null, parent, items, false); + this(null, parent, null, items, false); } /** * Create a new paged menu with automatic page size * - * @param parent - * @param items + * @param slots the slots where the items should be placed on a page + * @param items the pages the pages + */ + protected MenuPagged(final List slots, final Iterable items) { + this(null, null, slots, items, false); + } + + /** + * Create a new paged menu with automatic page size + * + * @param parent the parent menu + * @param slots the slots where the items should be placed on a page + * @param items the pages the pages + */ + protected MenuPagged(final Menu parent, final List slots, final Iterable items) { + this(null, parent, slots, items, false); + } + + /** + * Create a new paged menu + * + * @param parent the parent menu + * @param items the pages * @param returnMakesNewInstance */ protected MenuPagged(final Menu parent, final Iterable items, final boolean returnMakesNewInstance) { - this(null, parent, items, returnMakesNewInstance); + this(null, parent, null, items, returnMakesNewInstance); + } + + /** + * Create a new paged menu + * + * @param parent the parent menu + * @param slots the slots where the items should be placed on a page + * @param items the pages + * @param returnMakesNewInstance + */ + protected MenuPagged(final Menu parent, final List slots, final Iterable items, final boolean returnMakesNewInstance) { + this(null, parent, slots, items, returnMakesNewInstance); } /** @@ -142,7 +177,7 @@ protected MenuPagged(final Menu parent, final Iterable items, final boolean r * @param items the pages */ protected MenuPagged(final int pageSize, @NonNull final T... items) { - this(pageSize, null, Arrays.asList(items)); + this(pageSize, null, null, Arrays.asList(items), false); } /** @@ -153,7 +188,18 @@ protected MenuPagged(final int pageSize, @NonNull final T... items) { * @param items the pages */ protected MenuPagged(final int pageSize, final Iterable items) { - this(pageSize, null, items); + this(pageSize, null, null, items, false); + } + + /** + * Create a new paged menu + * + * @param pageSize size of the menu, a multiple of 9 (keep in mind we already add + * 1 row there) + * @param items the pages + */ + protected MenuPagged(final int pageSize, final List slots, final Iterable items) { + this(pageSize, null, slots, items, false); } /** @@ -165,7 +211,7 @@ protected MenuPagged(final int pageSize, final Iterable items) { * @param items the pages the pages */ protected MenuPagged(final int pageSize, final Menu parent, @NonNull T... items) { - this(pageSize, parent, Arrays.asList(items), false); + this(pageSize, parent, null, Arrays.asList(items), false); } /** @@ -177,7 +223,7 @@ protected MenuPagged(final int pageSize, final Menu parent, @NonNull T... items) * @param items the pages the pages */ protected MenuPagged(final int pageSize, final Menu parent, final Iterable items) { - this(pageSize, parent, items, false); + this(pageSize, parent, null, items, false); } /** @@ -189,7 +235,7 @@ protected MenuPagged(final int pageSize, final Menu parent, final Iterable it * @param returnMakesNewInstance */ protected MenuPagged(final int pageSize, final Menu parent, final Iterable items, final boolean returnMakesNewInstance) { - this((Integer) pageSize, parent, items, returnMakesNewInstance); + this(pageSize, parent, null, items, returnMakesNewInstance); } /** @@ -197,17 +243,20 @@ protected MenuPagged(final int pageSize, final Menu parent, final Iterable it * * @param pageSize size of the menu, a multiple of 9 (keep in mind we already add * 1 row there) + * @param slots the slots where the items should be placed on a page * @param parent the parent menu * @param items the pages the pages * @param returnMakesNewInstance should we re-instatiate the parent menu when returning to it? */ - private MenuPagged(@Nullable final Integer pageSize, final Menu parent, final Iterable items, final boolean returnMakesNewInstance) { + private MenuPagged(final Integer pageSize, final Menu parent, final List slots, final Iterable items, final boolean returnMakesNewInstance) { super(parent, returnMakesNewInstance); + this.slots = slots != null ? slots : new ArrayList<>(); this.items = items; this.manualPageSize = pageSize; this.calculatePages(); + this.setButtons(); } /* @@ -215,13 +264,21 @@ private MenuPagged(@Nullable final Integer pageSize, final Menu parent, final It */ private void calculatePages() { final int items = this.getItemAmount(this.items); - final int autoPageSize = this.manualPageSize != null ? this.manualPageSize : items <= 9 ? 9 * 1 : items <= 9 * 2 ? 9 * 2 : items <= 9 * 3 ? 9 * 3 : items <= 9 * 4 ? 9 * 4 : 9 * 5; + final int autoPageSize; + + if (this.slots.isEmpty()) { + autoPageSize = this.manualPageSize != null ? this.manualPageSize : items <= 9 ? 9 * 1 : items <= 9 * 2 ? 9 * 2 : items <= 9 * 3 ? 9 * 3 : items <= 9 * 4 ? 9 * 4 : 9 * 5; + + for (int i = 0; i < autoPageSize; i++) + this.slots.add(i); + + this.setSize(9 + autoPageSize); + + } else + autoPageSize = this.slots.size(); this.pages.clear(); this.pages.putAll(Common.fillPages(autoPageSize, this.items)); - - this.setSize(9 + autoPageSize); - this.setButtons(); } @SuppressWarnings("unused") @@ -320,7 +377,7 @@ private void updatePage() { private String compileTitle0() { final boolean canAddNumbers = this.addPageNumbers() && this.pages.size() > 1; - return this.getTitle() + (canAddNumbers ? " &8" + this.currentPage + "/" + this.pages.size() : ""); + return "&0" + this.getTitle() + (canAddNumbers ? " &8" + this.currentPage + "/" + this.pages.size() : ""); } /** @@ -405,8 +462,8 @@ protected boolean isEmpty() { */ @Override public ItemStack getItemAt(final int slot) { - if (slot < this.getCurrentPageItems().size()) { - final T object = this.getCurrentPageItems().get(slot); + if (this.slots.contains(slot) && this.slots.indexOf(slot) < this.getCurrentPageItems().size()) { + final T object = this.getCurrentPageItems().get(this.slots.indexOf(slot)); if (object != null) return this.convertToItemStack(object); @@ -446,8 +503,8 @@ protected int getNextButtonPosition() { */ @Override public final void onMenuClick(final Player player, final int slot, final InventoryAction action, final ClickType click, final ItemStack cursor, final ItemStack clicked, final boolean cancelled) { - if (slot < this.getCurrentPageItems().size()) { - final T obj = this.getCurrentPageItems().get(slot); + if (this.slots.contains(slot) && this.slots.indexOf(slot) < this.getCurrentPageItems().size()) { + final T obj = this.getCurrentPageItems().get(this.slots.indexOf(slot)); if (obj != null) { final val prevType = player.getOpenInventory().getType(); @@ -477,4 +534,4 @@ private List getCurrentPageItems() { return this.pages.get(this.currentPage - 1); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mineacademy/fo/menu/MenuTools.java b/src/main/java/org/mineacademy/fo/menu/MenuTools.java index 39c9fbed7..ba3c73527 100644 --- a/src/main/java/org/mineacademy/fo/menu/MenuTools.java +++ b/src/main/java/org/mineacademy/fo/menu/MenuTools.java @@ -145,6 +145,20 @@ protected String[] getInfo() { return null; } + /** + * Compiles an automated tools menu and shows to player. + * + * @param player + * @param pluginToolClasses We will scan your plugin for this kind of class and + * all classes extending it will be loaded into the + * menu + * @param description the menu description + * @return + */ + public static final void display(final Player player, final Class pluginToolClasses, final String... description) { + of(pluginToolClasses, description).displayTo(player); + } + /** * Compiles an automated tools menu. * diff --git a/src/main/java/org/mineacademy/fo/menu/model/ItemCreator.java b/src/main/java/org/mineacademy/fo/menu/model/ItemCreator.java index b26e51d55..ecd60f6dd 100644 --- a/src/main/java/org/mineacademy/fo/menu/model/ItemCreator.java +++ b/src/main/java/org/mineacademy/fo/menu/model/ItemCreator.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.attribute.AttributeModifier; import org.bukkit.enchantments.Enchantment; @@ -546,6 +547,15 @@ public void give(final Player player) { player.getInventory().addItem(this.make()); } + /** + * Convenience method for dropping this item at the given location + * + * @param location + */ + public void drop(final Location location) { + location.getWorld().dropItem(location, this.make()); + } + // ---------------------------------------------------------------------------------------- // Constructing items // ---------------------------------------------------------------------------------------- diff --git a/src/main/java/org/mineacademy/fo/menu/tool/BlockTool.java b/src/main/java/org/mineacademy/fo/menu/tool/BlockTool.java index 86e52fe52..a80118eb4 100644 --- a/src/main/java/org/mineacademy/fo/menu/tool/BlockTool.java +++ b/src/main/java/org/mineacademy/fo/menu/tool/BlockTool.java @@ -61,7 +61,7 @@ protected void onAirClick(final Player player, final ClickType click) { } /** - * Listen for air clicking to invoike {@link #onAirClick(Player, ClickType, Block)} + * Listen for air clicking to invoke {@link #onAirClick(Player, ClickType, Block)} * * @see org.mineacademy.fo.menu.tool.Tool#ignoreCancelled() */ diff --git a/src/main/java/org/mineacademy/fo/metrics/Metrics.java b/src/main/java/org/mineacademy/fo/metrics/Metrics.java index b9dd55d50..848f858bf 100644 --- a/src/main/java/org/mineacademy/fo/metrics/Metrics.java +++ b/src/main/java/org/mineacademy/fo/metrics/Metrics.java @@ -1,21 +1,16 @@ package org.mineacademy.fo.metrics; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Method; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.mineacademy.fo.Common; +import org.mineacademy.fo.plugin.SimplePlugin; +import org.mineacademy.fo.remain.Remain; + +import javax.net.ssl.HttpsURLConnection; +import java.io.*; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -26,14 +21,6 @@ import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; -import javax.net.ssl.HttpsURLConnection; - -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.plugin.SimplePlugin; - /** * bStats collects some data for plugin authors. *

@@ -120,7 +107,7 @@ public void addCustomChart(CustomChart chart) { } private void appendPlatformData(JsonObjectBuilder builder) { - builder.appendField("playerAmount", this.getPlayerAmount()); + builder.appendField("playerAmount", Remain.getOnlinePlayers().size()); builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); builder.appendField("bukkitVersion", Bukkit.getVersion()); builder.appendField("bukkitName", Bukkit.getName()); @@ -135,21 +122,6 @@ private void appendServiceData(JsonObjectBuilder builder) { builder.appendField("pluginVersion", SimplePlugin.getVersion()); } - private int getPlayerAmount() { - try { - // Around MC 1.8 the return type was changed from an array to a collection, - // This fixes java.lang.NoSuchMethodError: - // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; - final Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); - return onlinePlayersMethod.getReturnType().equals(Collection.class) - ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() - : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; - } catch (final Exception e) { - // Just use the new method if the reflection failed - return Bukkit.getOnlinePlayers().size(); - } - } - public static class MetricsBase { /** The version of the Metrics class. */ diff --git a/src/main/java/org/mineacademy/fo/model/ChunkedTask.java b/src/main/java/org/mineacademy/fo/model/ChunkedTask.java index 233a2c951..4a0010e6b 100644 --- a/src/main/java/org/mineacademy/fo/model/ChunkedTask.java +++ b/src/main/java/org/mineacademy/fo/model/ChunkedTask.java @@ -4,14 +4,12 @@ import org.mineacademy.fo.Valid; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; /** * Splits manipulating with large about of items in a list * into smaller pieces */ -@RequiredArgsConstructor public abstract class ChunkedTask { /** @@ -38,6 +36,28 @@ public abstract class ChunkedTask { private boolean processing = false; private boolean firstLaunch = false; + /** + * Create a new task that will process the given amount of times on each run + * (see {@link #waitPeriodTicks}) and wait for 1 second between each time + * + * @param processAmount + */ + public ChunkedTask(int processAmount) { + this.processAmount = processAmount; + } + + /** + * Create a new task that will process the given amount of times on each run + * and wait the given amount of ticks between each time + * + * @param processAmount + * @param waitPeriodTicks + */ + public ChunkedTask(int processAmount, int waitPeriodTicks) { + this.processAmount = processAmount; + this.waitPeriodTicks = waitPeriodTicks; + } + /** * Start the chain, will run several sync tasks until done */ @@ -71,8 +91,19 @@ public final void startChain() { break; } - this.onProcess(i); processed++; + + try { + this.onProcess(i); + + } catch (Throwable t) { + Common.error(t, "Error in " + this + " processing index " + processed); + this.processing = false; + this.firstLaunch = false; + + this.onFinish(false); + return; + } } if (processed > 0 || !finished) @@ -106,7 +137,7 @@ public final void cancel() { * * @param item */ - protected abstract void onProcess(int index); + protected abstract void onProcess(int index) throws Throwable; /** * Return if the task may execute the next index diff --git a/src/main/java/org/mineacademy/fo/model/HookManager.java b/src/main/java/org/mineacademy/fo/model/HookManager.java index 929860dc0..36e6c7312 100644 --- a/src/main/java/org/mineacademy/fo/model/HookManager.java +++ b/src/main/java/org/mineacademy/fo/model/HookManager.java @@ -1,48 +1,5 @@ package org.mineacademy.fo.model; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.annotation.Nullable; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredServiceProvider; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.MinecraftVersion; -import org.mineacademy.fo.MinecraftVersion.V; -import org.mineacademy.fo.PlayerUtil; -import org.mineacademy.fo.ReflectionUtil; -import org.mineacademy.fo.Valid; -import org.mineacademy.fo.collection.StrictSet; -import org.mineacademy.fo.debug.Debugger; -import org.mineacademy.fo.exception.FoException; -import org.mineacademy.fo.plugin.SimplePlugin; -import org.mineacademy.fo.region.Region; -import org.mineacademy.fo.remain.Remain; - import com.Zrips.CMI.CMI; import com.Zrips.CMI.Containers.CMIUser; import com.Zrips.CMI.Modules.TabList.TabListManager; @@ -50,13 +7,10 @@ import com.bekvon.bukkit.residence.protection.ClaimedResidence; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; -import com.earth2me.essentials.CommandSource; -import com.earth2me.essentials.Essentials; -import com.earth2me.essentials.IUser; -import com.earth2me.essentials.User; -import com.earth2me.essentials.UserMap; +import com.earth2me.essentials.*; import com.gmail.nossr50.datatypes.chat.ChatChannel; import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; @@ -68,13 +22,8 @@ import com.onarandombox.MultiverseCore.MultiverseCore; import com.onarandombox.MultiverseCore.api.MultiverseWorld; import com.palmergames.bukkit.towny.TownyUniverse; -import com.palmergames.bukkit.towny.object.Nation; -import com.palmergames.bukkit.towny.object.Resident; -import com.palmergames.bukkit.towny.object.Town; -import com.palmergames.bukkit.towny.object.TownBlock; -import com.palmergames.bukkit.towny.object.WorldCoord; +import com.palmergames.bukkit.towny.object.*; import com.sk89q.worldguard.protection.regions.ProtectedRegion; - import fr.xephi.authme.api.v3.AuthMeApi; import github.scarsz.discordsrv.DiscordSRV; import github.scarsz.discordsrv.dependencies.jda.api.entities.TextChannel; @@ -83,6 +32,7 @@ import lombok.NoArgsConstructor; import lombok.NonNull; import me.clip.placeholderapi.PlaceholderAPI; +import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderHook; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.Relational; @@ -93,10 +43,37 @@ import net.milkbowl.vault.chat.Chat; import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.permission.Permission; +import org.bukkit.OfflinePlayer; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.mineacademy.fo.*; +import org.mineacademy.fo.MinecraftVersion.V; +import org.mineacademy.fo.collection.StrictSet; +import org.mineacademy.fo.debug.Debugger; +import org.mineacademy.fo.exception.FoException; +import org.mineacademy.fo.plugin.SimplePlugin; +import org.mineacademy.fo.region.Region; +import org.mineacademy.fo.remain.Remain; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.RanksManager; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Our main class hooking into different plugins, providing you * convenience access to their methods @@ -108,6 +85,7 @@ public final class HookManager { // Store hook classes separately for below, avoiding no such method/field errors // ------------------------------------------------------------------------------------------------------------ + private static AdvancedVanishHook advancedVanishHook; private static AuthMeHook authMeHook; private static BanManagerHook banManagerHook; private static BentoBoxHook bentoBoxHook; @@ -149,6 +127,9 @@ public final class HookManager { */ public static void loadDependencies() { + if (Common.doesPluginExist("AdvancedVanish")) + advancedVanishHook = new AdvancedVanishHook(); + if (Common.doesPluginExist("AuthMe")) authMeHook = new AuthMeHook(); @@ -317,6 +298,15 @@ public static void unloadDependencies(final Plugin plugin) { // Methods for determining which plugins were loaded after you call the load method // ------------------------------------------------------------------------------------------------------------ + /** + * Is AdvancedVanish loaded? + * + * @return + */ + public static boolean isAdvancedVanishLoaded() { + return advancedVanishHook != null; + } + /** * Is AuthMe Reloaded loaded? We only support the latest version * @@ -666,83 +656,83 @@ public static String getMythicMobName(Entity entity) { // ------------------------------------------------------------------------------------------------------------ /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island visitors for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxVisitors(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandVisitors(islandLocation) : new HashSet<>(); + public static Set getBentoBoxVisitors(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandVisitors(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island coops for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxCoops(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandCoops(islandLocation) : new HashSet<>(); + public static Set getBentoBoxCoops(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandCoops(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island trustees for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxTrustees(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandTrustees(islandLocation) : new HashSet<>(); + public static Set getBentoBoxTrustees(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandTrustees(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island members for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxMembers(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandMembers(islandLocation) : new HashSet<>(); + public static Set getBentoBoxMembers(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandMembers(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island subowners for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxSubOwners(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandSubOwners(islandLocation) : new HashSet<>(); + public static Set getBentoBoxSubOwners(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandSubOwners(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island owners for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxOwners(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandOwners(islandLocation) : new HashSet<>(); + public static Set getBentoBoxOwners(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandOwners(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island moderators for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxMods(Location islandLocation) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandMods(islandLocation) : new HashSet<>(); + public static Set getBentoBoxMods(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandMods(player) : new HashSet<>(); } /** - * Return BentoBox island members if the given location is an island, or empty set if null + * Return BentoBox island admins for the specified player's island, or empty set if null * - * @param islandLocation + * @param player * @return */ - public static Set getBentoBoxAdmins(Location islandLocation, int rank) { - return isBentoBoxLoaded() ? bentoBoxHook.getIslandAdmins(islandLocation) : new HashSet<>(); + public static Set getBentoBoxAdmins(Player player) { + return isBentoBoxLoaded() ? bentoBoxHook.getIslandAdmins(player) : new HashSet<>(); } // ------------------------------------------------------------------------------------------------------------ @@ -760,7 +750,7 @@ public static Collection getLandPlayers(Player player) { } // ------------------------------------------------------------------------------------------------------------ - // EssentialsX or CMI + // EssentialsX, CMI or AdvancedVanish // ------------------------------------------------------------------------------------------------------------ /** @@ -801,19 +791,34 @@ public static boolean isVanishedCMI(final Player player) { } /** - * Sets the vanish status for player in CMI and Essentials + * Return true if the given player is vanished in AdvancedVanish + * + * @deprecated this does not call metadata check for most plugins nor NMS check, see {@link PlayerUtil#isVanished(Player)} + * @param player + * @return + */ + @Deprecated + public static boolean isVanishedAdvancedVanish(final Player player) { + return isAdvancedVanishLoaded() && advancedVanishHook.isVanished(player); + } + + /** + * Sets the vanish status for player in CMI, Essentials and AdvancedVanish * * @deprecated this does not remove vanish metadata and NMS invisibility, use {@link PlayerUtil#setVanished(Player, boolean)} for that * @param player * @param vanished */ @Deprecated - public static void setVanished(Player player, boolean vanished) { + public static void setVanished(@NonNull Player player, boolean vanished) { if (isEssentialsLoaded()) essentialsHook.setVanished(player.getName(), vanished); if (isCMILoaded()) CMIHook.setVanished(player, vanished); + + if (isAdvancedVanishLoaded()) + advancedVanishHook.setVanished(player, vanished); } /** @@ -1662,6 +1667,36 @@ public static void sendDiscordMessage(final String channel, @NonNull final Strin // // ------------------------------------------------------------------------------------------------------------ +class AdvancedVanishHook { + + boolean isVanished(Player player) { + final Class clazz = ReflectionUtil.lookupClass("me.quantiom.advancedvanish.util.AdvancedVanishAPI"); + final Object instance = ReflectionUtil.getStaticFieldContent(clazz, "INSTANCE"); + + final Method isPlayerVanished = ReflectionUtil.getMethod(clazz, "isPlayerVanished", Player.class); + + return ReflectionUtil.invoke(isPlayerVanished, instance, player); + } + + void setVanished(Player player, boolean vanished) { + final Class clazz = ReflectionUtil.lookupClass("me.quantiom.advancedvanish.util.AdvancedVanishAPI"); + final Object instance = ReflectionUtil.getStaticFieldContent(clazz, "INSTANCE"); + + if (vanished) { + if (!this.isVanished(player)) { + final Method vanishPlayer = ReflectionUtil.getMethod(clazz, "vanishPlayer", Player.class, boolean.class); + + ReflectionUtil.invoke(vanishPlayer, instance, player, false); + } + + } else if (this.isVanished(player)) { + final Method unVanishPlayer = ReflectionUtil.getMethod(clazz, "unVanishPlayer", Player.class, boolean.class); + + ReflectionUtil.invoke(unVanishPlayer, instance, player, false); + } + } +} + class AuthMeHook { boolean isLogged(final Player player) { @@ -2275,7 +2310,11 @@ final String replacePlaceholders(final OfflinePlayer player, final String msg) { private String setPlaceholders(final OfflinePlayer player, String text) { final String oldText = text; - final Map hooks = PlaceholderAPI.getPlaceholders(); + final Map hooks = new HashMap<>(); + + // MineAcademy edit: Case insensitive + for (final PlaceholderExpansion expansion : PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getExpansions()) + hooks.put(expansion.getIdentifier().toLowerCase(), expansion); if (hooks.isEmpty()) return text; @@ -2286,7 +2325,7 @@ private String setPlaceholders(final OfflinePlayer player, String text) { return text; } - private String setPlaceholders(OfflinePlayer player, String oldText, String text, Map hooks, Matcher matcher) { + private String setPlaceholders(OfflinePlayer player, String oldText, String text, Map hooks, Matcher matcher) { while (matcher.find()) { String format = matcher.group(1); boolean frontSpace = false; @@ -2309,7 +2348,7 @@ private String setPlaceholders(OfflinePlayer player, String oldText, String text if (index <= 0 || index >= format.length()) continue; - final String identifier = format.substring(0, index); + final String identifier = format.substring(0, index).toLowerCase(); final String params = format.substring(index + 1); final String finalFormat = format; @@ -3347,45 +3386,60 @@ boolean isMuted(final Player player) { class BentoBoxHook { - Set getIslandVisitors(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.VISITOR_RANK); + Set getIslandVisitors(Player player) { + return this.getIslandUsers(player, RanksManager.VISITOR_RANK); } - Set getIslandCoops(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.COOP_RANK); + Set getIslandCoops(Player player) { + return this.getIslandUsers(player, RanksManager.COOP_RANK); } - Set getIslandTrustees(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.TRUSTED_RANK); + Set getIslandTrustees(Player player) { + return this.getIslandUsers(player, RanksManager.TRUSTED_RANK); } - Set getIslandMembers(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.MEMBER_RANK); + Set getIslandMembers(Player player) { + return this.getIslandUsers(player, RanksManager.MEMBER_RANK); } - Set getIslandSubOwners(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.SUB_OWNER_RANK); + Set getIslandSubOwners(Player player) { + return this.getIslandUsers(player, RanksManager.SUB_OWNER_RANK); } - Set getIslandOwners(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.OWNER_RANK); + Set getIslandOwners(Player player) { + return this.getIslandUsers(player, RanksManager.OWNER_RANK); } - Set getIslandMods(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.MOD_RANK); + Set getIslandMods(Player player) { + return this.getIslandUsers(player, RanksManager.MOD_RANK); } - Set getIslandAdmins(Location islandLocation) { - return this.getIslandUsers(islandLocation, RanksManager.ADMIN_RANK); + Set getIslandAdmins(Player player) { + return this.getIslandUsers(player, RanksManager.ADMIN_RANK); } - private Set getIslandUsers(Location islandLocation, int rank) { - final Optional maybeIsland = BentoBox.getInstance().getIslands().getIslandAt(islandLocation); + private Set getIslandUsers(Player player, int rank) { + final IslandsManager manager = BentoBox.getInstance().getIslands(); + final Optional maybeIsland = manager.getIslandAt(player.getLocation()); if (maybeIsland.isPresent()) { final Island island = maybeIsland.get(); return island.getMemberSet(rank); + + } else { + final UUID uniqueId = player.getUniqueId(); + + for (World world : Bukkit.getWorlds()) { + try { + Island island = manager.getIsland(world, uniqueId); + + if (island != null) + return island.getMemberSet(rank); + + } catch (Throwable t) { + } + } } return new HashSet<>(); @@ -3522,16 +3576,16 @@ boolean isMuted(final Player player) { /*try { final Class api = ReflectionUtil.lookupClass("litebans.api.Database"); final Object instance = ReflectionUtil.invokeStatic(api, "get"); - + return ReflectionUtil.invoke("isPlayerMuted", instance, player.getUniqueId()); - + } catch (final Throwable t) { if (!t.toString().contains("Could not find class")) { Common.log("Unable to check if " + player.getName() + " is muted at LiteBans. Is the API hook outdated? See console error:"); - + t.printStackTrace(); } - + return false; }*/ } diff --git a/src/main/java/org/mineacademy/fo/model/JavaScriptExecutor.java b/src/main/java/org/mineacademy/fo/model/JavaScriptExecutor.java index 655ec57b8..01a61aee1 100644 --- a/src/main/java/org/mineacademy/fo/model/JavaScriptExecutor.java +++ b/src/main/java/org/mineacademy/fo/model/JavaScriptExecutor.java @@ -124,6 +124,7 @@ public static Object run(final String javascript, final CommandSender sender) { * @return */ public static Object run(@NonNull String javascript, final CommandSender sender, final Event event) { + final String oldCode = new String(javascript); // Cache for highest performance Map cached = sender instanceof Player ? resultCache.get(((Player) sender).getUniqueId()) : null; @@ -200,7 +201,7 @@ else if (resultString.equals("false") || resultString.equals("no")) throw new EventHandledException(true); } - throw new RuntimeException(error + " '" + javascript + "'", ex); + throw new RuntimeException(error + " '" + oldCode + "'", ex); } } diff --git a/src/main/java/org/mineacademy/fo/model/RandomNoRepeatPicker.java b/src/main/java/org/mineacademy/fo/model/RandomNoRepeatPicker.java index 99309d34e..7cef24997 100644 --- a/src/main/java/org/mineacademy/fo/model/RandomNoRepeatPicker.java +++ b/src/main/java/org/mineacademy/fo/model/RandomNoRepeatPicker.java @@ -101,6 +101,15 @@ public T pickRandom(final Player player) { return null; } + /** + * Return the remaining count of elements + * + * @return + */ + public int remaining() { + return this.list.size(); + } + /** * Should return true if the player can obtain the given item * diff --git a/src/main/java/org/mineacademy/fo/model/SimpleComponent.java b/src/main/java/org/mineacademy/fo/model/SimpleComponent.java index 692ea82dd..f4ad739e1 100644 --- a/src/main/java/org/mineacademy/fo/model/SimpleComponent.java +++ b/src/main/java/org/mineacademy/fo/model/SimpleComponent.java @@ -340,7 +340,7 @@ public TextComponent build(CommandSender receiver) { if (preparedComponent == null) preparedComponent = component; else - preparedComponent.addExtra(component); + this.addExtra(preparedComponent, component); } final TextComponent currentComponent = this.currentComponent == null ? null : this.currentComponent.toTextComponent(true, receiver); @@ -349,11 +349,29 @@ public TextComponent build(CommandSender receiver) { if (preparedComponent == null) preparedComponent = currentComponent; else - preparedComponent.addExtra(currentComponent); + this.addExtra(preparedComponent, currentComponent); return Common.getOrDefault(preparedComponent, new TextComponent("")); } + /* + * Mostly resolving some ancient MC version incompatibility: "UnsupportedOperationException" + */ + private void addExtra(BaseComponent component, BaseComponent extra) { + try { + component.addExtra(extra); + + } catch (final Throwable t) { + final List safeList = new ArrayList<>(); + + if (component.getExtra() != null) + safeList.addAll(component.getExtra()); + + safeList.add(extra); + component.setExtra(safeList); + } + } + /** * Quickly replaces an object in all parts of this component * @@ -839,7 +857,11 @@ private TextComponent toTextComponent(boolean checkForReceiver, CommandSender re part.setClickEvent(this.clickEvent); if (this.insertion != null) - part.setInsertion(this.insertion); + try { + part.setInsertion(this.insertion); + } catch (final Throwable t) { + // Unsupported MC version + } } return new TextComponent(base.toArray(new BaseComponent[base.size()])); diff --git a/src/main/java/org/mineacademy/fo/model/SimpleHologramStand.java b/src/main/java/org/mineacademy/fo/model/SimpleHologramStand.java index 34e72bfe5..e17c95f11 100644 --- a/src/main/java/org/mineacademy/fo/model/SimpleHologramStand.java +++ b/src/main/java/org/mineacademy/fo/model/SimpleHologramStand.java @@ -75,7 +75,7 @@ protected final Entity createEntity() { armorStand.setSmall(this.small); }; - return getLastTeleportLocation().getWorld().spawn(getLastTeleportLocation(), ArmorStand.class, consumer); + return this.getLastTeleportLocation().getWorld().spawn(this.getLastTeleportLocation(), ArmorStand.class, consumer); } /** diff --git a/src/main/java/org/mineacademy/fo/model/Variables.java b/src/main/java/org/mineacademy/fo/model/Variables.java index 30f1c76d7..63999c166 100644 --- a/src/main/java/org/mineacademy/fo/model/Variables.java +++ b/src/main/java/org/mineacademy/fo/model/Variables.java @@ -1,25 +1,11 @@ package org.mineacademy.fo.model; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.GeoAPI; +import org.mineacademy.fo.*; import org.mineacademy.fo.GeoAPI.GeoResponse; -import org.mineacademy.fo.Messenger; -import org.mineacademy.fo.MinecraftVersion; -import org.mineacademy.fo.PlayerUtil; -import org.mineacademy.fo.TimeUtil; import org.mineacademy.fo.collection.StrictList; import org.mineacademy.fo.collection.StrictMap; import org.mineacademy.fo.collection.expiringmap.ExpiringMap; @@ -28,6 +14,15 @@ import org.mineacademy.fo.settings.SimpleLocalization; import org.mineacademy.fo.settings.SimpleSettings; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * A simple engine that replaces variables in a message. */ @@ -67,7 +62,7 @@ public final class Variables { * Should we replace javascript placeholders from variables/ folder automatically? * Used internally to prevent race condition */ - static boolean REPLACE_JAVASCRIPT = true; + volatile static boolean REPLACE_JAVASCRIPT = true; // ------------------------------------------------------------------------------------------------------------ // Custom variables @@ -240,67 +235,69 @@ public static String replace(String message, CommandSender sender, Map replacements, boolean colorize) { - if (message == null || message.isEmpty()) - return ""; + synchronized (cache) { + if (message == null || message.isEmpty()) + return ""; - final String original = message; - final boolean senderIsPlayer = sender instanceof Player; + final String original = message; + final boolean senderIsPlayer = sender instanceof Player; - // Replace custom variables first - if (replacements != null && !replacements.isEmpty()) - message = Replacer.replaceArray(message, replacements); + // Replace custom variables first + if (replacements != null && !replacements.isEmpty()) + message = Replacer.replaceArray(message, replacements); - if (senderIsPlayer) { + if (senderIsPlayer) { - // Already cached ? Return. - final Map cached = cache.get(sender.getName()); - final String cachedVar = cached != null ? cached.get(message) : null; + // Already cached ? Return. + final Map cached = cache.get(sender.getName()); + final String cachedVar = cached != null ? cached.get(message) : null; - if (cachedVar != null) - return cachedVar; - } + if (cachedVar != null) + return cachedVar; + } - // Custom placeholders - if (REPLACE_JAVASCRIPT) { - REPLACE_JAVASCRIPT = false; + // Custom placeholders + if (REPLACE_JAVASCRIPT) { + REPLACE_JAVASCRIPT = false; - try { - message = replaceJavascriptVariables0(message, sender, replacements); + try { + message = replaceJavascriptVariables0(message, sender, replacements); - } finally { - REPLACE_JAVASCRIPT = true; + } finally { + REPLACE_JAVASCRIPT = true; + } } - } - // PlaceholderAPI and MvdvPlaceholderAPI - if (senderIsPlayer) - message = HookManager.replacePlaceholders((Player) sender, message); + // PlaceholderAPI and MvdvPlaceholderAPI + if (senderIsPlayer) + message = HookManager.replacePlaceholders((Player) sender, message); - else if (sender instanceof DiscordSender) - message = HookManager.replacePlaceholders(((DiscordSender) sender).getOfflinePlayer(), message); + else if (sender instanceof DiscordSender) + message = HookManager.replacePlaceholders(((DiscordSender) sender).getOfflinePlayer(), message); - // Default - message = replaceHardVariables0(sender, message); + // Default + message = replaceHardVariables0(sender, message); - // Support the & color system and replacing variables in variables - if (!message.startsWith("[JSON]")) { - message = Common.colorize(message); + // Support the & color system and replacing variables in variables + if (!message.startsWith("[JSON]")) { + message = Common.colorize(message); - if (!original.equals(message) && ((message.contains("{") && message.contains("}")) || message.contains("%"))) - return replace(message, sender, replacements, colorize); + if (!original.equals(message) && ((message.contains("{") && message.contains("}")) || message.contains("%"))) + return replace(message, sender, replacements, colorize); - } + } - if (senderIsPlayer) { - final Map map = cache.get(sender.getName()); + if (senderIsPlayer) { + final Map map = cache.get(sender.getName()); - if (map != null) - map.put(original, message); - else - cache.put(sender.getName(), Common.newHashMap(original, message)); - } + if (map != null) + map.put(original, message); + else + cache.put(sender.getName(), Common.newHashMap(original, message)); + } - return message; + return message; + } } /* diff --git a/src/main/java/org/mineacademy/fo/plugin/FoundationFilter.java b/src/main/java/org/mineacademy/fo/plugin/FoundationFilter.java index 7a813c0a1..3ee4a2af3 100644 --- a/src/main/java/org/mineacademy/fo/plugin/FoundationFilter.java +++ b/src/main/java/org/mineacademy/fo/plugin/FoundationFilter.java @@ -1,10 +1,8 @@ package org.mineacademy.fo.plugin; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.LogRecord; - +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Marker; @@ -15,9 +13,10 @@ import org.bukkit.ChatColor; import org.bukkit.plugin.Plugin; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.Setter; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.LogRecord; /** * Represents the console filtering module @@ -80,6 +79,11 @@ static boolean isFiltered(String message) { if (message.endsWith("which is not a depend, softdepend or loadbefore of this plugin.")) return true; + // Disable some annoying hikaripool or discordsrv messages + if (message.contains("HikariPool-1 - Starting...") || message.contains("HikariPool-1 - Start completed.") + || message.contains("[DiscordSRV] [JDA] Login Successful!") || message.contains("[DiscordSRV] [JDA] Connected to WebSocket")) + return true; + message = message.toLowerCase(); // Only filter this after plugin has been fully enabled diff --git a/src/main/java/org/mineacademy/fo/plugin/FoundationListener.java b/src/main/java/org/mineacademy/fo/plugin/FoundationListener.java index 9e336a4cd..6ffd041d2 100644 --- a/src/main/java/org/mineacademy/fo/plugin/FoundationListener.java +++ b/src/main/java/org/mineacademy/fo/plugin/FoundationListener.java @@ -1,8 +1,5 @@ package org.mineacademy.fo.plugin; -import java.util.List; -import java.util.Map; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -14,19 +11,14 @@ import org.bukkit.event.server.ServiceRegisterEvent; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.plugin.Plugin; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.MathUtil; -import org.mineacademy.fo.Messenger; -import org.mineacademy.fo.MinecraftVersion; +import org.mineacademy.fo.*; import org.mineacademy.fo.MinecraftVersion.V; -import org.mineacademy.fo.PlayerUtil; -import org.mineacademy.fo.model.ChatPaginator; -import org.mineacademy.fo.model.HookManager; -import org.mineacademy.fo.model.SimpleComponent; -import org.mineacademy.fo.model.SimpleScoreboard; -import org.mineacademy.fo.model.SpigotUpdater; +import org.mineacademy.fo.model.*; import org.mineacademy.fo.settings.SimpleLocalization; +import java.util.List; +import java.util.Map; + /** * Listens for some events we handle for you automatically */ @@ -181,9 +173,10 @@ public void onJoin(PlayerJoinEvent event) { if (!player.hasMetadata("vanished")) { final boolean essVanished = HookManager.isVanishedEssentials(player); final boolean cmiVanished = HookManager.isVanishedCMI(player); + final boolean advVanished = HookManager.isVanishedAdvancedVanish(player); - if (essVanished || cmiVanished) { - final Plugin plugin = Bukkit.getPluginManager().getPlugin(essVanished ? "Essentials" : "CMI"); + if (essVanished || cmiVanished || advVanished) { + final Plugin plugin = Bukkit.getPluginManager().getPlugin(essVanished ? "Essentials" : cmiVanished ? "CMI" : "AdvancedVanish"); player.setMetadata("vanished", new FixedMetadataValue(plugin, true)); } diff --git a/src/main/java/org/mineacademy/fo/plugin/SimplePlugin.java b/src/main/java/org/mineacademy/fo/plugin/SimplePlugin.java index 8d1bbcd44..5b8ac8584 100644 --- a/src/main/java/org/mineacademy/fo/plugin/SimplePlugin.java +++ b/src/main/java/org/mineacademy/fo/plugin/SimplePlugin.java @@ -207,14 +207,14 @@ public final void onLoad() { && !version.contains("NachoSpigot") && !version.contains("-Spigot") && MinecraftVersion.atLeast(V.v1_8)) { - Bukkit.getLogger().severe(Common.consoleLine()); - Bukkit.getLogger().warning("Warning about " + named + ": You're not using Paper!"); - Bukkit.getLogger().warning("Detected: " + version); - Bukkit.getLogger().warning(""); - Bukkit.getLogger().warning("Third party forks are known to alter server in unwanted"); - Bukkit.getLogger().warning("ways. If you have issues with " + named + " use Paper"); - Bukkit.getLogger().warning("from PaperMC.io otherwise you may not receive our support."); - Bukkit.getLogger().severe(Common.consoleLine()); + this.getLogger().warning(Common.consoleLine()); + this.getLogger().warning("You're not using Paper!"); + this.getLogger().warning("Detected: " + version); + this.getLogger().warning(""); + this.getLogger().warning("Third party forks are known to alter server in unwanted ways."); + this.getLogger().warning("If you experience issues with " + named + ", download Paper"); + this.getLogger().warning("from PaperMC.io, otherwise you may not receive support."); + this.getLogger().warning(Common.consoleLine()); } // Load libraries where Spigot does not do this automatically @@ -229,7 +229,7 @@ public final void onEnable() { // Disabled upstream if (!this.canLoad) { - Bukkit.getLogger().severe("Not loading, the plugin is disabled (look for console errors above)"); + this.getLogger().severe("Not loading, the plugin is disabled (look for console errors above)"); return; } @@ -292,6 +292,8 @@ public final void onEnable() { // -------------------------------------------- final Messenger messenger = this.getServer().getMessenger(); + + // Always make the main channel available if (!messenger.isOutgoingChannelRegistered(this, "BungeeCord")) messenger.registerOutgoingPluginChannel(this, "BungeeCord"); @@ -481,9 +483,10 @@ protected List getLibraries() { protected final void registerBungeeCord(@NonNull BungeeListener bungee) { final Messenger messenger = this.getServer().getMessenger(); - messenger.registerIncomingPluginChannel(this, bungee.getChannel(), bungee); - messenger.registerOutgoingPluginChannel(this, bungee.getChannel()); + if (!messenger.isIncomingChannelRegistered(this, "BungeeCord")) + messenger.registerIncomingPluginChannel(this, "BungeeCord", new BungeeListener.CommonBungeeListener()); + BungeeListener.addRegisteredListener(bungee); this.reloadables.registerEvents(bungee); } @@ -770,7 +773,11 @@ public final void reload() { this.unregisterReloadables(); - FileConfig.clearLoadedSections(); + final Messenger messenger = this.getServer().getMessenger(); + + // Always make the main channel available + if (!messenger.isOutgoingChannelRegistered(this, "BungeeCord")) + messenger.registerOutgoingPluginChannel(this, "BungeeCord"); // Load our dependency system try { @@ -833,6 +840,9 @@ private void unregisterReloadables() { BlockVisualizer.stopAll(); FolderWatcher.stopThreads(); + BungeeListener.clearRegisteredListeners(); + FileConfig.clearLoadedSections(); + try { if (HookManager.isDiscordSRVLoaded()) DiscordListener.clearRegisteredListeners(); diff --git a/src/main/java/org/mineacademy/fo/region/Region.java b/src/main/java/org/mineacademy/fo/region/Region.java index 33c15d89f..0d5e1242f 100644 --- a/src/main/java/org/mineacademy/fo/region/Region.java +++ b/src/main/java/org/mineacademy/fo/region/Region.java @@ -1,10 +1,8 @@ package org.mineacademy.fo.region; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Set; - +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -17,9 +15,11 @@ import org.mineacademy.fo.collection.SerializedMap; import org.mineacademy.fo.model.ConfigSerializable; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; /** * Represents a cuboid region @@ -275,6 +275,30 @@ public final boolean isWithin(@NonNull final Location location) { && z >= primary.getZ() && z <= secondary.getZ(); } + /** + * Returns true if X and Z coordinates irrespective of height of the given location are within + * this region. + * + * @param location + * @return + */ + public final boolean isWithinXZ(@NonNull final Location location) { + Valid.checkBoolean(this.isWhole(), "Cannot perform isWithinXZ on a non-complete region: " + this.toString()); + + if (!location.getWorld().getName().equals(this.primary.getWorld().getName())) + return false; + + final Location[] centered = this.getCorrectedPoints(); + final Location primary = centered[0]; + final Location secondary = centered[1]; + + final int x = (int) location.getX(); + final int z = (int) location.getZ(); + + return x >= primary.getX() && x <= secondary.getX() + && z >= primary.getZ() && z <= secondary.getZ(); + } + /** * Return true if both region points are set * @@ -312,6 +336,21 @@ public final void setLocation(Location location, ClickType click) { this.setLocation(location, click, false); } + /** + * Sets the primary and/or secondary locations points if they are not + * null. + * + * @param primary + * @param secondary + */ + public final void updateLocation(@Nullable Location primary, @Nullable Location secondary) { + if (primary != null) + this.setPrimary(primary); + + if (secondary != null) + this.setSecondary(secondary); + } + /** * Sets the location from the click type. LEFT = primary, RIGHT = secondary * diff --git a/src/main/java/org/mineacademy/fo/remain/CompColor.java b/src/main/java/org/mineacademy/fo/remain/CompColor.java index 8a5d894e3..ba8e2fd48 100644 --- a/src/main/java/org/mineacademy/fo/remain/CompColor.java +++ b/src/main/java/org/mineacademy/fo/remain/CompColor.java @@ -179,6 +179,15 @@ public Color getColor() { return this.color != null ? this.color : this.dye.getColor(); } + /** + * Converts this into a wool material + * + * @return + */ + public CompMaterial getWool() { + return CompColor.toWool(this.chatColor); + } + // ---------------------------------------------------------------------------------------------------- // Static access // ---------------------------------------------------------------------------------------------------- diff --git a/src/main/java/org/mineacademy/fo/remain/CompEquipmentSlot.java b/src/main/java/org/mineacademy/fo/remain/CompEquipmentSlot.java index 391458a25..a179a4e54 100644 --- a/src/main/java/org/mineacademy/fo/remain/CompEquipmentSlot.java +++ b/src/main/java/org/mineacademy/fo/remain/CompEquipmentSlot.java @@ -1,142 +1,291 @@ -package org.mineacademy.fo.remain; - -import javax.annotation.Nullable; - -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.inventory.EntityEquipment; -import org.bukkit.inventory.ItemStack; -import org.mineacademy.fo.MinecraftVersion; -import org.mineacademy.fo.MinecraftVersion.V; -import org.mineacademy.fo.Valid; -import org.mineacademy.fo.exception.FoException; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -/** - * Represents {@link EquipmentSlot} - */ -@RequiredArgsConstructor -public enum CompEquipmentSlot { - - HAND("HAND", "HAND"), - /** - * Requires Minecraft 1.9+ - */ - OFF_HAND("OFF_HAND", "OFF_HAND"), - HEAD("HEAD", "HELMET"), - CHEST("CHEST", "CHESTPLATE"), - LEGS("LEGS", "LEGGINGS"), - FEET("FEET", "BOOTS"); - - /** - * The localizable key - */ - @Getter - private final String key; - - /** - * The alternative Bukkit name. - */ - @Getter - private final String bukkitName; - - /** - * Applies this equipment slot to the given entity with the given item - * - * @param entity - * @param item - */ - public void applyTo(LivingEntity entity, ItemStack item) { - this.applyTo(entity, null); - } - - /** - * Applies this equipment slot to the given entity with the given item, - * and optional drop chance from 0 to 1.0 - * - * @param entity - * @param item - * @param dropChance - */ - public void applyTo(LivingEntity entity, ItemStack item, @Nullable Double dropChance) { - final EntityEquipment equip = entity.getEquipment(); - final boolean lacksDropChance = entity instanceof HumanEntity || entity.getType().toString().equals("ARMOR_STAND"); - - switch (this) { - - case HAND: - equip.setItemInHand(item); - - if (dropChance != null && !lacksDropChance) - equip.setItemInHandDropChance(dropChance.floatValue()); - - break; - - case OFF_HAND: - Valid.checkBoolean(MinecraftVersion.atLeast(V.v1_9), "Setting off hand item requires Minecraft 1.9+"); - - equip.setItemInOffHand(item); - - if (dropChance != null && !lacksDropChance) - equip.setItemInOffHandDropChance(dropChance.floatValue()); - - break; - - case HEAD: - equip.setHelmet(item); - - if (dropChance != null && !lacksDropChance) - equip.setHelmetDropChance(dropChance.floatValue()); - - break; - - case CHEST: - equip.setChestplate(item); - - if (dropChance != null && !lacksDropChance) - equip.setChestplateDropChance(dropChance.floatValue()); - - break; - - case LEGS: - equip.setLeggings(item); - - if (dropChance != null && !lacksDropChance) - equip.setLeggingsDropChance(dropChance.floatValue()); - - break; - - case FEET: - equip.setBoots(item); - - if (dropChance != null && !lacksDropChance) - equip.setBootsDropChance(dropChance.floatValue()); - - break; - } - } - - /** - * Attempts to parse equip. slot from the given key, or throwing - * an error if not found - * - * @param key - * @return - */ - public static CompEquipmentSlot fromKey(String key) { - key = key.toUpperCase().replace(" ", "_"); - - for (final CompEquipmentSlot slot : values()) - if (slot.key.equals(key) || slot.bukkitName.equals(key)) - return slot; - - throw new FoException("No such equipment slot from key: " + key); - } - - @Override - public String toString() { - return this.key.toUpperCase(); - } -} +package org.mineacademy.fo.remain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.ItemStack; +import org.mineacademy.fo.MinecraftVersion; +import org.mineacademy.fo.MinecraftVersion.V; +import org.mineacademy.fo.Valid; +import org.mineacademy.fo.exception.FoException; +import org.mineacademy.fo.menu.model.ItemCreator; + +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents {@link EquipmentSlot} + */ +@RequiredArgsConstructor +public enum CompEquipmentSlot { + + HAND("HAND", "HAND"), + /** + * Requires Minecraft 1.9+ + */ + OFF_HAND("OFF_HAND", "OFF_HAND"), + HEAD("HEAD", "HELMET"), + CHEST("CHEST", "CHESTPLATE"), + LEGS("LEGS", "LEGGINGS"), + FEET("FEET", "BOOTS"); + + /** + * The localizable key + */ + @Getter + private final String key; + + /** + * The alternative Bukkit name. + */ + @Getter + private final String bukkitName; + + /** + * Applies this equipment slot to the given entity with the given item + * + * @param entity + * @param item + */ + public void applyTo(LivingEntity entity, ItemStack item) { + this.applyTo(entity, item, null); + } + + /** + * Applies this equipment slot to the given entity with the given item, + * and optional drop chance from 0 to 1.0 + * + * @param entity + * @param item + * @param dropChance + */ + public void applyTo(LivingEntity entity, ItemStack item, @Nullable Double dropChance) { + final EntityEquipment equipment = entity instanceof LivingEntity ? entity.getEquipment() : null; + Valid.checkNotNull(equipment); + + final boolean lacksDropChance = entity instanceof HumanEntity || entity.getType().toString().equals("ARMOR_STAND"); + + switch (this) { + + case HAND: + equipment.setItemInHand(item); + + if (dropChance != null && !lacksDropChance) + equipment.setItemInHandDropChance(dropChance.floatValue()); + + break; + + case OFF_HAND: + Valid.checkBoolean(MinecraftVersion.atLeast(V.v1_9), "Setting off hand item requires Minecraft 1.9+"); + + equipment.setItemInOffHand(item); + + if (dropChance != null && !lacksDropChance) + equipment.setItemInOffHandDropChance(dropChance.floatValue()); + + break; + + case HEAD: + equipment.setHelmet(item); + + if (dropChance != null && !lacksDropChance) + equipment.setHelmetDropChance(dropChance.floatValue()); + + break; + + case CHEST: + equipment.setChestplate(item); + + if (dropChance != null && !lacksDropChance) + equipment.setChestplateDropChance(dropChance.floatValue()); + + break; + + case LEGS: + equipment.setLeggings(item); + + if (dropChance != null && !lacksDropChance) + equipment.setLeggingsDropChance(dropChance.floatValue()); + + break; + + case FEET: + equipment.setBoots(item); + + if (dropChance != null && !lacksDropChance) + equipment.setBootsDropChance(dropChance.floatValue()); + + break; + } + } + + /** + * Attempts to parse equip. slot from the given key, or throwing + * an error if not found + * + * @param key + * @return + */ + public static CompEquipmentSlot fromKey(String key) { + key = key.toUpperCase().replace(" ", "_"); + + for (final CompEquipmentSlot slot : values()) + if (slot.key.equals(key) || slot.bukkitName.equals(key)) + return slot; + + throw new FoException("No such equipment slot from key: " + key); + } + + /** + * A convenience shortcut to quickly give the entity a full leather armor in the given color + * that does not drop. + * + * @param entity + * @param color + */ + public static void applyArmor(LivingEntity entity, CompColor color) { + applyArmor(entity, color, 0D, new HashSet<>()); + } + + /** + * A convenience shortcut to quickly give the entity a full leather armor in the given color + * that does not drop. + * + * @param entity + * @param color + * @param ignoredSlots + */ + public static void applyArmor(LivingEntity entity, CompColor color, Set ignoredSlots) { + applyArmor(entity, color, 0D, ignoredSlots); + } + + /** + * A convenience shortcut to quickly give the entity a full leather armor in the given color + * + * @param entity + * @param color + * @param dropChance + */ + public static void applyArmor(LivingEntity entity, CompColor color, double dropChance) { + applyArmor(entity, color, dropChance, new HashSet<>()); + } + + /** + * A convenience shortcut to quickly give the entity a full leather armor in the given color + * + * @param entity + * @param color + * @param dropChance + */ + public static void applyArmor(LivingEntity entity, CompColor color, Double dropChance, Set ignoredSlots) { + if (!ignoredSlots.contains(HEAD)) + HEAD.applyTo(entity, ItemCreator.of(CompMaterial.LEATHER_HELMET).color(color).make(), dropChance); + + if (!ignoredSlots.contains(CHEST)) + CHEST.applyTo(entity, ItemCreator.of(CompMaterial.LEATHER_CHESTPLATE).color(color).make(), dropChance); + + if (!ignoredSlots.contains(LEGS)) + LEGS.applyTo(entity, ItemCreator.of(CompMaterial.LEATHER_LEGGINGS).color(color).make(), dropChance); + + if (!ignoredSlots.contains(FEET)) + FEET.applyTo(entity, ItemCreator.of(CompMaterial.LEATHER_BOOTS).color(color).make(), dropChance); + } + + /** + * A convenience shortcut to quickly give the entity a full armor of the given type + * with 0 drop chance + * + * @param entity + * @param type + */ + public static void applyArmor(LivingEntity entity, Type type) { + applyArmor(entity, type, 0d, new HashSet<>()); + } + + /** + * A convenience shortcut to quickly give the entity a full armor of the given type + * with 0 drop chance + * + * @param entity + * @param ignoredSlots + */ + public static void applyArmor(LivingEntity entity, Type type, Set ignoredSlots) { + applyArmor(entity, type, 0d, ignoredSlots); + } + + /** + * A convenience shortcut to quickly give the entity a full armor of the given type + * + * @param entity + * @param dropChance + */ + public static void applyArmor(LivingEntity entity, Type type, double dropChance) { + applyArmor(entity, type, dropChance, new HashSet<>()); + } + + /** + * A convenience shortcut to quickly give the entity a full armor of the given type + * + * @param entity + * @param type + * @param dropChance + * @param ignoredSlots + */ + public static void applyArmor(LivingEntity entity, Type type, Double dropChance, Set ignoredSlots) { + + // Compatibility + if (type == Type.NETHERITE && MinecraftVersion.olderThan(V.v1_16)) + type = Type.DIAMOND; + + String name = type == Type.GOLD ? "GOLDEN" : type.toString(); + + if (!ignoredSlots.contains(HEAD)) + HEAD.applyTo(entity, CompMaterial.valueOf(name + "_HELMET").toItem(), dropChance); + + if (!ignoredSlots.contains(CHEST)) + CHEST.applyTo(entity, CompMaterial.valueOf(name + "_CHESTPLATE").toItem(), dropChance); + + if (!ignoredSlots.contains(LEGS)) + LEGS.applyTo(entity, CompMaterial.valueOf(name + "_LEGGINGS").toItem(), dropChance); + + if (!ignoredSlots.contains(FEET)) + FEET.applyTo(entity, CompMaterial.valueOf(name + "_BOOTS").toItem(), dropChance); + } + + @Override + public String toString() { + return this.key.toUpperCase(); + } + + /** + * Denotes the main armor material type such as Leather or Diamond + * + */ + public static enum Type { + LEATHER, + CHAINMAIL, + IRON, + GOLD, + DIAMOND, + NETHERITE; + + /** + * Attempts to parse armor material (any helmet, chestplate, leggings or boots) + * to a type based on its type (i.e. iron_helmet -> iron) + * + * @param armorMaterial + * @return + */ + public static Type fromArmor(CompMaterial armorMaterial) { + String n = armorMaterial.name(); + + Valid.checkBoolean(n.contains("LEATHER") || n.contains("CHAINMAIL") || n.contains("IRON") || n.contains("GOLD") || n.contains("DIAMOND") || n.contains("NETHERITE"), + "Only leather to netherite armors are supported, not: " + armorMaterial); + + return Type.valueOf(n.split("_")[0]); + } + } +} diff --git a/src/main/java/org/mineacademy/fo/remain/CompMaterial.java b/src/main/java/org/mineacademy/fo/remain/CompMaterial.java index 5fdacf058..28667e2d2 100644 --- a/src/main/java/org/mineacademy/fo/remain/CompMaterial.java +++ b/src/main/java/org/mineacademy/fo/remain/CompMaterial.java @@ -1796,6 +1796,26 @@ public static boolean isAir(final Material material) { return material == null || nameEquals(material, "AIR", "CAVE_AIR", "VOID_AIR", "LEGACY_AIR"); } + /** + * Returns true if the given material is a bed block (any color) or a bed item. + * + * @param block + * @return + */ + public static boolean isBed(final Block block) { + return isBed(block.getType()); + } + + /** + * Returns true if the given material is a bed block (any color) or a bed item. + * + * @param mat + * @return + */ + public static boolean isBed(final Material mat) { + return nameEquals(mat, "BED", "BED_BLOCK") || nameContains(mat, "_BED"); + } + /** * Returns true if the given material is a horse armor (prev. named barding). * diff --git a/src/main/java/org/mineacademy/fo/remain/CompVillagerProfession.java b/src/main/java/org/mineacademy/fo/remain/CompVillagerProfession.java new file mode 100644 index 000000000..19ddc261b --- /dev/null +++ b/src/main/java/org/mineacademy/fo/remain/CompVillagerProfession.java @@ -0,0 +1,126 @@ +package org.mineacademy.fo.remain; + +import org.bukkit.entity.Villager; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Compatible enum for villager professions where we convert to the + * closest approximate (based on my non-native English understanding :)) + * to pre Village And Pillage update. + */ +@RequiredArgsConstructor +public enum CompVillagerProfession { + + NONE("FARMER"), + + /** + * Armorer profession. Wears a black apron. Armorers primarily trade for + * iron armor, chainmail armor, and sometimes diamond armor. + */ + ARMORER("BLACKSMITH"), + + /** + * Butcher profession. Wears a white apron. Butchers primarily trade for + * raw and cooked food. + */ + BUTCHER("BUTCHER"), + + /** + * Cartographer profession. Wears a white robe. Cartographers primarily + * trade for explorer maps and some paper. + */ + CARTOGRAPHER("FARMER"), + + /** + * Cleric profession. Wears a purple robe. Clerics primarily trade for + * rotten flesh, gold ingot, redstone, lapis, ender pearl, glowstone, + * and bottle o' enchanting. + */ + CLERIC("FARMER"), + + /** + * Farmer profession. Wears a brown robe. Farmers primarily trade for + * food-related items. + */ + FARMER("FARMER"), + + /** + * Fisherman profession. Wears a brown robe. Fisherman primarily trade + * for fish, as well as possibly selling string and/or coal. + */ + FISHERMAN("FARMER"), + + /** + * Fletcher profession. Wears a brown robe. Fletchers primarily trade + * for string, bows, and arrows. + */ + FLETCHER("PRIEST"), + + /** + * Leatherworker profession. Wears a white apron. Leatherworkers + * primarily trade for leather, and leather armor, as well as saddles. + */ + LEATHERWORKER("BUTCHER"), + + /** + * Librarian profession. Wears a white robe. Librarians primarily trade + * for paper, books, and enchanted books. + */ + LIBRARIAN("LIBRARIAN"), + + /** + * Mason profession. + */ + MASON("FARMER"), + + /** + * Nitwit profession. Wears a green apron, cannot trade. Nitwit + * villagers do not do anything. They do not have any trades by default. + */ + NITWIT("FARMER"), + + /** + * Sheperd profession. Wears a brown robe. Shepherds primarily trade for + * wool items, and shears. + */ + SHEPHERD("FARMER"), + + /** + * Toolsmith profession. Wears a black apron. Tool smiths primarily + * trade for iron and diamond tools. + */ + TOOLSMITH("BLACKSMITH"), + + /** + * Weaponsmith profession. Wears a black apron. Weapon smiths primarily + * trade for iron and diamond weapons, sometimes enchanted. + */ + WEAPONSMITH("BLACKSMITH"); + + @Getter + private final String legacyName; + + /** + * Return the villager profession in Bukkit enum + * + * @return + */ + public Villager.Profession toBukkit() { + try { + return Villager.Profession.valueOf(this.name()); + } catch (Throwable t) { + return Villager.Profession.valueOf(this.legacyName); + } + } + + /** + * Apply this profession to the given Villager + * + * @param villager + */ + public void apply(Villager villager) { + villager.setProfession(this.toBukkit()); + } +} \ No newline at end of file diff --git a/src/main/java/org/mineacademy/fo/remain/Remain.java b/src/main/java/org/mineacademy/fo/remain/Remain.java index 7534015b2..c7dc58fbe 100644 --- a/src/main/java/org/mineacademy/fo/remain/Remain.java +++ b/src/main/java/org/mineacademy/fo/remain/Remain.java @@ -10,10 +10,8 @@ import org.bukkit.Statistic.Type; import org.bukkit.advancement.Advancement; import org.bukkit.advancement.AdvancementProgress; -import org.bukkit.block.Biome; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.block.Chest; +import org.bukkit.block.*; +import org.bukkit.block.data.type.Bed; import org.bukkit.command.*; import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.YamlConfiguration; @@ -512,6 +510,23 @@ public static Collection getOnlinePlayers() { return isGetPlayersCollection ? Bukkit.getOnlinePlayers() : Arrays.asList(getPlayersLegacy()); } + /** + * Returns the player's view distance + * + * @param player + * @return + */ + public static int getViewDistance(Player player) { + try { + return player.getClientViewDistance(); + + } catch (NoSuchMethodError err) { + Method getViewDistance = ReflectionUtil.getMethod(player.spigot().getClass(), "getViewDistance"); + + return ReflectionUtil.invoke(getViewDistance, player.spigot()); + } + } + /** * Spawn a falling block at the given block location * @@ -687,6 +702,66 @@ public static void setTypeAndData(final Block block, final CompMaterial material } } + /** + * This will attempt to place a bed block to the initial block and the other head block in the facing direction + * + * Use {@link PlayerUtil#getFacing(Player)} to get where a player is looking at + * + * @param initialLocation + * @param facing + */ + public static void setBed(Location initialLocation, BlockFace facing) { + setBed(initialLocation.getBlock(), facing); + } + + /** + * This will attempt to place a bed block to the initial block and the other head block in the facing direction + * + * Use {@link PlayerUtil#getFacing(Player)} to get where a player is looking at + * + * @param initialBlock + * @param facing + */ + public static void setBed(Block initialBlock, BlockFace facing) { + + if (MinecraftVersion.atLeast(V.v1_13)) + for (final Bed.Part part : Bed.Part.values()) { + initialBlock.setBlockData(Bukkit.createBlockData(CompMaterial.WHITE_BED.getMaterial(), data -> { + ((Bed) data).setPart(part); + ((Bed) data).setFacing(facing); + })); + + initialBlock = initialBlock.getRelative(facing.getOppositeFace()); + } + + else { + initialBlock = initialBlock.getRelative(facing); + + final Material bedMaterial = Material.valueOf("BED_BLOCK"); + final Block bedFootBlock = initialBlock.getRelative(facing.getOppositeFace()); + + final BlockState bedFootState = bedFootBlock.getState(); + bedFootState.setType(bedMaterial); + + final org.bukkit.material.Bed bedFootData = new org.bukkit.material.Bed(bedMaterial); + bedFootData.setHeadOfBed(false); + bedFootData.setFacingDirection(facing); + + bedFootState.setData(bedFootData); + bedFootState.update(true); + + final BlockState bedHeadState = initialBlock.getState(); + bedHeadState.setType(bedMaterial); + + final org.bukkit.material.Bed bedHeadData = new org.bukkit.material.Bed(bedMaterial); + bedHeadData.setHeadOfBed(true); + bedHeadData.setFacingDirection(facing); + + bedHeadState.setData(bedHeadData); + bedHeadState.update(true); + } + } + /** * Converts json string into legacy colored text * @@ -2124,8 +2199,7 @@ public static void sendToast(final Player receiver, final String message, final */ public static void sendToast(final Player receiver, final String message, final CompMaterial icon, final CompToastStyle toastStyle) { if (message != null && !message.isEmpty()) { -// final String colorized = Common.colorize(message); - final String colorized = (message); + final String colorized = Common.colorize(message); if (!colorized.isEmpty()) { Valid.checkSync("Toasts may only be sent from the main thread"); @@ -2327,6 +2401,34 @@ public static LivingEntity getHitEntity(ProjectileHitEvent event) { return null; } + /** + * Attempts to resolve the hit block from projectile hit event + * + * @param event + * @return + */ + public static Block getHitBlock(ProjectileHitEvent event) { + try { + return event.getHitBlock(); + + } catch (final Throwable t) { + + final Block entityBlock = event.getEntity().getLocation().getBlock(); + + if (!CompMaterial.isAir(entityBlock)) + return entityBlock; + + for (final BlockFace face : Arrays.asList(BlockFace.UP, BlockFace.DOWN, BlockFace.WEST, BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH)) { + final Block adjucentBlock = entityBlock.getRelative(face); + + if (!CompMaterial.isAir(adjucentBlock)) + return adjucentBlock; + } + } + + return null; + } + /** * Return nearby entities in a location * @@ -2385,32 +2487,29 @@ public static void takeItemOnePiece(final Player player, final ItemStack item) { if (MinecraftVersion.atLeast(V.v1_15)) item.setAmount(item.getAmount() - 1); - else - Common.runLater(() -> { - if (item.getAmount() > 1) - item.setAmount(item.getAmount() - 1); - else if (MinecraftVersion.atLeast(V.v1_9)) - item.setAmount(0); + else { + if (item.getAmount() > 1) + item.setAmount(item.getAmount() - 1); - // Explanation: For some weird reason there is a bug not removing 1 piece of ItemStack in 1.8.8 - else { - final ItemStack[] content = player.getInventory().getContents(); + // Explanation: For some weird reason there is a bug not removing 1 piece of ItemStack in 1.8.8 + else { + final ItemStack[] content = player.getInventory().getContents(); - for (int i = 0; i < content.length; i++) { - final ItemStack c = content[i]; + for (int slot = 0; slot < content.length; slot++) { + final ItemStack slotItem = content[slot]; - if (c != null && c.equals(item)) { - content[i] = null; + if (slotItem != null && slotItem.equals(item)) { + content[slot] = null; - break; - } + break; } - - player.getInventory().setContents(content); } - player.updateInventory(); - }); + player.getInventory().setContents(content); + } + + player.updateInventory(); + } } /** diff --git a/src/main/java/org/mineacademy/fo/remain/internal/ChatInternals.java b/src/main/java/org/mineacademy/fo/remain/internal/ChatInternals.java index 565107e0b..2be051da2 100644 --- a/src/main/java/org/mineacademy/fo/remain/internal/ChatInternals.java +++ b/src/main/java/org/mineacademy/fo/remain/internal/ChatInternals.java @@ -1,9 +1,5 @@ package org.mineacademy.fo.remain.internal; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.mineacademy.fo.Common; @@ -16,6 +12,10 @@ import org.mineacademy.fo.exception.FoException; import org.mineacademy.fo.remain.Remain; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + /** * Reflection class for handling chat-related methods * @@ -202,7 +202,7 @@ public static void sendActionBarLegacy(final Player player, final String message } // http://wiki.vg/Protocol#Chat_Message - private static void sendChat(final Player pl, final String text, final byte type) { + private static void sendChat(final Player player, final String text, final byte type) { try { final Object message = serializeText(text); Valid.checkNotNull(message, "Message cannot be null!"); @@ -217,10 +217,10 @@ private static void sendChat(final Player pl, final String text, final byte type } else packet = chatMessageConstructor.newInstance(message, type); - Remain.sendPacket(pl, packet); + Remain.sendPacket(player, packet); } catch (final ReflectiveOperationException ex) { - Common.error(ex, "Failed to send chat packet type " + type + " to " + pl.getName() + ", message: " + text); + Common.error(ex, "Failed to send chat packet type " + type + " to " + player.getName() + ", message: " + text); } } diff --git a/src/main/java/org/mineacademy/fo/settings/ConfigItems.java b/src/main/java/org/mineacademy/fo/settings/ConfigItems.java index ba48f36af..ab04e428c 100644 --- a/src/main/java/org/mineacademy/fo/settings/ConfigItems.java +++ b/src/main/java/org/mineacademy/fo/settings/ConfigItems.java @@ -1,17 +1,6 @@ package org.mineacademy.fo.settings; -import java.io.File; -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; - -import javax.annotation.Nullable; - +import lombok.NonNull; import org.bukkit.configuration.file.YamlConfiguration; import org.mineacademy.fo.ChatUtil; import org.mineacademy.fo.Common; @@ -19,7 +8,13 @@ import org.mineacademy.fo.Valid; import org.mineacademy.fo.collection.StrictMap; -import lombok.NonNull; +import javax.annotation.Nullable; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; /** * A special class that can store loaded {@link YamlConfig} files @@ -49,9 +44,15 @@ public final class ConfigItems { private final String folder; /** - * The class we are loading in the list - *

- * *MUST* have a private constructor without any arguments + * How we are going to instantiate a single class from file? + * + * This is for advanced use only, by default, each config item is the same class + * for example in Boss plugin, each boss in bosses/ folder will make a Boss class. + * + * Examples where this can be useful: If you have a minigame plugin and want to store + * different minigames in one folder such as MobArena and BedWars both in games/ folder, + * then you will read the "Type" key in each arena file by opening the file name provided + * in the function as config and returning the specific arena class from a key in that file. */ private final Function> prototypeCreator; @@ -84,7 +85,7 @@ private ConfigItems(String type, String folder, Function> proto * @return */ public static

ConfigItems

fromFolder(String folder, Class

prototypeClass) { - return fromFolder(folder, s -> prototypeClass); + return fromFolder(folder, fileName -> prototypeClass); } /** @@ -109,7 +110,7 @@ public static

ConfigItems

fromFolder(String folder, Fu * @return */ public static

ConfigItems

fromFile(String path, String file, Class

prototypeClass) { - return fromFile(path, file, s -> prototypeClass); + return fromFile(path, file, fileName -> prototypeClass); } /** @@ -145,7 +146,6 @@ public void loadItems(@Nullable Function loader) { if (this.singleFile) { final File file = FileUtil.extract(this.folder); final YamlConfiguration config = YamlConfiguration.loadConfiguration(file); - //Valid.checkBoolean(config.isSet(this.type), "Unable to locate configuration section " + this.type + " in " + file); if (config.isSet(this.type)) for (final String name : config.getConfigurationSection(this.type).getKeys(false)) @@ -217,7 +217,9 @@ public T loadOrCreateItem(@NonNull final String name, @Nullable Supplier inst nameConstructor = false; } - Valid.checkBoolean(Modifier.isPrivate(constructor.getModifiers()), "Your class " + prototypeClass + " must have private constructor taking a String or nothing!"); + Valid.checkBoolean(Modifier.isPrivate(constructor.getModifiers()) || Modifier.isProtected(constructor.getModifiers()), + "Your class " + prototypeClass + " must have a private or protected constructor taking a String or nothing!"); + constructor.setAccessible(true); try { @@ -301,8 +303,8 @@ public T findItem(@NonNull final String name) { * * @return */ - public Collection getItems() { - return Collections.unmodifiableCollection(this.loadedItemsMap.values()); + public List getItems() { + return Collections.unmodifiableList(new ArrayList<>(this.loadedItemsMap.values())); } /** diff --git a/src/main/java/org/mineacademy/fo/settings/FileConfig.java b/src/main/java/org/mineacademy/fo/settings/FileConfig.java index 64ed67a75..379c837e7 100644 --- a/src/main/java/org/mineacademy/fo/settings/FileConfig.java +++ b/src/main/java/org/mineacademy/fo/settings/FileConfig.java @@ -1,30 +1,8 @@ package org.mineacademy.fo.settings; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; - -import javax.annotation.Nullable; - +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.Setter; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -37,19 +15,17 @@ import org.mineacademy.fo.collection.StrictList; import org.mineacademy.fo.command.SimpleCommand; import org.mineacademy.fo.command.SimpleCommandGroup; +import org.mineacademy.fo.exception.EventHandledException; import org.mineacademy.fo.exception.FoException; -import org.mineacademy.fo.model.BoxedMessage; -import org.mineacademy.fo.model.ConfigSerializable; -import org.mineacademy.fo.model.IsInList; -import org.mineacademy.fo.model.SimpleSound; -import org.mineacademy.fo.model.SimpleTime; -import org.mineacademy.fo.model.Tuple; +import org.mineacademy.fo.model.*; import org.mineacademy.fo.remain.CompMaterial; import org.mineacademy.fo.remain.Remain; -import lombok.AccessLevel; -import lombok.NonNull; -import lombok.Setter; +import javax.annotation.Nullable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.Function; /** * Represents any configuration that can be stored in a file @@ -153,8 +129,11 @@ public final Set getKeys(boolean deep) { * * @param deep * @return + * + * @deprecated it is recommended that you use getMap("") instead or for loop in getKeys(deep) and getMap(key) for each + * key and print out the results to console to understand the differences */ - @NonNull + @Deprecated public final Map getValues(boolean deep) { return this.section.getValues(deep); } @@ -992,7 +971,18 @@ public final LinkedHashMap getMap(@NonNull String path, if (exists) for (final Map.Entry entry : SerializedMap.of(this.section.retrieve(path))) { final Key key = SerializeUtil.deserialize(this.mode, keyType, entry.getKey()); - final Value value = SerializeUtil.deserialize(this.mode, valueType, entry.getValue(), valueDeserializeParams); + final Value value; + + if (LocationList.class.isAssignableFrom(valueType)) { + final List list = SerializeUtil.deserialize(this.mode, List.class, entry.getValue()); + final List copy = new ArrayList<>(); + + list.forEach(locationRaw -> copy.add(SerializeUtil.deserializeLocation(locationRaw))); + + value = (Value) new LocationList(this, copy); + + } else + value = SerializeUtil.deserialize(this.mode, valueType, entry.getValue(), valueDeserializeParams); // Ensure the pair values are valid for the given paramenters this.checkAssignable(path, key, keyType); @@ -1181,10 +1171,15 @@ final void load(@NonNull File file) { } else this.load(new InputStreamReader(stream, StandardCharsets.UTF_8)); - this.onLoad(); - this.onLoadFinish(); + try { + this.onLoad(); + this.onLoadFinish(); - if (this.shouldSave) { + } catch (final EventHandledException ex) { + // Handled successfully in the polymorphism pipeline + } + + if (this.shouldSave || this.alwaysSaveOnLoad()) { this.loading = false; this.save(); @@ -1237,6 +1232,8 @@ private final void load(@NonNull Reader reader) { /** * Called automatically when the configuration has been loaded, used to load your * fields in your class here. + * + * You can throw {@link EventHandledException} here to indicate to your child class to interrupt loading */ protected void onLoad() { } @@ -1276,7 +1273,12 @@ public final void save(@NonNull File file) { this.onPreSave(); if (this.canSaveFile()) { - this.onSave(); + + try { + this.onSave(); + } catch (final EventHandledException ex) { + // Ignore, indicated that we exited polymorphism inheritance prematurely by intention + } final File parent = file.getCanonicalFile().getParentFile(); @@ -1303,6 +1305,15 @@ public final void save(@NonNull File file) { } } + /** + * Return true if we should always save the file after loading it. + * + * @return + */ + protected boolean alwaysSaveOnLoad() { + return false; + } + /** * Called automatically before {@link #canSaveFile()} */ @@ -1613,6 +1624,10 @@ public static final class LocationList implements Iterable { private final FileConfig settings; private final List points; + public LocationList(final FileConfig settings) { + this(settings, new ArrayList<>()); + } + private LocationList(final FileConfig settings, final List points) { this.settings = settings; this.points = points; diff --git a/src/main/java/org/mineacademy/fo/settings/SimpleLocalization.java b/src/main/java/org/mineacademy/fo/settings/SimpleLocalization.java index e4341edff..63c3d47e6 100644 --- a/src/main/java/org/mineacademy/fo/settings/SimpleLocalization.java +++ b/src/main/java/org/mineacademy/fo/settings/SimpleLocalization.java @@ -82,6 +82,14 @@ protected int getConfigVersion() { return 1; } + /** + * Always keep the lang file up to date. + */ + @Override + protected final boolean alwaysSaveOnLoad() { + return true; + } + // -------------------------------------------------------------------- // Shared values // -------------------------------------------------------------------- @@ -94,6 +102,12 @@ protected int getConfigVersion() { */ public static final class Commands { + /** + * true = https://i.imgur.com/us88BCT.png + * false = https://i.imgur.com/N7jLu7v.png + */ + public static Boolean SIMPLE_HELP_DESIGN = false; + /** * The message at "No_Console" key shown when console is denied executing a command. */ @@ -246,6 +260,9 @@ public static final class Commands { private static void init() { setPathPrefix("Commands"); + if (isSetDefault("Simple_Help_Design")) + SIMPLE_HELP_DESIGN = getBoolean("Simple_Help_Design"); + if (isSetDefault("No_Console")) NO_CONSOLE = getString("No_Console"); diff --git a/src/main/java/org/mineacademy/fo/settings/SimpleSettings.java b/src/main/java/org/mineacademy/fo/settings/SimpleSettings.java index 2da0009e4..9cb8ead5a 100644 --- a/src/main/java/org/mineacademy/fo/settings/SimpleSettings.java +++ b/src/main/java/org/mineacademy/fo/settings/SimpleSettings.java @@ -47,6 +47,14 @@ protected String getSettingsFileName() { return FoConstants.File.SETTINGS; } + /** + * Always keep settings.yml file up to date + */ + @Override + protected final boolean alwaysSaveOnLoad() { + return true; + } + // -------------------------------------------------------------------- // Version // -------------------------------------------------------------------- diff --git a/src/main/java/org/mineacademy/fo/settings/YamlComments.java b/src/main/java/org/mineacademy/fo/settings/YamlComments.java index 201f42025..4b61a2080 100644 --- a/src/main/java/org/mineacademy/fo/settings/YamlComments.java +++ b/src/main/java/org/mineacademy/fo/settings/YamlComments.java @@ -1,404 +1,404 @@ -package org.mineacademy.fo.settings; - -import lombok.NonNull; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.mineacademy.fo.Common; -import org.mineacademy.fo.FileUtil; -import org.mineacademy.fo.Valid; -import org.mineacademy.fo.remain.Remain; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; - -import javax.annotation.Nullable; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.*; - -/** - * A class to update/add new sections/keys to your config while keeping your current values and keeping your comments - * Algorithm: - * Read the new file and scan for comments and ignored sections, if ignored section is found it is treated as a comment. - * Read and write each line of the new config, if the old config has value for the given key it writes that value in the new config. - * If a key has an attached comment above it, it is written first. - * - * @author tchristofferson, kangarko - * - * Source: https://github.com/tchristofferson/Config-Updater - */ -final class YamlComments { - - /** - * Update a yaml file from a resource inside your plugin jar - * - * @param jarPath The yaml file name to update from, typically config.yml - * @param diskFile The yaml file to update - * @param oldContents the actual yaml content from the old file, to prevent overriding values - * @param ignoredSections The sections to ignore from being forcefully updated & comments set - * - * @throws IOException If an IOException occurs - */ - static void writeComments(@NonNull String jarPath, @NonNull File diskFile, @Nullable String oldContents, @NonNull List ignoredSections) throws IOException { - - final List newLines = FileUtil.getInternalFileContent(jarPath); - - final YamlConfiguration oldConfig = new YamlConfiguration(); - - try { - if (oldContents != null) - oldConfig.loadFromString(oldContents); - else - oldConfig.load(diskFile); - - } catch (final Throwable t) { - Remain.sneaky(t); - } - - final YamlConfiguration newConfig = new YamlConfiguration(); - - try { - newConfig.loadFromString(String.join("\n", newLines)); - - } catch (final Throwable t) { - Remain.sneaky(t); - } - - final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(diskFile), StandardCharsets.UTF_8)); - - // ignoredSections can ONLY contain configurations sections - for (final String ignoredSection : ignoredSections) - if (newConfig.isSet(ignoredSection)) - Valid.checkBoolean(newConfig.isConfigurationSection(ignoredSection), "Can only ignore config sections in " + jarPath + " (file " + diskFile + ")" + " not '" + ignoredSection + "' that is " + newConfig.get(ignoredSection)); - - // Save keys added to config that are not in default and would otherwise be lost - final Set newKeys = newConfig.getKeys(true); - final Map removedKeys = new HashMap<>(); - - outerLoop: - for (final Map.Entry oldEntry : oldConfig.getValues(true).entrySet()) { - final String oldKey = oldEntry.getKey(); - - for (final String ignoredKey : ignoredSections) - if (oldKey.startsWith(ignoredKey)) - continue outerLoop; - - if (!newKeys.contains(oldKey)) - removedKeys.put(oldKey, oldEntry.getValue()); - } - - // Move to unused/ folder and retain old path - if (!removedKeys.isEmpty()) { - final File backupFile = FileUtil.getOrMakeFile("unused/" + diskFile.getName()); - - final FileConfiguration backupConfig = YamlConfiguration.loadConfiguration(backupFile); - - for (final Map.Entry entry : removedKeys.entrySet()) - backupConfig.set(entry.getKey(), entry.getValue()); - - backupConfig.save(backupFile); - - Common.warning("The following entries in " + diskFile.getName() + " are unused and were moved into " + backupFile.getName() + ": " + removedKeys.keySet()); - } - - final DumperOptions dumperOptions = new DumperOptions(); - dumperOptions.setWidth(4096); - - final Yaml yaml = new Yaml(dumperOptions); - final Map comments = parseComments(newLines, ignoredSections, oldConfig, yaml); - - write(newConfig, oldConfig, comments, ignoredSections, writer, yaml); - } - - // Write method doing the work. - // It checks if key has a comment associated with it and writes comment then the key and value - private static void write(FileConfiguration newConfig, FileConfiguration oldConfig, Map comments, List ignoredSections, BufferedWriter writer, Yaml yaml) throws IOException { - - final Set copyAllowed = new HashSet<>(); - final Set reverseCopy = new HashSet<>(); - - outerloop: - for (final String key : newConfig.getKeys(true)) { - - checkIgnore: - { - - for (final String allowed : copyAllowed) - if (key.startsWith(allowed)) - break checkIgnore; - - // These keys are already written below - for (final String allowed : reverseCopy) - if (key.startsWith(allowed)) - continue outerloop; - - for (final String ignoredSection : ignoredSections) { - if (key.equals(ignoredSection)) - // Write from new to old config - if ((!oldConfig.isSet(ignoredSection) || oldConfig.getConfigurationSection(ignoredSection).getKeys(false).isEmpty())) { - copyAllowed.add(ignoredSection); - - break; - } - - // Write from old to new, copying all keys and subkeys manually - else { - write0(key, true, newConfig, oldConfig, comments, ignoredSections, writer, yaml); - - for (final String oldKey : oldConfig.getConfigurationSection(ignoredSection).getKeys(true)) - write0(ignoredSection + "." + oldKey, true, oldConfig, newConfig, comments, ignoredSections, writer, yaml); - - reverseCopy.add(ignoredSection); - continue outerloop; - } - - if (key.startsWith(ignoredSection)) - continue outerloop; - } - } - - write0(key, false, newConfig, oldConfig, comments, ignoredSections, writer, yaml); - } - - final String danglingComments = comments.get(null); - - if (danglingComments != null) - writer.write(danglingComments); - - writer.close(); - } - - private static void write0(String key, boolean forceNew, FileConfiguration newConfig, FileConfiguration oldConfig, Map comments, List ignoredSections, BufferedWriter writer, Yaml yaml) throws IOException { - - final String[] keys = key.split("\\."); - final String actualKey = keys[keys.length - 1]; - final String comment = comments.remove(key); - - final StringBuilder prefixBuilder = new StringBuilder(); - final int indents = keys.length - 1; - appendPrefixSpaces(prefixBuilder, indents); - final String prefixSpaces = prefixBuilder.toString(); - - // No \n character necessary, new line is automatically at end of comment - if (comment != null) - writer.write(comment); - - final Object newObj = newConfig.get(key); - final Object oldObj = oldConfig.get(key); - - // Write the old section - if (newObj instanceof ConfigurationSection && !forceNew && oldObj instanceof ConfigurationSection) - writeSection(writer, actualKey, prefixSpaces, (ConfigurationSection) oldObj); - - // Write the new section, old value is no more - else if (newObj instanceof ConfigurationSection) - writeSection(writer, actualKey, prefixSpaces, (ConfigurationSection) newObj); - - // Write the old object - else if (oldObj != null && !forceNew) - write(oldObj, actualKey, prefixSpaces, yaml, writer); - - // Write new object - else - write(newObj, actualKey, prefixSpaces, yaml, writer); - } - - // Doesn't work with configuration sections, must be an actual object - // Auto checks if it is serializable and writes to file - private static void write(Object obj, String actualKey, String prefixSpaces, Yaml yaml, BufferedWriter writer) throws IOException { - - if (obj instanceof ConfigurationSerializable) - writer.write(prefixSpaces + actualKey + ": " + yaml.dump(((ConfigurationSerializable) obj).serialize())); - - else if (obj instanceof String || obj instanceof Character) { - if (obj instanceof String) { - final String string = (String) obj; - - // Split multi line strings using |- - if (string.contains("\n")) { - writer.write(prefixSpaces + actualKey + ": |-\n"); - - for (final String line : string.split("\n")) - writer.write(prefixSpaces + " " + line + "\n"); - - return; - } - } - - writer.write(prefixSpaces + actualKey + ": " + yaml.dump(obj)); - - } else if (obj instanceof List) - writeList((List) obj, actualKey, prefixSpaces, yaml, writer); - - else - writer.write(prefixSpaces + actualKey + ": " + yaml.dump(obj)); - - } - - // Writes a configuration section - private static void writeSection(BufferedWriter writer, String actualKey, String prefixSpaces, ConfigurationSection section) throws IOException { - if (section.getKeys(false).isEmpty()) - writer.write(prefixSpaces + actualKey + ":"); - - else - writer.write(prefixSpaces + actualKey + ":"); - - writer.write("\n"); - } - - // Writes a list of any object - private static void writeList(List list, String actualKey, String prefixSpaces, Yaml yaml, BufferedWriter writer) throws IOException { - writer.write(getListAsString(list, actualKey, prefixSpaces, yaml)); - } - - private static String getListAsString(List list, String actualKey, String prefixSpaces, Yaml yaml) { - final StringBuilder builder = new StringBuilder(prefixSpaces).append(actualKey).append(":"); - - if (list.isEmpty()) { - builder.append(" []\n"); - return builder.toString(); - } - - builder.append("\n"); - - for (int i = 0; i < list.size(); i++) { - final Object o = list.get(i); - - if (o instanceof Map) { - int entryIndex = 0; - final int mapSize = ((Map) o).size(); - - for (final Map.Entry entry : ((Map) o).entrySet()) { - builder.append(prefixSpaces); - - if (entryIndex == 0) - builder.append("- "); - else - builder.append(" "); - - builder.append(entry.getKey()).append(": ").append(yaml.dump(entry.getValue())); - entryIndex++; - - if (entryIndex != mapSize) - builder.append("\n"); - } - - } else if (o instanceof String || o instanceof Character) - builder.append(prefixSpaces).append("- '").append(o.toString().replace("'", "''")).append("'"); - else if (o instanceof List) - builder.append(prefixSpaces).append("- ").append(yaml.dump(o)); - else - builder.append(prefixSpaces).append("- ").append(o); - - if (i != list.size()) - builder.append("\n"); - } - - return builder.toString(); - } - - //Key is the config key, value = comment and/or ignored sections - //Parses comments, blank lines, and ignored sections - private static Map parseComments(List lines, List ignoredSections, FileConfiguration oldConfig, Yaml yaml) { - final Map comments = new HashMap<>(); - final StringBuilder builder = new StringBuilder(); - final StringBuilder keyBuilder = new StringBuilder(); - int lastLineIndentCount = 0; - - //outer: - for (final String line : lines) { - if (line != null && line.trim().startsWith("-")) - continue; - - if (line == null || line.trim().equals("") || line.trim().startsWith("#")) - builder.append(line).append("\n"); - else { - lastLineIndentCount = setFullKey(keyBuilder, line, lastLineIndentCount); - - if (keyBuilder.length() > 0) { - comments.put(keyBuilder.toString(), builder.toString()); - builder.setLength(0); - } - } - } - - if (builder.length() > 0) - comments.put(null, builder.toString()); - - return comments; - } - - //Counts spaces in front of key and divides by 2 since 1 indent = 2 spaces - private static int countIndents(String s) { - int spaces = 0; - - for (final char c : s.toCharArray()) - if (c == ' ') - spaces += 1; - else - break; - - return spaces / 2; - } - - //Ex. keyBuilder = key1.key2.key3 --> key1.key2 - private static void removeLastKey(StringBuilder keyBuilder) { - String temp = keyBuilder.toString(); - final String[] keys = temp.split("\\."); - - if (keys.length == 1) { - keyBuilder.setLength(0); - return; - } - - temp = temp.substring(0, temp.length() - keys[keys.length - 1].length() - 1); - keyBuilder.setLength(temp.length()); - } - - //Updates the keyBuilder and returns configLines number of indents - private static int setFullKey(StringBuilder keyBuilder, String configLine, int lastLineIndentCount) { - final int currentIndents = countIndents(configLine); - final String key = configLine.trim().split(":")[0]; - - if (keyBuilder.length() == 0) - keyBuilder.append(key); - else if (currentIndents == lastLineIndentCount) { - //Replace the last part of the key with current key - removeLastKey(keyBuilder); - - if (keyBuilder.length() > 0) - keyBuilder.append("."); - - keyBuilder.append(key); - } else if (currentIndents > lastLineIndentCount) - //Append current key to the keyBuilder - keyBuilder.append(".").append(key); - else { - final int difference = lastLineIndentCount - currentIndents; - - for (int i = 0; i < difference + 1; i++) - removeLastKey(keyBuilder); - - if (keyBuilder.length() > 0) - keyBuilder.append("."); - - keyBuilder.append(key); - } - - return currentIndents; - } - - private static String getPrefixSpaces(int indents) { - final StringBuilder builder = new StringBuilder(); - - for (int i = 0; i < indents; i++) - builder.append(" "); - - return builder.toString(); - } - - private static void appendPrefixSpaces(StringBuilder builder, int indents) { - builder.append(getPrefixSpaces(indents)); - } +package org.mineacademy.fo.settings; + +import lombok.NonNull; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.mineacademy.fo.Common; +import org.mineacademy.fo.FileUtil; +import org.mineacademy.fo.Valid; +import org.mineacademy.fo.remain.Remain; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import javax.annotation.Nullable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * A class to update/add new sections/keys to your config while keeping your current values and keeping your comments + * Algorithm: + * Read the new file and scan for comments and ignored sections, if ignored section is found it is treated as a comment. + * Read and write each line of the new config, if the old config has value for the given key it writes that value in the new config. + * If a key has an attached comment above it, it is written first. + * + * @author tchristofferson, kangarko + * + * Source: https://github.com/tchristofferson/Config-Updater + */ +final class YamlComments { + + /** + * Update a yaml file from a resource inside your plugin jar + * + * @param jarPath The yaml file name to update from, typically config.yml + * @param diskFile The yaml file to update + * @param oldContents the actual yaml content from the old file, to prevent overriding values + * @param ignoredSections The sections to ignore from being forcefully updated & comments set + * + * @throws IOException If an IOException occurs + */ + static void writeComments(@NonNull String jarPath, @NonNull File diskFile, @Nullable String oldContents, @NonNull List ignoredSections) throws IOException { + + final List newLines = FileUtil.getInternalFileContent(jarPath); + + final YamlConfiguration oldConfig = new YamlConfiguration(); + + try { + if (oldContents != null) + oldConfig.loadFromString(oldContents); + else + oldConfig.load(diskFile); + + } catch (final Throwable t) { + Remain.sneaky(t); + } + + final YamlConfiguration newConfig = new YamlConfiguration(); + + try { + newConfig.loadFromString(String.join("\n", newLines)); + + } catch (final Throwable t) { + Remain.sneaky(t); + } + + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(diskFile), StandardCharsets.UTF_8)); + + // ignoredSections can ONLY contain configurations sections + for (final String ignoredSection : ignoredSections) + if (newConfig.isSet(ignoredSection)) + Valid.checkBoolean(newConfig.isConfigurationSection(ignoredSection), "Can only ignore config sections in " + jarPath + " (file " + diskFile + ")" + " not '" + ignoredSection + "' that is " + newConfig.get(ignoredSection)); + + // Save keys added to config that are not in default and would otherwise be lost + final Set newKeys = newConfig.getKeys(true); + final Map removedKeys = new HashMap<>(); + + outerLoop: + for (final Map.Entry oldEntry : oldConfig.getValues(true).entrySet()) { + final String oldKey = oldEntry.getKey(); + + for (final String ignoredKey : ignoredSections) + if (oldKey.startsWith(ignoredKey)) + continue outerLoop; + + if (!newKeys.contains(oldKey)) + removedKeys.put(oldKey, oldEntry.getValue()); + } + + // Move to unused/ folder and retain old path + if (!removedKeys.isEmpty()) { + final File backupFile = FileUtil.getOrMakeFile("unused/" + diskFile.getName()); + + final FileConfiguration backupConfig = YamlConfiguration.loadConfiguration(backupFile); + + for (final Map.Entry entry : removedKeys.entrySet()) + backupConfig.set(entry.getKey(), entry.getValue()); + + backupConfig.save(backupFile); + + Common.warning("The following entries in " + diskFile.getName() + " are unused and were moved into " + backupFile + ": " + removedKeys.keySet()); + } + + final DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setWidth(4096); + + final Yaml yaml = new Yaml(dumperOptions); + final Map comments = parseComments(newLines, ignoredSections, oldConfig, yaml); + + write(newConfig, oldConfig, comments, ignoredSections, writer, yaml); + } + + // Write method doing the work. + // It checks if key has a comment associated with it and writes comment then the key and value + private static void write(FileConfiguration newConfig, FileConfiguration oldConfig, Map comments, List ignoredSections, BufferedWriter writer, Yaml yaml) throws IOException { + + final Set copyAllowed = new HashSet<>(); + final Set reverseCopy = new HashSet<>(); + + outerloop: + for (final String key : newConfig.getKeys(true)) { + + checkIgnore: + { + + for (final String allowed : copyAllowed) + if (key.startsWith(allowed)) + break checkIgnore; + + // These keys are already written below + for (final String allowed : reverseCopy) + if (key.startsWith(allowed)) + continue outerloop; + + for (final String ignoredSection : ignoredSections) { + if (key.equals(ignoredSection)) + // Write from new to old config + if ((!oldConfig.isSet(ignoredSection) || oldConfig.getConfigurationSection(ignoredSection).getKeys(false).isEmpty())) { + copyAllowed.add(ignoredSection); + + break; + } + + // Write from old to new, copying all keys and subkeys manually + else { + write0(key, true, newConfig, oldConfig, comments, ignoredSections, writer, yaml); + + for (final String oldKey : oldConfig.getConfigurationSection(ignoredSection).getKeys(true)) + write0(ignoredSection + "." + oldKey, true, oldConfig, newConfig, comments, ignoredSections, writer, yaml); + + reverseCopy.add(ignoredSection); + continue outerloop; + } + + if (key.startsWith(ignoredSection)) + continue outerloop; + } + } + + write0(key, false, newConfig, oldConfig, comments, ignoredSections, writer, yaml); + } + + final String danglingComments = comments.get(null); + + if (danglingComments != null) + writer.write(danglingComments); + + writer.close(); + } + + private static void write0(String key, boolean forceNew, FileConfiguration newConfig, FileConfiguration oldConfig, Map comments, List ignoredSections, BufferedWriter writer, Yaml yaml) throws IOException { + + final String[] keys = key.split("\\."); + final String actualKey = keys[keys.length - 1]; + final String comment = comments.remove(key); + + final StringBuilder prefixBuilder = new StringBuilder(); + final int indents = keys.length - 1; + appendPrefixSpaces(prefixBuilder, indents); + final String prefixSpaces = prefixBuilder.toString(); + + // No \n character necessary, new line is automatically at end of comment + if (comment != null) + writer.write(comment); + + final Object newObj = newConfig.get(key); + final Object oldObj = oldConfig.get(key); + + // Write the old section + if (newObj instanceof ConfigurationSection && !forceNew && oldObj instanceof ConfigurationSection) + writeSection(writer, actualKey, prefixSpaces, (ConfigurationSection) oldObj); + + // Write the new section, old value is no more + else if (newObj instanceof ConfigurationSection) + writeSection(writer, actualKey, prefixSpaces, (ConfigurationSection) newObj); + + // Write the old object + else if (oldObj != null && !forceNew) + write(oldObj, actualKey, prefixSpaces, yaml, writer); + + // Write new object + else + write(newObj, actualKey, prefixSpaces, yaml, writer); + } + + // Doesn't work with configuration sections, must be an actual object + // Auto checks if it is serializable and writes to file + private static void write(Object obj, String actualKey, String prefixSpaces, Yaml yaml, BufferedWriter writer) throws IOException { + + if (obj instanceof ConfigurationSerializable) + writer.write(prefixSpaces + actualKey + ": " + yaml.dump(((ConfigurationSerializable) obj).serialize())); + + else if (obj instanceof String || obj instanceof Character) { + if (obj instanceof String) { + final String string = (String) obj; + + // Split multi line strings using |- + if (string.contains("\n")) { + writer.write(prefixSpaces + actualKey + ": |-\n"); + + for (final String line : string.split("\n")) + writer.write(prefixSpaces + " " + line + "\n"); + + return; + } + } + + writer.write(prefixSpaces + actualKey + ": " + yaml.dump(obj)); + + } else if (obj instanceof List) + writeList((List) obj, actualKey, prefixSpaces, yaml, writer); + + else + writer.write(prefixSpaces + actualKey + ": " + yaml.dump(obj)); + + } + + // Writes a configuration section + private static void writeSection(BufferedWriter writer, String actualKey, String prefixSpaces, ConfigurationSection section) throws IOException { + if (section.getKeys(false).isEmpty()) + writer.write(prefixSpaces + actualKey + ":"); + + else + writer.write(prefixSpaces + actualKey + ":"); + + writer.write("\n"); + } + + // Writes a list of any object + private static void writeList(List list, String actualKey, String prefixSpaces, Yaml yaml, BufferedWriter writer) throws IOException { + writer.write(getListAsString(list, actualKey, prefixSpaces, yaml)); + } + + private static String getListAsString(List list, String actualKey, String prefixSpaces, Yaml yaml) { + final StringBuilder builder = new StringBuilder(prefixSpaces).append(actualKey).append(":"); + + if (list.isEmpty()) { + builder.append(" []\n"); + return builder.toString(); + } + + builder.append("\n"); + + for (int i = 0; i < list.size(); i++) { + final Object o = list.get(i); + + if (o instanceof Map) { + int entryIndex = 0; + final int mapSize = ((Map) o).size(); + + for (final Map.Entry entry : ((Map) o).entrySet()) { + builder.append(prefixSpaces); + + if (entryIndex == 0) + builder.append("- "); + else + builder.append(" "); + + builder.append(entry.getKey()).append(": ").append(yaml.dump(entry.getValue())); + entryIndex++; + + if (entryIndex != mapSize) + builder.append("\n"); + } + + } else if (o instanceof String || o instanceof Character) + builder.append(prefixSpaces).append("- '").append(o.toString().replace("'", "''")).append("'"); + else if (o instanceof List) + builder.append(prefixSpaces).append("- ").append(yaml.dump(o)); + else + builder.append(prefixSpaces).append("- ").append(o); + + if (i != list.size()) + builder.append("\n"); + } + + return builder.toString(); + } + + //Key is the config key, value = comment and/or ignored sections + //Parses comments, blank lines, and ignored sections + private static Map parseComments(List lines, List ignoredSections, FileConfiguration oldConfig, Yaml yaml) { + final Map comments = new HashMap<>(); + final StringBuilder builder = new StringBuilder(); + final StringBuilder keyBuilder = new StringBuilder(); + int lastLineIndentCount = 0; + + //outer: + for (final String line : lines) { + if (line != null && line.trim().startsWith("-")) + continue; + + if (line == null || line.trim().equals("") || line.trim().startsWith("#")) + builder.append(line).append("\n"); + else { + lastLineIndentCount = setFullKey(keyBuilder, line, lastLineIndentCount); + + if (keyBuilder.length() > 0) { + comments.put(keyBuilder.toString(), builder.toString()); + builder.setLength(0); + } + } + } + + if (builder.length() > 0) + comments.put(null, builder.toString()); + + return comments; + } + + //Counts spaces in front of key and divides by 2 since 1 indent = 2 spaces + private static int countIndents(String s) { + int spaces = 0; + + for (final char c : s.toCharArray()) + if (c == ' ') + spaces += 1; + else + break; + + return spaces / 2; + } + + //Ex. keyBuilder = key1.key2.key3 --> key1.key2 + private static void removeLastKey(StringBuilder keyBuilder) { + String temp = keyBuilder.toString(); + final String[] keys = temp.split("\\."); + + if (keys.length == 1) { + keyBuilder.setLength(0); + return; + } + + temp = temp.substring(0, temp.length() - keys[keys.length - 1].length() - 1); + keyBuilder.setLength(temp.length()); + } + + //Updates the keyBuilder and returns configLines number of indents + private static int setFullKey(StringBuilder keyBuilder, String configLine, int lastLineIndentCount) { + final int currentIndents = countIndents(configLine); + final String key = configLine.trim().split(":")[0]; + + if (keyBuilder.length() == 0) + keyBuilder.append(key); + else if (currentIndents == lastLineIndentCount) { + //Replace the last part of the key with current key + removeLastKey(keyBuilder); + + if (keyBuilder.length() > 0) + keyBuilder.append("."); + + keyBuilder.append(key); + } else if (currentIndents > lastLineIndentCount) + //Append current key to the keyBuilder + keyBuilder.append(".").append(key); + else { + final int difference = lastLineIndentCount - currentIndents; + + for (int i = 0; i < difference + 1; i++) + removeLastKey(keyBuilder); + + if (keyBuilder.length() > 0) + keyBuilder.append("."); + + keyBuilder.append(key); + } + + return currentIndents; + } + + private static String getPrefixSpaces(int indents) { + final StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < indents; i++) + builder.append(" "); + + return builder.toString(); + } + + private static void appendPrefixSpaces(StringBuilder builder, int indents) { + builder.append(getPrefixSpaces(indents)); + } } \ No newline at end of file diff --git a/src/main/java/org/mineacademy/fo/settings/YamlConfig.java b/src/main/java/org/mineacademy/fo/settings/YamlConfig.java index abdcf1fb9..2cb33b4a6 100644 --- a/src/main/java/org/mineacademy/fo/settings/YamlConfig.java +++ b/src/main/java/org/mineacademy/fo/settings/YamlConfig.java @@ -1,498 +1,510 @@ -package org.mineacademy.fo.settings; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.logging.Level; - -import javax.annotation.Nullable; - -import org.bukkit.Bukkit; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.configuration.serialization.ConfigurationSerialization; -import org.mineacademy.fo.FileUtil; -import org.mineacademy.fo.ReflectionUtil; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; -import org.yaml.snakeyaml.error.YAMLException; -import org.yaml.snakeyaml.nodes.Node; -import org.yaml.snakeyaml.nodes.Tag; -import org.yaml.snakeyaml.representer.Representer; - -import lombok.NonNull; - -/** - * The core settings class. Fully compatible with Minecraft 1.7.10 to the - * latest one, including comments support (default file required, see {@link #saveComments()}) - * and automatic config upgrading if we request a value that only exist in the default file. - */ -public class YamlConfig extends FileConfig { - - /** - * The Yaml instance - */ - private final Yaml yaml; - - /** - * Should we save empty sections or null values (requires NO default file) - */ - private boolean saveEmptyValues = true; - - /** - * Create a new instance (do not load it, use {@link #load(File)} to load) - */ - protected YamlConfig() { - final YamlConstructor constructor = new YamlConstructor(); - final YamlRepresenter representer = new YamlRepresenter(); - representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - - final DumperOptions dumperOptions = new DumperOptions(); - dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - dumperOptions.setIndent(2); - dumperOptions.setWidth(4096); // Do not wrap long lines - - // Load options only if available - if (ReflectionUtil.isClassAvailable("org.yaml.snakeyaml.LoaderOptions")) { - Yaml yaml; - - try { - final LoaderOptions loaderOptions = new LoaderOptions(); - loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); - - yaml = new Yaml(constructor, representer, dumperOptions, loaderOptions); - - } catch (final NoSuchMethodError ex) { - yaml = new Yaml(constructor, representer, dumperOptions); - } - - this.yaml = yaml; - } - - else - this.yaml = new Yaml(constructor, representer, dumperOptions); - } - - /** - * Return true if you have a default file and want to save comments from it - * - * Any user-generated comments will be lost, any user-written values will be lost. - * - * Please see {@link #getUncommentedSections()} to write sections containing maps users - * can create to prevent losing them. - * - * @return - */ - protected boolean saveComments() { - return true; - } - - /** - * See {@link #saveComments()} - * - * @return - */ - protected List getUncommentedSections() { - return new ArrayList<>(); - } - - /** - * (Requires no default file or {@link #saveComments()} on false) - * Set if we should remove empty lists or sections when saving. - * Defaults to true, that means that empty sections will be saved. - * - * @param saveEmptyValues - */ - public final void setSaveEmptyValues(boolean saveEmptyValues) { - this.saveEmptyValues = saveEmptyValues; - } - - /** - * Returns true if this config contains any keys what so ever. Override for - * custom logic. - * - * @return - */ - public boolean isValid() { - return !this.section.map.isEmpty(); - } - - // ------------------------------------------------------------------------------------ - // File manipulation - // ------------------------------------------------------------------------------------ - - /** - * Attempts to load configuration from the given internal path in your JAR. - * We automatically move the file to your plugin's folder if it does not exist. - * Subfolders are supported, example: localization/messages_en.yml - * - * @param internalPath - */ - public final void loadConfiguration(String internalPath) { - this.loadConfiguration(internalPath, internalPath); - } - - /** - * Load configuration from the optional from path in your JAR file, - * extracting it to the given path in your plugin's folder if it does not exist. - * - * @param from - * @param to - */ - public final void loadConfiguration(@Nullable String from, String to) { - File file; - - if (from != null) { - - // Copy if not exists yet - file = FileUtil.extract(from, to); - - // Initialize file early - this.file = file; - - // Keep a loaded copy to copy default values from - final YamlConfig defaultConfig = new YamlConfig(); - final String defaultContent = String.join("\n", FileUtil.getInternalFileContent(from)); - - defaultConfig.file = file; - defaultConfig.loadFromString(defaultContent); - - this.defaults = defaultConfig.section; - this.defaultsPath = from; - } - - else - file = FileUtil.getOrMakeFile(to); - - this.load(file); - } - - /** - * Loads the configuration from the internal path WITHOUT calling {@link #onLoad()}, - * without setting defaults and without extracting the file. - * - * @param internalPath - */ - public final void loadInternal(String internalPath) { - final String content = String.join("\n", FileUtil.getInternalFileContent(internalPath)); - - this.loadFromString(content); - } - - /* - * Dumps all values in this config into a saveable format - */ - @NonNull - @Override - final String saveToString() { - - // Do not use comments - if (this.defaults == null || !this.saveComments()) { - final String header = this.getHeader() == null ? "" : "# " + String.join("\n# ", this.getHeader().split("\n")) + "\n\n"; - final Map values = this.section.getValues(false); - - if (!this.saveEmptyValues) - removeEmptyValues(values); - - String dump = this.yaml.dump(values); - - // Blank config - if (dump.equals("{}\n")) - dump = ""; - - return header + dump; - } - - // Special case, write using comments engine - try { - YamlComments.writeComments(this.defaultsPath, this.file, null, this.getUncommentedSections()); - - } catch (final IOException ex) { - ex.printStackTrace(); - } - - return null; - } - - /* - * Attempts to remove empty maps, lists or arrays from the given map - */ - private static void removeEmptyValues(Map map) { - for (final Iterator> it = map.entrySet().iterator(); it.hasNext();) { - final Entry entry = it.next(); - final Object value = entry.getValue(); - - if (value instanceof ConfigSection) { - final Map childMap = ((ConfigSection) value).map; - - removeEmptyValues(childMap); - - if (childMap.isEmpty()) - it.remove(); - } - - if (value == null - || value instanceof Iterable && !((Iterable) value).iterator().hasNext() - || value.getClass().isArray() && ((Object[]) value).length == 0 - || value instanceof Map && ((Map) value).isEmpty()) { - - it.remove(); - - continue; - } - } - } - - /* - * Loads configuration from the given string contents - */ - @Override - final void loadFromString(@NonNull String contents) { - - Map input; - - try { - input = (Map) this.yaml.load(contents); - - } catch (final YAMLException ex) { - throw ex; - - } catch (final ClassCastException e) { - throw new IllegalArgumentException("Top level is not a Map."); - } - - final String header = this.parseHeader(contents); - - if (header.trim().length() > 0) - this.setHeader(header); - - this.section.map.clear(); - - if (input != null) - this.convertMapsToSections(input, this.section); - } - - /* - * Converts the given maps to sections - */ - private void convertMapsToSections(@NonNull Map input, @NonNull ConfigSection section) { - for (final Map.Entry entry : input.entrySet()) { - final String key = entry.getKey().toString(); - final Object value = entry.getValue(); - - if (value instanceof Map) - this.convertMapsToSections((Map) value, section.createSection(key)); - else - section.store(key, value); - } - } - - /* - * Converts the given input to header - */ - @NonNull - private String parseHeader(@NonNull String input) { - final String commentPrefix = "# "; - final String[] lines = input.split("\r?\n", -1); - final StringBuilder result = new StringBuilder(); - - boolean readingHeader = true; - boolean foundHeader = false; - - for (int i = 0; i < lines.length && readingHeader; i++) { - final String line = lines[i].trim(); - - if (line.startsWith(commentPrefix) || line.equals("#")) { - if (i > 0) - result.append("\n"); - - if (line.length() > commentPrefix.length()) - result.append(line.substring(commentPrefix.length())); - - foundHeader = true; - - } else if (foundHeader && line.length() == 0) - result.append("\n"); - - else if (foundHeader) - readingHeader = false; - } - - final String string = result.toString(); - - return string.trim().isEmpty() ? "" : string + "\n"; - } - - // ----------------------------------------------------------------------------------------------------- - // Static - // ----------------------------------------------------------------------------------------------------- - - /** - * Loads configuration from the internal JAR path, extracting it if needed. - * - * @param path - * @return - */ - @NonNull - public static final YamlConfig fromInternalPath(@NonNull String path) { - - final YamlConfig config = new YamlConfig(); - - try { - config.loadConfiguration(path); - - } catch (final Exception ex) { - Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + path, ex); - } - - return config; - } - - /** - * Loads configuration from the internal JAR path without setting the file, - * without extracting it and without defaults. - * - * @param path - * @return - */ - @NonNull - public static final YamlConfig fromInternalPathFast(@NonNull String path) { - - final YamlConfig config = new YamlConfig(); - - try { - config.loadInternal(path); - - } catch (final Exception ex) { - Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + path, ex); - } - - return config; - } - - /** - * Loads configuration from the file in your plugin's folder. - * - * @param file - * @return - */ - @NonNull - public static final YamlConfig fromFile(@NonNull File file) { - - final YamlConfig config = new YamlConfig(); - - try { - config.load(file); - } catch (final Exception ex) { - Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex); - } - - return config; - } - - /** - * Loads configuration from the file in your plugin's folder. - * - * @param file - * @return - */ - @NonNull - public static final YamlConfig fromFileFast(@NonNull File file) { - final YamlConfig config = new YamlConfig(); - - try { - final List content = FileUtil.readLines(file); - config.loadFromString(String.join("\n", content)); - - } catch (final Exception ex) { - Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex); - } - - return config; - } - - // ----------------------------------------------------------------------------------------------------- - // Classes - // ----------------------------------------------------------------------------------------------------- - - /** - * Helper class, credits to the original Bukkit/Spigot team, enhanced by MineAcademy - */ - private final static class YamlConstructor extends SafeConstructor { - - public YamlConstructor() { - this.yamlConstructors.put(Tag.MAP, new ConstructCustomObject()); - } - - private class ConstructCustomObject extends ConstructYamlMap { - - @Override - public Object construct(@NonNull Node node) { - if (node.isTwoStepsConstruction()) - throw new YAMLException("Unexpected referential mapping structure. Node: " + node); - - final Map raw = (Map) super.construct(node); - - if (raw.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) { - final Map typed = new LinkedHashMap<>(raw.size()); - for (final Map.Entry entry : raw.entrySet()) - typed.put(entry.getKey().toString(), entry.getValue()); - - try { - return ConfigurationSerialization.deserializeObject(typed); - } catch (final IllegalArgumentException ex) { - throw new YAMLException("Could not deserialize object", ex); - } - } - - return raw; - } - - @Override - public void construct2ndStep(@NonNull Node node, @NonNull Object object) { - throw new YAMLException("Unexpected referential mapping structure. Node: " + node); - } - } - } - - /** - * Helper class, credits to the original Bukkit/Spigot team, enhanced by MineAcademy - */ - private final static class YamlRepresenter extends Representer { - - public YamlRepresenter() { - this.multiRepresenters.put(ConfigurationSerializable.class, new RepresentConfigurationSerializable()); - this.multiRepresenters.put(ConfigSection.class, new RepresentConfigurationSection()); - this.multiRepresenters.remove(Enum.class); - } - - private class RepresentConfigurationSection extends RepresentMap { - - @NonNull - @Override - public Node representData(@NonNull Object data) { - return super.representData(((ConfigSection) data).getValues(false)); - } - } - - private class RepresentConfigurationSerializable extends RepresentMap { - - @NonNull - @Override - public Node representData(@NonNull Object data) { - final ConfigurationSerializable serializable = (ConfigurationSerializable) data; - final Map values = new LinkedHashMap<>(); - values.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(serializable.getClass())); - values.putAll(serializable.serialize()); - - return super.representData(values); - } - } - } -} +package org.mineacademy.fo.settings; + +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.mineacademy.fo.FileUtil; +import org.mineacademy.fo.ReflectionUtil; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.Map.Entry; +import java.util.logging.Level; + +/** + * The core settings class. Fully compatible with Minecraft 1.7.10 to the + * latest one, including comments support (default file required, see {@link #saveComments()}) + * and automatic config upgrading if we request a value that only exist in the default file. + */ +public class YamlConfig extends FileConfig { + + /** + * The Yaml instance + */ + private final Yaml yaml; + + /** + * Should we save empty sections or null values (requires NO default file) + */ + private boolean saveEmptyValues = true; + + /** + * Create a new instance (do not load it, use {@link #load(File)} to load) + */ + protected YamlConfig() { + final YamlConstructor constructor = new YamlConstructor(); + final YamlRepresenter representer = new YamlRepresenter(); + representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + final DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dumperOptions.setIndent(2); + dumperOptions.setWidth(4096); // Do not wrap long lines + + // Load options only if available + if (ReflectionUtil.isClassAvailable("org.yaml.snakeyaml.LoaderOptions")) { + Yaml yaml; + + try { + final LoaderOptions loaderOptions = new LoaderOptions(); + + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + + try { + loaderOptions.setCodePointLimit(Integer.MAX_VALUE); + } catch (Throwable t) { + // Thankfully unsupported + // https://i.imgur.com/wAgKukK.png + } + + yaml = new Yaml(constructor, representer, dumperOptions, loaderOptions); + + } catch (final NoSuchMethodError ex) { + yaml = new Yaml(constructor, representer, dumperOptions); + } + + this.yaml = yaml; + } + + else + this.yaml = new Yaml(constructor, representer, dumperOptions); + } + + /** + * Return true if you have a default file and want to save comments from it + * + * Any user-generated comments will be lost, any user-written values will be lost. + * + * Please see {@link #getUncommentedSections()} to write sections containing maps users + * can create to prevent losing them. + * + * @return + */ + protected boolean saveComments() { + return true; + } + + /** + * See {@link #saveComments()} + * + * @return + */ + protected List getUncommentedSections() { + return new ArrayList<>(); + } + + /** + * (Requires no default file or {@link #saveComments()} on false) + * Set if we should remove empty lists or sections when saving. + * Defaults to true, that means that empty sections will be saved. + * + * @param saveEmptyValues + */ + public final void setSaveEmptyValues(boolean saveEmptyValues) { + this.saveEmptyValues = saveEmptyValues; + } + + /** + * Returns true if this config contains any keys what so ever. Override for + * custom logic. + * + * @return + */ + public boolean isValid() { + return !this.section.map.isEmpty(); + } + + // ------------------------------------------------------------------------------------ + // File manipulation + // ------------------------------------------------------------------------------------ + + /** + * Attempts to load configuration from the given internal path in your JAR. + * We automatically move the file to your plugin's folder if it does not exist. + * Subfolders are supported, example: localization/messages_en.yml + * + * @param internalPath + */ + public final void loadConfiguration(String internalPath) { + this.loadConfiguration(internalPath, internalPath); + } + + /** + * Load configuration from the optional from path in your JAR file, + * extracting it to the given path in your plugin's folder if it does not exist. + * + * @param from + * @param to + */ + public final void loadConfiguration(@Nullable String from, String to) { + File file; + + if (from != null) { + + // Copy if not exists yet + file = FileUtil.extract(from, to); + + // Initialize file early + this.file = file; + + // Keep a loaded copy to copy default values from + final YamlConfig defaultConfig = new YamlConfig(); + final String defaultContent = String.join("\n", FileUtil.getInternalFileContent(from)); + + defaultConfig.file = file; + defaultConfig.loadFromString(defaultContent); + + this.defaults = defaultConfig.section; + this.defaultsPath = from; + } + + else + file = FileUtil.getOrMakeFile(to); + + this.load(file); + } + + /** + * Loads the configuration from the internal path WITHOUT calling {@link #onLoad()}, + * without setting defaults and without extracting the file. + * + * @param internalPath + */ + public final void loadInternal(String internalPath) { + final String content = String.join("\n", FileUtil.getInternalFileContent(internalPath)); + + this.loadFromString(content); + } + + /* + * Dumps all values in this config into a saveable format + */ + @NonNull + @Override + final String saveToString() { + + // Do not use comments + if (this.defaults == null || !this.saveComments()) { + final String header = this.getHeader() == null ? "" : "# " + String.join("\n# ", this.getHeader().split("\n")) + "\n\n"; + final Map values = this.section.getValues(false); + + if (!this.saveEmptyValues) + removeEmptyValues(values); + + String dump = this.yaml.dump(values); + + // Blank config + if (dump.equals("{}\n")) + dump = ""; + + return header + dump; + } + + // Special case, write using comments engine + try { + YamlComments.writeComments(this.defaultsPath, this.file, null, this.getUncommentedSections()); + + } catch (final IOException ex) { + ex.printStackTrace(); + } + + return null; + } + + /* + * Attempts to remove empty maps, lists or arrays from the given map + */ + private static void removeEmptyValues(Map map) { + for (final Iterator> it = map.entrySet().iterator(); it.hasNext();) { + final Entry entry = it.next(); + final Object value = entry.getValue(); + + if (value instanceof ConfigSection) { + final Map childMap = ((ConfigSection) value).map; + + removeEmptyValues(childMap); + + if (childMap.isEmpty()) + it.remove(); + } + + if (value == null + || value instanceof Iterable && !((Iterable) value).iterator().hasNext() + || value.getClass().isArray() && ((Object[]) value).length == 0 + || value instanceof Map && ((Map) value).isEmpty()) { + + it.remove(); + + continue; + } + } + } + + /* + * Loads configuration from the given string contents + */ + @Override + final void loadFromString(@NonNull String contents) { + + Map input; + + try { + input = (Map) this.yaml.load(contents); + + } catch (final YAMLException ex) { + throw ex; + + } catch (final ClassCastException e) { + throw new IllegalArgumentException("Top level is not a Map."); + } + + final String header = this.parseHeader(contents); + + if (header.trim().length() > 0) + this.setHeader(header); + + this.section.map.clear(); + + if (input != null) + this.convertMapsToSections(input, this.section); + } + + /* + * Converts the given maps to sections + */ + private void convertMapsToSections(@NonNull Map input, @NonNull ConfigSection section) { + for (final Map.Entry entry : input.entrySet()) { + final String key = entry.getKey().toString(); + final Object value = entry.getValue(); + + if (value instanceof Map) + this.convertMapsToSections((Map) value, section.createSection(key)); + else + section.store(key, value); + } + } + + /* + * Converts the given input to header + */ + @NonNull + private String parseHeader(@NonNull String input) { + final String commentPrefix = "# "; + final String[] lines = input.split("\r?\n", -1); + final StringBuilder result = new StringBuilder(); + + boolean readingHeader = true; + boolean foundHeader = false; + + for (int i = 0; i < lines.length && readingHeader; i++) { + final String line = lines[i].trim(); + + if (line.startsWith(commentPrefix) || line.equals("#")) { + if (i > 0) + result.append("\n"); + + if (line.length() > commentPrefix.length()) + result.append(line.substring(commentPrefix.length())); + + foundHeader = true; + + } else if (foundHeader && line.length() == 0) + result.append("\n"); + + else if (foundHeader) + readingHeader = false; + } + + final String string = result.toString(); + + return string.trim().isEmpty() ? "" : string + "\n"; + } + + @Override + public int hashCode() { + return this.getFileName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof YamlConfig && ((YamlConfig) obj).getFileName().equals(this.getFileName()); + } + + // ----------------------------------------------------------------------------------------------------- + // Static + // ----------------------------------------------------------------------------------------------------- + + /** + * Loads configuration from the internal JAR path, extracting it if needed. + * + * @param path + * @return + */ + @NonNull + public static final YamlConfig fromInternalPath(@NonNull String path) { + + final YamlConfig config = new YamlConfig(); + + try { + config.loadConfiguration(path); + + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + path, ex); + } + + return config; + } + + /** + * Loads configuration from the internal JAR path without setting the file, + * without extracting it and without defaults. + * + * @param path + * @return + */ + @NonNull + public static final YamlConfig fromInternalPathFast(@NonNull String path) { + + final YamlConfig config = new YamlConfig(); + + try { + config.loadInternal(path); + + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + path, ex); + } + + return config; + } + + /** + * Loads configuration from the file in your plugin's folder. + * + * @param file + * @return + */ + @NonNull + public static final YamlConfig fromFile(@NonNull File file) { + + final YamlConfig config = new YamlConfig(); + + try { + config.load(file); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex); + } + + return config; + } + + /** + * Loads configuration from the file in your plugin's folder. + * + * @param file + * @return + */ + @NonNull + public static final YamlConfig fromFileFast(@NonNull File file) { + final YamlConfig config = new YamlConfig(); + + try { + final List content = FileUtil.readLines(file); + config.loadFromString(String.join("\n", content)); + + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex); + } + + return config; + } + + // ----------------------------------------------------------------------------------------------------- + // Classes + // ----------------------------------------------------------------------------------------------------- + + /** + * Helper class, credits to the original Bukkit/Spigot team, enhanced by MineAcademy + */ + private final static class YamlConstructor extends SafeConstructor { + + public YamlConstructor() { + this.yamlConstructors.put(Tag.MAP, new ConstructCustomObject()); + } + + private class ConstructCustomObject extends ConstructYamlMap { + + @Override + public Object construct(@NonNull Node node) { + if (node.isTwoStepsConstruction()) + throw new YAMLException("Unexpected referential mapping structure. Node: " + node); + + final Map raw = (Map) super.construct(node); + + if (raw.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) { + final Map typed = new LinkedHashMap<>(raw.size()); + for (final Map.Entry entry : raw.entrySet()) + typed.put(entry.getKey().toString(), entry.getValue()); + + try { + return ConfigurationSerialization.deserializeObject(typed); + } catch (final IllegalArgumentException ex) { + throw new YAMLException("Could not deserialize object", ex); + } + } + + return raw; + } + + @Override + public void construct2ndStep(@NonNull Node node, @NonNull Object object) { + throw new YAMLException("Unexpected referential mapping structure. Node: " + node); + } + } + } + + /** + * Helper class, credits to the original Bukkit/Spigot team, enhanced by MineAcademy + */ + private final static class YamlRepresenter extends Representer { + + public YamlRepresenter() { + this.multiRepresenters.put(ConfigurationSerializable.class, new RepresentConfigurationSerializable()); + this.multiRepresenters.put(ConfigSection.class, new RepresentConfigurationSection()); + this.multiRepresenters.remove(Enum.class); + } + + private class RepresentConfigurationSection extends RepresentMap { + + @NonNull + @Override + public Node representData(@NonNull Object data) { + return super.representData(((ConfigSection) data).getValues(false)); + } + } + + private class RepresentConfigurationSerializable extends RepresentMap { + + @NonNull + @Override + public Node representData(@NonNull Object data) { + final ConfigurationSerializable serializable = (ConfigurationSerializable) data; + final Map values = new LinkedHashMap<>(); + values.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(serializable.getClass())); + values.putAll(serializable.serialize()); + + return super.representData(values); + } + } + } +} diff --git a/src/main/java/org/mineacademy/fo/settings/YamlStaticConfig.java b/src/main/java/org/mineacademy/fo/settings/YamlStaticConfig.java index 647512808..259785d39 100644 --- a/src/main/java/org/mineacademy/fo/settings/YamlStaticConfig.java +++ b/src/main/java/org/mineacademy/fo/settings/YamlStaticConfig.java @@ -1,14 +1,5 @@ package org.mineacademy.fo.settings; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Set; - import org.mineacademy.fo.Common; import org.mineacademy.fo.Valid; import org.mineacademy.fo.collection.SerializedMap; @@ -23,6 +14,15 @@ import org.mineacademy.fo.settings.FileConfig.AccusativeHelper; import org.mineacademy.fo.settings.FileConfig.TitleHelper; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + /** * A special case {@link YamlConfig} that allows static access to config. *

@@ -60,6 +60,11 @@ protected boolean saveComments() { return YamlStaticConfig.this.saveComments(); } + @Override + protected boolean alwaysSaveOnLoad() { + return YamlStaticConfig.this.alwaysSaveOnLoad(); + } + @Override protected List getUncommentedSections() { return YamlStaticConfig.this.getUncommentedSections(); @@ -131,6 +136,15 @@ protected boolean saveComments() { return true; } + /** + * Return true if we should always save the file after loading it. + * + * @return + */ + protected boolean alwaysSaveOnLoad() { + return false; + } + /** * See {@link #saveComments()} * diff --git a/src/main/java/org/mineacademy/fo/visual/VisualTool.java b/src/main/java/org/mineacademy/fo/visual/VisualTool.java index c221d9ac1..f096a6e7e 100644 --- a/src/main/java/org/mineacademy/fo/visual/VisualTool.java +++ b/src/main/java/org/mineacademy/fo/visual/VisualTool.java @@ -1,21 +1,19 @@ package org.mineacademy.fo.visual; -import java.util.ArrayList; -import java.util.List; - +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.mineacademy.fo.Messenger; -import org.mineacademy.fo.Valid; import org.mineacademy.fo.menu.tool.BlockTool; import org.mineacademy.fo.region.Region; import org.mineacademy.fo.remain.CompMaterial; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.NonNull; +import java.util.ArrayList; +import java.util.List; /** * A class that can visualize selection of blocks in the arena @@ -44,6 +42,7 @@ protected final void onBlockClick(final Player player, final ClickType click, fi /** * Handles block clicking. Any changes here will be reflected automatically in the visualization + * You need to override this method if you want to save the selected region or block clicked! * * @param player * @param click @@ -54,18 +53,20 @@ protected void handleBlockClick(Player player, ClickType click, Block block) { final Location location = block.getLocation(); final Region region = this.getVisualizedRegion(player); - Valid.checkNotNull(region, "Got null region on block clicking for player " + player.getName()); - // If you place primary location over a secondary location point, remove secondary - if (!isPrimary && region.hasPrimary() && region.isPrimary(location)) - region.setPrimary(null); + if (region != null) { + + // If you place primary location over a secondary location point, remove secondary + if (!isPrimary && region.hasPrimary() && region.isPrimary(location)) + region.setPrimary(null); - // ...and vice versa - if (isPrimary && region.hasSecondary() && region.isSecondary(location)) - region.setSecondary(null); + // ...and vice versa + if (isPrimary && region.hasSecondary() && region.isSecondary(location)) + region.setSecondary(null); - final boolean removed = !region.toggleLocation(location, click); - Messenger.success(player, (isPrimary ? "&cPrimary" : "&6Secondary") + " &7location has been " + (removed ? "&cremoved" : "&2set") + "&7."); + final boolean removed = !region.toggleLocation(location, click); + Messenger.success(player, (isPrimary ? "&cPrimary" : "&6Secondary") + " &7location has been " + (removed ? "&cremoved" : "&2set") + "&7."); + } } /** @@ -96,7 +97,7 @@ protected void handleAirClick(final Player player, final ClickType click) { * @see org.mineacademy.fo.menu.tool.Tool#onHotbarFocused(org.bukkit.entity.Player) */ @Override - protected final void onHotbarFocused(final Player player) { + protected void onHotbarFocused(final Player player) { this.visualize(player); } @@ -104,12 +105,12 @@ protected final void onHotbarFocused(final Player player) { * @see org.mineacademy.fo.menu.tool.Tool#onHotbarDefocused(org.bukkit.entity.Player) */ @Override - protected final void onHotbarDefocused(final Player player) { + protected void onHotbarDefocused(final Player player) { this.stopVisualizing(player); } /** - * Return a list of points we should render in this visualization + * Return a list of points or a single point we should render in this visualization * * @param player * diff --git a/src/main/java/org/mineacademy/fo/visual/VisualizedRegion.java b/src/main/java/org/mineacademy/fo/visual/VisualizedRegion.java index afb98a728..586cc0625 100644 --- a/src/main/java/org/mineacademy/fo/visual/VisualizedRegion.java +++ b/src/main/java/org/mineacademy/fo/visual/VisualizedRegion.java @@ -1,10 +1,7 @@ package org.mineacademy.fo.visual; -import java.util.Map; -import java.util.Set; - -import javax.annotation.Nullable; - +import lombok.Getter; +import lombok.Setter; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -18,8 +15,9 @@ import org.mineacademy.fo.region.Region; import org.mineacademy.fo.remain.CompParticle; -import lombok.Getter; -import lombok.Setter; +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Set; /** * A simply way to visualize two locations in the world @@ -220,6 +218,11 @@ private void stopVisualizing() { * @return */ public static VisualizedRegion deserialize(final SerializedMap map) { + + // Support loading an empty key with "{}" empty map + if (map.isEmpty()) + return new VisualizedRegion(); + Valid.checkBoolean(map.containsKey("Primary") && map.containsKey("Secondary"), "The region must have Primary and a Secondary location"); final String name = map.getString("Name");