public class VersionHandler {

    public static final int VERSION_1_11 = 1_11_00;
    public static final int VERSION_1_12 = 1_12_00;
    public static final int VERSION_1_13 = 1_13_00;
    public static final int VERSION_1_14 = 1_14_00;
    public static final int VERSION_1_15 = 1_15_00;
    public static final int VERSION_1_16 = 1_16_00;
    public static final int VERSION_1_17 = 1_17_00;
    public static final int VERSION_1_18 = 1_18_00;

    private static final Pattern pattern = Pattern.compile("(?<version>\\d\\.\\d{1,2}(\\.\\d)?)-.*");

    private static int versionCode = -1;

    public static boolean isNotSupportedVersion() {
        return getVersionCode() < VERSION_1_14 || getVersionCode() >= VERSION_1_18;
    }

    public static boolean isExperimentalSupport() {
        return getVersionCode() == VERSION_1_17;
    }

    public static boolean isLegacy() {
        return getVersionCode() < VERSION_1_13;
    }

    public static int getVersionCode() {
        if (versionCode == -1) {
            initVersionCode();
        }

        return versionCode;
    }

    private static void initVersionCode() {
        Matcher matcher = pattern.matcher(Bukkit.getBukkitVersion());
        if (matcher.find()) {
            String versionString = matcher.group("version");
            versionCode = Version.parseVersion(versionString).getVersionCode();
        } else {
            versionCode = 0;
        }
    }
} public class RPGInventoryItemsRegistry implements BukkitItemsRegistry {

    public static String ID = "rpginventory";

    @NotNull
    @Override
    public String getId() {
        return ID;
    }

    @Override
    public boolean isEnabled() {
        return RPGInventory.getInstance().isEnabled();
    }

    @NotNull
    @Override
    public Collection<String> getKnownIds() {
        return ItemManager.getItemList();
    }

    @Nullable
    @Override
    public ItemStack getItem(@NotNull String itemId, @Nullable Object payload, int amount) {
        ItemStack item = ItemManager.getItem(itemId);
        if (item.getType() != Material.AIR) {
            item.setAmount(amount);
            return item;
        } else {
            return null;
        }
    }

    @Nullable
    @Override
    public String getItemId(@NotNull ItemStack item) {
        CustomItem customItem = ItemManager.getCustomItem(item);
        if (customItem != null) {
            return customItem.getId();
        } else {
            return null;
        }
    }

    @Override
    public boolean isItemExists(@NotNull String itemId) {
        return ItemManager.hasItem(itemId);
    }
} public class MyPetManager implements Listener {
    private static final String MYPET_TAG = "mypet.uuid";

    public static boolean init(@NotNull RPGInventory plugin) {
        if (MyPetManager.getMyPetSlot() == null) {
            Log.s("MyPet found, but slot for MyPet not configured!");
            return false;
        }

        try {
            for (MyPetPlayer mpPlayer : MyPetApi.getPlayerManager().getMyPetPlayers()) {
                syncPlayer(mpPlayer);
            }

            plugin.getServer().getPluginManager().registerEvents(new MyPetManager(), plugin);
            return true;
        } catch (Exception e) {
            plugin.getReporter().report(e.getMessage(), e);
            return false;
        }
    }

    private static void syncPlayer(MyPetPlayer mpPlayer) {
        Slot petSlot = getMyPetSlot();
        if (mpPlayer.isOnline() && mpPlayer.hasMyPet()) {
            Player player = mpPlayer.getPlayer();

            if (!InventoryManager.playerIsLoaded(player) || petSlot == null) {
                return;
            }

            Inventory inventory = InventoryManager.get(player).getInventory();
            ItemStack currentPet = inventory.getItem(petSlot.getSlotId());
            if (isMyPetItem(currentPet)) {
                MyPet pet = mpPlayer.getMyPet();
                String petId = ItemUtils.getTag(currentPet, MYPET_TAG);
                if (petId.isEmpty()) {
                    return;
                }

                UUID petUuid = UUID.fromString(petId);
                if (petUuid.equals(pet.getUUID())) {
                    return;
                }
            } else {
                inventory.setItem(petSlot.getSlotId(), petSlot.getCup());
            }

            MyPetApi.getMyPetManager().deactivateMyPet(mpPlayer, false);
        }
    }

    @Nullable
    private static Slot getMyPetSlot() {
        for (Slot slot : SlotManager.instance().getSlots()) {
            if (slot.getSlotType() == Slot.SlotType.PET) {
                return slot;
            }
        }

        return null;
    }

    public static boolean validatePet(
            @NotNull Player player,
            InventoryAction action,
            @Nullable ItemStack currentItem,
            ItemStack cursor
    ) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        boolean isAllowedAction = actionType == ActionType.GET
                || action == InventoryAction.SWAP_WITH_CURSOR
                || actionType == ActionType.DROP;

        return !(ItemUtils.isNotEmpty(currentItem) && isAllowedAction)
                || swapMyPets(player, isMyPetItem(currentItem), cursor);
    }

    private static boolean swapMyPets(@NotNull final Player player, boolean hasPet, ItemStack newPet) {
        if (hasPet) {
            PetUnequipEvent event = new PetUnequipEvent(player);
            RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);

            deactivateMyPet(player);
        }

        if (!isMyPetItem(newPet)) {
            return true;
        }

        PetEquipEvent event = new PetEquipEvent(player, newPet);
        Bukkit.getPluginManager().callEvent(event);

        if (event.isCancelled()) {
            return false;
        }

        String petId = ItemUtils.getTag(newPet, MYPET_TAG);
        if (petId.isEmpty()) {
            return false;
        }

        final UUID petUUID = UUID.fromString(petId);
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                activateMyPet(player, petUUID);
            }
        }.runTaskLater(RPGInventory.getInstance(), 1);

        return true;
    }

    @Contract("null -> false")
    private static boolean isMyPetItem(@Nullable ItemStack item) {
        return ItemUtils.isNotEmpty(item) && ItemUtils.hasTag(item, MYPET_TAG);
    }

    private static void deactivateMyPet(@NotNull final Player player) {
        if (!MyPetApi.getPlayerManager().isMyPetPlayer(player)) {
            return;
        }

        final MyPetPlayer user = MyPetApi.getPlayerManager().getMyPetPlayer(player);

        if (user.hasMyPet()) {
            MyPetApi.getMyPetManager().deactivateMyPet(user, true);
            user.setMyPetForWorldGroup(WorldGroup.getGroupByWorld(player.getWorld().getName()).getName(), null);
            MyPetApi.getRepository().updateMyPetPlayer(user, null);
        }

    }

    private static void activateMyPet(@NotNull final Player player, UUID petUUID) {
        final MyPetPlayer user;
        if (MyPetApi.getPlayerManager().isMyPetPlayer(player)) {
            user = MyPetApi.getPlayerManager().getMyPetPlayer(player);
        } else {
            user = MyPetApi.getPlayerManager().createMyPetPlayer(player);
        }

        if (user.hasMyPet()) {
            MyPetApi.getMyPetManager().deactivateMyPet(user, true);
        }

        final WorldGroup wg = WorldGroup.getGroupByWorld(player.getWorld().getName());
        MyPetApi.getRepository().getMyPet(petUUID, new RepositoryCallback<StoredMyPet>() {
            @Override
            public void callback(@NotNull StoredMyPet storedMyPet) {
                if (!storedMyPet.getWorldGroup().equals(wg.getName())) {
                    PlayerUtils.sendMessage(player, "This pet doesn't belong into this world.");
                    return;
                }

                boolean savePet = storedMyPet.getOwner().getInternalUUID().equals(user.getInternalUUID());
                storedMyPet.setOwner(user);
                String worldName = player.getWorld().getName();
                user.setMyPetForWorldGroup(WorldGroup.getGroupByWorld(worldName).getName(), storedMyPet.getUUID());
                Optional<MyPet> pet = activateMyPetCompat(storedMyPet);
                if (pet.isPresent()) {
                    MyPet myPet = pet.get();
                    if (myPet.getStatus() != MyPet.PetState.Dead) {
                        pet.get().setStatus(MyPet.PetState.Here);
                    }
                }

                if (savePet) {
                    MyPetApi.getRepository().updateMyPet(storedMyPet, null);
                }

                MyPetApi.getRepository().updateMyPetPlayer(user, null);
            }
        });
    }

    @SuppressWarnings("unchecked")
    private static Optional<MyPet> activateMyPetCompat(StoredMyPet storedMyPet) {
        Object pet = MyPetApi.getMyPetManager().activateMyPet(storedMyPet);
        final Optional<MyPet> optionalPet;
        // MyPet switched to Java's Optional from Guava
        //noinspection ConstantConditions
        if (pet instanceof com.google.common.base.Optional) {
            optionalPet = Optional.ofNullable(((com.google.common.base.Optional<MyPet>) pet).orNull());
        } else {
            optionalPet = (Optional<MyPet>) pet;
        }
        return optionalPet;
    }

    @EventHandler
    public void onPlayerInventoryLoaded(@NotNull PlayerInventoryLoadEvent.Post event) {
        Player player = event.getPlayer();
        PlayerManager playerManager = MyPetApi.getPlayerManager();
        if (playerManager.isMyPetPlayer(player)) {
            syncPlayer(playerManager.getMyPetPlayer(player));
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onMyPetCreate(@NotNull MyPetCreateEvent event) {
        Player player = event.getOwner().getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        ItemStack petItem = new ItemStack(MaterialCompat.getMaterial("MONSTER_EGG"));
        ItemMeta meta = petItem.getItemMeta();
        meta.setDisplayName(RPGInventory.getLanguage().getMessage("mypet.egg", event.getMyPet().getPetName()));
        petItem.setItemMeta(meta);
        petItem = ItemUtils.setTag(petItem, MYPET_TAG, event.getMyPet().getUUID().toString());

        Inventory inventory = InventoryManager.get(player).getInventory();
        Slot petSlot = Objects.requireNonNull(getMyPetSlot(), "MyPet slot should be exist!");

        ItemStack currentPet = inventory.getItem(petSlot.getSlotId());
        boolean hasPet = currentPet != null && !petSlot.isCup(currentPet);
        inventory.setItem(petSlot.getSlotId(), petItem);

        if (hasPet) {
            player.getInventory().addItem(currentPet);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onMyPetCall(@NotNull MyPetCallEvent event) {
        Player player = event.getOwner().getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        Slot petSlot = Objects.requireNonNull(getMyPetSlot(), "MyPet slot should be exist!");
        ItemStack currentPet = inventory.getItem(petSlot.getSlotId());
        boolean keepPet = true;

        if (!isMyPetItem(currentPet)) {
            keepPet = false;
        } else {
            String petTag = ItemUtils.getTag(currentPet, MYPET_TAG);
            if (petTag.isEmpty()) {
                keepPet = false;
            } else {
                UUID petUUID = UUID.fromString(petTag);
                if (!petUUID.equals(event.getMyPet().getUUID())) {
                    keepPet = false;
                }
            }
        }

        if (!keepPet) {
            event.setCancelled(true);
            MyPetApi.getMyPetManager().deactivateMyPet(event.getOwner(), true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onMyPetRemove(@NotNull MyPetRemoveEvent event) {
        Player player = event.getOwner().getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        Slot petSlot = Objects.requireNonNull(getMyPetSlot(), "MyPet slot should be exist!");
        ItemStack currentPetItem = inventory.getItem(petSlot.getSlotId());

        if (isMyPetItem(currentPetItem)) {
            String petTag = ItemUtils.getTag(currentPetItem, MYPET_TAG);
            if (petTag.isEmpty()) {
                inventory.setItem(petSlot.getSlotId(), petSlot.getCup());
                return;
            }

            UUID petUUID = UUID.fromString(petTag);
            if (petUUID.equals(event.getMyPet().getUUID())) {
                inventory.setItem(petSlot.getSlotId(), petSlot.getCup());
            }
        }
    }

    @EventHandler
    public void onMyPetItemUse(@NotNull PlayerInteractEvent event) {
        if (event.getItem() != null) {
            Player player = event.getPlayer();

            if (!InventoryManager.playerIsLoaded(player)) {
                return;
            }

            Inventory inventory = InventoryManager.get(player).getInventory();

            if (isMyPetItem(event.getItem())
                    && (event.getAction() == Action.RIGHT_CLICK_BLOCK
                    || event.getAction() == Action.RIGHT_CLICK_AIR)) {
                Slot petSlot = Objects.requireNonNull(getMyPetSlot(), "MyPet slot should be exist!");
                ItemStack currentPet = inventory.getItem(petSlot.getSlotId());
                boolean hasPet = !petSlot.isCup(currentPet);
                ItemStack newPet = event.getItem();

                if (!hasPet) {
                    currentPet = null;
                }

                player.getEquipment().setItemInMainHand(currentPet);
                inventory.setItem(petSlot.getSlotId(), newPet);

                swapMyPets(player, hasPet, newPet);
            }
        }
    }
} public class InventoryLocker {

    private static ItemStack LOCKED_SLOT = null;
    private static ItemStack BUYABLE_SLOT = null;

    private static final String TAG = "locked";

    private InventoryLocker() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        if (!isEnabled()) {
            Log.i("Inventory lock system is disabled in config");
            return false;
        }

        try {
            InventoryLocker.LOCKED_SLOT = initSlotItem("locked");
            InventoryLocker.BUYABLE_SLOT = initSlotItem("buyable");
            if (ItemUtils.isEmpty(InventoryLocker.LOCKED_SLOT) || ItemUtils.isEmpty(InventoryLocker.BUYABLE_SLOT)) {
                return false;
            }
        } catch (Exception e) {
            instance.getReporter().report("Error on InventoryLocker initialization", e);
            return false;
        }

        instance.getServer().getPluginManager().registerEvents(new LockerListener(), instance);
        return true;
    }

    private static ItemStack initSlotItem(String slotId) {
        Texture texture = Texture.parseTexture(Config.getConfig().getString("slots." + slotId));
        ItemStack slotItem = texture.getItemStack();
        if (ItemUtils.isEmpty(slotItem)) {
            Log.s("Texture specified in ''slots.{0}'' must be valid and must not be AIR", slotId);
            return slotItem;
        }

        ItemMeta meta = slotItem.getItemMeta();
        if (meta != null) {
            meta.setDisplayName(RPGInventory.getLanguage().getMessage(slotId + ".name"));
            meta.setLore(Collections.singletonList(RPGInventory.getLanguage().getMessage(slotId + ".lore")));
            slotItem.setItemMeta(meta);
        }

        return addTag(slotItem);
    }

    private static boolean isEnabled() {
        return Config.getConfig().getBoolean("slots.enabled");
    }

    public static boolean buySlot(@NotNull Player player, int line) {
        final FileConfiguration config = Config.getConfig();
        if (config.getBoolean("slots.money.enabled")) {
            if (!RPGInventory.economyConnected()) {
                return false;
            }
            final EconomyResponse economyResponse = RPGInventory.getEconomy().withdrawPlayer(
                    player, config.getDouble("slots.money.cost.line" + line)
            );
            if (!economyResponse.transactionSuccess()) {
                return false;
            }
        }
        if (config.getBoolean("slots.level.enabled") && config.getBoolean("slots.level.spend")) {
            BukkitLevelSystem levelSystem = RPGInventory.getLevelSystem(player);
            int requiredLevels = config.getInt("slots.level.required.line" + line);
            if (levelSystem.didReachLevel(requiredLevels)) {
                levelSystem.takeLevels(requiredLevels);
            } else {
                return false;
            }
        }
        return true;
    }

    @Contract(pure = true)
    public static int getLine(int slot) {
        return (slot - 9) / 9 + 1;
    }

    @NotNull
    public static ItemStack getBuyableSlotForLine(int line) {
        ItemStack slot = InventoryLocker.BUYABLE_SLOT.clone();
        ItemMeta meta = slot.getItemMeta();
        if (meta != null) {
            List<String> lore = meta.getLore();
            if (lore == null) {
                lore = new ArrayList<>();
            }
            FileLanguage lang = RPGInventory.getLanguage();
            final FileConfiguration config = Config.getConfig();
            if (config.getBoolean("slots.money.enabled")) {
                lore.add(lang.getMessage("buyable.money", StringUtils.doubleToString(config.getDouble("slots.money.cost.line" + line))));
            }

            if (config.getBoolean("slots.level.enabled")) {
                lore.add(lang.getMessage("buyable.level", config.getInt("slots.level.required.line" + line)));
            }
            meta.setLore(lore);
            slot.setItemMeta(meta);
        }

        return addTag(slot);
    }

    @NotNull
    private static ItemStack addTag(ItemStack item) {
        return ItemUtils.setTag(item, TAG, "0");
    }

    public static boolean isLockedSlot(@Nullable ItemStack item) {
        return InventoryLocker.isEnabled() && ItemUtils.isNotEmpty(item) && ItemUtils.hasTag(item, TAG);
    }

    public static boolean isBuyableSlot(ItemStack currentItem, int line) {
        return InventoryLocker.getBuyableSlotForLine(line).equals(currentItem);
    }

    public static void lockSlots(@NotNull Player player) {
        InventoryLocker.lockSlots(player, false);
    }

    public static void lockSlots(@NotNull Player player, boolean force) {
        if (!force && player.getGameMode() == GameMode.CREATIVE) {
            return;
        }

        if (InventoryLocker.isEnabled()) {
            int maxSlot = getSlots(player) + 8;
            for (int i = 35; i > maxSlot; i--) {
                player.getInventory().setItem(i, InventoryLocker.LOCKED_SLOT);
            }

            if (maxSlot < 35) {
                player.getInventory().setItem(maxSlot + 1, getBuyableSlotForLine(getLine(maxSlot + 1)));
            }
        }

        InventoryManager.lockQuickSlots(player);
        InventoryManager.lockEmptySlots(player);
    }

    public static void unlockSlots(@NotNull Player player) {
        if (InventoryLocker.isEnabled()) {
            for (int i = 8 + getSlots(player); i < 36; i++) {
                ItemStack itemStack = player.getInventory().getItem(i);
                if (InventoryLocker.isLockedSlot(itemStack)) {
                    player.getInventory().setItem(i, null);
                }
            }
        }

        InventoryManager.unlockQuickSlots(player);
        InventoryManager.unlockEmptySlots(player);
    }

    public static boolean canBuySlot(@NotNull Player player, int line) {
        final FileConfiguration config = Config.getConfig();
        if (config.getBoolean("slots.money.enabled")) {
            double cost = config.getDouble("slots.money.cost.line" + line);

            if (!PlayerUtils.checkMoney(player, cost)) {
                return false;
            }
        }

        if (config.getBoolean("slots.level.enabled")) {
            int requirement = config.getInt("slots.level.required.line" + line);

            if (!PlayerUtils.checkLevel(player, requirement)) {
                PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.level", requirement));
                return false;
            }
        }

        return true;
    }

    private static int getSlots(OfflinePlayer player) {
        int slots = Config.getConfig().getInt("slots.free") + InventoryManager.get(player).getBuyedGenericSlots();
        return Math.min(slots, 27);
    }
} public class InventoryManager {
    static final String TITLE = RPGInventory.getLanguage().getMessage("title");
    private static final Map<UUID, PlayerWrapper> INVENTORIES = new HashMap<>();

    private static ItemStack FILL_SLOT = null;
    private static Reporter reporter;

    private InventoryManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        reporter = instance.getReporter();

        try {
            Texture texture = Texture.parseTexture(Config.getConfig().getString("fill"));
            InventoryManager.FILL_SLOT = texture.getItemStack();
            ItemMeta meta = InventoryManager.FILL_SLOT.getItemMeta();
            if (meta != null) {
                meta.setDisplayName(" ");
                InventoryManager.FILL_SLOT.setItemMeta(meta);
            }
        } catch (Exception e) {
            reporter.report("Error on InventoryManager initialization", e);
            return false;
        }

        // Register events
        instance.getServer().getPluginManager().registerEvents(new InventoryListener(), instance);
        return true;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean validateUpdate(Player player, ActionType actionType, @NotNull Slot slot, ItemStack item) {
        return actionType == ActionType.GET || actionType == ActionType.DROP
                || actionType == ActionType.SET && slot.isValidItem(item)
                && ItemManager.allowedForPlayer(player, item, true);
    }

    @NotNull
    public static ItemStack getFillSlot() {
        return InventoryManager.FILL_SLOT;
    }

    public static boolean validatePet(Player player, InventoryAction action, @Nullable ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);

        if (ItemUtils.isNotEmpty(currentItem)
                && (actionType == ActionType.GET || action == InventoryAction.SWAP_WITH_CURSOR || actionType == ActionType.DROP)
                && PetManager.getCooldown(currentItem) > 0) {
            return false;
        }

        if (actionType == ActionType.SET) {
            if (PetType.isPetItem(cursor) && ItemManager.allowedForPlayer(player, cursor, true)) {
                PetEquipEvent event = new PetEquipEvent(player, cursor);
                RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);

                if (event.isCancelled()) {
                    return false;
                }

                PetManager.respawnPet(event.getPlayer(), event.getPetItem());
                return true;
            }
        } else if (actionType == ActionType.GET || actionType == ActionType.DROP) {
            PetUnequipEvent event = new PetUnequipEvent(player);
            RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);
            PetManager.despawnPet(event.getPlayer());
            return true;
        }

        return false;
    }

    public static boolean validateArmor(Player player, InventoryAction action, @NotNull Slot slot, ItemStack item) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        return actionType != ActionType.OTHER && (actionType != ActionType.SET || slot.isValidItem(item))
                && ItemManager.allowedForPlayer(player, item, true);
    }

    public static void updateShieldSlot(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId,
                                        InventoryType.SlotType slotType, InventoryAction action,
                                        ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        if (actionType == ActionType.GET) {
            if (slot.isCup(currentItem)) {
                return;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), slot.getCup());
            } else {
                player.getEquipment().setItemInOffHand(new ItemStack(Material.AIR));
            }
        } else if (actionType == ActionType.SET) {
            if (slot.isCup(currentItem)) {
                currentItem = null;
                action = InventoryAction.PLACE_ALL;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), cursor);
            } else {
                player.getEquipment().setItemInOffHand(cursor);
            }
        }

        InventoryManager.updateInventory(player, inventory, slotId, slotType, action, currentItem, cursor);
    }

    public static void updateQuickSlot(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId,
                                       InventoryType.SlotType slotType, InventoryAction action,
                                       ItemStack currentItem, ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        if (actionType == ActionType.GET) {
            if (slot.isCup(currentItem)) {
                return;
            }

            if (player.getInventory().getHeldItemSlot() == slot.getQuickSlot()) {
                InventoryUtils.heldFreeSlot(player, slot.getQuickSlot(), InventoryUtils.SearchType.NEXT);
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), slot.getCup());
            } else {
                player.getInventory().setItem(slot.getQuickSlot(), slot.getCup());
            }

            action = InventoryAction.SWAP_WITH_CURSOR;
            cursor = slot.getCup();
        } else if (actionType == ActionType.SET) {
            if (slot.isCup(currentItem)) {
                currentItem = null;
                action = InventoryAction.PLACE_ALL;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), cursor);
            } else {
                player.getInventory().setItem(slot.getQuickSlot(), cursor);
            }
        }

        InventoryManager.updateInventory(player, inventory, slotId, slotType, action, currentItem, cursor);
    }

    public static void updateArmor(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId, InventoryAction action, ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);

        EntityEquipment equipment = player.getEquipment();
        assert equipment != null;

        // Equip armor
        if (actionType == ActionType.SET || action == InventoryAction.UNKNOWN) {
            switch (slot.getName()) {
                case "helmet":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setHelmet(inventory.getItem(slotId));
                    break;
                case "chestplate":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setChestplate(inventory.getItem(slotId));
                    break;
                case "leggings":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setLeggings(inventory.getItem(slotId));
                    break;
                case "boots":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setBoots(inventory.getItem(slotId));
                    break;
            }
        } else if (actionType == ActionType.GET || actionType == ActionType.DROP) { // Unequip armor
            InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);

            switch (slot.getName()) {
                case "helmet":
                    equipment.setHelmet(null);
                    break;
                case "chestplate":
                    equipment.setChestplate(null);
                    break;
                case "leggings":
                    equipment.setLeggings(null);
                    break;
                case "boots":
                    equipment.setBoots(null);
                    break;
            }
        }
    }

    public static void syncArmor(PlayerWrapper playerWrapper) {
        Player player = (Player) playerWrapper.getPlayer();
        Inventory inventory = playerWrapper.getInventory();
        SlotManager sm = SlotManager.instance();
        EntityEquipment equipment = player.getEquipment();
        assert equipment != null;

        if (ArmorType.HELMET.getSlot() != -1) {
            ItemStack helmet = equipment.getHelmet();
            Slot helmetSlot = sm.getSlot(ArmorType.HELMET.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.HELMET.getSlot(), (ItemUtils.isEmpty(helmet))
                    && helmetSlot != null ? helmetSlot.getCup() : helmet);
        }

        if (ArmorType.CHESTPLATE.getSlot() != -1) {
            ItemStack savedChestplate = playerWrapper.getSavedChestplate();
            ItemStack chestplate = savedChestplate == null ? equipment.getChestplate() : savedChestplate;
            Slot chestplateSlot = sm.getSlot(ArmorType.CHESTPLATE.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.CHESTPLATE.getSlot(), (ItemUtils.isEmpty(chestplate))
                    && chestplateSlot != null ? chestplateSlot.getCup() : chestplate);
        }

        if (ArmorType.LEGGINGS.getSlot() != -1) {
            ItemStack leggings = equipment.getLeggings();
            Slot leggingsSlot = sm.getSlot(ArmorType.LEGGINGS.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.LEGGINGS.getSlot(), (ItemUtils.isEmpty(leggings))
                    && leggingsSlot != null ? leggingsSlot.getCup() : leggings);
        }

        if (ArmorType.BOOTS.getSlot() != -1) {
            ItemStack boots = equipment.getBoots();
            Slot bootsSlot = sm.getSlot(ArmorType.BOOTS.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.BOOTS.getSlot(), (ItemUtils.isEmpty(boots))
                    && bootsSlot != null ? bootsSlot.getCup() : boots);
        }
    }

    public static void syncQuickSlots(PlayerWrapper playerWrapper) {
        Player player = (Player) playerWrapper.getPlayer();
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            playerWrapper.getInventory().setItem(quickSlot.getSlotId(), player.getInventory().getItem(quickSlot.getQuickSlot()));
        }
    }

    public static void syncInfoSlots(PlayerWrapper playerWrapper) {
        final Player player = (Player) playerWrapper.getPlayer();
        for (Slot infoSlot : SlotManager.instance().getInfoSlots()) {
            ItemStack cup = infoSlot.getCup();
            if (ItemUtils.isEmpty(cup)) {
                continue;
            }

            ItemMeta meta = cup.getItemMeta();
            if (meta != null) {
                List<String> lore = meta.getLore();
                if (lore == null) {
                    lore = new ArrayList<>();
                }

                for (int i = 0; i < lore.size(); i++) {
                    String line = lore.get(i);
                    lore.set(i, StringUtils.setPlaceholders(player, line));
                }

                meta.setLore(lore);
                cup.setItemMeta(meta);
            }
            playerWrapper.getInventory().setItem(infoSlot.getSlotId(), cup);
        }

        player.updateInventory();
    }

    public static void syncShieldSlot(@NotNull PlayerWrapper playerWrapper) {
        Slot slot = SlotManager.instance().getShieldSlot();
        if (slot == null) {
            return;
        }

        Player player = (Player) playerWrapper.getPlayer();
        ItemStack itemInHand = player.getEquipment().getItemInOffHand();
        playerWrapper.getInventory().setItem(slot.getSlotId(), ItemUtils.isEmpty(itemInHand) ? slot.getCup() : itemInHand);
    }

    private static void updateInventory(
            @NotNull Player player,
            @NotNull Inventory inventory,
            int slot,
            InventoryAction action,
            ItemStack currentItem,
            @NotNull ItemStack cursor
    ) {
        InventoryManager.updateInventory(player, inventory, slot, InventoryType.SlotType.CONTAINER, action, currentItem, cursor);
    }

    private static void updateInventory(
            @NotNull Player player,
            @NotNull Inventory inventory,
            int slot,
            InventoryType.SlotType slotType,
            InventoryAction action,
            ItemStack currentItem,
            @NotNull ItemStack cursorItem
    ) {
        if (ActionType.getTypeOfAction(action) == ActionType.DROP) {
            return;
        }

        if (action == InventoryAction.PLACE_ALL) {
            if (ItemUtils.isEmpty(currentItem)) {
                currentItem = cursorItem.clone();
            } else {
                currentItem.setAmount(currentItem.getAmount() + cursorItem.getAmount());
            }

            cursorItem = null;
        } else if (action == InventoryAction.PLACE_ONE) {
            if (ItemUtils.isEmpty(currentItem)) {
                currentItem = cursorItem.clone();
                currentItem.setAmount(1);
                cursorItem.setAmount(cursorItem.getAmount() - 1);
            } else if (currentItem.getMaxStackSize() < currentItem.getAmount() + 1) {
                currentItem.setAmount(currentItem.getAmount() + 1);
                cursorItem.setAmount(cursorItem.getAmount() - 1);
            }
        } else if (action == InventoryAction.PLACE_SOME) {
            cursorItem.setAmount(currentItem.getMaxStackSize() - currentItem.getAmount());
            currentItem.setAmount(currentItem.getMaxStackSize());
        } else if (action == InventoryAction.SWAP_WITH_CURSOR) {
            ItemStack tempItem = cursorItem.clone();
            cursorItem = currentItem.clone();
            currentItem = tempItem;
        } else if (action == InventoryAction.PICKUP_ALL) {
            cursorItem = currentItem.clone();
            currentItem = null;
        } else if (action == InventoryAction.PICKUP_HALF) {
            ItemStack item = currentItem.clone();
            if (currentItem.getAmount() % 2 == 0) {
                item.setAmount(item.getAmount() / 2);
                currentItem = item.clone();
                cursorItem = item.clone();
            } else {
                currentItem = item.clone();
                currentItem.setAmount(item.getAmount() / 2);
                cursorItem = item.clone();
                cursorItem.setAmount(item.getAmount() / 2 + 1);
            }
        } else if (action == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
            player.getInventory().addItem(currentItem);
            currentItem = null;
        }

        if (slotType == InventoryType.SlotType.QUICKBAR) {
            if (slot < 9) { // Exclude shield
                player.getInventory().setItem(slot, currentItem);
            }
        } else {
            inventory.setItem(slot, currentItem);
        }

        player.setItemOnCursor(cursorItem);
        player.updateInventory();
    }

    static void lockEmptySlots(Player player) {
        lockEmptySlots(INVENTORIES.get(player.getUniqueId()).getInventory());
    }

    public static void lockEmptySlots(Inventory inventory) {
        for (int i = 0; i < inventory.getSize(); i++) {
            Slot slot = SlotManager.instance().getSlot(i, InventoryType.SlotType.CONTAINER);
            if (slot == null) {
                inventory.setItem(i, FILL_SLOT);
            } else if (ItemUtils.isEmpty(inventory.getItem(i))) {
                inventory.setItem(i, slot.getCup());
            }
        }
    }

    static void unlockEmptySlots(Player player) {
        Inventory inventory = INVENTORIES.get(player.getUniqueId()).getInventory();

        for (int i = 0; i < inventory.getSize(); i++) {
            Slot slot = SlotManager.instance().getSlot(i, InventoryType.SlotType.CONTAINER);
            if (slot == null || slot.isCup(inventory.getItem(i))) {
                inventory.setItem(i, null);
            }
        }
    }

    public static boolean isQuickSlot(int slot) {
        return getQuickSlot(slot) != null;
    }

    @Nullable
    public static Slot getQuickSlot(int slot) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            if (slot == quickSlot.getQuickSlot()) {
                return quickSlot;
            }
        }

        return null;
    }

    static void lockQuickSlots(@NotNull Player player) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            int slotId = quickSlot.getQuickSlot();

            if (ItemUtils.isEmpty(player.getInventory().getItem(slotId))) {
                player.getInventory().setItem(slotId, quickSlot.getCup());
            }

            if (player.getInventory().getHeldItemSlot() == slotId) {
                if (quickSlot.isCup(player.getInventory().getItem(slotId))) {
                    InventoryUtils.heldFreeSlot(player, slotId, InventoryUtils.SearchType.NEXT);
                }
            }
        }
    }

    static void unlockQuickSlots(@NotNull Player player) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            int slotId = quickSlot.getQuickSlot();
            if (quickSlot.isCup(player.getInventory().getItem(slotId))) {
                player.getInventory().setItem(slotId, null);
            }
        }
    }

    public static boolean isNewPlayer(@NotNull Player player) {
        Path dataFolder = RPGInventory.getInstance().getDataFolder().toPath();
        return Files.notExists(dataFolder.resolve("inventories/" + player.getUniqueId() + ".inv"));
    }

    public static void loadPlayerInventory(Player player) {
        if (!InventoryManager.isAllowedWorld(player.getWorld())) {
            InventoryManager.INVENTORIES.remove(player.getUniqueId());
            return;
        }

        try {
            Path dataFolder = RPGInventory.getInstance().getDataPath();
            Path folder = dataFolder.resolve("inventories");
            Files.createDirectories(folder);

            // Load inventory from file
            Path file = folder.resolve(player.getUniqueId() + ".inv");

            PlayerWrapper playerWrapper = null;
            if (Files.exists(file)) {
                playerWrapper = Serialization.loadPlayerOrNull(player, file);
                if (playerWrapper == null) {
                    Log.s("Error on loading {0}''s inventory.", player.getName());
                    Log.s("Will be created new inventory. Old file was renamed.");
                }
            }

            if (playerWrapper == null) {
                playerWrapper = new PlayerWrapper(player);
                playerWrapper.setBuyedSlots(0);
            }

            PlayerInventoryLoadEvent.Pre event = new PlayerInventoryLoadEvent.Pre(player);
            RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);

            if (event.isCancelled()) {
                return;
            }

            InventoryManager.INVENTORIES.put(player.getUniqueId(), playerWrapper);

            InventoryLocker.lockSlots(player);
            PetManager.initPlayer(player);

            RPGInventory.getInstance().getServer().getPluginManager().callEvent(new PlayerInventoryLoadEvent.Post(player));
        } catch (IOException e) {
            reporter.report("Error on inventory load", e);
        }
    }

    public static void unloadPlayerInventory(@NotNull Player player) {
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        InventoryManager.INVENTORIES.get(player.getUniqueId()).onUnload();
        savePlayerInventory(player);
        InventoryLocker.unlockSlots(player);

        InventoryManager.INVENTORIES.remove(player.getUniqueId());

        RPGInventory.getInstance().getServer().getPluginManager().callEvent(new PlayerInventoryUnloadEvent.Post(player));
    }

    public static void savePlayerInventory(@NotNull Player player) {
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        PlayerWrapper playerWrapper = InventoryManager.INVENTORIES.get(player.getUniqueId());
        try {
            Path dataFolder = RPGInventory.getInstance().getDataPath();
            Path folder = dataFolder.resolve("inventories");
            Files.createDirectories(folder);

            Path file = folder.resolve(player.getUniqueId() + ".inv");
            Files.deleteIfExists(file);

            Serialization.save(playerWrapper.createSnapshot(), file);
        } catch (IOException | NullPointerException e) {
            Log.w(e, "Error on inventory save");
        }
    }

    @NotNull
    public static PlayerWrapper get(@NotNull OfflinePlayer player) {
        PlayerWrapper playerWrapper = InventoryManager.INVENTORIES.get(player.getUniqueId());
        if (playerWrapper == null) {
            throw new IllegalStateException(player.getName() + "'s inventory should be initialized!");
        }

        return playerWrapper;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean isQuickEmptySlot(@Nullable ItemStack item) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            if (quickSlot.isCup(item)) {
                return true;
            }
        }

        return false;
    }

    public static boolean isFilledSlot(@Nullable ItemStack item) {
        return InventoryManager.FILL_SLOT.equals(item);
    }

    public static boolean isEmptySlot(@Nullable ItemStack item) {
        for (Slot slot : SlotManager.instance().getSlots()) {
            if (slot.isCup(item)) {
                return true;
            }
        }

        return false;
    }

    @Contract("null -> false")
    public static boolean playerIsLoaded(@Nullable AnimalTamer player) {
        return player != null && InventoryManager.INVENTORIES.containsKey(player.getUniqueId());
    }

    public static boolean isAllowedWorld(@NotNull World world) {
        List<String> list = Config.getConfig().getStringList("worlds.list");

        ListType listType = SafeEnums.valueOf(ListType.class, Config.getConfig().getString("worlds.mode"), "list type");
        if (listType != null) {
            switch (listType) {
                case BLACKLIST:
                    return !list.contains(world.getName());
                case WHITELIST:
                    return list.contains(world.getName());
            }
        }

        return true;
    }

    public static boolean buySlot(@NotNull Player player, PlayerWrapper playerWrapper, Slot slot) {
        double cost = slot.getCost();

        if (!playerWrapper.isPreparedToBuy(slot)) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.buyable", slot.getCost()));
            playerWrapper.prepareToBuy(slot);
            return false;
        }

        if (!PlayerUtils.checkMoney(player, cost) || !RPGInventory.getEconomy().withdrawPlayer(player, cost).transactionSuccess()) {
            return false;
        }

        playerWrapper.setBuyedSlots(slot.getName());
        PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("message.buyed"));

        return true;
    }

    public static void initPlayer(@NotNull final Player player, boolean skipJoinMessage) {
        ResourcePackModule rpModule = RPGInventory.getResourcePackModule();
        if (rpModule != null) {
            rpModule.loadResourcePack(player, skipJoinMessage);
        } else {
            if (!skipJoinMessage) {
                EffectUtils.showDefaultJoinMessage(player);
            }
            InventoryManager.loadPlayerInventory(player);
        }

        if (RPGInventory.getPermissions().has(player, "rpginventory.admin")) {
            RPGInventory.getInstance().checkUpdates(player);
        }
    }

    private enum ListType {
        BLACKLIST, WHITELIST
    }
} public class BackpackManager {

    private static final String CONFIG_NAME = "backpacks.yml";

    private static final HashMap<String, BackpackType> BACKPACK_TYPES = new HashMap<>();
    private static final HashMap<UUID, Backpack> BACKPACKS = new HashMap<>();
    private static int BACKPACK_LIMIT;

    private static Reporter reporter;

    public static boolean init(@NotNull RPGInventory instance) {
        if (!isEnabled()) {
            Log.i("Slot for backpacks not found");
            return false;
        }

        reporter = instance.getReporter();

        try {
            Path petsFile = RPGInventory.getInstance().getDataPath().resolve(CONFIG_NAME);
            if (Files.notExists(petsFile)) {
                RPGInventory.getInstance().saveResource(CONFIG_NAME, false);
            }

            FileConfiguration petsConfig = YamlConfiguration.loadConfiguration(petsFile.toFile());

            @Nullable final ConfigurationSection backpacks = petsConfig.getConfigurationSection("backpacks");
            if (backpacks == null) {
                Log.s("Section ''backpacks'' not found in {0}", CONFIG_NAME);
                return false;
            }

            BACKPACK_TYPES.clear();
            for (String key : backpacks.getKeys(false)) {
                ConfigurationSection section = backpacks.getConfigurationSection(key);
                if (section != null) {
                    tryToAddBackpack(key, section);
                }
            }

            if (BACKPACK_TYPES.isEmpty()) {
                Log.i("No one backpack type found");
                return false;
            }

            BACKPACKS.clear();
            BackpackManager.loadBackpacks();

            Log.i("{0} backpack type(s) has been loaded", BACKPACK_TYPES.size());
            Log.i("{0} backpack(s) has been loaded", BACKPACKS.size());

            BACKPACK_LIMIT = Config.getConfig().getInt("backpacks.limit", 0);

            // Register events
            instance.getServer().getPluginManager().registerEvents(new BackpackListener(), instance);
            return true;
        } catch (Exception e) {
            reporter.report("Error on BackpackManager initialization", e);
            return false;
        }
    }

    private static void tryToAddBackpack(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("item"));
            if (texture.isEmpty()) {
                Log.s("Backpack ''{0}'' has not been added because its item is not valid.", name);
                return;
            }
            BackpackType backpackType = new BackpackType(texture, config);
            BACKPACK_TYPES.put(name, backpackType);
        } catch (Exception e) {
            Log.s("Backpack ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    private static boolean isEnabled() {
        return SlotManager.instance().getBackpackSlot() != null;
    }

    @NotNull
    public static List<String> getBackpackList() {
        return new ArrayList<>(BACKPACK_TYPES.keySet());
    }

    @NotNull
    public static ItemStack getItem(String id) {
        BackpackType backpackType = BACKPACK_TYPES.get(id);
        return backpackType == null ? new ItemStack(Material.AIR) : backpackType.getItem();
    }

    @Contract("_, null -> false")
    public static boolean open(@NotNull Player player, @Nullable ItemStack bpItem) {
        if (ItemUtils.isEmpty(bpItem)) {
            return false;
        }

        BackpackType type;
        String bpId = ItemUtils.getTag(bpItem, ItemUtils.BACKPACK_TAG);
        if (bpId.isEmpty() || (type = BackpackManager.getBackpackType(bpId)) == null) {
            return false;
        }

        Backpack backpack;
        String bpUid = ItemUtils.getTag(bpItem, ItemUtils.BACKPACK_UID_TAG);
        UUID uuid = bpUid.isEmpty() ? null : UUID.fromString(bpUid);
        if (!BACKPACKS.containsKey(uuid)) {
            if (uuid == null) {
                backpack = type.createBackpack();
                ItemUtils.setTag(bpItem, ItemUtils.BACKPACK_UID_TAG, backpack.getUniqueId().toString());
            } else {
                backpack = type.createBackpack(uuid);
            }

            BACKPACKS.put(backpack.getUniqueId(), backpack);
        } else {
            backpack = BACKPACKS.get(uuid);
        }

        backpack.open(player);
        return true;
    }

    @Nullable
    public static BackpackType getBackpackType(String bpId) {
        return BACKPACK_TYPES.get(bpId);
    }

    public static void saveBackpacks() {
        Path folder = RPGInventory.getInstance().getDataPath().resolve("backpacks");

        try {
            Files.createDirectories(folder);
            for (Map.Entry<UUID, Backpack> entry : BACKPACKS.entrySet()) {
                Path bpFile = folder.resolve(entry.getKey().toString() + ".bp");
                Serialization.save(entry.getValue(), bpFile);
            }
        } catch (IOException | NullPointerException e) {
            Log.w(e, "Error on backpack save");
        }
    }

    private static void loadBackpacks() {
        try {
            Path folder = RPGInventory.getInstance().getDataPath().resolve("backpacks");
            Files.createDirectories(folder);

            Files.list(folder)
                    .filter((file) -> Files.isRegularFile(file) && file.toString().endsWith(".bp"))
                    .forEach(BackpackManager::tryToLoadBackpack);
        } catch (IOException e) {
            Log.w(e, "Error on backpack loading");
        }
    }

    private static void tryToLoadBackpack(@NotNull Path path) {
        try {
            loadBackpack(path);
        } catch (IOException | InvalidConfigurationException e) {
            Log.w(e);
            FileUtils.resolveException(path);
            Log.s("Error on loading backpack {0}", path.getFileName().toString());
            Log.s("Will be created new backpack. Old file was renamed.");
        }
    }

    private static void loadBackpack(@NotNull Path path) throws IOException, InvalidConfigurationException {
        Backpack backpack = Serialization.loadBackpack(path);
        if (backpack == null || backpack.isOverdue()) {
            Files.delete(path);
        } else {
            BACKPACKS.put(backpack.getUniqueId(), backpack);
        }
    }

    @Contract("null -> false")
    public static boolean isBackpack(ItemStack item) {
        return ItemUtils.isNotEmpty(item) && ItemUtils.hasTag(item, ItemUtils.BACKPACK_TAG);
    }

    public static boolean backpackLimitReached(@NotNull Player player) {
        if (BACKPACK_LIMIT == 0) {
            return false;
        }

        // Check vanilla inventory
        Inventory inventory = player.getInventory();

        int count = 0;
        for (ItemStack item : inventory.getContents()) {
            if (isBackpack(item)) {
                count++;
            }
        }

        // Check RPGInventory slots
        inventory = InventoryManager.get(player).getInventory();
        Slot backpackSlot = SlotManager.instance().getBackpackSlot();
        if (backpackSlot != null
                && BackpackManager.isBackpack(inventory.getItem(backpackSlot.getSlotId()))
                && !backpackSlot.isQuick()) {
            count++;
        }

        return count >= BACKPACK_LIMIT;
    }

    public static int getLimit() {
        return BACKPACK_LIMIT;
    }
} public class CraftManager {
    @NotNull
    private static final List<CraftExtension> EXTENSIONS = new ArrayList<>();
    private static Texture textureOfExtendable;

    private CraftManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        MemorySection config = (MemorySection) Config.getConfig().get("craft");

        if (config == null) {
            Log.w("Section ''craft'' not found in config.yml");
            return false;
        }

        if (!config.getBoolean("enabled")) {
            Log.i("Craft system is disabled in config");
            return false;
        }

        try {
            Texture texture = Texture.parseTexture(config.getString("extendable"));
            if (texture.isEmpty()) {
                Log.s("Invalid texture in ''craft.extendable''");
                return false;
            }
            textureOfExtendable = texture;

            @Nullable final ConfigurationSection extensions = config.getConfigurationSection("extensions");
            if (extensions == null) {
                Log.s("Section ''craft.extensions'' not found in config.yml");
                return false;
            }

            EXTENSIONS.clear();
            for (String extensionName : extensions.getKeys(false)) {
                EXTENSIONS.add(new CraftExtension(extensionName, extensions.getConfigurationSection(extensionName)));
            }

            // Register listeners
            ProtocolLibrary.getProtocolManager().addPacketListener(new CraftListener(instance));

            // Disable recipe book if need
            if (VersionHandler.getVersionCode() >= VersionHandler.VERSION_1_12) {
                disableRecipeBook();
            }

            return true;
        } catch (Exception e) {
            instance.getReporter().report("Error on CraftManager initialization", e);
            return false;
        }
    }

    private static void disableRecipeBook() {
        ProtocolLibrary.getProtocolManager().addPacketListener(
                new PacketAdapter(RPGInventory.getInstance(), PacketType.Play.Server.RECIPES) {
                    @Override
                    public void onPacketSending(@NotNull PacketEvent event) {
                        event.setCancelled(true);
                    }
                });
        Log.i("Recipe book conflicts with craft extensions and was disabled");
    }

    @NotNull
    public static List<CraftExtension> getExtensions(Player player) {
        List<CraftExtension> extensions = new ArrayList<>(EXTENSIONS);
        for (CraftExtension extension : EXTENSIONS) {
            if (extension.isUnlockedForPlayer(player)) {
                extension.registerExtension(extensions);
            }
        }

        return extensions;
    }

    public static Texture getTextureOfExtendable() {
        return textureOfExtendable;
    }

    @Nullable
    static CraftExtension getByName(String childName) {
        for (CraftExtension extension : EXTENSIONS) {
            if (extension.getName().equals(childName)) {
                return extension;
            }
        }

        return null;
    }
} public class SlotManager {

    private static final String CONFIG_NAME = "slots.yml";

    @Nullable
    private static SlotManager slotManager = null;

    private final List<Slot> slots = new ArrayList<>();

    @NotNull
    private final Path slotsFile;
    @NotNull
    private final FileConfiguration slotsConfig;

    private SlotManager() {
        this.slotsFile = RPGInventory.getInstance().getDataPath().resolve(CONFIG_NAME);
        if (Files.notExists(slotsFile)) {
            RPGInventory.getInstance().saveResource(CONFIG_NAME, false);
        }

        this.slotsConfig = YamlConfiguration.loadConfiguration(slotsFile.toFile());

        @Nullable final ConfigurationSection slots = this.slotsConfig.getConfigurationSection("slots");
        if (slots == null) {
            Log.s("Section ''slots'' not found in {0}", CONFIG_NAME);
            return;
        }

        for (String slotName : slots.getKeys(false)) {
            final ConfigurationSection slotConfiguration = slots.getConfigurationSection(slotName);
            assert slotConfiguration != null;

            Slot.SlotType slotType = SafeEnums.valueOfOrDefault(Slot.SlotType.class, slotConfiguration.getString("type"), Slot.SlotType.GENERIC, "slot type");
            Slot slot;
            if (slotType == Slot.SlotType.ACTION) {
                slot = new ActionSlot(slotName, slotConfiguration);
            } else {
                slot = new Slot(slotName, slotType, slotConfiguration);
            }

            if (this.validateSlot(slot)) {
                this.slots.add(slot);
            } else {
                Log.w("Slot \"{0}\" was not been added.", slot.getName());
            }
        }
    }

    public static boolean init() {
        try {
            SlotManager.slotManager = new SlotManager();
        } catch (Exception e) {
            Log.w(e, "Failed to initialize SlotManager");
            return false;
        }

        return true;
    }

    @NotNull
    public static SlotManager instance() {
        return Objects.requireNonNull(SlotManager.slotManager, "Plugin is not initialized yet");
    }

    private boolean validateSlot(Slot slot) {
        if (slot.getSlotType().isReadItemList() && slot.itemListIsEmpty()) {
            Log.w("Slot with type {0} must contains list of allowed items", slot.getSlotType());
        }

        if (!validateItems(slot.getAllowedItems()) || !validateItems(slot.getDeniedItems())) {
            return false;
        }

        if (!slot.getSlotType().isAllowMultiSlots() && slot.getSlotIds().size() > 1) {
            Log.w("Slot with type {0} can not contain more than one slotId", slot.getSlotType());
            return false;
        }

        for (int slotId : slot.getSlotIds()) {
            if (slotId < 0 || slotId > 53) {
                Log.w("Slot IDs should be in range 0..53, but it was {0}", slotId);
                return false;
            }
        }

        for (Slot existingSlot : this.slots) {
            for (int slotId : slot.getSlotIds()) {
                if (existingSlot.getSlotIds().contains(slotId)) {
                    Log.w("Slot {0} is occupied by {1}", slotId, existingSlot.getName());
                    return false;
                }
            }

            if (slot.isQuick() && existingSlot.isQuick()) {
                int slotId = slot.getQuickSlot();
                int existingSlotId = existingSlot.getQuickSlot();
                if (slotId == existingSlotId) {
                    Log.w("Quickbar slot {0} is occupied by {1}", slotId, existingSlot.getName());
                    return false;
                }
            }

            if (slot.getSlotType().isUnique() && slot.getSlotType() == existingSlot.getSlotType()) {
                Log.w("You can not create more then one slot with type {0}!", slot.getSlotType());
                return false;
            }
        }

        return true;
    }

    private boolean validateItems(List<String> itemsPatterns) {
        for (String itemPattern : itemsPatterns) {
            if (!itemPattern.matches("^[\\w_]+(:\\d+(-\\d)?)?$")) {
                Log.w("Allowed and denied items should fit to pattern ''[string]:[number]-[number]''");
                Log.w("But it was: {0}", itemPattern);
                return false;
            }
        }
        return true;
    }

    @Nullable
    public Slot getSlot(String name) {
        for (Slot slot : this.slots) {
            if (slot.getName().equalsIgnoreCase(name)) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getSlot(int slotId, InventoryType.SlotType slotType) {
        for (Slot slot : this.slots) {
            if (slotType == InventoryType.SlotType.QUICKBAR) {
                if ((slot.isQuick() || slot.getSlotType() == Slot.SlotType.SHIELD) && slot.getQuickSlot() == slotId) {
                    return slot;
                }
            } else if (slot.containsSlot(slotId)) {
                return slot;
            }
        }

        return null;
    }

    @NotNull
    public List<Slot> getQuickSlots() {
        List<Slot> quickSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.isQuick()) {
                quickSlots.add(slot);
            }
        }

        return quickSlots;
    }

    @NotNull
    public List<Slot> getPassiveSlots() {
        List<Slot> passiveSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            final Slot.SlotType type = slot.getSlotType();
            if (type == Slot.SlotType.PASSIVE || type == Slot.SlotType.BACKPACK || type == Slot.SlotType.ELYTRA) {
                passiveSlots.add(slot);
            }
        }

        return passiveSlots;
    }

    @NotNull
    public List<Slot> getActiveSlots() {
        List<Slot> activeSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ACTIVE) {
                activeSlots.add(slot);
            }
        }

        return activeSlots;
    }

    @NotNull
    public List<Slot> getArmorSlots() {
        List<Slot> armorSlots = new ArrayList<>(4);
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ARMOR) {
                armorSlots.add(slot);
            }
        }

        return armorSlots;
    }

    @NotNull
    public List<Slot> getInfoSlots() {
        List<Slot> infoSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.INFO) {
                infoSlots.add(slot);
            }
        }

        return infoSlots;
    }

    @NotNull
    public List<Slot> getSlots() {
        return this.slots;
    }

    @Nullable
    public Slot getPetSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.PET) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getShieldSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.SHIELD) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getBackpackSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.BACKPACK) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getElytraSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ELYTRA) {
                return slot;
            }
        }

        return null;
    }

    public void saveDefaults() {
        try {
            this.slotsConfig.save(this.slotsFile.toFile());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
} public class ItemManager {

    private static final String CONFIG_NAME = "items.yml";

    private static final Map<String, CustomItem> CUSTOM_ITEMS = new HashMap<>();
    private static final List<String> LORE_PATTERN = Config.getConfig().getStringList("items.lore-pattern");
    private static final String LORE_SEPARATOR = Config.getConfig().getString("items.separator");

    private ItemManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        try {
            Path itemsFile = RPGInventory.getInstance().getDataPath().resolve(CONFIG_NAME);
            if (Files.notExists(itemsFile)) {
                RPGInventory.getInstance().saveResource(CONFIG_NAME, false);
            }

            FileConfiguration itemsConfig = YamlConfiguration.loadConfiguration(itemsFile.toFile());

            @Nullable final ConfigurationSection items = itemsConfig.getConfigurationSection("items");
            if (items == null) {
                Log.s("Section ''items'' not found in {0}", CONFIG_NAME);
                return false;
            }

            CUSTOM_ITEMS.clear();
            for (String key : items.getKeys(false)) {
                ConfigurationSection section = items.getConfigurationSection(key);
                if (section != null) {
                    tryToAddItem(key, section);
                }
            }

            if (CUSTOM_ITEMS.isEmpty()) {
                Log.i("No one configured item found");
                return false;
            }

            Log.i("{0} item(s) has been loaded", CUSTOM_ITEMS.size());

            instance.getServer().getPluginManager().registerEvents(new ItemListener(), instance);
            return true;
        } catch (Exception e) {
            instance.getReporter().report("Error on InventoryManager initialization", e);
            return false;
        }
    }

    private static void tryToAddItem(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("texture"));
            if (texture.isEmpty()) {
                Log.s("Item ''{0}'' has not been added because its texture is not valid.", name);
                return;
            }
            CustomItem customItem = new CustomItem(name, texture, config);
            CUSTOM_ITEMS.put(name, customItem);
        } catch (Exception e) {
            Log.s("Item ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    public static Modifier getModifier(Player player, ItemStat.StatType statType) {
        return getModifier(player, statType, false);
    }

    @SuppressWarnings("SameParameterValue")
    private static Modifier getModifier(Player player, ItemStat.StatType statType, boolean notifyPlayer) {
        List<ItemStack> effectiveItems = InventoryUtils.collectEffectiveItems(player, notifyPlayer);
        double minBonus = 0;
        double maxBonus = 0;
        float minMultiplier = 1;
        float maxMultiplier = 1;
        for (ItemStack item : effectiveItems) {
            CustomItem customItem;
            ItemStat stat;
            if (!CustomItem.isCustomItem(item) || (customItem = ItemManager.getCustomItem(item)) == null
                    || (stat = customItem.getStat(statType)) == null) {
                continue;
            }

            int sign = stat.getOperationType() == ItemStat.OperationType.MINUS ? -1 : 1;
            if (stat.isPercentage()) {
                minMultiplier += sign * stat.getMinValue() / 100;
                maxMultiplier += sign * stat.getMaxValue() / 100;
            } else {
                minBonus += sign * stat.getMinValue();
                maxBonus += sign * stat.getMaxValue();
            }
        }

        return new Modifier(minBonus, maxBonus, minMultiplier, maxMultiplier);
    }

    @NotNull
    public static List<String> getItemList() {
        return new ArrayList<>(CUSTOM_ITEMS.keySet());
    }

    public static boolean hasItem(String itemId) {
        return CUSTOM_ITEMS.containsKey(itemId);
    }

    @NotNull
    public static ItemStack getItem(String itemId) {
        CustomItem customItem = CUSTOM_ITEMS.get(itemId);
        return customItem == null ? new ItemStack(Material.AIR) : customItem.getItemStack();
    }

    @Nullable
    public static CustomItem getCustomItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return null;
        }

        String tag = ItemUtils.getTag(item, ItemUtils.ITEM_TAG);
        if (tag.isEmpty()) {
            return null;
        }

        return CUSTOM_ITEMS.get(tag);
    }

    public static boolean allowedForPlayer(Player player, ItemStack item, boolean notifyPlayer) {
        ClassedItem classedItem;
        if (CustomItem.isCustomItem(item)) {
            classedItem = ItemManager.getCustomItem(item);
        } else if (PetType.isPetItem(item)) {
            classedItem = PetManager.getPetFromItem(item);
        } else {
            return true;
        }

        if (classedItem == null) {
            return true;
        }

        if (!PlayerUtils.checkLevel(player, classedItem.getLevel())) {
            if (notifyPlayer) {
                PlayerUtils.sendMessage(
                        player, RPGInventory.getLanguage().getMessage("error.item.level", classedItem.getLevel())
                );
            }

            return false;
        }

        if (classedItem.getClasses() == null || PlayerUtils.checkClass(player, classedItem.getClasses())) {
            return true;
        }

        if (notifyPlayer) {
            PlayerUtils.sendMessage(
                    player, RPGInventory.getLanguage().getMessage("error.item.class", classedItem.getClassesString())
            );
        }

        return false;
    }

    public static void updateStats(final Player player) {
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        InventoryManager.get(player).updateStatsLater();
    }

    @NotNull
    static List<String> buildLore(@NotNull CustomItem item) {
        FileLanguage lang = RPGInventory.getLanguage();
        List<String> lore = new ArrayList<>();
        boolean lastIsSeparator = false;
        for (String loreElement : LORE_PATTERN) {
            switch (loreElement) {
                case "_UNBREAKABLE_":
                    if (item.isUnbreakable()) {
                        lore.add(lang.getMessage("item.unbreakable"));
                        lastIsSeparator = false;
                    }
                    break;
                case "_DROP_":
                    if (!item.isDrop()) {
                        lore.add(lang.getMessage("item.nodrop"));
                        lastIsSeparator = false;
                    }
                    break;
                case "_SEPARATOR_":
                    if (!lastIsSeparator) {
                        lore.add(LORE_SEPARATOR);
                        lastIsSeparator = true;
                    }
                    break;
                case "_LEVEL_":
                    if (item.getLevel() != -1) {
                        lore.add(lang.getMessage("item.level", item.getLevel()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_CLASS_":
                    if (item.getClasses() != null) {
                        lore.add(lang.getMessage("item.class", item.getClassesString()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_LORE_":
                    if (item.getLore() != null) {
                        lore.addAll(item.getLore());
                        lastIsSeparator = false;
                    }
                    break;
                case "_SKILLS_":
                    if (item.hasLeftClickCaption()) {
                        lore.add(lang.getMessage("item.left-click", item.getLeftClickCaption()));
                        lastIsSeparator = false;
                    }
                    if (item.hasRightClickCaption()) {
                        lore.add(lang.getMessage("item.right-click", item.getRightClickCaption()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_STATS_":
                    if (item.isStatsHidden()) {
                        lore.add(lang.getMessage("item.hide"));
                        lastIsSeparator = false;
                    } else {
                        for (ItemStat stat : item.getStats()) {
                            lore.add(lang.getMessage("stat." + stat.getType().name().toLowerCase(), stat.getStringValue()));
                            lastIsSeparator = false;
                        }
                    }
                    break;
                default:
                    lore.add(StringUtils.coloredLine(loreElement));
            }
        }

        if (lastIsSeparator) {
            lore.remove(lore.size() - 1);
        }

        if (lore.get(0).equals(LORE_SEPARATOR)) {
            lore.remove(0);
        }

        return lore;
    }
} public class FileLanguage {
    private final RPGInventory plugin;
    private final HashMap<String, MessageFormat> messageCache = new HashMap<>();
    private final Properties language = new Properties();
    @NotNull
    private final Path langFile;

    public FileLanguage(RPGInventory plugin) {
        this.plugin = plugin;
        String locale = Config.getConfig().getString("language");
        this.langFile = this.plugin.getDataPath().resolve(String.format("lang/%s.lang", locale));
        this.saveDefault();
        this.checkAndUpdateOutdatedLocaleFile();
        this.load();
        this.validateLocaleFile();
    }

    private void saveDefault() {
        if (Files.exists(this.langFile)) {
            return;
        }

        String path = "lang/" + this.langFile.getFileName();
        try {
            this.plugin.saveResource(path, true);
        } catch (Exception ex) {
            Log.w("Failed to load {0}: {1}; using en.lang", this.langFile.getFileName(), ex.toString());

            try (InputStream is = this.plugin.getResource("lang/en.lang")) {
                Objects.requireNonNull(is);
                Files.copy(is, Paths.get(this.langFile.toUri()), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException | NullPointerException e) {
                Log.s("Failed to write default locale to {0}: {1}; continue without localization.",
                        this.langFile.getFileName(), e.toString());
            }
        }
    }

    private void load() {
        try (InputStream is = Files.newInputStream(this.langFile);
             InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) {
            this.language.load(isr);
        } catch (IOException e) {
            Log.s("Failed to load locale file: {0}; continue without localization.", e.toString());
        }
    }

    //Oh crap.
    private void checkAndUpdateOutdatedLocaleFile() {
        final Path path = this.langFile;
        List<String> lines;
        try {
            lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        } catch (IOException e) {
            Log.w(e, "Failed to read locale file");
            return;
        }

        if (lines.get(0).startsWith("#version")) {
            return;
        }

        final Pattern pattern = Pattern.compile("%(s|d|.2f)");
        final LinkedList<String> newLines = new LinkedList<>();
        newLines.add("#version: 2.0 | Do not remove this line!");

        for (int i1 = 0; i1 < lines.size(); i1++) {
            String line = lines.get(i1);
            String newLine = line.replace("\"", "");
            Matcher m;
            for (int i = 0; (m = pattern.matcher(newLine)).find(); i++) {
                newLine = m.replaceFirst("{" + i + "}");
            }

            if (lines.size() > i1 + 1) {
                String nextLine = lines.get(i1 + 1);
                if ("\n ".length() < nextLine.length()
                        && (!nextLine.contains(":") || nextLine.indexOf(':') > nextLine.indexOf(' '))) {
                    newLine = newLine + "\\";
                }
            }

            newLines.add(newLine);
        }

        try {
            Files.write(
                    path, newLines, StandardCharsets.UTF_8,
                    StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING
            );
        } catch (IOException e) {
            Log.w("Failed to save locale file: {0}", e.toString());
        }
    }

    private void validateLocaleFile() {
        Properties properties = new Properties();
        InputStream defaultLocale = this.plugin.getResource("lang/en.lang");
        try (InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(defaultLocale), StandardCharsets.UTF_8)) {
            properties.load(isr);
        } catch (IOException | NullPointerException e) {
            Log.w(e, "Failed to read inbuilt locale file");
            //Just ignore. We can't help with that shit.
            return;
        }

        if (this.language.keySet().containsAll(properties.keySet())) {
            return;
        }

        for (Object key : properties.keySet()) {
            if (!this.language.containsKey(key)) {
                this.language.setProperty((String) key, properties.getProperty((String) key));
            }
        }
    }

    @NotNull
    @Deprecated
    public String getCaption(String name, Object... args) {
        return this.getMessage(name, args);
    }

    @NotNull
    public String getMessage(String key) {
        return this.getMessage(key, false);
    }

    @NotNull
    public String getMessage(String key, boolean stripColor) {
        return this.getMessage(key, stripColor, (Object[]) null);
    }

    @NotNull
    public String getMessage(String key, Object... args) {
        return this.getMessage(key, false, args);
    }

    @NotNull
    public String getMessage(String key, boolean stripColor, Object... args) {
        if (!this.messageCache.containsKey(key)) {
            this.messageCache.put(key, new MessageFormat(
                    ChatColor.translateAlternateColorCodes(
                            '&', this.language.getProperty(key, "Unknown localization key: \"" + key + "\"")
                    )
            ));
        }
        String out = this.messageCache.get(key).format(args);
        return stripColor ? ChatColor.stripColor(out) : out;
    }
} public class Updater {

    /* Constants */

    // Remote file's title
    private static final String TITLE_VALUE = "name";
    // Remote file's download link
    private static final String LINK_VALUE = "fileUrl";
    // Remote file's build version
    private static final String VERSION_VALUE = "gameVersion";
    // Remote description
    private static final String DESCRIPTION_VALUE = "description";
    // Path to GET
    private static final String QUERY = "/updates/versions.json";
    // Slugs will be appended to this to get to the project's RSS feed
    private static final String HOST = "http://rpginventory.endlesscode.ru";
    // User-agent when querying Curse
    private static final String USER_AGENT = "RPGInventory";
    // Used for locating version numbers in file names
    private static final String DELIMITER = "^v|[\\s_-]v";
    // If the version number contains one of these, don't update.
    private static final String[] NO_UPDATE_TAG = {"-DEV", "-PRE", "-SNAPSHOT"};
    // Config key for api key
    private static final String API_KEY_CONFIG_KEY = "api-key";
    // Config key for disabling Updater
    private static final String DISABLE_CONFIG_KEY = "disable";
    // Default api key value in config
    private static final String API_KEY_DEFAULT = "PUT_API_KEY_HERE";
    // Default disable value in config
    private static final boolean DISABLE_DEFAULT = false;

    /* User-provided variables */

    // Plugin running Updater
    private final Plugin plugin;
    // Type of update check to run
    private final UpdateType type;

    /* Collected from Curse API */

    private String versionName;
    private String infoLink;
    private String description;

    /* Update process variables */
    // Connection to RSS
    private URL url;
    // Updater thread
    private Thread thread;
    // Used for determining the outcome of the update process
    @NotNull
    private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS;

    /**
     * Initialize the updater with the provided callback.
     *
     * @param plugin   The plugin that is checking for an update.
     * @param type     Specify the type of update this will be. See {@link UpdateType}
     */
    public Updater(Plugin plugin, UpdateType type) {
        this.plugin = plugin;
        this.type = type;

        final File pluginFile = this.plugin.getDataFolder().getParentFile();
        final File updaterFile = new File(pluginFile, "Updater");
        final File updaterConfigFile = new File(updaterFile, "config.yml");

        YamlConfiguration config = new YamlConfiguration();
        config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n'
                + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n'
                + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration.");
        config.addDefault(API_KEY_CONFIG_KEY, API_KEY_DEFAULT);
        config.addDefault(DISABLE_CONFIG_KEY, DISABLE_DEFAULT);

        if (!updaterFile.exists()) {
            this.fileIOOrError(updaterFile, updaterFile.mkdir(), true);
        }

        boolean createFile = !updaterConfigFile.exists();
        try {
            if (createFile) {
                this.fileIOOrError(updaterConfigFile, updaterConfigFile.createNewFile(), true);
                config.options().copyDefaults(true);
                config.save(updaterConfigFile);
            } else {
                config.load(updaterConfigFile);
            }
        } catch (@NotNull final Exception e) {
            final String message;
            message = createFile ? "The updater could not create configuration at " + updaterFile.getAbsolutePath()
                    : "The updater could not load configuration at " + updaterFile.getAbsolutePath();
            this.plugin.getLogger().log(Level.SEVERE, message, e);
        }

        if (config.getBoolean(DISABLE_CONFIG_KEY)) {
            this.result = UpdateResult.DISABLED;
            return;
        }

        try {
            this.url = new URL(Updater.HOST + Updater.QUERY);
            this.thread = new Thread(new UpdateRunnable());
            this.thread.start();
        } catch (@NotNull final MalformedURLException e) {
            runUpdater();
        }
    }

    /**
     * Get the result of the update process.
     *
     * @return result of the update process.
     * @see UpdateResult
     */
    @NotNull
    public Updater.UpdateResult getResult() {
        this.waitForThread();
        return this.result;
    }

    /**
     * Get the latest version's name (such as "Project v1.0").
     *
     * @return latest version's name.
     */
    public String getLatestName() {
        this.waitForThread();
        return this.versionName;
    }

    /**
     * Get the version info link.
     *
     * @return version info link.
     */
    public String getInfoLink() {
        return this.infoLink;
    }

    /**
     * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish
     * before allowing anyone to check the result.
     */
    private void waitForThread() {
        if ((this.thread != null) && this.thread.isAlive()) {
            try {
                this.thread.join();
            } catch (@NotNull final InterruptedException e) {
                this.plugin.getLogger().log(Level.SEVERE, null, e);
            }
        }
    }

    /**
     * Check to see if the program should continue by evaluating whether the plugin is already updated, or shouldn't be updated.
     *
     * @return true if the version was located and is not the same as the remote's newest.
     */
    private boolean versionCheck() {
        final String title = this.versionName;
        if (this.type != UpdateType.NO_VERSION_CHECK) {
            final String localVersion = this.plugin.getDescription().getVersion();
            if (title.split(DELIMITER).length == 2) {
                // Get the newest file's version number
                final String remoteVersion = title.split(DELIMITER)[1].split(" ")[0];

                if (this.hasTag(localVersion) || !this.shouldUpdate(localVersion, remoteVersion)) {
                    // We already have the latest version, or this build is tagged for no-update
                    this.result = Updater.UpdateResult.NO_UPDATE;
                    return false;
                }
            } else {
                // The file's name did not contain the string 'vVersion'
                final String authorInfo = this.plugin.getDescription().getAuthors().isEmpty() ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")";
                this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system");
                this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'");
                this.plugin.getLogger().warning("Please notify the author of this error.");
                this.result = Updater.UpdateResult.FAIL_NOVERSION;
                return false;
            }
        }
        return true;
    }

    /**
     * <b>If you wish to run mathematical versioning checks, edit this method.</b>
     * <p>
     * With default behavior, Updater will NOT verify that a remote version available on BukkitDev
     * which is not this version is indeed an "update".
     * If a version is present on BukkitDev that is not the version that is currently running,
     * Updater will assume that it is a newer version.
     * This is because there is no standard versioning scheme, and creating a calculation that can
     * determine whether a new update is actually an update is sometimes extremely complicated.
     * </p>
     * <p>
     * Updater will call this method from {@link #versionCheck()} before deciding whether
     * the remote version is actually an update.
     * If you have a specific versioning scheme with which a mathematical determination can
     * be reliably made to decide whether one version is higher than another, you may
     * revise this method, using the local and remote version parameters, to execute the
     * appropriate check.
     * </p>
     * <p>
     * Returning a value of <b>false</b> will tell the update process that this is NOT a new version.
     * Without revision, this method will always consider a remote version at all different from
     * that of the local version a new update.
     * </p>
     *
     * @param localVersion  the current version
     * @param remoteVersion the remote version
     * @return true if Updater should consider the remote version an update, false if not.
     */
    private boolean shouldUpdate(String localVersion, String remoteVersion) {
        return !localVersion.equalsIgnoreCase(remoteVersion);
    }

    /**
     * Evaluate whether the version number is marked showing that it should not be updated by this program.
     *
     * @param version a version number to check for tags in.
     * @return true if updating should be disabled.
     */
    private boolean hasTag(@NotNull String version) {
        for (final String string : Updater.NO_UPDATE_TAG) {
            if (version.contains(string)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Make a connection to the BukkitDev API and request the newest file's details.
     *
     * @return true if successful.
     */
    private boolean read() {
        try {
            return tryToRead();
        } catch (@NotNull final IOException | NullPointerException e) {
            Log.s("The updater could not contact {0} for check updates.", HOST);
            Log.s("The site experiencing temporary downtime.");
            this.result = UpdateResult.FAIL_DBO;
            return false;
        }
    }

    private boolean tryToRead() throws IOException {
        final URLConnection conn = this.url.openConnection();
        conn.setConnectTimeout(5000);

        conn.addRequestProperty("User-Agent", Updater.USER_AGENT);
        conn.setDoOutput(true);
        StringBuilder responseBuilder = new StringBuilder();

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                responseBuilder.append(line.trim());
            }
        }

        UpdateData latestUpdate = null;
        Type listType = new TypeToken<ArrayList<UpdateData>>() {}.getType();
        List<UpdateData> updates = new Gson().fromJson(responseBuilder.toString(), listType);
        for (UpdateData update : updates) {
            String[] versions = update.mcVersions.split(", ");

            for (String version : versions) {
                if (Bukkit.getBukkitVersion().startsWith(version)) {
                    latestUpdate = update;
                    break;
                }
            }
        }

        if (latestUpdate == null) {
            this.result = UpdateResult.NO_UPDATE;
            return false;
        }

        this.versionName = latestUpdate.versionName;
        this.infoLink = latestUpdate.infoLink;
        this.description = latestUpdate.description;

        return true;
    }

    /**
     * Perform a file operation and log any errors if it fails.
     *
     * @param file   file operation is performed on.
     * @param result result of file operation.
     * @param create true if a file is being created, false if deleted.
     */
    private void fileIOOrError(@NotNull File file, boolean result, boolean create) {
        if (!result) {
            this.plugin.getLogger().severe("The updater could not " + (create ? "create" : "delete") + " file at: " + file.getAbsolutePath());
        }
    }

    private void runUpdater() {
        if (this.url != null && (this.read() && this.versionCheck())) {
            // Obtain the results of the project's file feed
            this.result = UpdateResult.UPDATE_AVAILABLE;
        }
    }

    public String getDescription() {
        return description;
    }

    /**
     * Gives the developer the result of the update process. Can be obtained by called {@link #getResult()}
     */
    public enum UpdateResult {
        /**
         * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads.
         */
        SUCCESS,
        /**
         * The updater did not find an update, and nothing was downloaded.
         */
        NO_UPDATE,
        /**
         * The server administrator has disabled the updating system.
         */
        DISABLED,
        /**
         * For some reason, the updater was unable to contact dev.bukkit.org to download the file.
         */
        FAIL_DBO,
        /**
         * When running the version check, the file on DBO did not contain a recognizable version.
         */
        FAIL_NOVERSION,
        /**
         * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded.
         */
        UPDATE_AVAILABLE
    }

    /**
     * Allows the developer to specify the type of update that will be run.
     */
    public enum UpdateType {
        /**
         * Don't run a version check, just find the latest update and download it.
         */
        NO_VERSION_CHECK,
        /**
         * Get information about the version and the download size, but don't actually download anything.
         */
        NO_DOWNLOAD
    }

    /**
     * Called on main thread when the Updater has finished working, regardless
     * of result.
     */
    public interface UpdateCallback {
        /**
         * Called when the updater has finished working.
         *
         * @param updater The updater instance
         */
        void onFinish(Updater updater);
    }

    private class UpdateRunnable implements Runnable {
        @Override
        public void run() {
            runUpdater();
        }
    }

    public static final class UpdateData {
        @SerializedName(VERSION_VALUE)
        public String mcVersions;

        @SerializedName(TITLE_VALUE)
        public String versionName;

        @SerializedName(LINK_VALUE)
        public String infoLink;

        @SerializedName(DESCRIPTION_VALUE)
        public String description;
    }
} public class Config {

    /* Config options */
    public static VanillaSlotAction craftSlotsAction = VanillaSlotAction.RPGINV;
    public static VanillaSlotAction armorSlotsAction = VanillaSlotAction.DEFAULT;

    public static TexturesType texturesType = TexturesType.DAMAGE;

    private static final FileConfiguration config = new YamlConfiguration();
    private static Path configFile;

    public static void init(RPGInventory plugin) {
        configFile = plugin.getDataPath().resolve("config.yml");
        plugin.saveDefaultConfig();

        final InputStream defaultConfigStream = plugin.getResource("config.yml");
        if (defaultConfigStream != null) {
            loadDefaultConfig(defaultConfigStream);
        }

        reload();
    }

    public static FileConfiguration getConfig() {
        return config;
    }

    public static void reload() {
        try {
            config.load(configFile.toFile());
        } catch (IOException | InvalidConfigurationException e) {
            Log.w(e, "Error on load config.yml");
        }

        copyOptionsFromConfig();
        save();
    }

    public static void save() {
        try {
            config.save(configFile.toFile());
        } catch (IOException e) {
            Log.w(e, "Error on save config.yml");
        }
    }

    private static void loadDefaultConfig(@NotNull InputStream defaultConfigStream) {
        final Path exampleConfigPath = configFile.getParent().resolve("config-example.yml");

        // Update example config
        try (final InputStream stream = defaultConfigStream) {
            Files.copy(stream, exampleConfigPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            Log.w(e, "Error on copying config-example.yml");
        }

        config.setDefaults(YamlConfiguration.loadConfiguration(exampleConfigPath.toFile()));
        config.options().copyDefaults(true);
    }

    private static void copyOptionsFromConfig() {
        craftSlotsAction = VanillaSlotAction.parseString(config.getString("craft-slots-action"));
        armorSlotsAction = VanillaSlotAction.parseString(config.getString("armor-slots-action"));

        texturesType = TexturesType.parseString(config.getString("textures-type"));
    }
} public class ConfigUpdater {
    public static void update(Version configVersion) {
        FileConfiguration config = Config.getConfig();

        if (configVersion.compareTo("1.0.0") < 0) {
            config.set("ids", null);
            config.set("attack.auto-held", true);
        }

        if (configVersion.compareTo("1.0.1") < 0) {
            config.set("slots.pet", null);
            config.set("slots.crafting", null);
            config.set("slots.enabled", true);
            config.set("slots.level.spend", false);
            SlotManager.instance().saveDefaults();
        }

        if (configVersion.compareTo("1.1.0") < 0) {
            config.set("alternate-view.enabled", false);
            config.set("alternate-view.fill", "STAINED_GLASS_PANE:0");
        }

        if (configVersion.compareTo("1.0.4") < 0) {
            config.set("alternate-view.item", "ENCHANTED_BOOK");
            config.set("alternate-view.slot", 8);
            config.set("alternate-view.name", "&6Equipment");
            config.set("alternate-view.lore", "&7&o(Right click to open equipment)");
        }

        if (configVersion.compareTo("1.0.5") < 0) {
            if (config.getInt("alternate-view.slot") == 9) {
                config.set("alternate-view.slot", 8);
            }
            config.set("resource-pack.mode", config.getBoolean("alternate-view.enabled") ? "DISABLED" : "AUTO");
            config.set("resource-pack.url", "PUT_YOUR_URL_HERE");
            config.set("resource-pack.hash", "PUT_YOUR_HASH_HERE");
            config.set("alternate-view.enabled", null);
            config.set("alternate-view.use-item", true);
        }

        if (configVersion.compareTo("1.1.8") < 0) {
            config.set("alternate-view.enable-craft", true);
            config.set("worlds.mode", "BLACKLIST");
            config.set("worlds.list", new String[]{"blocked_world"});
        }

        if (configVersion.compareTo("1.2.4") < 0) {
            config.set("backpacks.expiration-time", 30);
        }

        if (configVersion.compareTo("1.2.5") < 0) {
            config.set("level-system", "EXP");
            config.set("class-system", "PERMISSIONS");
            config.set("metrics", true);
        }

        if (configVersion.compareTo("1.2.7") < 0) {
            config.set("items.lore-pattern", new String[]{
                    "_UNBREAKABLE_",
                    "_DROP_",
                    "_LEVEL_",
                    "_CLASS_",
                    "_SEPARATOR_",
                    "_LORE_",
                    "_SEPARATOR_",
                    "_SKILLS_",
                    "_SEPARATOR_",
                    "_STATS_"
            });
            config.set("items.separator", "");
            config.set("auto-update", false);
        }

        if (configVersion.compareTo("1.3.0") < 0) {
            config.set("auto-update", true);
        }

        if (configVersion.compareTo("1.3.3") < 0) {
            config.set("health.base", 20);
            config.set("health.scale", false);
            config.set("health.hearts", 20);
            config.set("health.heart-capacity.min", 1);
            config.set("health.heart-capacity.max", 5);
        }

        if (configVersion.compareTo("2.0.0") < 0) {
            // Removed RP settings
            config.set("resource-pack.fill", "DIAMOND_HOE:1");
            config.set("resource-pack.mode", null);
            config.set("alternate-view", null);
            config.set("containers.block", false);
            config.set("slots.locked", "DIAMOND_HOE:19");
            config.set("slots.buyable", "DIAMOND_HOE:18");

            // Added craft extensions
            config.set("craft.enabled", true);
            config.set("craft.extendable", "DIAMOND_HOE:0");
            config.set("craft.extensions.journeyman.name", "&aJourneyman slots");
            config.set("craft.extensions.journeyman.lore", "&eYou must be a journeyman to use it");
            config.set("craft.extensions.journeyman.slots", Arrays.asList(8, 9));
            config.set("craft.extensions.master.name", "&3Master slots");
            config.set("craft.extensions.master.lore", "&eYou must be a master to use it");
            config.set("craft.extensions.master.includes", Collections.singletonList("journeyman"));
            config.set("craft.extensions.master.slots", Arrays.asList(1, 4, 7));

            // Added join-messages
            config.set("join-messages.enabled", true);
            config.set("join-messages.delay", 3);
            config.set("join-messages.default.title", "&l&2Welcome to server!");
            config.set("join-messages.default.text", Arrays.asList("&6Glad to see you, &3%PLAYER%", "&6This server using &9RPGInventory"));
            config.set("join-messages.rp-info.title", "&l&4It is important!");
            config.set("join-messages.rp-info.text", Arrays.asList(
                    "&6You should &callow &6resource pack to play on this server",
                    "&6This will allow you fully immerse in the RPG atmosphere",
                    "&6But if you declined downloading of RP you can fix it...",
                    "&6Select the server in list, click &e'Edit' &6 and set &e'Resource-Pack: Accept'"));
        }

        if (configVersion.compareTo("2.0.1") < 0) {
            if ("&l&2Welcome to server!".equals(config.get("join-messages.rp-info.title"))) {
                config.set("join-messages.rp-info.title", "&l&4It is important!");
                config.set("join-messages.default.title", "&l&2Welcome to server!");
            }
        }

        if (configVersion.compareTo("2.0.4") < 0) {
            // Fixing join messages disabling
            config.set("join-messages.enabled", null);
            config.set("join-messages.rp-info.enabled", true);
            config.set("join-messages.default.enabled", true);

            // Added backpacks limit
            config.set("backpacks.limit", 1);

            // Added ability to disable extension for workbench
            config.set("craft.workbench", true);
            if ("DIAMOND_HOE:0".equals(config.get("craft.extendable"))) {
                config.set("craft.extendable", "DIAMOND_HOE:27");
            }
        }

        if (configVersion.compareTo("2.0.8") < 0) {
            config.set("fill", config.get("resource-pack.fill"));
            config.set("resource-pack.fill", null);
            config.set("resource-pack.enabled", true);
        }

        if (configVersion.compareTo("2.1.0") < 0) {
            config.set("resource-pack.delay", 2);
            config.set("check-update", config.getBoolean("auto-update"));
            config.set("auto-update", null);
        }

        if (configVersion.compareTo("2.1.4") < 0) {
            config.set("health", null);
        }

        if (configVersion.compareTo("2.2.0") < 0) {
            config.set("armor-slots-action", "default");
            config.set("craft-slots-action", "rpginv");
        }

        if (configVersion.compareTo("2.3.0") < 0) {
            config.set("metrics", null);
            config.set("containers", null);
        }

        if (configVersion.compareTo("2.3.2") < 0) {
            config.set("textures-type", "damage");
        }

        if (configVersion.compareTo("2.4.0") < 0) {
            config.set("level-system", null);
            config.set("class-system", null);
        }
    }
} public class Serialization {

    private static final String ROOT_TAG = "data";

    public static void registerTypes() {
        ConfigurationSerialization.registerClass(InventorySnapshot.class);
        ConfigurationSerialization.registerClass(SlotSnapshot.class);
        ConfigurationSerialization.registerClass(Backpack.class);
    }

    @Nullable
    public static PlayerWrapper loadPlayerOrNull(Player player, @NotNull Path file) {
        PlayerWrapper playerWrapper;
        try {
            playerWrapper = loadPlayer(player, file);
        } catch (IOException | InvalidConfigurationException e) {
            Log.w(e);
            FileUtils.resolveException(file);
            playerWrapper = null;
        }
        return playerWrapper;
    }

    @NotNull
    private static PlayerWrapper loadPlayer(Player player, @NotNull Path file)
            throws IOException, InvalidConfigurationException {
        PlayerWrapper playerWrapper;
        try {
            InventorySnapshot inventorySnapshot = (InventorySnapshot) load(file);
            playerWrapper = inventorySnapshot.restore(player);
        } catch (InvalidConfigurationException e) {
            if (e.getCause() instanceof ReaderException) {
                Log.w("Can''t load {0}''s inventory. Trying to use legacy loader...", player.getName());
                playerWrapper = LegacySerialization.loadPlayer(player, file);
            } else {
                throw e;
            }
        }
        return playerWrapper;
    }

    public static Backpack loadBackpack(@NotNull Path file) throws IOException, InvalidConfigurationException {
        Backpack backpack;
        try {
            backpack = (Backpack) load(file);
        } catch (InvalidConfigurationException e) {
            if (e.getCause() instanceof ReaderException) {
                Log.w("Can''t load backpack {0}. Trying to use legacy loader...", file.getFileName().toString());
                backpack = LegacySerialization.loadBackpack(file);
            } else {
                throw e;
            }
        }

        return backpack;
    }

    public static void save(@NotNull Object data, @NotNull Path file) throws IOException {
        final FileConfiguration serializedData = new YamlConfiguration();
        serializedData.set(ROOT_TAG, data);

        Path tempFile = Files.createTempFile(file.getParent(), file.getFileName().toString(), null);
        try (OutputStreamWriter stream = new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(tempFile)), StandardCharsets.UTF_8)) {
            stream.write(serializedData.saveToString());
        }
        Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING);
    }

    @NotNull
    private static Object load(@NotNull Path file)
            throws IOException, InvalidConfigurationException {
        final FileConfiguration serializedData = new YamlConfiguration();
        try (InputStreamReader reader = new InputStreamReader(new GZIPInputStream(Files.newInputStream(file)), StandardCharsets.UTF_8)) {
            serializedData.load(reader);
        }

        return Objects.requireNonNull(serializedData.get(ROOT_TAG), "Serialized data not found");
    }
} public class PetManager {

    private static final String CONFIG_NAME = "pets.yml";

    private static final String METADATA_KEY_PET_OWNER = "rpginventory:petowner";
    private static final Map<String, PetType> PETS = new HashMap<>();
    private static final Map<String, PetFood> PET_FOOD = new HashMap<>();
    private static final String DEATH_TIME_TAG = "pet.deathTime";
    private static CooldownsTimer COOLDOWNS_TIMER;
    private static int SLOT_PET;

    private PetManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        SLOT_PET = SlotManager.instance().getPetSlot() != null ? SlotManager.instance().getPetSlot().getSlotId() : -1;

        if (!PetManager.isEnabled()) {
            Log.i("Slot for pets not found");
            return false;
        }

        try {
            Path dataPath = RPGInventory.getInstance().getDataPath();
            Path petsFile = dataPath.resolve(CONFIG_NAME);
            if (Files.notExists(petsFile)) {
                String suffix = VersionHandler.isLegacy() ? ".legacy" : "";
                String defaultConfigName = CONFIG_NAME + suffix;
                RPGInventory.getInstance().saveResource(defaultConfigName, false);
                if (!suffix.isEmpty()) {
                    Files.move(dataPath.resolve(defaultConfigName), petsFile);
                }
            }

            FileConfiguration petsConfig = YamlConfiguration.loadConfiguration(petsFile.toFile());

            @Nullable final ConfigurationSection pets = petsConfig.getConfigurationSection("pets");
            if (pets == null) {
                Log.s("Section ''pets'' not found in {0}", CONFIG_NAME);
                return false;
            }

            PETS.clear();
            for (String key : pets.getKeys(false)) {
                ConfigurationSection section = pets.getConfigurationSection(key);
                if (section != null) {
                    tryToAddPet(key, section);
                }
            }

            @Nullable final ConfigurationSection food = petsConfig.getConfigurationSection("food");
            if (food == null) {
                Log.s("Section ''food'' not found in {0}", CONFIG_NAME);
            } else {
                PET_FOOD.clear();
                for (String key : food.getKeys(false)) {
                    ConfigurationSection section = food.getConfigurationSection(key);
                    if (section != null) {
                        tryToAddPetFood(key, section);
                    }
                }
            }
        } catch (Exception e) {
            instance.getReporter().report("Error on PetManager initialization", e);
            return false;
        }

        if (PETS.isEmpty()) {
            Log.i("No one configured pet found");
            return false;
        }

        Log.i("{0} pet(s) has been loaded", PetManager.PETS.size());
        Log.i("{0} food(s) has been loaded", PetManager.PET_FOOD.size());

        // Register events
        instance.getServer().getPluginManager().registerEvents(new PetListener(), instance);
        PetManager.COOLDOWNS_TIMER = new CooldownsTimer(instance);
        PetManager.COOLDOWNS_TIMER.runTaskTimer(instance, 20, CooldownsTimer.TICK_PERIOD);
        return true;
    }

    private static void tryToAddPet(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("item"));
            if (texture.isEmpty()) {
                Log.s("Pet ''{0}'' has not been added because its item is not valid.", name);
                return;
            }
            PetType petType = new PetType(texture, config);
            PetManager.PETS.put(name, petType);
        } catch (Exception e) {
            Log.s("Pet ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    private static void tryToAddPetFood(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("item"));
            if (texture.isEmpty()) {
                Log.s("Pet food ''{0}'' has not been added because its item is not valid.", name);
                return;
            }
            PetFood pet = new PetFood(texture, config);
            PetManager.PET_FOOD.put(name, pet);
        } catch (Exception e) {
            Log.s("Pet food ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    public static void initPlayer(@NotNull Player player) {
        if (!isEnabled() || !InventoryManager.playerIsLoaded(player)) {
            return;
        }

        respawnPet(player);
    }

    @Contract(pure = true)
    public static boolean isEnabled() {
        return SLOT_PET != -1;
    }

    public static int getPetSlotId() {
        return SLOT_PET;
    }

    @NotNull
    public static List<String> getPetList() {
        return new ArrayList<>(PETS.keySet());
    }

    @NotNull
    public static List<String> getFoodList() {
        return new ArrayList<>(PET_FOOD.keySet());
    }

    public static void startCooldownTimer(Player player, ItemStack petItem) {
        PetManager.COOLDOWNS_TIMER.addPetCooldown(player, petItem);
    }

    public static void teleportPet(@NotNull final Player player, @Nullable final Location to) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        final LivingEntity petEntity = InventoryManager.get(player).getPet();
        if (petEntity == null) {
            return;
        }

        Location baseLocation = to != null ? to : player.getLocation();
        Location newPetLoc = LocationUtils.getLocationNearPoint(baseLocation, 3);
        petEntity.teleport(newPetLoc);
    }

    /** @see #respawnPet(Player, ItemStack) */
    @Deprecated
    public static void spawnPet(@NotNull final Player player, @NotNull ItemStack petItem) {
        respawnPet(player, petItem);
    }

    public static void respawnPet(@Nullable OfflinePlayer player) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        ItemStack petItem = inventory.getItem(SLOT_PET);
        if (petItem != null) {
            respawnPet((Player) player, petItem);
        }
    }

    public static void respawnPet(@NotNull final Player player, @NotNull ItemStack petItem) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        PetManager.despawnPet(player);

        final PetType petType = PetManager.getPetFromItem(petItem);
        if (petType == null) {
            return;
        }

        if (PetManager.getCooldown(petItem) > 0) {
            PetManager.startCooldownTimer(player, petItem);
            return;
        }

        Location petLoc = LocationUtils.getLocationNearPoint(player.getLocation(), 3);
        Animals pet = (Animals) player.getWorld().spawnEntity(petLoc, petType.getSkin());
        pet.teleport(petLoc);
        EffectUtils.playSpawnEffect(pet);
        Map<String, String> features = petType.getFeatures();

        boolean hasInitializationErrors = false;
        switch (petType.getRole()) {
            case MOUNT:
                switch (pet.getType()) {
                    case HORSE:
                        Horse horsePet = (Horse) pet;
                        HorseInventory horseInv = horsePet.getInventory();
                        horseInv.setSaddle(new ItemStack(Material.SADDLE));

                        if (features.containsKey("CHEST") && features.get("CHEST").equals("TRUE")) {
                            try {
                                //noinspection deprecation
                                horsePet.setCarryingChest(true);
                            } catch (UnsupportedOperationException ignored) {
                                //org.bukkit.craftbukkit.entity.CraftHorse.setCarryingChest (CraftHorse.class:56)
                                Log.w("Failed to add a chest to the horse");
                            }
                        }

                        if (features.containsKey("ARMOR")) {
                            String materialName = features.get("ARMOR");
                            Material armorMaterial = MaterialCompat.getMaterialOrNull(materialName);
                            if (armorMaterial != null) {
                                horseInv.setArmor(new ItemStack(armorMaterial));
                            } else {
                                Log.w("Failed to add an armor to the horse. Unknown material: {0}", materialName);
                            }
                        }

                        Horse.Color color = SafeEnums.getHorseColor(features.getOrDefault("COLOR", "BROWN"));
                        if (color != null) {
                            horsePet.setColor(color);
                        } else {
                            hasInitializationErrors = true;
                        }

                        Horse.Style style = SafeEnums.getHorseStyle(features.getOrDefault("STYLE", "NONE"));
                        if (style != null) {
                            horsePet.setStyle(style);
                        } else {
                            hasInitializationErrors = true;
                        }

                        break;
                    case PIG:
                        Pig pigPet = (Pig) pet;
                        pigPet.setSaddle(true);
                }

                break;
            case COMPANION:
                switch (pet.getType()) {
                    case WOLF:
                        Wolf wolfPet = (Wolf) pet;
                        DyeColor wolfCollarColor = SafeEnums.getDyeColor(features.getOrDefault("COLLAR", "RED"));
                        if (wolfCollarColor != null) {
                            wolfPet.setCollarColor(wolfCollarColor);
                        } else {
                            hasInitializationErrors = true;
                        }
                        break;
                    case CAT:
                        Cat catPet = (Cat) pet;
                        Cat.Type catType = SafeEnums.getCatType(features.getOrDefault("TYPE", "TABBY"));
                        if (catType != null) {
                            catPet.setCatType(catType);
                        } else {
                            hasInitializationErrors = true;
                        }

                        DyeColor catCollarColor = SafeEnums.getDyeColor(features.getOrDefault("COLLAR", "RED"));
                        if (catCollarColor != null) {
                            catPet.setCollarColor(catCollarColor);
                        } else {
                            hasInitializationErrors = true;
                        }
                }
        }

        if (pet instanceof Tameable) {
            ((Tameable) pet).setTamed(true);
            ((Tameable) pet).setOwner(player);
        }

        pet.setBreed(false);
        if (petType.isAdult()) {
            pet.setAdult();
        } else {
            pet.setBaby();
        }
        pet.setAgeLock(true);

        pet.setCustomName(RPGInventory.getLanguage().getMessage("pet.name", petType.getName(), player.getName()));
        pet.setCustomNameVisible(true);
        pet.setCanPickupItems(false);
        pet.setRemoveWhenFarAway(false);

        AttributeInstance maxHealth = pet.getAttribute(Attribute.GENERIC_MAX_HEALTH);
        assert maxHealth != null;
        maxHealth.setBaseValue(petType.getHealth());
        pet.setHealth(PetManager.getHealth(petItem, maxHealth.getBaseValue()));

        AttributeInstance speedAttribute = pet.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
        assert speedAttribute != null;
        speedAttribute.setBaseValue(petType.getSpeed());

        pet.setMetadata(PetManager.METADATA_KEY_PET_OWNER, new FixedMetadataValue(RPGInventory.getInstance(), player.getUniqueId()));

        if (hasInitializationErrors) {
            Log.w("The pet ''{0}'' has errors on initialization and can be differ from expected result.", pet.getCustomName());
        }

        InventoryManager.get(player).setPet(pet);
    }

    public static void despawnPet(OfflinePlayer player) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        Inventory inventory = playerWrapper.getInventory();

        LivingEntity pet = playerWrapper.getPet();
        ItemStack petItem = inventory.getItem(SLOT_PET);

        if (pet == null) {
            return;
        }

        if (petItem != null) {
            PetManager.saveHealth(petItem, pet.getHealth());
            inventory.setItem(SLOT_PET, petItem);
        }

        EffectUtils.playDespawnEffect(pet);
        pet.remove();
        playerWrapper.setPet(null);
    }

    public static void despawnPet(Tameable petEntity) {
        if (!PetManager.isEnabled()) {
            return;
        }

        EffectUtils.playDespawnEffect(petEntity);
        petEntity.remove();
    }

    /**
     * Gets the owner of present LivingEntity, if he is exists.
     *
     * @param entity LivingEntity
     * @return if exists, the UUID of the Player that owning that entity, otherwise null
     * @since 2.1.7
     */
    @Nullable
    public static UUID getPetOwner(@NotNull LivingEntity entity) {
        if (!entity.hasMetadata(PetManager.METADATA_KEY_PET_OWNER)) {
            return null;
        }

        final List<MetadataValue> metadata = entity.getMetadata(PetManager.METADATA_KEY_PET_OWNER);
        return metadata.stream()
                .filter(value -> RPGInventory.getInstance().equals(value.getOwningPlugin()))
                .findFirst()
                .map(it -> (UUID) it.value())
                .orElse(null);
    }

    @Nullable
    @Contract("null -> null")
    public static PetFood getFoodFromItem(@Nullable ItemStack item) {
        String id;

        if (ItemUtils.isEmpty(item) || (id = ItemUtils.getTag(item, ItemUtils.FOOD_TAG)).isEmpty()) {
            return null;
        }

        return PET_FOOD.get(id);
    }

    @Nullable
    @Contract("null -> null")
    public static PetType getPetFromItem(@Nullable ItemStack item) {
        String id;

        if (ItemUtils.isEmpty(item) || (id = ItemUtils.getTag(item, ItemUtils.PET_TAG)).isEmpty()) {
            return null;
        }

        return PETS.get(id);
    }

    @Nullable
    public static PetType getPetFromEntity(@Nullable LivingEntity entity, OfflinePlayer player) {
        if (entity == null || !InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return null;
        }

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        return (entity == playerWrapper.getPet())
                ? PetManager.getPetFromItem(playerWrapper.getInventory().getItem(SLOT_PET))
                : null;
    }

    static void addGlow(@NotNull ItemMeta meta) {
        meta.addEnchant(Enchantment.DURABILITY, 88, true);
        meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
    }

    public static void saveDeathTime(@NotNull ItemStack item) {
        saveDeathTime(item, System.currentTimeMillis());
    }

    public static void saveDeathTime(@NotNull ItemStack item, long deathTime) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        if (deathTime == 0) {
            nbt.remove(DEATH_TIME_TAG);
        } else {
            nbt.put(DEATH_TIME_TAG, deathTime);
        }

        NbtFactoryMirror.setItemTag(item, nbt);
    }

    public static long getDeathTime(@NotNull ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return 0L;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item.clone());
        return nbt.containsKey(DEATH_TIME_TAG) ? nbt.getLong(DEATH_TIME_TAG) : 0L;
    }

    public static int getCooldown(@NotNull ItemStack item) {
        long deathTime = getDeathTime(item);
        if (deathTime == 0) {
            return 0;
        }

        PetType petFromItem = getPetFromItem(item);
        if (petFromItem == null) {
            return 0;
        }

        int secondsSinceDeath = (int) ((System.currentTimeMillis() - deathTime) / 1000);
        int petCooldown = petFromItem.getCooldown();
        int itemCooldown = petCooldown - secondsSinceDeath;
        if (itemCooldown < 0 || itemCooldown > petCooldown) {
            itemCooldown = 0;
        }

        if (itemCooldown == 0) {
            PetManager.saveDeathTime(item, 0);
        }

        return itemCooldown;
    }

    public static void saveHealth(@NotNull ItemStack item, double health) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);

        if (health == 0) {
            nbt.remove("pet.health");
        } else {
            nbt.put("pet.health", health);
        }

        NbtFactoryMirror.setItemTag(item, nbt);
    }

    public static double getHealth(ItemStack item, double maxHealth) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item.clone());

        if (!nbt.containsKey("pet.health")) {
            return maxHealth;
        }

        double health = nbt.getDouble("pet.health");
        return Math.min(health, maxHealth);
    }

    @Nullable
    public static ItemStack getPetItem(String petId) {
        PetType petType = PETS.get(petId);
        return petType == null ? null : petType.getSpawnItem();
    }

    @Nullable
    public static ItemStack getFoodItem(String petId) {
        PetFood food = PET_FOOD.get(petId);
        return food == null ? null : food.getFoodItem();
    }

    public static ItemStack toPetItem(ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return item;
        }

        List<String> itemLore = item.getItemMeta().getLore();
        for (PetType petType : PETS.values()) {
            List<String> petItemLore = petType.getSpawnItem().getItemMeta().getLore();
            if (itemLore.equals(petItemLore)) {
                return petType.getSpawnItem();
            }
        }

        return item;
    }

    public static boolean isPetItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item) || !item.hasItemMeta() || !item.getItemMeta().hasLore()) {
            return false;
        }

        List<String> itemLore = item.getItemMeta().getLore();
        for (PetType petType : PETS.values()) {
            List<String> petItemLore = petType.getSpawnItem().getItemMeta().getLore();
            if (itemLore.equals(petItemLore)) {
                return true;
            }
        }

        return false;
    }
} public class ResourcePackModule implements Listener {

    private static final int TICKS_IN_SECOND = 20;

    private final Plugin plugin;
    private final String resourcePackUrl;
    private final String resourcePackHash;

    private final List<UUID> loadList = new ArrayList<>();

    private ResourcePackModule(Plugin plugin, @NotNull String resourcePackUrl, String resourcePackHash) {
        this.plugin = plugin;
        this.resourcePackUrl = resourcePackUrl;
        this.resourcePackHash = resourcePackHash;
    }

    @Nullable
    public static ResourcePackModule init(Plugin plugin) {
        final FileConfiguration config = Config.getConfig();
        if (!config.getBoolean("resource-pack.enabled", false)) {
            Log.i("Resource-pack is disabled in config");
            return null;
        }

        final String rpUrl = Config.getConfig().getString("resource-pack.url");
        final String rpHash = Config.getConfig().getString("resource-pack.hash");
        ResourcePackValidator validator = new ResourcePackValidator();
        boolean isLegalUrlAndHash = validator.validateUrlAndHash(rpUrl, rpHash);
        printErrorsIfNotEmpty(validator.getErrors());
        if (!isLegalUrlAndHash) {
            Log.s("Resource-pack can not be enabled");
            return null;
        }

        ResourcePackModule resourcePackModule = new ResourcePackModule(plugin, rpUrl, rpHash);
        plugin.getServer().getPluginManager().registerEvents(resourcePackModule, plugin);
        ProtocolLibrary.getProtocolManager().addPacketListener(resourcePackModule.new ResourcePackPacketAdapter(plugin));
        return resourcePackModule;
    }

    private static void printErrorsIfNotEmpty(@NotNull List<String> messages) {
        if (messages.isEmpty()) {
            return;
        }

        Log.w("");
        Log.w("######### Something wrong with RP settings! ##########");
        for (String message : messages) {
            Log.w("# {0}", message);
        }
        Log.w("######################################################");
        Log.w("");
    }

    public void loadResourcePack(@NotNull Player player, boolean skipJoinMessage) {
        player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 60, 1));

        if (skipJoinMessage) {
            this.sendResourcePack(player);
        } else if (InventoryManager.isNewPlayer(player)) {
            if (!EffectUtils.showJoinMessage(player, "rp-info", () -> this.sendResourcePack(player))) {
                this.sendResourcePack(player);
            }
        } else {
            EffectUtils.showDefaultJoinMessage(player);
            this.sendResourcePack(player);
        }
    }

    private void sendResourcePack(@NotNull final Player player) {
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                player.setResourcePack(resourcePackUrl);
            }
        }.runTaskLater(this.plugin, TICKS_IN_SECOND);
    }

    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
    public void onTargetWhenPlayerNotLoaded(@NotNull EntityTargetLivingEntityEvent event) {
        if (event.getTarget() == null || event.getTarget().getType() != EntityType.PLAYER) {
            return;
        }

        if (!InventoryManager.playerIsLoaded((Player) event.getTarget())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.LOW)
    public void onPlayerInteractWhenNotLoaded(@NotNull PlayerInteractEvent event) {
        Player player = event.getPlayer();
        if (InventoryManager.isAllowedWorld(player.getWorld()) && !InventoryManager.playerIsLoaded(player)) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.rp.denied"));
            event.setCancelled(true);
        }
    }

    @EventHandler
    public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
        Player player = event.getPlayer();
        if (InventoryManager.isAllowedWorld(player.getWorld()) && !InventoryManager.playerIsLoaded(player)) {
            player.removePotionEffect(PotionEffectType.BLINDNESS);
            this.removePlayerFromLoadList(event.getPlayer());
        }
    }

    private void removePlayerFromLoadList(@NotNull Player player) {
        loadList.remove(player.getUniqueId());
    }


    class ResourcePackPacketAdapter extends PacketAdapter {

        ResourcePackPacketAdapter(Plugin plugin) {
            super(plugin, WrapperPlayClientResourcePackStatus.TYPE, WrapperPlayServerResourcePackSend.TYPE);
        }

        @Override
        public void onPacketSending(@NotNull PacketEvent event) {
            WrapperPlayServerResourcePackSend packet = new WrapperPlayServerResourcePackSend(event.getPacket());
            if (!packet.getUrl().equals(resourcePackUrl)) {
                return;
            }

            packet.setHash(resourcePackHash);
            final Player player = event.getPlayer();
            loadList.add(player.getUniqueId());
        }

        @Override
        public void onPacketReceiving(@NotNull PacketEvent event) {
            WrapperPlayClientResourcePackStatus packet = new WrapperPlayClientResourcePackStatus(event.getPacket());
            final Player player = event.getPlayer();
            if (loadList.contains(player.getUniqueId())) {
                switch (packet.getResult()) {
                    case ACCEPTED:
                        break;
                    case SUCCESSFULLY_LOADED:
                        onSuccessfullyLoaded(player);
                        break;
                    case DECLINED:
                    case FAILED_DOWNLOAD:
                        onFailedOrDeclined(player);
                }
            }
        }

        private void onSuccessfullyLoaded(@NotNull Player player) {
            final UUID playerId = player.getUniqueId();
            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    InventoryManager.loadPlayerInventory(player);
                    player.removePotionEffect(PotionEffectType.BLINDNESS);
                }
            }.runTaskLater(this.plugin, 1);
            loadList.remove(playerId);
        }

        private void onFailedOrDeclined(@NotNull Player player) {
            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    player.kickPlayer(RPGInventory.getLanguage().getMessage("error.rp.denied"));
                }
            }.runTaskLater(this.plugin, TICKS_IN_SECOND);
            loadList.remove(player.getUniqueId());
        }
    }
} @SuppressWarnings("CheckStyle")
public final class Log {

