public class RPGInventory extends PluginLifecycle {
    private static RPGInventory instance;

    private Permission perms;
    private Economy economy;

    private BukkitLevelSystem.Provider levelSystemProvider;
    private BukkitClassSystem.Provider classSystemProvider;

    private FileLanguage language;
    private boolean placeholderApiHooked = false;
    private boolean myPetHooked = false;
    private ResourcePackModule resourcePackModule = null;

    public static RPGInventory getInstance() {
        return instance;
    }

    public static FileLanguage getLanguage() {
        return instance.language;
    }

    public static Permission getPermissions() {
        return instance.perms;
    }

    public static Economy getEconomy() {
        return instance.economy;
    }

    @Contract(pure = true)
    public static boolean economyConnected() {
        return instance.economy != null;
    }

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

    @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");
        Log.i(BackpackManager.init(this) ? "Backpack system is enabled" : "Backpack system isn''t loaded");

        // Registering other listeners
        pm.registerEvents(new ArmorEquipListener(), this);
        pm.registerEvents(new HandSwapListener(), this);
        pm.registerEvents(new PlayerListener(), this);
        pm.registerEvents(new WorldListener(), this);

        if (SlotManager.instance().getElytraSlot() != null) {
            pm.registerEvents(new ElytraListener(), this);
        }
        this.resourcePackModule = ResourcePackModule.init(this);
    }

    private void removeListeners() {
        ProtocolLibrary.getProtocolManager().removePacketListeners(this);
        HandlerList.unregisterAll(this);
    }

    private boolean checkMimic() {
        if (getServer().getPluginManager().getPlugin("Mimic") == null) {
            return false;
        } else if (MimicApiLevel.checkApiLevel(MimicApiLevel.VERSION_0_6)) {
            return true;
        } else {
            Log.w("At least Mimic 0.6.1 required for RPGInventory.");
            return false;
        }
    }

    private boolean checkRequirements() {
        // Check if plugin is enabled
        if (!Config.getConfig().getBoolean("enabled")) {
            Log.w("RPGInventory is disabled in the config!");
            return false;
        }

        // Check version compatibility
        if (VersionHandler.isNotSupportedVersion()) {
            Log.w("This version of RPG Inventory is not tested with \"{0}\"!", Bukkit.getBukkitVersion());
        } else if (VersionHandler.isExperimentalSupport()) {
            Log.w("Support of {0} is experimental! Use RPGInventory with caution.", Bukkit.getBukkitVersion());
        }

        // Check dependencies
        if (this.setupPermissions()) {
            Log.i("Permissions hooked: {0}", perms.getName());
        } else {
            Log.s("Permissions not found!");
            return false;
        }

        if (this.setupEconomy()) {
            Log.i("Economy hooked: {0}", economy.getName());
        } else {
            Log.w("Economy not found!");
        }

        return InventoryManager.init(this) && SlotManager.init();
    }

    @Override
    public void onDisable() {
        saveData();
    }

    private void saveData() {
        BackpackManager.saveBackpacks();
        this.savePlayers();
    }

    private void startMetrics() {
        new Metrics(holder, 4210);
    }

    private void savePlayers() {
        if (this.getServer().getOnlinePlayers().size() == 0) {
            return;
        }

        Log.i("Saving players inventories...");
        for (Player player : this.getServer().getOnlinePlayers()) {
            InventoryManager.unloadPlayerInventory(player);
        }
    }

    private void loadPlayers() {
        if (this.getServer().getOnlinePlayers().size() == 0) {
            return;
        }

        Log.i("Loading players inventories...");
        for (Player player : this.getServer().getOnlinePlayers()) {
            InventoryManager.loadPlayerInventory(player);
        }
    }

    private boolean setupPermissions() {
        RegisteredServiceProvider<Permission> permissionProvider = getServer().getServicesManager().getRegistration(Permission.class);
        if (permissionProvider != null) {
            perms = permissionProvider.getProvider();
        }

        return perms != null;
    }

    private boolean setupEconomy() {
        RegisteredServiceProvider<Economy> rsp = this.getServer().getServicesManager().getRegistration(Economy.class);
        if (rsp != null) {
            economy = rsp.getProvider();
        }

        return economy != null;
    }

    public void checkUpdates(@Nullable final Player player) {
        if (!Config.getConfig().getBoolean("check-update")) {
            return;
        }

        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                Updater updater = new Updater(RPGInventory.instance, Updater.UpdateType.NO_DOWNLOAD);
                if (updater.getResult() == Updater.UpdateResult.UPDATE_AVAILABLE) {
                    String[] lines = {
                            StringUtils.coloredLine("&3=================&b[&eRPGInventory&b]&3==================="),
                            StringUtils.coloredLine("&6New version available: &a" + updater.getLatestName() + "&6!"),
                            StringUtils.coloredLine(updater.getDescription()),
                            StringUtils.coloredLine("&6Changelog: &e" + updater.getInfoLink()),
                            StringUtils.coloredLine("&6Download it on &eSpigotMC&6!"),
                            StringUtils.coloredLine("&3==================================================")
                    };

                    for (String line : lines) {
                        if (player == null) {
                            StringUtils.coloredConsole(line);
                        } else {
                            PlayerUtils.sendMessage(player, line);
                        }
                    }
                }
            }
        }.runTaskAsynchronously(RPGInventory.getInstance());
    }

    private void updateConfig() {
        final Version version = Version.parseVersion(this.getDescription().getVersion());

        if (!Config.getConfig().contains("version")) {
            Config.getConfig().set("version", version.toString());
            Config.save();
            return;
        }

        final Version configVersion = Version.parseVersion(Config.getConfig().getString("version"));
        if (version.compareTo(configVersion) > 0) {
            ConfigUpdater.update(configVersion);
            Config.getConfig().set("version", null);
            Config.getConfig().set("version", version.toString());
            Config.save();
        }
    }

    @NotNull
    public Path getDataPath() {
        return getDataFolder().toPath();
    }
} public class MaterialCompat {
    @NotNull
    public static Material getMaterial(String name) {
        return Objects.requireNonNull(getMaterialOrNull(name));
    }

    @NotNull
    public static Material getMaterialOrAir(String name) {
        Material material = getMaterialOrNull(name);
        if (material == null) {
            return Material.AIR;
        } else {
            return material;
        }
    }

    @Nullable
    public static Material getMaterialOrNull(String name) {
        Material material = Material.getMaterial(name);
        if (material == null && !VersionHandler.isLegacy()) {
            material = Material.getMaterial(name, true);
        }

        return material;
    }
} 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 PetListener implements Listener {

    @EventHandler(ignoreCancelled = true)
    public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
        if (!(event.getRightClicked() instanceof LivingEntity)) {
            return;
        }

        //Avoid using owner's pet by other players
        final LivingEntity rightClicked = (LivingEntity) event.getRightClicked();
        final UUID petOwner = PetManager.getPetOwner(rightClicked);
        if (petOwner == null) {
            return;
        }
        if (!event.getPlayer().getUniqueId().equals(petOwner)) {
            event.setCancelled(true);
        }
    }

    @EventHandler
    public void onItemUse(@NotNull PlayerInteractEvent event) {
        Player player = event.getPlayer();

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

        Inventory inventory = InventoryManager.get(player).getInventory();
        ItemStack petItem = event.getItem();

        if (player.getGameMode() == GameMode.CREATIVE && PetManager.isPetItem(petItem)) {
            petItem = PetManager.toPetItem(petItem);
            player.getEquipment().setItemInMainHand(petItem);
        }

        if (PetType.isPetItem(petItem)
                && (event.getAction() == Action.RIGHT_CLICK_BLOCK || event.getAction() == Action.RIGHT_CLICK_AIR)) {
            Slot petSlot = SlotManager.instance().getPetSlot();
            if (petSlot != null && petSlot.isCup(inventory.getItem(PetManager.getPetSlotId()))
                    && ItemManager.allowedForPlayer(player, petItem, false)) {
                inventory.setItem(PetManager.getPetSlotId(), event.getItem());
                PetManager.respawnPet(player, petItem);
                player.getEquipment().setItemInMainHand(null);
            }

            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onPlayerDeath(@NotNull PlayerDeathEvent event) {
        Player player = event.getEntity();

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

        PetManager.despawnPet(player);
    }

    @EventHandler
    public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) {
        Player player = event.getPlayer();

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

        PetManager.respawnPet(player);
    }

    //Possible fix #110
    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
    public void onPlayerTeleport(@NotNull PlayerTeleportEvent event) {
        Player player = event.getPlayer();

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

        if (!InventoryManager.get(player).hasPet()) {
            return;
        }

        final Location from = event.getFrom();
        final Location to = event.getTo();

        // Avoid case when pet and player are in different worlds
        if (!from.getWorld().getName().equals(to.getWorld().getName())) {
            PetManager.respawnPet(player);
            return;
        }

        // Ugly trick to avoid infinite pet spawning when player teleports from non-solid/non-cuboid block
        if (from.getBlockX() != to.getBlockX() || from.getBlockZ() != to.getBlockZ() || from.distance(to) < 0.775D) {
            return;
        }

        final double maxDistance = (event.getPlayer().getServer().getViewDistance() / 2.0D) * 15.75D;
        final ItemStack item = InventoryManager.get(player).getInventory().getItem(PetManager.getPetSlotId());
        if (from.distance(to) > maxDistance && item != null) {
            PetManager.respawnPet(player);
        } else if (LocationUtils.isSafeLocation(player.getLocation())) {
            PetManager.teleportPet(player, to);
        }
    }

    @EventHandler
    public void onPetFeed(@NotNull PlayerInteractEntityEvent event) {
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getEquipment().getItemInMainHand();

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

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        if (PetFood.isFoodItem(itemInHand) && playerWrapper.hasPet() && event.getRightClicked() == playerWrapper.getPet()) {
            event.setCancelled(true);

            LivingEntity pet = (LivingEntity) event.getRightClicked();
            PetFood petFood = PetManager.getFoodFromItem(itemInHand);

            if (pet.getHealth() == getMaxHealth(pet) || petFood == null || !petFood.canBeEaten(playerWrapper.getPet())) {
                return;
            }

            double health = pet.getHealth() + petFood.getValue();
            pet.setHealth(Math.min(health, getMaxHealth(pet)));
            itemInHand.setAmount(itemInHand.getAmount() - 1);
            player.getEquipment().setItemInMainHand(itemInHand);

            pet.getWorld().playSound(pet.getLocation(), SoundCompat.EAT.get(), 1.0f, (float) (1.0 + Math.random() * 0.4));
        }
    }

    private double getMaxHealth(LivingEntity pet) {
        AttributeInstance maxHealthAttribute = pet.getAttribute(Attribute.GENERIC_MAX_HEALTH);
        Objects.requireNonNull(maxHealthAttribute);
        return maxHealthAttribute.getValue();
    }

    @EventHandler
    public void onPetDeath(@NotNull EntityDeathEvent event) {
        if (event.getEntity() == null || PetManager.getPetOwner(event.getEntity()) == null) {
            return;
        }

        LivingEntity petEntity = event.getEntity();
        final Player player = Bukkit.getPlayer(PetManager.getPetOwner(petEntity));

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

        final PlayerWrapper playerWrapper = InventoryManager.get(player);
        if (petEntity == playerWrapper.getPet()) {
            Inventory inventory = playerWrapper.getInventory();
            ItemStack petItem = inventory.getItem(PetManager.getPetSlotId());
            PetType petType = PetManager.getPetFromItem(petItem);

            if (petType != null && petType.isRevival()) {
                PetManager.saveDeathTime(petItem);
                PetManager.saveHealth(petItem, 0);
                inventory.setItem(PetManager.getPetSlotId(), petItem);
                PetManager.startCooldownTimer(player.getPlayer(), petItem);
            } else {
                inventory.setItem(PetManager.getPetSlotId(), null);
            }

            event.getDrops().clear();
            playerWrapper.setPet(null);
        }
    }

    @EventHandler
    public void onTarget(@NotNull EntityTargetLivingEntityEvent event) {
        if (event.getEntity() == null
                || !(event.getEntity() instanceof Tameable)
                || !(event.getEntity() instanceof LivingEntity)
                || event.getTarget() == null
                || !InventoryManager.isAllowedWorld(event.getTarget().getWorld())) {
            return;
        }

        LivingEntity petEntity = (LivingEntity) event.getEntity();
        final OfflinePlayer player = (OfflinePlayer) ((Tameable) petEntity).getOwner();

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

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        if (playerWrapper.hasPet() && petEntity == playerWrapper.getPet()) {
            PetType petType = PetManager.getPetFromEntity(petEntity, player);

            if (petType == null) {
                return;
            }

            if (event.getTarget() != null && event.getTarget().getType() == EntityType.PLAYER) {
                event.setCancelled(!petType.isAttackPlayers());
            } else {
                event.setCancelled(!petType.isAttackMobs());
            }
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onAttack(@NotNull EntityDamageByEntityEvent event) {
        if (event.getEntity() == null
                || event.getDamager() == null
                || !InventoryManager.isAllowedWorld(event.getEntity().getWorld())) {
            return;
        }

        Player player = null;
        if (event.getDamager().getType() == EntityType.PLAYER) {
            player = (Player) event.getDamager();
        } else if (event.getDamager() instanceof Projectile) {
            Projectile projectile = (Projectile) event.getDamager();
            if (projectile.getShooter() instanceof Player) {
                player = (Player) projectile.getShooter();
            }
        }

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

        //Trying to get a pet as damager
        if (event.getDamager() instanceof LivingEntity && event.getDamager() instanceof Tameable) {
            //Casting to Tameable because we don't need in LivingEntity
            final Tameable petEntity = (Tameable) event.getDamager();
            final AnimalTamer petOwner = petEntity.getOwner();
            if (petOwner != null) {
                OfflinePlayer petOwnerPlayer = (OfflinePlayer) petOwner;
                PetType petType = PetManager.getPetFromEntity((LivingEntity) petEntity, petOwnerPlayer);
                if (petType != null) {
                    if (petOwnerPlayer.isOnline()) {
                        event.setDamage(petType.getDamage());
                    } else {
                        PetManager.despawnPet(petEntity);
                    }
                }
            }
            //or as damage reciever
        } else if (event.getEntity() instanceof LivingEntity && event.getEntity() instanceof Tameable
                && !Config.getConfig().getBoolean("attack.own-pet") && player != null) {
            final Tameable petEntity = (Tameable) event.getEntity();
            final AnimalTamer petOwner = petEntity.getOwner();
            if (petOwner != null && player.getUniqueId().equals(petOwner.getUniqueId())) {
                event.setCancelled(true);
            }
        } else if (player != null) {
            PlayerWrapper wrapper = InventoryManager.get(player);
            LivingEntity pet = wrapper.getPet();

            if (pet != null && pet.getType() == EntityType.WOLF && player != ((Wolf) pet).getTarget()) {
                Location target = player.getLocation();
                if (target.distance(pet.getLocation()) > 20) {
                    PetManager.respawnPet(player);
                }
            }
        }
    }

    @EventHandler
    public void onMountPet(@NotNull VehicleEnterEvent event) {
        if (event.getEntered().getType() != EntityType.PLAYER || event.getVehicle().getType() != EntityType.HORSE) {
            return;
        }

        Player player = (Player) event.getEntered();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        Horse vehicle = (Horse) event.getVehicle();
        if (PetManager.getPetFromEntity(vehicle, player) != null && player != vehicle.getOwner()) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.mount.owner", vehicle.getOwner().getName()));
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onWorldChanged(@NotNull EntityPortalEnterEvent event) {
        if (event.getEntity() == null
                || !(event.getEntity() instanceof Tameable)
                || !(event.getEntity() instanceof LivingEntity)) {
            return;
        }

        OfflinePlayer player = (OfflinePlayer) ((Tameable) event.getEntity()).getOwner();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        if (event.getEntity() == playerWrapper.getPet()) {
            PetManager.respawnPet(player);
        }
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onPetInventoryOpened(@NotNull InventoryOpenEvent event) {
        Player player = (Player) event.getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        final PlayerWrapper playerWrapper = InventoryManager.get(player);
        final LivingEntity petEntity = InventoryManager.get(player).getPet();
        if (petEntity != null && event.getInventory().getHolder() == petEntity) {
            playerWrapper.openInventory();
            event.setCancelled(true);

            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    HorseInventory horseInv = ((Horse) petEntity).getInventory();
                    horseInv.setSaddle(new ItemStack(Material.SADDLE));
                }
            }.runTaskLater(RPGInventory.getInstance(), 1);
        }
    }

    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
    public void onPlayerMove(@NotNull PlayerMoveEvent event) {
        Player player = event.getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        LivingEntity petEntity = InventoryManager.get(player).getPet();
        if (petEntity != null && !petEntity.getPassengers().isEmpty() && petEntity.getPassengers().get(0) != player) {
            PetType pet = PetManager.getPetFromEntity(petEntity, player);
            if (pet != null && pet.getRole() != PetType.Role.COMPANION) {
                EntityUtils.goPetToPlayer(player, petEntity);
            }
        }
    }
} 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 PlayerWrapper implements InventoryHolder {

    private final OfflinePlayer player;
    private final Inventory inventory;
    private final Map<String, Integer> buyedSlots = new HashMap<>();
    private final List<String> permissions = new ArrayList<>();

    @Nullable
    private InventoryView inventoryView;
    @Nullable
    private Slot slotPreparedToBuy = null;
    private long timeWhenPreparedToBuy = 0;
    @Nullable
    private Backpack backpack = null;
    private LivingEntity pet;

    @Nullable
    private ItemStack savedChestplate = null;
    private boolean falling = false;
    private boolean flying = false;
    private int fallTime = 0;

    private String lastMessage = "";
    private long lastMessageTime = 0;
    private boolean pocketCraft = false;

    public PlayerWrapper(OfflinePlayer player) {
        this.player = player;
        this.inventory = Bukkit.createInventory(this, 54, InventoryManager.TITLE);
    }

    @NotNull
    @Override
    public Inventory getInventory() {
        return inventory;
    }

    @Nullable
    public InventoryView getInventoryView() {
        return inventoryView;
    }

    public void openInventoryDeferred(boolean softOpen) {
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                openInventory(softOpen);
            }
        }.runTaskLater(RPGInventory.getInstance(), 0);
    }

    public void openInventory() {
        this.openInventory(false);
    }

    public void openInventory(boolean softOpen) {
        Player player = this.player.getPlayer();
        if (player == null) {
            return;
        }

        if (player.getGameMode() == GameMode.CREATIVE) {
            player.sendMessage(RPGInventory.getLanguage().getMessage("error.creative"));
            return;
        }

        if (!softOpen) {
            player.closeInventory();
        }

        this.inventoryView = player.getOpenInventory();
        player.openInventory(this.inventory);
    }

    void prepareToBuy(Slot slot) {
        this.slotPreparedToBuy = slot;
        this.timeWhenPreparedToBuy = System.currentTimeMillis();
    }

    public boolean isOpened() {
        return this.inventoryView != null;
    }

    public void onClose() {
        this.inventoryView = null;
    }

    public int getBuyedGenericSlots() {
        return this.buyedSlots.get("{generic}");
    }

    public void setBuyedSlots(int buyedSlots) {
        this.setBuyedSlots("{generic}", buyedSlots);
    }

    public void setBuyedSlots(String slotType) {
        this.setBuyedSlots(slotType, 1);
    }

    private void setBuyedSlots(String slotType, int buyedSlots) {
        this.buyedSlots.put(slotType, buyedSlots);
    }

    public boolean isBuyedSlot(String slotType) {
        return this.buyedSlots.containsKey(slotType);
    }

    boolean isPreparedToBuy(Slot slot) {
        if (this.timeWhenPreparedToBuy == 0 || this.slotPreparedToBuy != slot
                || System.currentTimeMillis() - this.timeWhenPreparedToBuy > 10 * 1000) {
            return false;
        }

        this.timeWhenPreparedToBuy = 0;
        this.slotPreparedToBuy = null;
        return true;
    }

    public void addPermissions(@NotNull List<String> permissions) {
        try {
            for (String permission : permissions) {
                if (RPGInventory.getPermissions().playerAdd(this.player.getPlayer(), permission)) {
                    this.permissions.add(permission);
                }
            }
        } catch (Exception e) {
            Log.w(e);
        }
    }

    private void clearPermissions() {
        try {
            for (String permission : this.permissions) {
                RPGInventory.getPermissions().playerRemove(this.player.getPlayer(), permission);
            }

            this.permissions.clear();
        } catch (Exception e) {
            Log.w(e);
        }
    }

    private void clearStats() {
        Player player = this.player.getPlayer();

        AttributeInstance speedAttribute = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
        assert speedAttribute != null;
        AttributeModifier rpgInvModifier = null;
        for (AttributeModifier modifier : speedAttribute.getModifiers()) {
            if (modifier.getUniqueId().compareTo(Attributes.SPEED_MODIFIER_ID) == 0) {
                rpgInvModifier = modifier;
            }
        }

        if (rpgInvModifier != null) {
            speedAttribute.removeModifier(rpgInvModifier);
        }
    }

    @Nullable
    public Backpack getBackpack() {
        return this.backpack;
    }

    public void setBackpack(@Nullable Backpack backpack) {
        this.backpack = backpack;
    }

    @Nullable
    public LivingEntity getPet() {
        return pet;
    }

    public void setPet(LivingEntity pet) {
        this.pet = pet;
    }

    public boolean hasPet() {
        return pet != null;
    }

    public OfflinePlayer getPlayer() {
        return player;
    }

    public void onFall() {
        if (isFalling()) {
            if (this.fallTime == 0) {
                this.startFlight();
            }
            this.fallTime++;
        } else {
            this.setFalling(true);
        }
    }

    private void startFlight() {
        Slot elytraSlot = SlotManager.instance().getElytraSlot();
        if (elytraSlot == null) {
            return;
        }
        ItemStack itemStack = this.inventory.getItem(elytraSlot.getSlotId());
        if (!elytraSlot.isCup(itemStack)) {
            Player player = this.player.getPlayer();
            ItemStack chestplate = player.getEquipment().getChestplate();
            this.savedChestplate = ItemUtils.isEmpty(chestplate) ? new ItemStack(Material.AIR) : chestplate;
            player.getEquipment().setChestplate(this.inventory.getItem(elytraSlot.getSlotId()));

            this.flying = true;
        }
    }

    public void onStartGliding() {
        if (this.hasPet()) {
            PetManager.despawnPet(player);
        }
    }

    public boolean isFalling() {
        return falling;
    }

    public void setFalling(boolean falling) {
        if (!falling && flying) {
            stopFlight();
        }

        fallTime = 0;
        this.falling = falling;
    }

    private void stopFlight() {
        Slot elytraSlot = SlotManager.instance().getElytraSlot();
        if (savedChestplate != null && elytraSlot != null) {
            Player player = this.player.getPlayer();
            this.inventory.setItem(elytraSlot.getSlotId(), player.getEquipment().getChestplate());
            player.getEquipment().setChestplate(this.savedChestplate);
            this.savedChestplate = null;
        }

        this.flying = false;

        if (PetManager.isEnabled() && !this.hasPet()) {
            PetManager.respawnPet(player.getPlayer());
        }
    }

    public boolean isFlying() {
        return flying;
    }

    @Nullable ItemStack getSavedChestplate() {
        return savedChestplate;
    }

    void onUnload() {
        // Disabling of flight mode
        if (this.flying) {
            this.setFalling(false);
        }

        this.clearStats();

        // Removing pet
        if (PetManager.isEnabled()) {
            PetManager.despawnPet(player);
            Inventory inventory = this.inventory;
            ItemStack petItem = inventory.getItem(PetManager.getPetSlotId());
            if (petItem != null) {
                inventory.setItem(PetManager.getPetSlotId(), PetType.clone(petItem));
            }
        }
    }

    public void updateStatsLater() {
        new StatsUpdater(player.getPlayer()).runTaskLater(RPGInventory.getInstance(), 1);
    }

    public void updatePermissions() {
        Player player = this.player.getPlayer();
        this.clearPermissions();
        List<CustomItem> customItems = new ArrayList<>();
        for (ItemStack item : this.getInventory().getContents()) {
            if (CustomItem.isCustomItem(item)) {
                customItems.add(ItemManager.getCustomItem(item));
            }
        }

        ItemStack itemInOffHand = player.getEquipment().getItemInOffHand();
        ItemStack itemInMainHand = player.getEquipment().getItemInMainHand();

        if (CustomItem.isCustomItem(itemInOffHand)) {
            customItems.add(ItemManager.getCustomItem(itemInOffHand));
        }

        if (CustomItem.isCustomItem(itemInMainHand)) {
            customItems.add(ItemManager.getCustomItem(itemInMainHand));
        }

        for (CustomItem customItem : customItems) {
            customItem.onEquip(player);
        }
    }

    public String getLastMessage() {
        if (System.currentTimeMillis() - this.lastMessageTime > 5 * 1000) {
            lastMessage = "";
        }

        return lastMessage;
    }

    public void setLastMessage(String lastMessage) {
        this.lastMessage = lastMessage;
        this.lastMessageTime = System.currentTimeMillis();
    }

    public void openWorkbench() {
        player.getPlayer().openWorkbench(null, true);
        this.pocketCraft = true;
    }

    public void onWorkbenchClosed() {
        this.pocketCraft = false;
    }

    public boolean isPocketCraft() {
        return this.pocketCraft;
    }

    InventorySnapshot createSnapshot() {
        return InventorySnapshot.create(this);
    }
} 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 Texture {

    private static final Texture EMPTY_TEXTURE = new Texture(new ItemStack(Material.AIR));

    @NotNull
    private final ItemStack prototype;
    private final int data;

    private Texture(@NotNull ItemStack prototype) {
        this(prototype, (short) -1);
    }

    private Texture(@NotNull ItemStack prototype, int data) {
        this.prototype = prototype;
        this.data = data;
    }

    public boolean isEmpty() {
        return this.equals(EMPTY_TEXTURE);
    }

    @NotNull
    public ItemStack getItemStack() {
        return prototype.clone();
    }

    public int getData() {
        return data;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof Texture)) {
            return false;
        }

        Texture texture = (Texture) o;
        return prototype.equals(texture.prototype);
    }

    @Override
    public int hashCode() {
        return Objects.hash(prototype);
    }

    public static Texture parseTexture(String texture) {
        if (texture == null) {
            return EMPTY_TEXTURE;
        }

        String[] textureParts = texture.split(":");

        Material material = MaterialCompat.getMaterialOrNull(textureParts[0]);
        if (material == null) {
            Log.w("Unknown material: {0}", textureParts[0]);
            return EMPTY_TEXTURE;
        }

        ItemStack item = ItemUtils.toBukkitItemStack(new ItemStack(material));
        if (ItemUtils.isEmpty(item)) {
            return EMPTY_TEXTURE;
        }

        if (textureParts.length > 1) {
            // MONSTER_EGG before 1.13
            if (material.name().equals("MONSTER_EGG")) {
                return parseLegacyMonsterEgg(item, textureParts[1]);
            } else if (material.name().startsWith("LEATHER_")) {
                return parseLeatherArmor(item, textureParts[1]);
            } else {
                return parseItemWithData(item, textureParts[1]);
            }
        }

        return new Texture(item);
    }

    private static Texture parseLegacyMonsterEgg(ItemStack item, String entityType) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        nbt.put(ItemUtils.ENTITY_TAG, NbtFactory.ofCompound("temp").put("id", entityType));

        return new Texture(item);
    }

    private static Texture parseLeatherArmor(ItemStack item, String hexColor) {
        try {
            LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta();
            assert meta != null;
            meta.setColor(Color.fromRGB(Integer.parseInt(hexColor, 16)));
            item.setItemMeta(meta);
        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
            Log.w("Can''t parse leather color: {0}", e.toString());
        }

        return new Texture(item);
    }

    private static Texture parseItemWithData(ItemStack item, String textureDataValue) {
        if (item.getItemMeta() == null) {
            return new Texture(item);
        }

        int textureData = -1;
        try {
            textureData = Integer.parseInt(textureDataValue);
        } catch (NumberFormatException e) {
            Log.w("Can''t parse texture modifier. Specify a number instead of \"{0}\"", textureData);
        }

        if (Config.texturesType == TexturesType.DAMAGE) {
            return parseItemWithDurability(item, textureData);
        }
        return parseItemWithCustomModelData(item, textureData);
    }

    private static Texture parseItemWithDurability(ItemStack item, int damage) {
        ItemMeta meta = item.getItemMeta();
        assert meta != null;

        meta.addItemFlags(ItemFlag.values());
        if (damage != -1) {
            ((Damageable) meta).setDamage(damage);
            if (ItemUtils.isItemHasDurability(item)) {
                meta.setUnbreakable(true);
            }
        }
        item.setItemMeta(meta);

        return new Texture(item, damage);
    }

    private static Texture parseItemWithCustomModelData(ItemStack item, int customModelData) {
        ItemMeta meta = item.getItemMeta();
        assert meta != null;

        meta.addItemFlags(ItemFlag.values());
        if (customModelData != -1) {
            meta.setCustomModelData(customModelData);
        }
        item.setItemMeta(meta);

        return new Texture(item, customModelData);
    }
} public class EffectUtils {

    public static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location) {
        playParticlesToAll(particle, particleNum, location, 30.0D);
    }

    private static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location, double distance) {
        playParticlesToAll(particle, particleNum, location, LocationUtils.getRandomVector(), distance);
    }

    private static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location, @NotNull Vector direction, double distance) {
        for (Player player : LocationUtils.getNearbyPlayers(location, distance)) {
            playParticles(player, particle, particleNum, location, direction);
        }
    }

    private static void playParticles(Player player, Particle particle, int particleNum, Location location, Vector direction) {
        player.spawnParticle(particle, location, particleNum, direction.getX(), direction.getY(), direction.getZ());
    }

    public static void playSpawnEffect(Entity entity) {
        Location loc = entity.getLocation();

        entity.getWorld().playSound(loc, SoundCompat.ENDERMAN_TELEPORT.get(), 1, (float) (1.2 + Math.random() * 0.4));
        playParticlesToAll(Particle.EXPLOSION_LARGE, 3, loc);
    }

    public static void playDespawnEffect(Entity entity) {
        Location loc = entity.getLocation();

        entity.getWorld().playSound(loc, SoundCompat.ENDERMAN_TELEPORT.get(), 1, (float) (0.6 + Math.random() * 0.4));
        playParticlesToAll(Particle.SMOKE_NORMAL, 3, loc);
    }


    public static void showDefaultJoinMessage(Player player) {
        showJoinMessage(player, "default", null);
    }

    public static boolean showJoinMessage(Player player, String messageId, @Nullable Runnable callback) {
        String configPrefix = "join-messages." + messageId;
        if (Config.getConfig().getBoolean(configPrefix + ".enabled", false)) {
            EffectUtils.sendTitle(player,
                    Config.getConfig().getInt("join-messages.delay", 2),
                    Config.getConfig().getString(configPrefix + ".title"),
                    Config.getConfig().getStringList(configPrefix + ".text"),
                    callback);
            return true;
        } else {
            return false;
        }
    }

    private static void sendTitle(final Player player, int delay, String title, @NotNull final List<String> subtitles, @Nullable final Runnable callback) {
        if (delay < 2) {
            delay = 2;
        }

        final WrapperPlayServerTitle titlePacket = new WrapperPlayServerTitle();
        int time = (subtitles.size() == 0 ? delay : delay * subtitles.size()) - 1;
        try {
            WrapperPlayServerTitle resetPacket = new WrapperPlayServerTitle();
            resetPacket.setAction(EnumWrappers.TitleAction.RESET);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, resetPacket.getHandle());

            WrapperPlayServerTitle timesPacket = new WrapperPlayServerTitle();
            timesPacket.setAction(EnumWrappers.TitleAction.TIMES);
            timesPacket.setFadeIn(10);
            timesPacket.setFadeOut(10);
            timesPacket.setStay(20 * time);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, timesPacket.getHandle());

            title = StringUtils.coloredLine(StringUtils.setPlaceholders(player, title));
            titlePacket.setAction(EnumWrappers.TitleAction.TITLE);
            titlePacket.setTitle(WrappedChatComponent.fromChatMessage(StringUtils.coloredLine(title))[0]);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, titlePacket.getHandle());
        } catch (InvocationTargetException e) {
            throw new IllegalStateException("Unable to send packet", e);
        }

        new TrackedBukkitRunnable() {
            int line = 0;

            @Override
            public void run() {
                try {
                    if (line == subtitles.size()) {
                        this.cancel();
                        if (callback != null) {
                            callback.run();
                        }
                        return;
                    }

                    String subtitle = StringUtils.coloredLine(StringUtils.setPlaceholders(player, subtitles.get(line)));
                    titlePacket.setAction(EnumWrappers.TitleAction.SUBTITLE);
                    titlePacket.setTitle(WrappedChatComponent.fromChatMessage(subtitle)[0]);
                    ProtocolLibrary.getProtocolManager().sendServerPacket(player, titlePacket.getHandle());
                } catch (InvocationTargetException e) {
                    throw new IllegalStateException("Unable to send packet", e);
                }

                line++;
            }
        }.runTaskTimer(RPGInventory.getInstance(), 0, 20 * delay);
    }
} public class ItemUtils {
    public static final String ENTITY_TAG = "EntityTag";

    public static final String BACKPACK_UID_TAG = "backpack.uid";
    public static final String BACKPACK_TAG = "backpack.id";
    public static final String ITEM_TAG = "rpginv.id";
    public static final String FOOD_TAG = "food.id";
    public static final String PET_TAG = "pet.id";

    @NotNull
    public static ItemStack setTag(ItemStack item, @NotNull String tag, @NotNull String value) {
        ItemStack bukkitItem = toBukkitItemStack(item);
        if (isEmpty(bukkitItem)) {
            return bukkitItem;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(bukkitItem);
        if (!nbt.containsKey(tag)) {
            nbt.put(tag, value);
        }
        NbtFactoryMirror.setItemTag(bukkitItem, nbt);

        return bukkitItem;
    }

    @NotNull
    public static String getTag(@NotNull ItemStack item, @NotNull String tag) {
        return getTag(item, tag, "");
    }

    @NotNull
    @SuppressWarnings("WeakerAccess")
    public static String getTag(@NotNull ItemStack item, @NotNull String tag, @NotNull String defaultValue) {
        final ItemStack bukkitItem = toBukkitItemStack(item);
        if (isEmpty(bukkitItem)) {
            return "";
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(bukkitItem);
        return nbt.containsKey(tag) ? nbt.getString(tag) : defaultValue;
    }

    @Contract("null, _ -> false")
    public static boolean hasTag(@Nullable ItemStack originalItem, String tag) {
        if (isEmpty(originalItem) || !originalItem.hasItemMeta()) {
            return false;
        }

        ItemStack item = toBukkitItemStack(originalItem.clone());
        if (isEmpty(item)) {
            return false;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        return nbt.containsKey(tag);
    }

    public static boolean isItemHasDurability(ItemStack item) {
        return item.getType().getMaxDurability() > 0;
    }

    @NotNull
    public static ItemStack[] syncItems(ItemStack[] items) {
        for (int i = 0; i < items.length; i++) {
            items[i] = ItemUtils.syncItem(items[i]);
        }

        return items;
    }

    @NotNull
    @Contract("null -> !null")
    private static ItemStack syncItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return new ItemStack(Material.AIR);
        }

        int itemTextureData = getTextureData(item);
        int amount = item.getAmount();
        int foundTextureData;
        if (CustomItem.isCustomItem(item)) {
            CustomItem custom = ItemManager.getCustomItem(item);

            if (custom == null) {
                return new ItemStack(Material.AIR);
            }

            foundTextureData = custom.getTextureData();
            item = ItemManager.getItem(ItemUtils.getTag(item, ItemUtils.ITEM_TAG));
        } else if (BackpackManager.isBackpack(item)) {
            BackpackType type = BackpackManager.getBackpackType(ItemUtils.getTag(item, ItemUtils.BACKPACK_TAG));

            if (type == null) {
                return new ItemStack(Material.AIR);
            }

            foundTextureData = type.getTextureData();

            String bpUID = ItemUtils.getTag(item, ItemUtils.BACKPACK_UID_TAG);
            if (!bpUID.isEmpty()) {
                ItemUtils.setTag(item, ItemUtils.BACKPACK_UID_TAG, bpUID);
            }
        } else if (PetType.isPetItem(item)) {
            PetType petType = PetManager.getPetFromItem(item);
            if (petType == null) {
                return new ItemStack(Material.AIR);
            }
            foundTextureData = petType.getTextureData();

            long deathTime = PetManager.getDeathTime(item);
            double health = PetManager.getHealth(item, petType.getHealth());

            item = petType.getSpawnItem();
            PetManager.saveDeathTime(item, deathTime);
            PetManager.saveHealth(item, health);
        } else if (PetFood.isFoodItem(item)) {
            PetFood food = PetManager.getFoodFromItem(item);
            if (food == null) {
                return new ItemStack(Material.AIR);
            }
            foundTextureData = food.getTextureData();

            item = food.getFoodItem();
        } else {
            return item;
        }

        setTextureData(item, foundTextureData == -1 ? itemTextureData : foundTextureData);
        item.setAmount(amount);
        return item;
    }

    public static int getTextureData(@NotNull ItemStack itemStack) {
        ItemMeta meta = itemStack.getItemMeta();
        if (meta == null) {
            return 0;
        }

        int data;
        if (Config.texturesType == TexturesType.DAMAGE) {
            data = ((Damageable) meta).getDamage();
        } else if (meta.hasCustomModelData()) {
            data = meta.getCustomModelData();
        } else {
            data = 0;
        }
        return data;
    }

    private static void setTextureData(@NotNull ItemStack itemStack, int data) {
        ItemMeta meta = itemStack.getItemMeta();
        if (meta != null) {
            if (Config.texturesType == TexturesType.DAMAGE) {
                ((Damageable) meta).setDamage(data);
            } else {
                meta.setCustomModelData(data);
            }
            itemStack.setItemMeta(meta);
        }
    }

    @Contract("null -> true")
    public static boolean isEmpty(@Nullable ItemStack item) {
        return item == null || item.getType() == Material.AIR;
    }

    @Contract("null -> false")
    public static boolean isNotEmpty(@Nullable ItemStack item) {
        return item != null && item.getType() != Material.AIR;
    }

    @NotNull
    public static ItemStack toBukkitItemStack(ItemStack item) {
        return MinecraftReflection.getBukkitItemStack(item);
    }
} public class LocationUtils {
    private static final List<String> UNSAFE_BLOCK_MATERIALS = Arrays.asList(
            "LADDER", "LAVA", "STATIONARY_LAVA", "LEGACY_STATIONARY_LAVA", "CACTUS", "GREEN_CACTUS", "FIRE"
    );
    private static final Random RANDOM = new Random();

    @Deprecated
    public static Location getLocationNearPlayer(Player player, int radius) {
        return LocationUtils.getLocationNearPoint(player.getLocation(), radius);
    }

    public static Location getLocationNearPoint(Location location, int radius) {
        Block playerBlock = location.getBlock();
        List<Location> availableLocations = new ArrayList<>();

        World world = location.getWorld();
        for (int x = playerBlock.getX() - radius; x < playerBlock.getX() + radius; x++) {
            for (int y = playerBlock.getY() - radius; y < playerBlock.getY() + radius; y++) {
                for (int z = playerBlock.getZ() - radius; z < playerBlock.getZ() + radius; z++) {
                    Location loc = getBlockCenter(new Location(world, x, y, z));
                    if (!loc.getBlock().isEmpty()) {
                        continue;
                    }
                    Block underBlock = loc.clone().subtract(0, 1, 0).getBlock();
                    if (!underBlock.isEmpty() && !underBlock.isLiquid() && LocationUtils.isSafeLocation(underBlock.getLocation())) {
                        loc.setYaw(-180 + LocationUtils.RANDOM.nextFloat() * 360);
                        availableLocations.add(loc);
                    }
                }
            }
        }

        if (availableLocations.size() == 0) {
            return getBlockCenter(playerBlock.getLocation().clone());
        }

        return availableLocations.get(LocationUtils.RANDOM.nextInt(availableLocations.size()));
    }

    public static Location getBlockCenter(Location loc) {
        return loc.add(0.5, 0, 0.5);
    }

    public static boolean isUnderAnyBlockHonestly(@NotNull Location loc, double entityWidth, int distance) {
        for (Block block : getStandingOn(loc, entityWidth)) {
            if (isUnderAnyBlock(block, distance)) {
                return true;
            }
        }

        return false;
    }

    @NotNull
    public static List<Block> getStandingOn(@NotNull Location playerLoc, double entityWidth) {
        double halfWidth = entityWidth / 2;

        List<Block> blocksUnderPlayer = new ArrayList<>(2);
        for (int firstSign = -1; firstSign <= 1; firstSign += 2) {
            for (int secondSign = -1; secondSign <= 1; secondSign += 2) {
                Location blockLoc = playerLoc.clone().add(firstSign * halfWidth, 0, secondSign * halfWidth);
                Block block = blockLoc.getBlock();
                if (!blocksUnderPlayer.contains(block)) {
                    blocksUnderPlayer.add(block);
                }
            }
        }

        return blocksUnderPlayer;
    }

    public static boolean isUnderAnyBlock(@NotNull Block block, int distance) {
        for (int i = 1; i <= distance; i++) {
            Block blockUnderPlayer = block.getRelative(BlockFace.DOWN, i);
            if (!blockUnderPlayer.isEmpty()) {
                return true;
            }
        }

        return false;
    }

    @NotNull
    public static List<Player> getNearbyPlayers(Location location, double distance) {
        List<Player> nearbyPlayers = new ArrayList<>();
        for (Player entity : location.getWorld().getPlayers()) {
            try {
                if (entity.getLocation().distance(location) <= distance) {
                    nearbyPlayers.add(entity);
                }
            } catch (IllegalArgumentException ignore) {
                // https://gitlab.com/endlesscodegroup/rpginventory/rpginventory/issues/162
                // Is it magic?
            }
        }

        return nearbyPlayers;
    }

    @NotNull
    public static Vector getRandomVector() {
        Vector vector = new Vector();
        vector.setX(0.0D + Math.random() - Math.random());
        vector.setY(Math.random());
        vector.setZ(0.0D + Math.random() - Math.random());

        return vector;
    }

    public static boolean isSafeLocation(@Nullable Location location) {
        return location != null && !LocationUtils.UNSAFE_BLOCK_MATERIALS.contains(location.getBlock().getType().name());
    }
} @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 final class NbtFactoryMirror {

    private static StructureModifier<Object> itemStackModifier;

    private NbtFactoryMirror() {
    }

    public static NbtCompound fromItemCompound(ItemStack stack) {
        return NbtFactory.asCompound(fromItemTag(stack));
    }

    public static void setItemTag(ItemStack stack, NbtCompound compound) {
        checkItemStack(stack);
        StructureModifier<NbtBase<?>> modifier = getStackModifier(stack);
        modifier.write(0, compound);
    }

    private static NbtWrapper<?> fromItemTag(ItemStack stack) {
        checkItemStack(stack);
        StructureModifier<NbtBase<?>> modifier = getStackModifier(stack);
        NbtBase<?> result = modifier.read(0);
        if (result == null) {
            result = com.comphenix.protocol.wrappers.nbt.NbtFactory.ofCompound("tag");
            modifier.write(0, result);
        }

        return NbtFactory.fromBase((NbtBase<?>) result);
    }

    private static void checkItemStack(ItemStack stack) {
        if (stack == null) {
            throw new IllegalArgumentException("Stack cannot be NULL.");
        } else if (!MinecraftReflection.isCraftItemStack(stack)) {
            throw new IllegalArgumentException("Stack must be a CraftItemStack.");
        } else if (stack.getType() == Material.AIR) {
            throw new IllegalArgumentException("ItemStacks representing air cannot store NMS information.");
        }
    }

    private static StructureModifier<NbtBase<?>> getStackModifier(ItemStack stack) {
        Object nmsStack = MinecraftReflection.getMinecraftItemStack(stack);
        if (itemStackModifier == null) {
            itemStackModifier = new StructureModifier<>(nmsStack.getClass(), Object.class, false);
        }

        return itemStackModifier.withTarget(nmsStack).withType(MinecraftReflection.getNBTBaseClass(), BukkitConverters.getNbtConverter());
    }
} public class SafeEnums {

    private SafeEnums() {
        // Shouldn't be instantiated
    }

    @Nullable
    public static DyeColor getDyeColor(String name) {
        return valueOf(DyeColor.class, name, "color");
    }

    @Nullable
    public static Horse.Color getHorseColor(String name) {
        return valueOf(Horse.Color.class, name, "horse color");
    }

    @Nullable
    public static Horse.Style getHorseStyle(String name) {
        return valueOf(Horse.Style.class, name, "horse style");
    }

    @Nullable
    public static Cat.Type getCatType(String name) {
        return valueOf(Cat.Type.class, name, "cat type");
    }

    @NotNull
    public static <T extends Enum<T>> T valueOfOrDefault(Class<T> enumClass, String name, T defaultValue) {
        return valueOfOrDefault(enumClass, name, defaultValue, enumClass.getSimpleName());
    }

    @NotNull
    public static <T extends Enum<T>> T valueOfOrDefault(Class<T> enumClass, String name, T defaultValue, String alias) {
        T value = valueOf(enumClass, name, alias);
        if (value != null) {
            return value;
        } else {
            Log.w("Used {0} {1} by default.", defaultValue.name(), alias);
            return defaultValue;
        }
    }

    @Nullable
    public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name, String alias) {
        if (name == null) {
            return null;
        }

        try {
            return Enum.valueOf(enumClass, name.toUpperCase());
        } catch (IllegalArgumentException e) {
            Log.w("Unknown {0}: {1}. Available values: {2}", alias, name, Arrays.toString(enumClass.getEnumConstants()));
            return null;
        }
    }
}
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import org.bukkit.DyeColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Animals;
import org.bukkit.entity.Cat;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Pig;
import org.bukkit.entity.Player;
import org.bukkit.entity.Tameable;
import org.bukkit.entity.Wolf;
import org.bukkit.inventory.HorseInventory;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.endlesscode.rpginventory.RPGInventory;
import ru.endlesscode.rpginventory.compat.MaterialCompat;
import ru.endlesscode.rpginventory.compat.VersionHandler;
import ru.endlesscode.rpginventory.event.listener.PetListener;
import ru.endlesscode.rpginventory.inventory.InventoryManager;
import ru.endlesscode.rpginventory.inventory.PlayerWrapper;
import ru.endlesscode.rpginventory.inventory.slot.SlotManager;
import ru.endlesscode.rpginventory.item.Texture;
import ru.endlesscode.rpginventory.utils.EffectUtils;
import ru.endlesscode.rpginventory.utils.ItemUtils;
import ru.endlesscode.rpginventory.utils.LocationUtils;
import ru.endlesscode.rpginventory.utils.Log;
import ru.endlesscode.rpginventory.utils.NbtFactoryMirror;
import ru.endlesscode.rpginventory.utils.SafeEnums;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/*
 * This file is part of RPGInventory.
 * Copyright (C) 2015-2017 Osip Fatkullin
 *
 * RPGInventory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RPGInventory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RPGInventory.  If not, see <http://www.gnu.org/licenses/>.
 */

package ru.endlesscode.rpginventory.pet;



/**
 * Created by OsipXD on 26.08.2015
 * It is part of the RpgInventory.
 * All rights reserved 2014 - 2016 © «EndlessCode Group»
 */
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)) {