names = Lists.newArrayList(aliases);
+ names.add(name);
+ this.aliases = Collections.unmodifiableList(names);
+ }
+
+ @SafeVarargs
+ protected final void child(final AbstractCommand... commands) {
+ if (commands == null || commands.length == 0) {
+ return;
+ }
+
+ if (children == null) {
+ children = new HashMap<>();
+ }
+
+ for (final AbstractCommand
child : commands) {
+ for (final String alias : child.aliases) {
+ children.put(alias.toLowerCase(), child);
+ }
+ }
+ }
+
+ protected void handleMessage(
+ final CommandSender sender, final MessageType type, final String... args) {
+ sender.sendMessage(type.defaultMessage.format(args));
+ }
+
+ private PluginCommand getCommand() {
+ PluginCommand pluginCommand = plugin.getCommand(name);
+
+ if (pluginCommand == null) {
+ throw new IllegalArgumentException("Command is not registered in plugin.yml");
+ }
+
+ return pluginCommand;
+ }
+
+ public final void register() {
+ final PluginCommand pluginCommand = getCommand();
+
+ pluginCommand.setExecutor(
+ (sender, command, label, args) -> {
+ if (playerOnly && !(sender instanceof Player)) {
+ handleMessage(sender, MessageType.PLAYER_ONLY);
+ return true;
+ }
+
+ if (permission != null && !sender.hasPermission(permission)) {
+ handleMessage(sender, MessageType.NO_PERMISSION, permission);
+ return true;
+ }
+
+ if (args.length > 0 && children != null) {
+ final AbstractCommand
child = children.get(args[0].toLowerCase());
+
+ if (child == null) {
+ handleMessage(sender, MessageType.SUB_COMMAND_INVALID, label, args[0]);
+ return true;
+ }
+
+ if (child.playerOnly && !(sender instanceof Player)) {
+ handleMessage(sender, MessageType.PLAYER_ONLY);
+ return true;
+ }
+
+ if (child.permission != null && !sender.hasPermission(child.permission)) {
+ handleMessage(sender, MessageType.NO_PERMISSION, child.permission);
+ return true;
+ }
+
+ if (args.length < child.length) {
+ handleMessage(sender, MessageType.SUB_COMMAND_USAGE, label, child.usage);
+ return true;
+ }
+
+ child.execute(sender, label, args);
+ return true;
+ }
+
+ execute(sender, label, args);
+ return true;
+ });
+ pluginCommand.setTabCompleter(
+ (sender, command, alias, args) -> {
+ if (children != null && args.length > 1) {
+ final AbstractCommand
child = children.get(args[0].toLowerCase());
+
+ if (child != null) {
+ final List result = child.onTabComplete(sender, command, alias, args);
+
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+
+ return onTabComplete(sender, command, alias, args);
+ });
+ }
+
+ @Override
+ public List onTabComplete(
+ CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) {
+ if (args.length == 0) {
+ return null;
+ }
+
+ if (args.length == 1 && children != null) {
+ return children.values().stream()
+ .filter(child -> child.name.startsWith(args[0].toLowerCase()))
+ .map(child -> child.name)
+ .distinct()
+ .sorted(String::compareTo)
+ .collect(Collectors.toList());
+ }
+
+ return null;
+ }
+
+ protected abstract void execute(
+ final CommandSender sender, final String label, final String[] args);
+
+ protected enum MessageType {
+ PLAYER_ONLY("&cThis command can only be executed by a player!"),
+ NO_PERMISSION("&cYou need the following permission: {0}"),
+ SUB_COMMAND_INVALID("&c''{1}'' is not a valid sub command. Type /{0} for help."),
+ SUB_COMMAND_USAGE("&cUsage: /{0} {1}");
+
+ private final MessageFormat defaultMessage;
+
+ MessageType(final String defaultMessage) {
+ this.defaultMessage = new MessageFormat(StringUtil.color(defaultMessage));
+ }
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java
new file mode 100644
index 00000000..0249a220
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java
@@ -0,0 +1,50 @@
+package me.realized.tokenmanager.util.compat;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.UUID;
+import org.bukkit.inventory.ItemStack;
+
+class CompatBase {
+
+ static final Method AS_NMS_COPY, AS_BUKKIT_COPY;
+
+ static final Class> TAG_COMPOUND;
+ static final Method GET_TAG, SET_TAG, SET, SET_STRING;
+ static final Constructor> GAME_PROFILE_CONST, PROPERTY_CONST;
+ static final Method GET_PROPERTIES, PUT;
+ static final Field PROFILE;
+
+ static {
+ final Class> CB_ITEMSTACK = ReflectionUtil.getCBClass("inventory.CraftItemStack");
+ final Class> NMS_ITEMSTACK =
+ ReflectionUtil.getNMSClass(
+ (ReflectionUtil.getMajorVersion() >= 17 ? "world.item." : "") + "ItemStack");
+ AS_NMS_COPY = ReflectionUtil.getMethod(CB_ITEMSTACK, "asNMSCopy", ItemStack.class);
+ AS_BUKKIT_COPY = ReflectionUtil.getMethod(CB_ITEMSTACK, "asBukkitCopy", NMS_ITEMSTACK);
+ TAG_COMPOUND =
+ ReflectionUtil.getNMSClass(
+ (ReflectionUtil.getMajorVersion() >= 17 ? "nbt." : "") + "NBTTagCompound");
+
+ final Class> TAG_BASE =
+ ReflectionUtil.getNMSClass(
+ (ReflectionUtil.getMajorVersion() >= 17 ? "nbt." : "") + "NBTBase");
+ GET_TAG = ReflectionUtil.getMethod(NMS_ITEMSTACK, "getTag");
+ SET_TAG = ReflectionUtil.getMethod(NMS_ITEMSTACK, "setTag", TAG_COMPOUND);
+ SET = ReflectionUtil.getMethod(TAG_COMPOUND, "set", String.class, TAG_BASE);
+ SET_STRING = ReflectionUtil.getMethod(TAG_COMPOUND, "setString", String.class, String.class);
+
+ final Class> GAME_PROFILE = ReflectionUtil.getALClass("GameProfile");
+ GAME_PROFILE_CONST = ReflectionUtil.getConstructor(GAME_PROFILE, UUID.class, String.class);
+ GET_PROPERTIES = ReflectionUtil.getMethod(GAME_PROFILE, "getProperties");
+ final Class> PROPERTY = ReflectionUtil.getALClass("properties.Property");
+ PROPERTY_CONST = ReflectionUtil.getConstructor(PROPERTY, String.class, String.class);
+ PUT =
+ ReflectionUtil.getMethod(
+ ReflectionUtil.getALClass("properties.PropertyMap"), "put", Object.class, Object.class);
+ PROFILE =
+ ReflectionUtil.getDeclaredField(
+ ReflectionUtil.getCBClass("inventory.CraftMetaSkull"), "profile");
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java
new file mode 100644
index 00000000..56f7db61
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java
@@ -0,0 +1,23 @@
+package me.realized.tokenmanager.util.compat;
+
+public final class CompatUtil {
+ public static boolean isPre1_17() {
+ return false;
+ }
+
+ public static boolean isPre1_14() {
+ return false;
+ }
+
+ public static boolean isPre1_13() {
+ return false;
+ }
+
+ public static boolean isPre1_12() {
+ return false;
+ }
+
+ public static boolean isPre1_9() {
+ return false;
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java
new file mode 100644
index 00000000..55258270
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java
@@ -0,0 +1,48 @@
+package me.realized.tokenmanager.util.compat;
+
+import me.realized.tokenmanager.util.inventory.ItemBuilder;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+public final class Items {
+
+ private static final String PANE = "STAINED_GLASS_PANE";
+
+ public static final ItemStack RED_PANE;
+ public static final ItemStack GRAY_PANE;
+ public static final ItemStack GREEN_PANE;
+
+ public static final ItemStack HEAD;
+
+ static {
+ RED_PANE =
+ (CompatUtil.isPre1_13()
+ ? ItemBuilder.of(PANE, 1, (short) 14)
+ : ItemBuilder.of(Material.RED_STAINED_GLASS_PANE))
+ .name(" ")
+ .build();
+ GRAY_PANE =
+ (CompatUtil.isPre1_13()
+ ? ItemBuilder.of(PANE, 1, (short) 7)
+ : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE))
+ .name(" ")
+ .build();
+ GREEN_PANE =
+ (CompatUtil.isPre1_13()
+ ? ItemBuilder.of(PANE, 1, (short) 13)
+ : ItemBuilder.of(Material.GREEN_STAINED_GLASS_PANE))
+ .name(" ")
+ .build();
+ HEAD =
+ (CompatUtil.isPre1_13()
+ ? ItemBuilder.of("SKULL_ITEM", 1, (short) 3)
+ : ItemBuilder.of(Material.PLAYER_HEAD))
+ .build();
+ }
+
+ public static boolean equals(final ItemStack item, final ItemStack other) {
+ return item.getType() == other.getType() && item.getDurability() == other.getDurability();
+ }
+
+ private Items() {}
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java
new file mode 100644
index 00000000..b6d64879
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java
@@ -0,0 +1,152 @@
+package me.realized.tokenmanager.util.compat;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import me.realized.tokenmanager.util.NumberUtil;
+import org.bukkit.Bukkit;
+
+public final class ReflectionUtil {
+ private static final String PACKAGE_VERSION;
+ private static final int MAJOR_VERSION;
+
+ static {
+ final String packageName = Bukkit.getServer().getClass().getPackage().getName();
+ PACKAGE_VERSION = packageName.substring(packageName.lastIndexOf('.') + 1);
+ MAJOR_VERSION = (int) NumberUtil.parseLong(PACKAGE_VERSION.split("_")[1]).orElse(0);
+ }
+
+ public static int getMajorVersion() {
+ return MAJOR_VERSION;
+ }
+
+ public static Class> getClassUnsafe(final String name) {
+ try {
+ return Class.forName(name);
+ } catch (ClassNotFoundException ex) {
+ return null;
+ }
+ }
+
+ public static Method getMethodUnsafe(
+ final Class> clazz, final String name, final Class>... parameters) {
+ try {
+ return clazz.getMethod(name, parameters);
+ } catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ public static Class> getNMSClass(final String name, final boolean logError) {
+ try {
+ return Class.forName(
+ "net.minecraft"
+ + (getMajorVersion() < 17 ? (".server." + PACKAGE_VERSION) : "")
+ + "."
+ + name);
+ } catch (ClassNotFoundException ex) {
+ if (logError) {
+ ex.printStackTrace();
+ }
+
+ return null;
+ }
+ }
+
+ public static Class> getNMSClass(final String name) {
+ return getNMSClass(name, true);
+ }
+
+ public static Class> getCBClass(final String path, final boolean logError) {
+ try {
+ return Class.forName("org.bukkit.craftbukkit." + PACKAGE_VERSION + "." + path);
+ } catch (ClassNotFoundException ex) {
+ if (logError) {
+ ex.printStackTrace();
+ }
+
+ return null;
+ }
+ }
+
+ public static Class> getCBClass(final String path) {
+ return getCBClass(path, true);
+ }
+
+ public static Class> getALClass(final String name) {
+ try {
+ return Class.forName("com.mojang.authlib." + name);
+ } catch (ClassNotFoundException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Method getMethod(
+ final Class> clazz, final String name, final Class>... parameters) {
+ try {
+ return clazz.getMethod(name, parameters);
+ } catch (NoSuchMethodException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ private static Method findDeclaredMethod(
+ final Class> clazz, final String name, final Class>... parameters)
+ throws NoSuchMethodException {
+ final Method method = clazz.getDeclaredMethod(name, parameters);
+ method.setAccessible(true);
+ return method;
+ }
+
+ public static Method getDeclaredMethod(
+ final Class> clazz, final String name, final Class>... parameters) {
+ try {
+ return findDeclaredMethod(clazz, name, parameters);
+ } catch (NoSuchMethodException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Method getDeclaredMethodUnsafe(
+ final Class> clazz, final String name, final Class>... parameters) {
+ try {
+ return findDeclaredMethod(clazz, name, parameters);
+ } catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ public static Field getField(final Class> clazz, final String name) {
+ try {
+ return clazz.getField(name);
+ } catch (NoSuchFieldException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Field getDeclaredField(final Class> clazz, final String name) {
+ try {
+ final Field field = clazz.getDeclaredField(name);
+ field.setAccessible(true);
+ return field;
+ } catch (NoSuchFieldException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Constructor> getConstructor(final Class> clazz, final Class>... parameters) {
+ try {
+ return clazz.getConstructor(parameters);
+ } catch (NoSuchMethodException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ private ReflectionUtil() {}
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java
new file mode 100644
index 00000000..65db7531
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java
@@ -0,0 +1,26 @@
+package me.realized.tokenmanager.util.compat;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.UUID;
+import org.bukkit.inventory.meta.SkullMeta;
+
+public final class Skulls extends CompatBase {
+
+ public static void setSkull(final SkullMeta meta, final String value) {
+ try {
+ final Object profile = GAME_PROFILE_CONST.newInstance(UUID.randomUUID(), null);
+ final Object propertyMap = GET_PROPERTIES.invoke(profile);
+
+ if (propertyMap == null) {
+ throw new IllegalStateException("Profile doesn't contain a property map");
+ }
+
+ PUT.invoke(propertyMap, "textures", PROPERTY_CONST.newInstance("textures", value));
+ PROFILE.set(meta, profile);
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ private Skulls() {}
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java
new file mode 100644
index 00000000..da26100c
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java
@@ -0,0 +1,38 @@
+package me.realized.tokenmanager.util.compat;
+
+import lombok.Getter;
+import org.bukkit.Material;
+import org.bukkit.entity.EntityType;
+import org.bukkit.inventory.ItemStack;
+
+public class SpawnEggs extends CompatBase {
+
+ @Getter private final EntityType type;
+
+ public SpawnEggs(EntityType type) {
+ this.type = type;
+ }
+
+ @SuppressWarnings("deprecation")
+ public ItemStack toItemStack() {
+ try {
+ final ItemStack item = new ItemStack(Material.getMaterial("MONSTER_EGG"));
+ Object nmsItem = AS_NMS_COPY.invoke(null, item);
+ Object tag = GET_TAG.invoke(nmsItem);
+
+ if (tag == null) {
+ tag = TAG_COMPOUND.newInstance();
+ }
+
+ final Object id = TAG_COMPOUND.newInstance();
+ SET_STRING.invoke(id, "id", type.getName());
+ SET.invoke(tag, "EntityTag", id);
+ SET_TAG.invoke(nmsItem, tag);
+ return (ItemStack) AS_BUKKIT_COPY.invoke(null, nmsItem);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java
new file mode 100644
index 00000000..a53ad3d4
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java
@@ -0,0 +1,35 @@
+package me.realized.tokenmanager.util.compat;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.bukkit.Material;
+
+public final class Terracottas {
+
+ private static final Map DATA_TO_TERRACOTTA = new HashMap<>();
+
+ static {
+ DATA_TO_TERRACOTTA.put((short) 0, "WHITE_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 1, "ORANGE_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 2, "MAGENTA_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 3, "LIGHT_BLUE_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 4, "YELLOW_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 5, "LIME_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 6, "PINK_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 7, "GRAY_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 8, "LIGHT_GRAY_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 9, "CYAN_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 10, "PURPLE_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 11, "BLUE_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 12, "BROWN_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 13, "GREEN_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 14, "RED_TERRACOTTA");
+ DATA_TO_TERRACOTTA.put((short) 15, "BLACK_TERRACOTTA");
+ }
+
+ public static Material from(final short data) {
+ return Material.getMaterial(DATA_TO_TERRACOTTA.get(data));
+ }
+
+ private Terracottas() {}
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java
new file mode 100644
index 00000000..c34ecf3d
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java
@@ -0,0 +1,209 @@
+package me.realized.tokenmanager.util.config;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import me.realized.tokenmanager.util.Loadable;
+import me.realized.tokenmanager.util.config.convert.Converter;
+import org.bukkit.configuration.MemorySection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public abstract class AbstractConfiguration implements Loadable {
+
+ private static final String CONVERT_START =
+ "[!] Converting your current configuration (%s) to the new version...";
+ private static final String CONVERT_SAVE = "[!] Your old configuration was stored as %s.";
+ private static final String CONVERT_DONE = "[!] Conversion complete!";
+
+ private static final Pattern KEY_PATTERN = Pattern.compile("^([ ]*)([^ \"]+)[:].*$");
+ private static final Pattern COMMENT_PATTERN = Pattern.compile("^([ ]*[#].*)|[ ]*$");
+
+ protected final P plugin;
+
+ private final String name;
+ private final File file;
+
+ private FileConfiguration configuration;
+
+ public AbstractConfiguration(final P plugin, final String name) {
+ this.plugin = plugin;
+ this.name = name + ".yml";
+ this.file = new File(plugin.getDataFolder(), this.name);
+ }
+
+ @Override
+ public void handleLoad() throws Exception {
+ if (!file.exists()) {
+ plugin.saveResource(name, true);
+ }
+
+ loadValues(configuration = YamlConfiguration.loadConfiguration(file));
+ }
+
+ @Override
+ public void handleUnload() {}
+
+ protected abstract void loadValues(final FileConfiguration configuration) throws Exception;
+
+ protected int getLatestVersion() throws Exception {
+ final InputStream stream = plugin.getClass().getResourceAsStream("/" + name);
+
+ if (stream == null) {
+ throw new IllegalStateException(
+ plugin.getName()
+ + "'s jar file was replaced, but a reload was called! Please restart your server instead when updating this plugin.");
+ }
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
+ return YamlConfiguration.loadConfiguration(reader).getInt("config-version", -1);
+ }
+ }
+
+ protected FileConfiguration convert(final Converter converter) throws IOException {
+ plugin.getLogger().info(String.format(CONVERT_START, name));
+
+ final Map oldValues = new HashMap<>();
+
+ for (final String key : configuration.getKeys(true)) {
+ if (key.equals("config-version")) {
+ continue;
+ }
+
+ final Object value = configuration.get(key);
+
+ if (value instanceof MemorySection) {
+ continue;
+ }
+
+ oldValues.put(key, value);
+ }
+
+ if (converter != null) {
+ converter
+ .renamedKeys()
+ .forEach(
+ (old, changed) -> {
+ final Object previous = oldValues.get(old);
+
+ if (previous != null) {
+ oldValues.remove(old);
+ oldValues.put(changed, previous);
+ }
+ });
+ }
+
+ final String newName = name.replace(".yml", "") + "-" + System.currentTimeMillis() + ".yml";
+ final File copied =
+ Files.copy(file.toPath(), new File(plugin.getDataFolder(), newName).toPath()).toFile();
+ plugin.getLogger().info(String.format(CONVERT_SAVE, copied.getName()));
+ plugin.saveResource(name, true);
+
+ // Loads comments of the new configuration file
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ final Multimap> comments = LinkedListMultimap.create();
+ final List currentComments = new ArrayList<>();
+
+ String line;
+ Matcher matcher;
+
+ while ((line = reader.readLine()) != null) {
+ if ((matcher = KEY_PATTERN.matcher(line)).find()
+ && !COMMENT_PATTERN.matcher(line).matches()) {
+ comments.put(matcher.group(2), Lists.newArrayList(currentComments));
+ currentComments.clear();
+ } else if (COMMENT_PATTERN.matcher(line).matches()) {
+ currentComments.add(line);
+ }
+ }
+
+ configuration = YamlConfiguration.loadConfiguration(file);
+ configuration.options().header(null);
+
+ // Transfer values from the old configuration
+ for (Map.Entry entry : oldValues.entrySet()) {
+ final String key = entry.getKey();
+ final Object value = configuration.get(key);
+
+ if ((value != null && !(value instanceof MemorySection))
+ || transferredSections().stream().anyMatch(section -> key.startsWith(section + "."))) {
+ configuration.set(key, entry.getValue());
+ }
+ }
+
+ final List commentlessData =
+ Lists.newArrayList(configuration.saveToString().split("\n"));
+
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+ for (final String data : commentlessData) {
+ matcher = KEY_PATTERN.matcher(data);
+
+ if (matcher.find()) {
+ final String key = matcher.group(2);
+ final Collection> result = comments.get(key);
+
+ if (result != null) {
+ final List> commentData = Lists.newArrayList(result);
+
+ if (!commentData.isEmpty()) {
+ for (final String comment : commentData.get(0)) {
+ writer.write(comment);
+ writer.newLine();
+ }
+
+ commentData.remove(0);
+ comments.replaceValues(key, commentData);
+ }
+ }
+ }
+
+ writer.write(data);
+
+ if (commentlessData.indexOf(data) + 1 < commentlessData.size()) {
+ writer.newLine();
+ } else if (!currentComments.isEmpty()) {
+ writer.newLine();
+ }
+ }
+
+ // Handles comments at the end of the file without any key
+ for (final String comment : currentComments) {
+ writer.write(comment);
+
+ if (currentComments.indexOf(comment) + 1 < currentComments.size()) {
+ writer.newLine();
+ }
+ }
+
+ writer.flush();
+ }
+
+ plugin.getLogger().info(CONVERT_DONE);
+ }
+
+ return configuration;
+ }
+
+ protected Set transferredSections() {
+ return Collections.emptySet();
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java
new file mode 100644
index 00000000..290c3146
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java
@@ -0,0 +1,8 @@
+package me.realized.tokenmanager.util.config.convert;
+
+import java.util.Map;
+
+public interface Converter {
+
+ Map renamedKeys();
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java
new file mode 100644
index 00000000..d0630620
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java
@@ -0,0 +1,53 @@
+package me.realized.tokenmanager.util.hook;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+import me.realized.tokenmanager.util.Loadable;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public abstract class AbstractHookManager implements Loadable {
+
+ protected final P plugin;
+ private final Map>, PluginHook> hooks = new HashMap<>();
+
+ public AbstractHookManager(final P plugin) {
+ this.plugin = plugin;
+ }
+
+ protected boolean register(final String name, final Class extends PluginHook
> clazz) {
+ final Plugin target = Bukkit.getPluginManager().getPlugin(name);
+
+ if (target == null || !target.isEnabled()) {
+ return false;
+ }
+
+ try {
+ final Constructor extends PluginHook
> constructor =
+ clazz.getConstructor(plugin.getClass());
+ final boolean result;
+
+ if (result =
+ constructor != null
+ && hooks.putIfAbsent(clazz, constructor.newInstance(plugin)) == null) {
+ plugin.getLogger().info("Successfully hooked into '" + name + "'!");
+ }
+
+ return result;
+ } catch (NoSuchMethodException
+ | IllegalAccessException
+ | InstantiationException
+ | InvocationTargetException ex) {
+ plugin.getLogger().warning("Failed to hook into " + name + ": " + ex.getMessage());
+ }
+
+ return false;
+ }
+
+ public > T getHook(Class clazz) {
+ return clazz != null ? clazz.cast(hooks.get(clazz)) : null;
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java
new file mode 100644
index 00000000..ba0397a6
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java
@@ -0,0 +1,19 @@
+package me.realized.tokenmanager.util.hook;
+
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class PluginHook {
+
+ protected final P plugin;
+
+ private final String name;
+
+ public PluginHook(final P plugin, final String name) {
+ this.plugin = plugin;
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java
new file mode 100644
index 00000000..b4022048
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java
@@ -0,0 +1,98 @@
+package me.realized.tokenmanager.util.inventory;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+
+public final class GUIBuilder {
+
+ private final Inventory gui;
+
+ private GUIBuilder(final String title, final int rows) {
+ this.gui = Bukkit.createInventory(null, rows * 9, title);
+ }
+
+ public static GUIBuilder of(final String title, final int rows) {
+ return new GUIBuilder(title, rows);
+ }
+
+ public GUIBuilder set(final int slot, final ItemStack item) {
+ gui.setItem(slot, item);
+ return this;
+ }
+
+ public GUIBuilder fill(final ItemStack item, final int... slots) {
+ for (final int slot : slots) {
+ gui.setItem(slot, item);
+ }
+
+ return this;
+ }
+
+ public GUIBuilder fillRange(final int from, final int to, final ItemStack item) {
+ for (int slot = from; slot < to; slot++) {
+ gui.setItem(slot, item);
+ }
+
+ return this;
+ }
+
+ public GUIBuilder fillEmpty(final ItemStack item) {
+ for (int slot = 0; slot < gui.getSize(); slot++) {
+ final ItemStack target = gui.getItem(slot);
+
+ if (target != null && target.getType() != Material.AIR) {
+ gui.setItem(slot, item);
+ }
+ }
+
+ return this;
+ }
+
+ public GUIBuilder pattern(final Pattern pattern) {
+ pattern.apply(gui);
+ return this;
+ }
+
+ public Inventory build() {
+ return gui;
+ }
+
+ public static final class Pattern {
+
+ private final List rows;
+ private final Map keys = new HashMap<>();
+
+ private Pattern(final String... rows) {
+ this.rows = Arrays.asList(rows);
+ }
+
+ public static Pattern of(final String... rows) {
+ return new Pattern(rows);
+ }
+
+ public Pattern specify(final char key, final ItemStack item) {
+ keys.put(key, item);
+ return this;
+ }
+
+ private void apply(final Inventory inventory) {
+ for (int row = 0; row < rows.size(); row++) {
+ final String pattern = rows.get(row);
+
+ for (int i = 0; i < pattern.length(); i++) {
+ if (i > 8) {
+ break;
+ }
+
+ inventory.setItem(row * 9 + i, keys.get(pattern.charAt(i)).clone());
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java
new file mode 100644
index 00000000..abc1f66d
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java
@@ -0,0 +1,50 @@
+package me.realized.tokenmanager.util.inventory;
+
+import me.realized.tokenmanager.util.StringUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.ItemStack;
+
+public final class InventoryUtil {
+
+ private InventoryUtil() {}
+
+ public static Inventory deepCopyOf(final Inventory inventory, final String title) {
+ final Inventory result =
+ Bukkit.createInventory(null, inventory.getSize(), StringUtil.color(title));
+
+ for (int i = 0; i < inventory.getSize(); i++) {
+ result.setItem(i, inventory.getItem(i).clone());
+ }
+
+ return result;
+ }
+
+ public static int getEmptySlots(final Inventory inventory) {
+ int empty = 0;
+
+ for (int i = 0; i < 36; i++) {
+ final ItemStack item = inventory.getItem(i);
+
+ if (item == null) {
+ empty++;
+ }
+ }
+
+ return empty;
+ }
+
+ public static boolean isInventoryFull(final Player player) {
+ return player.getInventory().firstEmpty() == -1;
+ }
+
+ public static Inventory getClickedInventory(final int rawSlot, final InventoryView view) {
+ if (rawSlot < view.getTopInventory().getSize()) {
+ return view.getTopInventory();
+ } else {
+ return view.getBottomInventory();
+ }
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java
new file mode 100644
index 00000000..6c2194f2
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java
@@ -0,0 +1,55 @@
+package me.realized.tokenmanager.util.inventory;
+
+import java.util.Arrays;
+import java.util.List;
+import me.realized.tokenmanager.util.StringUtil;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+public final class ItemBuilder {
+
+ private final ItemStack result;
+
+ private ItemBuilder(final Material type, final int amount, final short durability) {
+ this.result = new ItemStack(type, amount, durability);
+ }
+
+ public static ItemBuilder of(final Material type) {
+ return of(type, 1);
+ }
+
+ public static ItemBuilder of(final Material type, final int amount) {
+ return of(type, amount, (short) 0);
+ }
+
+ public static ItemBuilder of(final Material type, final int amount, final short durability) {
+ return new ItemBuilder(type, amount, durability);
+ }
+
+ public static ItemBuilder of(final String type, final int amount, final short durability) {
+ return new ItemBuilder(Material.getMaterial(type), amount, durability);
+ }
+
+ public ItemBuilder name(final String name) {
+ ItemMeta meta = result.getItemMeta();
+ meta.setDisplayName(StringUtil.color(name));
+ result.setItemMeta(meta);
+ return this;
+ }
+
+ public ItemBuilder lore(final String... lore) {
+ return lore(Arrays.asList(lore));
+ }
+
+ public ItemBuilder lore(final List lore) {
+ ItemMeta meta = result.getItemMeta();
+ meta.setLore(StringUtil.color(lore));
+ result.setItemMeta(meta);
+ return this;
+ }
+
+ public ItemStack build() {
+ return result;
+ }
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java
new file mode 100644
index 00000000..7cf6c554
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java
@@ -0,0 +1,335 @@
+package me.realized.tokenmanager.util.inventory;
+
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalLong;
+import java.util.function.Consumer;
+import me.realized.tokenmanager.util.EnumUtil;
+import me.realized.tokenmanager.util.NumberUtil;
+import me.realized.tokenmanager.util.StringUtil;
+import me.realized.tokenmanager.util.compat.CompatUtil;
+import me.realized.tokenmanager.util.compat.Items;
+import me.realized.tokenmanager.util.compat.Skulls;
+import me.realized.tokenmanager.util.compat.SpawnEggs;
+import me.realized.tokenmanager.util.compat.Terracottas;
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.EntityType;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.inventory.meta.SkullMeta;
+import org.bukkit.potion.PotionData;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.bukkit.potion.PotionType;
+
+public final class ItemUtil {
+
+ private static final Map ENCHANTMENTS;
+ private static final Map EFFECTS;
+
+ static {
+ final Map enchantments = new HashMap<>();
+ Arrays.stream(Enchantment.values())
+ .forEach(
+ enchantment -> {
+ enchantments.put(enchantment.getName(), enchantment);
+
+ if (!CompatUtil.isPre1_13()) {
+ enchantments.put(enchantment.getKey().getKey(), enchantment);
+ }
+ });
+ // enchantments.put("power", Enchantment.ARROW_DAMAGE);
+ // enchantments.put("flame", Enchantment.ARROW_FIRE);
+ // enchantments.put("infinity", Enchantment.ARROW_INFINITE);
+ // enchantments.put("punch", Enchantment.ARROW_KNOCKBACK);
+ // enchantments.put("sharpness", Enchantment.DAMAGE_ALL);
+ // enchantments.put("baneofarthopods", Enchantment.DAMAGE_ARTHROPODS);
+ // enchantments.put("smite", Enchantment.DAMAGE_UNDEAD);
+ // enchantments.put("efficiency", Enchantment.DIG_SPEED);
+ // enchantments.put("unbreaking", Enchantment.DURABILITY);
+ // enchantments.put("thorns", Enchantment.THORNS);
+ // enchantments.put("fireaspect", Enchantment.FIRE_ASPECT);
+ // enchantments.put("knockback", Enchantment.KNOCKBACK);
+ // enchantments.put("fortune", Enchantment.LOOT_BONUS_BLOCKS);
+ // enchantments.put("looting", Enchantment.LOOT_BONUS_MOBS);
+ // enchantments.put("respiration", Enchantment.OXYGEN);
+ // enchantments.put("blastprotection", Enchantment.PROTECTION_EXPLOSIONS);
+ // enchantments.put("featherfalling", Enchantment.PROTECTION_FALL);
+ // enchantments.put("fireprotection", Enchantment.PROTECTION_FIRE);
+ // enchantments.put("projectileprotection", Enchantment.PROTECTION_PROJECTILE);
+ // enchantments.put("protection", Enchantment.PROTECTION_ENVIRONMENTAL);
+ // enchantments.put("silktouch", Enchantment.SILK_TOUCH);
+ // enchantments.put("aquaaffinity", Enchantment.WATER_WORKER);
+ // enchantments.put("luck", Enchantment.LUCK);
+ ENCHANTMENTS = Collections.unmodifiableMap(enchantments);
+
+ final Map effects = new HashMap<>();
+ Arrays.stream(PotionEffectType.values())
+ .forEach(
+ type -> {
+ if (type == null) {
+ return;
+ }
+
+ effects.put(type.getName(), type);
+ });
+ effects.put("speed", PotionEffectType.SPEED);
+ // effects.put("slowness", PotionEffectType.SLOW);
+ // effects.put("haste", PotionEffectType.FAST_DIGGING);
+ // effects.put("fatigue", PotionEffectType.SLOW_DIGGING);
+ // effects.put("strength", PotionEffectType.INCREASE_DAMAGE);
+ // effects.put("heal", PotionEffectType.HEAL);
+ // effects.put("harm", PotionEffectType.HARM);
+ // effects.put("jump", PotionEffectType.JUMP);
+ // effects.put("nausea", PotionEffectType.CONFUSION);
+ // effects.put("regeneration", PotionEffectType.REGENERATION);
+ // effects.put("resistance", PotionEffectType.DAMAGE_RESISTANCE);
+ effects.put("fireresistance", PotionEffectType.FIRE_RESISTANCE);
+ effects.put("waterbreathing", PotionEffectType.WATER_BREATHING);
+ effects.put("invisibility", PotionEffectType.INVISIBILITY);
+ effects.put("blindness", PotionEffectType.BLINDNESS);
+ effects.put("nightvision", PotionEffectType.NIGHT_VISION);
+ effects.put("hunger", PotionEffectType.HUNGER);
+ effects.put("weakness", PotionEffectType.WEAKNESS);
+ effects.put("poison", PotionEffectType.POISON);
+ effects.put("wither", PotionEffectType.WITHER);
+ effects.put("healthboost", PotionEffectType.HEALTH_BOOST);
+ effects.put("absorption", PotionEffectType.ABSORPTION);
+ effects.put("saturation", PotionEffectType.SATURATION);
+ EFFECTS = Collections.unmodifiableMap(effects);
+ }
+
+ public static ItemStack loadFromString(final String line) {
+ if (line == null || line.isEmpty()) {
+ throw new IllegalArgumentException("Line is empty or null");
+ }
+
+ final String[] args = line.split(" +");
+ String[] materialData = args[0].split(":");
+ Material material = Material.matchMaterial(materialData[0]);
+
+ // TEMP: Allow confirm button item loading in 1.13
+ if (!CompatUtil.isPre1_13()) {
+ if (materialData[0].equalsIgnoreCase("STAINED_CLAY")) {
+ material = Material.TERRACOTTA;
+
+ if (materialData.length > 1) {
+ material = Terracottas.from((short) NumberUtil.parseLong(materialData[1]).orElse(0));
+ }
+ }
+ }
+
+ if (material == null) {
+ throw new IllegalArgumentException("'" + args[0] + "' is not a valid material");
+ }
+
+ ItemStack result = new ItemStack(material, 1);
+
+ if (materialData.length > 1) {
+ // Handle potions and spawn eggs switching to NBT in 1.9+
+ if (!CompatUtil.isPre1_9()) {
+ if (material.name().contains("POTION")) {
+ final List values = Arrays.asList(materialData[1].split("-"));
+ final PotionType type;
+
+ if ((type = EnumUtil.getByName(values.get(0), PotionType.class)) == null) {
+ throw new IllegalArgumentException(
+ "'"
+ + values.get(0)
+ + "' is not a valid PotionType. Available: "
+ + EnumUtil.getNames(PotionType.class));
+ }
+
+ final PotionMeta meta = (PotionMeta) result.getItemMeta();
+ meta.setBasePotionData(
+ new PotionData(type, values.contains("extended"), values.contains("strong")));
+ result.setItemMeta(meta);
+ } else if (CompatUtil.isPre1_13() && material.name().equals("MONSTER_EGG")) {
+ final EntityType type;
+
+ if ((type = EnumUtil.getByName(materialData[1], EntityType.class)) == null) {
+ throw new IllegalArgumentException(
+ "'"
+ + materialData[0]
+ + "' is not a valid EntityType. Available: "
+ + EnumUtil.getNames(EntityType.class));
+ }
+
+ result = new SpawnEggs(type).toItemStack();
+ }
+ }
+
+ final OptionalLong value;
+
+ if ((value = NumberUtil.parseLong(materialData[1])).isPresent()) {
+ result.setDurability((short) value.getAsLong());
+ }
+ }
+
+ if (args.length < 2) {
+ return result;
+ }
+
+ result.setAmount(Integer.parseInt(args[1]));
+
+ if (args.length > 2) {
+ for (int i = 2; i < args.length; i++) {
+ final String argument = args[i];
+ final String[] pair = argument.split(":", 2);
+
+ if (pair.length < 2) {
+ continue;
+ }
+
+ applyMeta(result, pair[0], pair[1]);
+ }
+ }
+
+ return result;
+ }
+
+ public static ItemStack loadFromString(final String line, final Consumer errorHandler) {
+ ItemStack result;
+
+ try {
+ result = loadFromString(line);
+ } catch (Exception ex) {
+ result =
+ ItemBuilder.of(Material.REDSTONE_BLOCK)
+ .name("&4&m------------------")
+ .lore(
+ "&cThere was an error",
+ "&cwhile loading this",
+ "&citem, please contact",
+ "&can administrator.",
+ "&4&m------------------")
+ .build();
+ errorHandler.accept(ex.getMessage());
+ }
+
+ return result;
+ }
+
+ private static void applyMeta(final ItemStack item, final String key, final String value) {
+ final ItemMeta meta = item.getItemMeta();
+
+ if (key.equalsIgnoreCase("name")) {
+ meta.setDisplayName(StringUtil.color(value.replace("_", " ")));
+ item.setItemMeta(meta);
+ return;
+ }
+
+ if (key.equalsIgnoreCase("lore")) {
+ meta.setLore(
+ StringUtil.color(Lists.newArrayList(value.split("\\|")), line -> line.replace("_", " ")));
+ item.setItemMeta(meta);
+ return;
+ }
+
+ if (key.equalsIgnoreCase("unbreakable") && value.equalsIgnoreCase("true")) {
+ if (CompatUtil.isPre1_12()) {
+ // meta.spigot().setUnbreakable(true);
+ } else {
+ meta.setUnbreakable(true);
+ }
+
+ item.setItemMeta(meta);
+ return;
+ }
+
+ if (key.equalsIgnoreCase("flags")) {
+ final String[] flags = value.split(",");
+
+ for (final String flag : flags) {
+ final ItemFlag itemFlag = EnumUtil.getByName(flag, ItemFlag.class);
+
+ if (itemFlag == null) {
+ continue;
+ }
+
+ meta.addItemFlags(itemFlag);
+ }
+
+ item.setItemMeta(meta);
+ return;
+ }
+
+ final Enchantment enchantment = ENCHANTMENTS.get(key);
+
+ if (enchantment != null) {
+ item.addUnsafeEnchantment(enchantment, Integer.parseInt(value));
+ return;
+ }
+
+ if (item.getType().name().contains("POTION")) {
+ final PotionEffectType effectType = EFFECTS.get(key);
+
+ if (effectType != null) {
+ final String[] values = value.split(":");
+ final PotionMeta potionMeta = (PotionMeta) meta;
+ potionMeta.addCustomEffect(
+ new PotionEffect(effectType, Integer.parseInt(values[1]), Integer.parseInt(values[0])),
+ true);
+ item.setItemMeta(potionMeta);
+ return;
+ }
+ }
+
+ if (Items.equals(Items.HEAD, item)
+ && (key.equalsIgnoreCase("player")
+ || key.equalsIgnoreCase("owner")
+ || key.equalsIgnoreCase("texture"))) {
+ final SkullMeta skullMeta = (SkullMeta) meta;
+
+ // Since Base64 texture strings are much longer than usernames...
+ if (value.length() > 16) {
+ Skulls.setSkull(skullMeta, value);
+ } else {
+ skullMeta.setOwner(value);
+ }
+
+ item.setItemMeta(skullMeta);
+ }
+
+ if (item.getType().name().contains("LEATHER_") && key.equalsIgnoreCase("color")) {
+ final LeatherArmorMeta leatherArmorMeta = (LeatherArmorMeta) meta;
+ final String[] values = value.split(",");
+ leatherArmorMeta.setColor(
+ Color.fromRGB(
+ Integer.parseInt(values[0]),
+ Integer.parseInt(values[1]),
+ Integer.parseInt(values[2])));
+ item.setItemMeta(leatherArmorMeta);
+ }
+
+ if (key.equalsIgnoreCase("custommodeldata") && !CompatUtil.isPre1_14()) {
+ meta.setCustomModelData(Integer.parseInt(value));
+ item.setItemMeta(meta);
+ }
+ }
+
+ public static void copyNameLore(final ItemStack from, final ItemStack to) {
+ final ItemMeta fromMeta = from.getItemMeta(), toMeta = to.getItemMeta();
+
+ if (fromMeta.hasDisplayName()) {
+ toMeta.setDisplayName(fromMeta.getDisplayName());
+ }
+
+ if (fromMeta.hasLore()) {
+ toMeta.setLore(fromMeta.getLore());
+ }
+
+ to.setItemMeta(toMeta);
+ }
+
+ private ItemUtil() {}
+}
diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java
new file mode 100644
index 00000000..0b7e405d
--- /dev/null
+++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java
@@ -0,0 +1,129 @@
+package me.realized.tokenmanager.util.profile;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public final class NameFetcher {
+
+ private static final ScheduledExecutorService EXECUTOR_SERVICE =
+ Executors.newSingleThreadScheduledExecutor();
+ private static final String MOJANG_URL =
+ "https://sessionserver.mojang.com/session/minecraft/profile/";
+ private static final String MINETOOLS_URL = "https://api.minetools.eu/uuid/";
+ private static final JsonParser JSON_PARSER = new JsonParser();
+ private static final Cache UUID_TO_NAME =
+ CacheBuilder.newBuilder()
+ .concurrencyLevel(4)
+ .maximumSize(1000)
+ .expireAfterWrite(30, TimeUnit.MINUTES)
+ .build();
+
+ private NameFetcher() {}
+
+ static void getNames(final List uuids, final Consumer