    private static Logger logger;

    private Log() {
        // static class
    }

    public static void init(@NotNull Logger logger) {
        Log.logger = logger;
    }

    public static void i(@NotNull String message, Object... args) {
        logger.info(prepareMessage(message, args));
    }

    public static void w(Throwable t) {
        logger.log(Level.WARNING, t.getMessage(), t);
    }

    public static void w(@NotNull String message, Object... args) {
        logger.warning(prepareMessage(message, args));
    }

    public static void w(Throwable t, @NotNull String message, Object... args) {
        logger.log(Level.WARNING, prepareMessage(message, args), t);
    }

    public static void d(Throwable t) {
        logger.log(Level.FINE, "", t);
    }

    public static void s(@NotNull String message, Object... args) {
        logger.severe(prepareMessage(message, args));
    }

    private static String prepareMessage(String message, Object... args) {
        return MessageFormat.format(message, args);
    }
} public class PlayerUtils {
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean checkLevel(@NotNull Player player, int required) {
        BukkitLevelSystem levelSystem = RPGInventory.getLevelSystem(player);
        return levelSystem.didReachLevel(required);
    }

    public static boolean checkClass(@NotNull Player player, @NotNull List<String> classes) {
        BukkitClassSystem classSystem = RPGInventory.getClassSystem(player);
        return classSystem.hasAnyOfClasses(classes);
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean checkMoney(@NotNull Player player, double cost) {
        double balance = (RPGInventory.economyConnected() ? RPGInventory.getEconomy().getBalance(player) : 0);
        if (balance < cost) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.money", StringUtils.doubleToString(cost - balance)));
            return false;
        }

        return true;
    }

    public static void updateInventory(@NotNull final Player player) {
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                player.updateInventory();
            }
        }.runTaskLater(RPGInventory.getInstance(), 1);
    }

    public static void sendMessage(@NotNull Player player, String message) {
        if (InventoryManager.playerIsLoaded(player)) {
            PlayerWrapper wrapper = InventoryManager.get(player);

            if (wrapper.getLastMessage().equals(message)) {
                return;
            }

            wrapper.setLastMessage(message);
        }

        player.sendMessage(message);
    }
} public class StringUtils {
    @NotNull
    public static String coloredLine(@NotNull String line) {
        return ChatColor.translateAlternateColorCodes('&', line);
    }

    @NotNull
    public static List<String> coloredLines(@Nullable List<String> lines) {
        if (lines == null) {
            return Collections.emptyList();
        }

        List<String> coloredLines = new ArrayList<>(lines.size());
        for (String line : lines) {
            coloredLines.add(StringUtils.coloredLine(line));
        }

        return coloredLines;
    }

    public static String doubleToString(double value) {
        return value == (long) value ? String.format("%d", (long) value) : String.format("%s", value);
    }

    public static void coloredConsole(String message) {
        Bukkit.getServer().getConsoleSender().sendMessage(message);
    }

    @NotNull
    public static String setPlaceholders(@NotNull Player player, @NotNull String line) {
        if (RPGInventory.isPlaceholderApiHooked()) {
            try {
                return PlaceholderAPI.setPlaceholders(player, line);
            } catch (Throwable e) {
                Log.w("Can''t set placeholders for line \"{0}\"", line);
                Log.w("Make sure that you use the latest version of PlaceholderAPI.");
                Log.w(e, "If it is, please report about the error to PlaceholderAPI author:");
                return line;
            }
        }

        return line;
    }

    public static class Placeholders extends PlaceholderExpansion {
        @Override
        public String getIdentifier() {
            return "rpginv";
        }

        @Override
        public String getAuthor() {
            return "osipxd";
        }

        @Override
        public String getVersion() {
            return "1.0";
        }

        @Override
        public boolean canRegister() {
            return true;
        }

        @Override
        public String onPlaceholderRequest(@Nullable Player player, @NotNull String identifier) {
            if (!InventoryManager.playerIsLoaded(player)) {
                return null;
            }

            switch (identifier) {
                case "damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.DAMAGE).toString();
                case "bow_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.BOW_DAMAGE).toString();
                case "hand_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.HAND_DAMAGE).toString();
                case "crit_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.CRIT_DAMAGE).toString();
                case "crit_chance":
                    return ItemManager.getModifier(player, ItemStat.StatType.CRIT_CHANCE).toString();
                case "armor_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.ARMOR).toString();
                case "speed_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.SPEED).toString();
                case "jump_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.JUMP).toString();
            }

            return null;
        }
    }
} public final class Version implements Comparable<Version> {

    private final int major;
    private final int minor;
    private final int patch;
    private final String qualifier;

    /**
     * Constructs Version object from the given data according to semantic versioning.
     *
     * @param major Major version.
     * @param minor Minor version.
     * @param patch Patch version.
     */
    public Version(int major, int minor, int patch) {
        this(major, minor, patch, "");
    }

    /**
     * Constructs Version object from the given data according to semantic versioning.
     *
     * @param major     Major version.
     * @param minor     Minor version.
     * @param patch     Patch version.
     * @param qualifier Qualifier, or empty string if qualifier not exists.
     * @throws IllegalArgumentException when passed negative version codes.
     */
    public Version(int major, int minor, int patch, @NotNull String qualifier) {
        if (major < 0 || minor < 0 || patch < 0) {
            throw new IllegalArgumentException("Version can't include negative numbers");
        }

        this.major = major;
        this.minor = minor;
        this.patch = patch;
        this.qualifier = Objects.requireNonNull(qualifier, "'qualifier' shouldn't be null");
    }

    /**
     * Parses version from the given string.
     *
     * @param version String representation of version.
     * @return The constructed version.
     * @throws IllegalArgumentException If passed string with wrong format.
     */
    @NotNull
    public static Version parseVersion(@NotNull String version) {
        String[] parts = version.split("-", 2);
        String qualifier = (parts.length > 1) ? parts[1] : "";
        parts = parts[0].split("\\.", 3);

        try {
            int patch = (parts.length > 2) ? Integer.parseInt(parts[2]) : 0;
            int minor = (parts.length > 1) ? Integer.parseInt(parts[1]) : 0;
            int major = Integer.parseInt(parts[0]);

            return new Version(major, minor, patch, qualifier);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(String.format("Can not parse version string: %s", version), e);
        }
    }

    public int compareTo(String version) {
        return this.compareTo(Version.parseVersion(version));
    }

    @Override
    public int compareTo(@NotNull Version o) {
        int result;
        if ((result = Integer.compare(major, o.major)) != 0
                || (result = Integer.compare(minor, o.minor)) != 0
                || (result = Integer.compare(patch, o.patch)) != 0) {
            // TODO: Maybe compare also qualifiers
            return result;
        }

        return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o || o == null || getClass() != o.getClass()) {
            return true;
        }

        Version version = (Version) o;
        return major == version.major
                && minor == version.minor
                && patch == version.patch
                && Objects.equals(qualifier, version.qualifier);
    }

    @Override
    public int hashCode() {
        return Objects.hash(major, minor, patch, qualifier);
    }

    @Override
    public String toString() {
        return String.format("%d.%d.%d%s", major, minor, patch, qualifier.isEmpty() ? "" : "-" + qualifier);
    }

    public int getMajor() {
        return major;
    }

    public int getMinor() {
        return minor;
    }

    public int getPatch() {
        return patch;
    }

    @NotNull
    public String getQualifier() {
        return qualifier;
    }

    /**
     * Returns version code in format xxyyzz, where x - major version, y - minor and z - patch.
     * Example:
     *   1.12.2  ->  11202
     *   21.0.12 -> 210012
     *   1.9     ->  10900
     *   2.2.1   ->  20201
     * Major, minor and patch versions shouldn't be higher than 99.
     */
    public int getVersionCode() {
        return major * 10000 + minor * 100 + patch;
    }

    /**
     * Checks that version has qualifier.
     *
     * @return true if version contains qualifier, otherwise false.
     */
    public boolean hasQualifier() {
        return !qualifier.isEmpty();
    }
}
import com.comphenix.protocol.ProtocolLibrary;
import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.permission.Permission;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.ServicesManager;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.endlesscode.inspector.bukkit.command.TrackedCommandExecutor;
import ru.endlesscode.inspector.bukkit.plugin.PluginLifecycle;
import ru.endlesscode.inspector.bukkit.scheduler.TrackedBukkitRunnable;
import ru.endlesscode.mimic.MimicApiLevel;
import ru.endlesscode.mimic.classes.BukkitClassSystem;
import ru.endlesscode.mimic.items.BukkitItemsRegistry;
import ru.endlesscode.mimic.level.BukkitLevelSystem;
import ru.endlesscode.rpginventory.compat.VersionHandler;
import ru.endlesscode.rpginventory.compat.mimic.RPGInventoryItemsRegistry;
import ru.endlesscode.rpginventory.compat.mypet.MyPetManager;
import ru.endlesscode.rpginventory.event.listener.*;
import ru.endlesscode.rpginventory.inventory.InventoryLocker;
import ru.endlesscode.rpginventory.inventory.InventoryManager;
import ru.endlesscode.rpginventory.inventory.backpack.BackpackManager;
import ru.endlesscode.rpginventory.inventory.craft.CraftManager;
import ru.endlesscode.rpginventory.inventory.slot.SlotManager;
import ru.endlesscode.rpginventory.item.ItemManager;
import ru.endlesscode.rpginventory.misc.FileLanguage;
import ru.endlesscode.rpginventory.misc.Updater;
import ru.endlesscode.rpginventory.misc.config.Config;
import ru.endlesscode.rpginventory.misc.config.ConfigUpdater;
import ru.endlesscode.rpginventory.misc.serialization.Serialization;
import ru.endlesscode.rpginventory.pet.PetManager;
import ru.endlesscode.rpginventory.resourcepack.ResourcePackModule;
import ru.endlesscode.rpginventory.utils.Log;
import ru.endlesscode.rpginventory.utils.PlayerUtils;
import ru.endlesscode.rpginventory.utils.StringUtils;
import ru.endlesscode.rpginventory.utils.Version;
import java.nio.file.Path;

    @Contract(pure = true)
    public static boolean isMyPetHooked() {
        return instance.myPetHooked;
    }

    public static BukkitLevelSystem getLevelSystem(@NotNull Player player) {
        return instance.levelSystemProvider.getSystem(player);
    }

    public static BukkitClassSystem getClassSystem(@NotNull Player player) {
        return instance.classSystemProvider.getSystem(player);
    }

    @Nullable
    public static ResourcePackModule getResourcePackModule() {
        return instance.resourcePackModule;
    }

    @Override
    public void init() {
        instance = this;
        Log.init(this.getLogger());
        Config.init(this);
    }

    @Override
    public void onLoad() {
        if (checkMimic()) {
            getServer()
                    .getServicesManager()
                    .register(BukkitItemsRegistry.class, new RPGInventoryItemsRegistry(), this, ServicePriority.High);
        }
    }

    @Override
    public void onEnable() {
        if (!initMimicSystems()) {
            return;
        }

        loadConfigs();
        Serialization.registerTypes();

        if (!this.checkRequirements()) {
            this.getPluginLoader().disablePlugin(this);
            return;
        }

        PluginManager pm = this.getServer().getPluginManager();

        // Hook Placeholder API
        if (pm.isPluginEnabled("PlaceholderAPI")) {
            new StringUtils.Placeholders().register();
            placeholderApiHooked = true;
            Log.i("Placeholder API hooked!");
        } else {
            placeholderApiHooked = false;
        }

        loadModules();

        this.loadPlayers();
        this.startMetrics();

        // Enable commands
        this.getCommand("rpginventory")
                .setExecutor(new TrackedCommandExecutor(new RPGInventoryCommandExecutor(), getReporter()));

        this.checkUpdates(null);
    }

    private boolean initMimicSystems() {
        boolean isMimicFound = checkMimic();
        if (isMimicFound) {
            ServicesManager servicesManager = getServer().getServicesManager();
            this.levelSystemProvider = servicesManager.load(BukkitLevelSystem.Provider.class);
            Log.i("Level system ''{0}'' found.", this.levelSystemProvider.getId());
            this.classSystemProvider = servicesManager.load(BukkitClassSystem.Provider.class);
            Log.i("Class system ''{0}'' found.", this.classSystemProvider.getId());
        } else {
            Log.s("Mimic is required for RPGInventory to use levels and classes from other RPG plugins!");
            Log.s("Download it from SpigotMC: https://www.spigotmc.org/resources/82515/");
            getServer().getPluginManager().disablePlugin(this);
        }
        return isMimicFound;
    }

    void reload() {
        // Unload
        saveData();
        removeListeners();

        // Load
        loadConfigs();
        loadModules();
        loadPlayers();
    }

    private void loadConfigs() {
        this.updateConfig();
        Config.reload();
        language = new FileLanguage(this);
    }

    private void loadModules() {
        PluginManager pm = getServer().getPluginManager();

        // Hook MyPet
        if (pm.isPluginEnabled("MyPet") && MyPetManager.init(this)) {
            myPetHooked = true;
            Log.i("MyPet used as pet system");
        } else {
            myPetHooked = false;
            Log.i(PetManager.init(this) ? "Pet system is enabled" : "Pet system isn''t loaded");
        }

        // Load modules
        Log.i(CraftManager.init(this) ? "Craft extensions is enabled" : "Craft extensions isn''t loaded");
        Log.i(InventoryLocker.init(this) ? "Inventory lock system is enabled" : "Inventory lock system isn''t loaded");
        Log.i(ItemManager.init(this) ? "Item system is enabled" : "Item system isn''t loaded");