All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cryptomorin.xseries.XItemStack Maven / Gradle / Ivy

There is a newer version: 12.1.0
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2024 Crypto Morin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.cryptomorin.xseries;

import com.google.common.base.Enums;
import com.google.common.base.Strings;
import com.google.common.collect.Multimap;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.block.Banner;
import org.bukkit.block.BlockState;
import org.bukkit.block.CreatureSpawner;
import org.bukkit.block.ShulkerBox;
import org.bukkit.block.banner.Pattern;
import org.bukkit.block.banner.PatternType;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Axolotl;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.TropicalFish;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.*;
import org.bukkit.inventory.meta.trim.ArmorTrim;
import org.bukkit.inventory.meta.trim.TrimMaterial;
import org.bukkit.inventory.meta.trim.TrimPattern;
import org.bukkit.map.MapView;
import org.bukkit.material.MaterialData;
import org.bukkit.material.SpawnEgg;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionType;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.cryptomorin.xseries.XMaterial.supports;

/**
 * XItemStack - YAML Item Serializer
* Using ConfigurationSection Example: *
 *     ConfigurationSection section = plugin.getConfig().getConfigurationSection("staffs.dragon-staff");
 *     ItemStack item = XItemStack.deserialize(section);
 * 
* ItemStack * * @author Crypto Morin * @version 7.5.0 * @see XMaterial * @see XPotion * @see XSkull * @see XEnchantment * @see ItemStack */ public final class XItemStack { public static final ItemFlag[] ITEM_FLAGS = ItemFlag.values(); /** * Because item metas cannot be applied to AIR, apparently. */ private static final XMaterial DEFAULT_MATERIAL = XMaterial.NETHER_PORTAL; private static final boolean SUPPORTS_POTION_COLOR; static { boolean supportsPotionColor = false; try { Class.forName("org.bukkit.inventory.meta.PotionMeta").getMethod("setColor", Color.class); supportsPotionColor = true; } catch (Throwable ignored) { } SUPPORTS_POTION_COLOR = supportsPotionColor; } private XItemStack() { } public static boolean isDefaultItem(ItemStack item) { return DEFAULT_MATERIAL.isSimilar(item); } private static BlockState safeBlockState(BlockStateMeta meta) { try { return meta.getBlockState(); } catch (IllegalStateException ex) { // Due to a bug in the latest paper v1.9-1.10 (and some older v1.11) versions. // java.lang.IllegalStateException: Missing blockState for BREWING_STAND_ITEM // BREWING_STAND_ITEM, ENCHANTMENT_TABLE, REDSTONE_COMPARATOR // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/diff/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBlockState.java?until=b6ad714e853042def52620befe9bc85d0137cd71 if (ex.getMessage().toLowerCase(Locale.ENGLISH).contains("missing blockstate")) { return null; } else { throw ex; } } catch (ClassCastException ex) { // java.lang.ClassCastException: net.minecraft.server.v1_9_R2.TileEntityDispenser cannot be cast to net.minecraft.server.v1_9_R2.TileEntityDropper return null; } } /** * @see #serialize(ItemStack, ConfigurationSection, Function) * @since 1.0.0 */ public static void serialize(@Nonnull ItemStack item, @Nonnull ConfigurationSection config) { serialize(item, config, Function.identity()); } /** * Writes an ItemStack object into a config. * The config file will not save after the object is written. * * @param item the ItemStack to serialize. * @param config the config section to write this item to. * @param translator the function applied to item name and each lore lines. * @since 7.4.0 */ @SuppressWarnings("deprecation") public static void serialize(@Nonnull ItemStack item, @Nonnull ConfigurationSection config, @Nonnull Function translator) { Objects.requireNonNull(item, "Cannot serialize a null item"); Objects.requireNonNull(config, "Cannot serialize item from a null configuration section."); // Material config.set("material", XMaterial.matchXMaterial(item).name()); // Amount if (item.getAmount() > 1) config.set("amount", item.getAmount()); ItemMeta meta = item.getItemMeta(); if (meta == null) return; // Durability - Damage if (supports(13)) { if (meta instanceof Damageable) { Damageable damageable = (Damageable) meta; if (damageable.hasDamage()) config.set("damage", damageable.getDamage()); } } else { config.set("damage", item.getDurability()); } // Display Name & Lore if (meta.hasDisplayName()) config.set("name", translator.apply(meta.getDisplayName())); if (meta.hasLore()) config.set("lore", meta.getLore().stream().map(translator).collect(Collectors.toList())); if (supports(14)) { if (meta.hasCustomModelData()) config.set("custom-model-data", meta.getCustomModelData()); } if (supports(11)) { if (meta.isUnbreakable()) config.set("unbreakable", true); } // Enchantments for (Map.Entry enchant : meta.getEnchants().entrySet()) { String entry = "enchants." + XEnchantment.matchXEnchantment(enchant.getKey()).name(); config.set(entry, enchant.getValue()); } // Flags if (!meta.getItemFlags().isEmpty()) { Set flags = meta.getItemFlags(); List flagNames = new ArrayList<>(flags.size()); for (ItemFlag flag : flags) flagNames.add(flag.name()); config.set("flags", flagNames); } // Attributes - https://minecraft.wiki/w/Attribute if (supports(13)) { Multimap attributes = meta.getAttributeModifiers(); if (attributes != null) { for (Map.Entry attribute : attributes.entries()) { String path = "attributes." + attribute.getKey().name() + '.'; AttributeModifier modifier = attribute.getValue(); config.set(path + "id", modifier.getUniqueId().toString()); config.set(path + "name", modifier.getName()); config.set(path + "amount", modifier.getAmount()); config.set(path + "operation", modifier.getOperation().name()); if (modifier.getSlot() != null) config.set(path + "slot", modifier.getSlot().name()); } } } if (meta instanceof BlockStateMeta) { BlockState state = safeBlockState((BlockStateMeta) meta); if (supports(11) && state instanceof ShulkerBox) { ShulkerBox box = (ShulkerBox) state; ConfigurationSection shulker = config.createSection("contents"); int i = 0; for (ItemStack itemInBox : box.getInventory().getContents()) { if (itemInBox != null) serialize(itemInBox, shulker.createSection(Integer.toString(i)), translator); i++; } } else if (state instanceof CreatureSpawner) { CreatureSpawner cs = (CreatureSpawner) state; if (cs.getSpawnedType() != null) config.set("spawner", cs.getSpawnedType().name()); } } else if (meta instanceof EnchantmentStorageMeta) { EnchantmentStorageMeta book = (EnchantmentStorageMeta) meta; for (Map.Entry enchant : book.getStoredEnchants().entrySet()) { String entry = "stored-enchants." + XEnchantment.matchXEnchantment(enchant.getKey()).name(); config.set(entry, enchant.getValue()); } } else if (meta instanceof SkullMeta) { String skull = XSkull.getSkinValue(meta); if (skull != null) config.set("skull", skull); } else if (meta instanceof BannerMeta) { BannerMeta banner = (BannerMeta) meta; ConfigurationSection patterns = config.createSection("patterns"); for (Pattern pattern : banner.getPatterns()) { patterns.set(pattern.getPattern().name(), pattern.getColor().name()); } } else if (meta instanceof LeatherArmorMeta) { LeatherArmorMeta leather = (LeatherArmorMeta) meta; Color color = leather.getColor(); config.set("color", color.getRed() + ", " + color.getGreen() + ", " + color.getBlue()); } else if (meta instanceof PotionMeta) { if (supports(9)) { PotionMeta potion = (PotionMeta) meta; List customEffects = potion.getCustomEffects(); List effects = new ArrayList<>(customEffects.size()); for (PotionEffect effect : customEffects) { effects.add(effect.getType().getName() + ", " + effect.getDuration() + ", " + effect.getAmplifier()); } if (!effects.isEmpty()) config.set("effects", effects); PotionType basePotionType = potion.getBasePotionType(); // PotionData potionData = potion.getBasePotionData(); // config.set("base-effect", potionData.getType().name() + ", " + potionData.isExtended() + ", " + potionData.isUpgraded()); config.set("base-type", basePotionType.name()); config.set("effects", potion.getCustomEffects().stream().map(x -> { NamespacedKey type = x.getType().getKey(); String typeStr = type.getNamespace() + ':' + type.getKey(); return typeStr + ", " + x.getDuration() + ", " + x.getAmplifier(); })); if (SUPPORTS_POTION_COLOR && potion.hasColor()) config.set("color", potion.getColor().asRGB()); } else { // Check for water bottles in 1.8 // Potion class is now removed... // if (item.getDurability() != 0) { // Potion potion = Potion.fromItemStack(item); // config.set("level", potion.getLevel()); // config.set("base-effect", potion.getType().name() + ", " + potion.hasExtendedDuration() + ", " + potion.isSplash()); // } } } else if (meta instanceof FireworkMeta) { FireworkMeta firework = (FireworkMeta) meta; config.set("power", firework.getPower()); int i = 0; for (FireworkEffect fw : firework.getEffects()) { config.set("firework." + i + ".type", fw.getType().name()); ConfigurationSection fwc = config.getConfigurationSection("firework." + i); fwc.set("flicker", fw.hasFlicker()); fwc.set("trail", fw.hasTrail()); List fwBaseColors = fw.getColors(); List fwFadeColors = fw.getFadeColors(); List baseColors = new ArrayList<>(fwBaseColors.size()); List fadeColors = new ArrayList<>(fwFadeColors.size()); ConfigurationSection colors = fwc.createSection("colors"); for (Color color : fwBaseColors) baseColors.add(color.getRed() + ", " + color.getGreen() + ", " + color.getBlue()); colors.set("base", baseColors); for (Color color : fwFadeColors) fadeColors.add(color.getRed() + ", " + color.getGreen() + ", " + color.getBlue()); colors.set("fade", fadeColors); i++; } } else if (meta instanceof BookMeta) { BookMeta book = (BookMeta) meta; if (book.getTitle() != null || book.getAuthor() != null || book.getGeneration() != null || !book.getPages().isEmpty()) { ConfigurationSection bookInfo = config.createSection("book"); if (book.getTitle() != null) bookInfo.set("title", book.getTitle()); if (book.getAuthor() != null) bookInfo.set("author", book.getAuthor()); if (supports(9)) { BookMeta.Generation generation = book.getGeneration(); if (generation != null) { bookInfo.set("generation", book.getGeneration().toString()); } } if (!book.getPages().isEmpty()) bookInfo.set("pages", book.getPages()); } } else if (meta instanceof MapMeta) { MapMeta map = (MapMeta) meta; ConfigurationSection mapSection = config.createSection("map"); mapSection.set("scaling", map.isScaling()); if (supports(11)) { if (map.hasLocationName()) mapSection.set("location", map.getLocationName()); if (map.hasColor()) { Color color = map.getColor(); mapSection.set("color", color.getRed() + ", " + color.getGreen() + ", " + color.getBlue()); } } if (supports(14)) { if (map.hasMapView()) { MapView mapView = map.getMapView(); ConfigurationSection view = mapSection.createSection("view"); view.set("scale", mapView.getScale().toString()); view.set("world", mapView.getWorld().getName()); ConfigurationSection centerSection = view.createSection("center"); centerSection.set("x", mapView.getCenterX()); centerSection.set("z", mapView.getCenterZ()); view.set("locked", mapView.isLocked()); view.set("tracking-position", mapView.isTrackingPosition()); view.set("unlimited-tracking", mapView.isUnlimitedTracking()); } } } else { if (supports(20)) { if (meta instanceof ArmorMeta) { ArmorMeta armorMeta = (ArmorMeta) meta; if (armorMeta.hasTrim()) { ArmorTrim trim = armorMeta.getTrim(); ConfigurationSection trimConfig = config.createSection("trim"); trimConfig.set("material", trim.getMaterial().getKey().getNamespace() + ':' + trim.getMaterial().getKey().getKey()); trimConfig.set("pattern", trim.getPattern().getKey().getNamespace() + ':' + trim.getPattern().getKey().getKey()); } } } if (supports(17)) { if (meta instanceof AxolotlBucketMeta) { AxolotlBucketMeta bucket = (AxolotlBucketMeta) meta; if (bucket.hasVariant()) config.set("color", bucket.getVariant().toString()); } } if (supports(16)) { if (meta instanceof CompassMeta) { CompassMeta compass = (CompassMeta) meta; ConfigurationSection subSection = config.createSection("lodestone"); subSection.set("tracked", compass.isLodestoneTracked()); if (compass.hasLodestone()) { Location location = compass.getLodestone(); subSection.set("location.world", location.getWorld().getName()); subSection.set("location.x", location.getX()); subSection.set("location.y", location.getY()); subSection.set("location.z", location.getZ()); } } } if (supports(14)) { if (meta instanceof CrossbowMeta) { CrossbowMeta crossbow = (CrossbowMeta) meta; int i = 0; for (ItemStack projectiles : crossbow.getChargedProjectiles()) { serialize(projectiles, config.getConfigurationSection("projectiles." + i), translator); i++; } } else if (meta instanceof TropicalFishBucketMeta) { TropicalFishBucketMeta tropical = (TropicalFishBucketMeta) meta; config.set("pattern", tropical.getPattern().name()); config.set("color", tropical.getBodyColor().name()); config.set("pattern-color", tropical.getPatternColor().name()); } else if (meta instanceof SuspiciousStewMeta) { SuspiciousStewMeta stew = (SuspiciousStewMeta) meta; List customEffects = stew.getCustomEffects(); List effects = new ArrayList<>(customEffects.size()); for (PotionEffect effect : customEffects) { effects.add(effect.getType().getName() + ", " + effect.getDuration() + ", " + effect.getAmplifier()); } config.set("effects", effects); } } if (!supports(13)) { // Spawn Eggs if (supports(11)) { if (meta instanceof SpawnEggMeta) { SpawnEggMeta spawnEgg = (SpawnEggMeta) meta; config.set("creature", spawnEgg.getSpawnedType().getName()); } } else { MaterialData data = item.getData(); if (data instanceof SpawnEgg) { SpawnEgg spawnEgg = (SpawnEgg) data; config.set("creature", spawnEgg.getSpawnedType().getName()); } } } } } /** * Writes an ItemStack properties into a {@code Map}. * * @param item the ItemStack to serialize. * @return a Map containing the serialized ItemStack properties. */ public static Map serialize(@Nonnull ItemStack item) { Objects.requireNonNull(item, "Cannot serialize a null item"); ConfigurationSection config = new MemoryConfiguration(); serialize(item, config); return configSectionToMap(config); } /** * Deserialize an ItemStack from the config. * * @param config the config section to deserialize the ItemStack object from. * @return a deserialized ItemStack. * @since 1.0.0 */ @Nonnull public static ItemStack deserialize(@Nonnull ConfigurationSection config) { return edit(new ItemStack(DEFAULT_MATERIAL.parseMaterial()), config, Function.identity(), null); } /** * Deserialize an ItemStack from a {@code Map}. * * @param serializedItem the map holding the item configurations to deserialize * the ItemStack object from. * @return a deserialized ItemStack. */ @Nonnull public static ItemStack deserialize(@Nonnull Map serializedItem) { Objects.requireNonNull(serializedItem, "serializedItem cannot be null."); return deserialize(mapToConfigSection(serializedItem)); } @Nonnull public static ItemStack deserialize(@Nonnull ConfigurationSection config, @Nonnull Function translator) { return deserialize(config, translator, null); } /** * Deserialize an ItemStack from the config. * * @param config the config section to deserialize the ItemStack object from. * @return an edited ItemStack. * @since 7.2.0 */ @Nonnull public static ItemStack deserialize(@Nonnull ConfigurationSection config, @Nonnull Function translator, @Nullable Consumer restart) { return edit(new ItemStack(DEFAULT_MATERIAL.parseMaterial()), config, translator, restart); } /** * Deserialize an ItemStack from a {@code Map}. * * @param serializedItem the map holding the item configurations to deserialize * the ItemStack object from. * @param translator the translator to use for translating the item's name. * @return a deserialized ItemStack. */ @Nonnull public static ItemStack deserialize(@Nonnull Map serializedItem, @Nonnull Function translator) { Objects.requireNonNull(serializedItem, "serializedItem cannot be null."); Objects.requireNonNull(translator, "translator cannot be null."); return deserialize(mapToConfigSection(serializedItem), translator); } private static int toInt(String str, @SuppressWarnings("SameParameterValue") int defaultValue) { try { return Integer.parseInt(str); } catch (NumberFormatException nfe) { return defaultValue; } } private static List split(@Nonnull String str, @SuppressWarnings("SameParameterValue") char separatorChar) { List list = new ArrayList<>(5); boolean match = false, lastMatch = false; int len = str.length(); int start = 0; for (int i = 0; i < len; i++) { if (str.charAt(i) == separatorChar) { if (match) { list.add(str.substring(start, i)); match = false; lastMatch = true; } // This is important, it should not be i++ start = i + 1; continue; } lastMatch = false; match = true; } if (match || lastMatch) { list.add(str.substring(start, len)); } return list; } private static List splitNewLine(String str) { int len = str.length(); List list = new ArrayList<>(); int i = 0, start = 0; boolean match = false, lastMatch = false; while (i < len) { if (str.charAt(i) == '\n') { if (match) { list.add(str.substring(start, i)); match = false; lastMatch = true; } start = ++i; continue; } lastMatch = false; match = true; i++; } if (match || lastMatch) { list.add(str.substring(start, i)); } return list; } /** * Deserialize an ItemStack from the config. * * @param config the config section to deserialize the ItemStack object from. * @param translator the function applied to item name and each lore line. * @param restart the function called when an error occurs while deserializing one of the properties. * @return an edited ItemStack. * @since 1.0.0 */ @SuppressWarnings("deprecation") @Nonnull public static ItemStack edit(@Nonnull ItemStack item, @Nonnull final ConfigurationSection config, @Nonnull final Function translator, @Nullable final Consumer restart) { Objects.requireNonNull(item, "Cannot operate on null ItemStack, considering using an AIR ItemStack instead"); Objects.requireNonNull(config, "Cannot deserialize item to a null configuration section."); Objects.requireNonNull(translator, "Translator function cannot be null"); // Material String materialName = config.getString("material"); if (!Strings.isNullOrEmpty(materialName)) { Optional materialOpt = XMaterial.matchXMaterial(materialName); XMaterial material; if (materialOpt.isPresent()) material = materialOpt.get(); else { UnknownMaterialCondition unknownMaterialCondition = new UnknownMaterialCondition(materialName); if (restart == null) throw unknownMaterialCondition; restart.accept(unknownMaterialCondition); if (unknownMaterialCondition.hasSolution()) material = unknownMaterialCondition.solution; else throw unknownMaterialCondition; } if (!material.isSupported()) { UnAcceptableMaterialCondition unsupportedMaterialCondition = new UnAcceptableMaterialCondition(material, UnAcceptableMaterialCondition.Reason.UNSUPPORTED); if (restart == null) throw unsupportedMaterialCondition; restart.accept(unsupportedMaterialCondition); if (unsupportedMaterialCondition.hasSolution()) material = unsupportedMaterialCondition.solution; else throw unsupportedMaterialCondition; } if (XTag.INVENTORY_NOT_DISPLAYABLE.isTagged(material)) { UnAcceptableMaterialCondition unsupportedMaterialCondition = new UnAcceptableMaterialCondition(material, UnAcceptableMaterialCondition.Reason.NOT_DISPLAYABLE); if (restart == null) throw unsupportedMaterialCondition; restart.accept(unsupportedMaterialCondition); if (unsupportedMaterialCondition.hasSolution()) material = unsupportedMaterialCondition.solution; else throw unsupportedMaterialCondition; } material.setType(item); } // Amount int amount = config.getInt("amount"); if (amount > 1) item.setAmount(amount); ItemMeta meta; { // For Java's stupid closure capture system. ItemMeta tempMeta = item.getItemMeta(); if (tempMeta == null) { // When AIR is null. Useful for when you just want to use the meta to save data and // set the type later. A simple CraftMetaItem. meta = Bukkit.getItemFactory().getItemMeta(XMaterial.STONE.parseMaterial()); } else { meta = tempMeta; } } // Durability - Damage if (supports(13)) { if (meta instanceof Damageable) { int damage = config.getInt("damage"); if (damage > 0) ((Damageable) meta).setDamage(damage); } } else { int damage = config.getInt("damage"); if (damage > 0) item.setDurability((short) damage); } // Special Items if (meta instanceof SkullMeta) { String skull = config.getString("skull"); if (skull != null) XSkull.of(meta).profile(skull).apply(); } else if (meta instanceof BannerMeta) { BannerMeta banner = (BannerMeta) meta; ConfigurationSection patterns = config.getConfigurationSection("patterns"); if (patterns != null) { for (String pattern : patterns.getKeys(false)) { PatternType type = Enums.getIfPresent(PatternType.class, pattern).orNull(); if (type == null) type = Enums.getIfPresent(PatternType.class, pattern.toUpperCase(Locale.ENGLISH)).or(PatternType.BASE); DyeColor color = Enums.getIfPresent(DyeColor.class, patterns.getString(pattern).toUpperCase(Locale.ENGLISH)).or(DyeColor.WHITE); banner.addPattern(new Pattern(color, type)); } } } else if (meta instanceof LeatherArmorMeta) { LeatherArmorMeta leather = (LeatherArmorMeta) meta; String colorStr = config.getString("color"); if (colorStr != null) { leather.setColor(parseColor(colorStr)); } } else if (meta instanceof PotionMeta) { if (supports(9)) { PotionMeta potion = (PotionMeta) meta; for (String effects : config.getStringList("effects")) { XPotion.Effect effect = XPotion.parseEffect(effects); if (effect.hasChance()) potion.addCustomEffect(effect.getEffect(), true); } String baseType = config.getString("base-type"); if (!Strings.isNullOrEmpty(baseType)) { XPotion.matchXPotion(baseType).ifPresent(x -> potion.setBasePotionType(x.getPotionType())); } if (SUPPORTS_POTION_COLOR && config.contains("color")) { potion.setColor(Color.fromRGB(config.getInt("color"))); } } else { // What do we do for 1.8? // if (config.contains("level")) { // int level = config.getInt("level"); // String baseEffect = config.getString("base-effect"); // if (!Strings.isNullOrEmpty(baseEffect)) { // List split = split(baseEffect, ','); // PotionType type = Enums.getIfPresent(PotionType.class, split.get(0).trim().toUpperCase(Locale.ENGLISH)).or(PotionType.SLOWNESS); // boolean extended = split.size() != 1 && Boolean.parseBoolean(split.get(1).trim()); // boolean splash = split.size() > 2 && Boolean.parseBoolean(split.get(2).trim()); // // item = (splash ? XMaterial.SPLASH_POTION : XMaterial.POTION).parseItem(); // PotionMeta potion = (PotionMeta) item.getItemMeta(); // // potion.addCustomEffect(XPotion.matchXPotion(type).buildPotionEffect(extended ? 3 : 1, level), true); // item.setItemMeta(potion); // item = (new Potion(type, level, splash, extended)).toItemStack(1); // } // } } } else if (meta instanceof BlockStateMeta) { BlockStateMeta bsm = (BlockStateMeta) meta; BlockState state = safeBlockState(bsm); if (state instanceof CreatureSpawner) { // Do we still need this? XMaterial handles it, doesn't it? CreatureSpawner spawner = (CreatureSpawner) state; String spawnerStr = config.getString("spawner"); if (!Strings.isNullOrEmpty(spawnerStr)) { spawner.setSpawnedType(Enums.getIfPresent(EntityType.class, spawnerStr.toUpperCase(Locale.ENGLISH)).orNull()); spawner.update(true); bsm.setBlockState(spawner); } } else if (supports(11) && state instanceof ShulkerBox) { ConfigurationSection shulkerSection = config.getConfigurationSection("contents"); if (shulkerSection != null) { ShulkerBox box = (ShulkerBox) state; for (String key : shulkerSection.getKeys(false)) { ItemStack boxItem = deserialize(shulkerSection.getConfigurationSection(key)); int slot = toInt(key, 0); box.getInventory().setItem(slot, boxItem); } box.update(true); bsm.setBlockState(box); } } else if (state instanceof Banner) { Banner banner = (Banner) state; ConfigurationSection patterns = config.getConfigurationSection("patterns"); if (!supports(14)) { // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/diff/src/main/java/org/bukkit/craftbukkit/block/CraftBanner.java?until=b3dc236663a55450c69356e660c0c84f0abbb3aa banner.setBaseColor(DyeColor.WHITE); } if (patterns != null) { for (String pattern : patterns.getKeys(false)) { PatternType type = Enums.getIfPresent(PatternType.class, pattern).orNull(); if (type == null) type = Enums.getIfPresent(PatternType.class, pattern.toUpperCase(Locale.ENGLISH)).or(PatternType.BASE); DyeColor color = Enums.getIfPresent(DyeColor.class, patterns.getString(pattern).toUpperCase(Locale.ENGLISH)).or(DyeColor.WHITE); banner.addPattern(new Pattern(color, type)); } banner.update(true); bsm.setBlockState(banner); } } } else if (meta instanceof FireworkMeta) { FireworkMeta firework = (FireworkMeta) meta; firework.setPower(config.getInt("power")); ConfigurationSection fireworkSection = config.getConfigurationSection("firework"); if (fireworkSection != null) { FireworkEffect.Builder builder = FireworkEffect.builder(); for (String fws : fireworkSection.getKeys(false)) { ConfigurationSection fw = config.getConfigurationSection("firework." + fws); builder.flicker(fw.getBoolean("flicker")); builder.trail(fw.getBoolean("trail")); builder.with(Enums.getIfPresent(FireworkEffect.Type.class, fw.getString("type") .toUpperCase(Locale.ENGLISH)) .or(FireworkEffect.Type.STAR)); ConfigurationSection colorsSection = fw.getConfigurationSection("colors"); if (colorsSection != null) { List fwColors = colorsSection.getStringList("base"); List colors = new ArrayList<>(fwColors.size()); for (String colorStr : fwColors) colors.add(parseColor(colorStr)); builder.withColor(colors); fwColors = colorsSection.getStringList("fade"); colors = new ArrayList<>(fwColors.size()); for (String colorStr : fwColors) colors.add(parseColor(colorStr)); builder.withFade(colors); } firework.addEffect(builder.build()); } } } else if (meta instanceof BookMeta) { BookMeta book = (BookMeta) meta; ConfigurationSection bookInfo = config.getConfigurationSection("book"); if (bookInfo != null) { book.setTitle(bookInfo.getString("title")); book.setAuthor(bookInfo.getString("author")); book.setPages(bookInfo.getStringList("pages")); if (supports(9)) { String generationValue = bookInfo.getString("generation"); if (generationValue != null) { BookMeta.Generation generation = Enums.getIfPresent(BookMeta.Generation.class, generationValue).orNull(); book.setGeneration(generation); } } } } else if (meta instanceof MapMeta) { MapMeta map = (MapMeta) meta; ConfigurationSection mapSection = config.getConfigurationSection("map"); if (mapSection != null) { map.setScaling(mapSection.getBoolean("scaling")); if (supports(11)) { if (mapSection.isSet("location")) map.setLocationName(mapSection.getString("location")); if (mapSection.isSet("color")) { Color color = parseColor(mapSection.getString("color")); map.setColor(color); } } if (supports(14)) { ConfigurationSection view = mapSection.getConfigurationSection("view"); if (view != null) { World world = Bukkit.getWorld(view.getString("world")); if (world != null) { MapView mapView = Bukkit.createMap(world); mapView.setWorld(world); mapView.setScale(Enums.getIfPresent(MapView.Scale.class, view.getString("scale")).or(MapView.Scale.NORMAL)); mapView.setLocked(view.getBoolean("locked")); mapView.setTrackingPosition(view.getBoolean("tracking-position")); mapView.setUnlimitedTracking(view.getBoolean("unlimited-tracking")); ConfigurationSection centerSection = view.getConfigurationSection("center"); if (centerSection != null) { mapView.setCenterX(centerSection.getInt("x")); mapView.setCenterZ(centerSection.getInt("z")); } map.setMapView(mapView); } } } } } else { if (supports(20)) { if (meta instanceof ArmorMeta) { ArmorMeta armorMeta = (ArmorMeta) meta; if (config.isSet("trim")) { ConfigurationSection trim = config.getConfigurationSection("trim"); TrimMaterial trimMaterial = Registry.TRIM_MATERIAL.get(NamespacedKey.fromString(trim.getString("material"))); TrimPattern trimPattern = Registry.TRIM_PATTERN.get(NamespacedKey.fromString(trim.getString("pattern"))); armorMeta.setTrim(new ArmorTrim(trimMaterial, trimPattern)); } } } if (supports(17)) { if (meta instanceof AxolotlBucketMeta) { AxolotlBucketMeta bucket = (AxolotlBucketMeta) meta; String variantStr = config.getString("color"); if (variantStr != null) { Axolotl.Variant variant = Enums.getIfPresent(Axolotl.Variant.class, variantStr.toUpperCase(Locale.ENGLISH)).or(Axolotl.Variant.BLUE); bucket.setVariant(variant); } } } if (supports(16)) { if (meta instanceof CompassMeta) { CompassMeta compass = (CompassMeta) meta; compass.setLodestoneTracked(config.getBoolean("tracked")); ConfigurationSection lodestone = config.getConfigurationSection("lodestone"); if (lodestone != null) { World world = Bukkit.getWorld(lodestone.getString("world")); double x = lodestone.getDouble("x"); double y = lodestone.getDouble("y"); double z = lodestone.getDouble("z"); compass.setLodestone(new Location(world, x, y, z)); } } } if (supports(15)) { if (meta instanceof SuspiciousStewMeta) { SuspiciousStewMeta stew = (SuspiciousStewMeta) meta; for (String effects : config.getStringList("effects")) { XPotion.Effect effect = XPotion.parseEffect(effects); if (effect.hasChance()) stew.addCustomEffect(effect.getEffect(), true); } } } if (supports(14)) { if (meta instanceof CrossbowMeta) { CrossbowMeta crossbow = (CrossbowMeta) meta; ConfigurationSection projectiles = config.getConfigurationSection("projectiles"); if (projectiles != null) { for (String projectile : projectiles.getKeys(false)) { ItemStack projectileItem = deserialize(config.getConfigurationSection("projectiles." + projectile)); crossbow.addChargedProjectile(projectileItem); } } } else if (meta instanceof TropicalFishBucketMeta) { TropicalFishBucketMeta tropical = (TropicalFishBucketMeta) meta; DyeColor color = Enums.getIfPresent(DyeColor.class, config.getString("color")).or(DyeColor.WHITE); DyeColor patternColor = Enums.getIfPresent(DyeColor.class, config.getString("pattern-color")).or(DyeColor.WHITE); TropicalFish.Pattern pattern = Enums.getIfPresent(TropicalFish.Pattern.class, config.getString("pattern")).or(TropicalFish.Pattern.BETTY); tropical.setBodyColor(color); tropical.setPatternColor(patternColor); tropical.setPattern(pattern); } } // Apparently Suspicious Stew was never added in 1.14 if (!supports(13)) { // Spawn Eggs if (supports(11)) { if (meta instanceof SpawnEggMeta) { String creatureName = config.getString("creature"); if (!Strings.isNullOrEmpty(creatureName)) { SpawnEggMeta spawnEgg = (SpawnEggMeta) meta; com.google.common.base.Optional creature = Enums.getIfPresent(EntityType.class, creatureName.toUpperCase(Locale.ENGLISH)); if (creature.isPresent()) spawnEgg.setSpawnedType(creature.get()); } } } else { MaterialData data = item.getData(); if (data instanceof SpawnEgg) { String creatureName = config.getString("creature"); if (!Strings.isNullOrEmpty(creatureName)) { SpawnEgg spawnEgg = (SpawnEgg) data; com.google.common.base.Optional creature = Enums.getIfPresent(EntityType.class, creatureName.toUpperCase(Locale.ENGLISH)); if (creature.isPresent()) spawnEgg.setSpawnedType(creature.get()); item.setData(data); } } } } } // Display Name String name = config.getString("name"); if (!Strings.isNullOrEmpty(name)) { String translated = translator.apply(name); meta.setDisplayName(translated); } else if (name != null && name.isEmpty()) meta.setDisplayName(" "); // For GUI easy access configuration purposes // Unbreakable if (supports(11) && config.isSet("unbreakable")) meta.setUnbreakable(config.getBoolean("unbreakable")); // Custom Model Data if (supports(14)) { int modelData = config.getInt("custom-model-data"); if (modelData != 0) meta.setCustomModelData(modelData); } // Lore if (config.isSet("lore")) { List translatedLore; List lores = config.getStringList("lore"); if (!lores.isEmpty()) { translatedLore = new ArrayList<>(lores.size()); for (String lore : lores) { if (lore.isEmpty()) { translatedLore.add(" "); continue; } for (String singleLore : splitNewLine(lore)) { if (singleLore.isEmpty()) { translatedLore.add(" "); continue; } translatedLore.add(translator.apply(singleLore)); } } } else { String lore = config.getString("lore"); translatedLore = new ArrayList<>(10); if (!Strings.isNullOrEmpty(lore)) { for (String singleLore : splitNewLine(lore)) { if (singleLore.isEmpty()) { translatedLore.add(" "); continue; } translatedLore.add(translator.apply(singleLore)); } } } meta.setLore(translatedLore); } // Enchantments ConfigurationSection enchants = config.getConfigurationSection("enchants"); if (enchants != null) { for (String ench : enchants.getKeys(false)) { Optional enchant = XEnchantment.matchXEnchantment(ench); enchant.ifPresent(xEnchantment -> meta.addEnchant(xEnchantment.getEnchant(), enchants.getInt(ench), true)); } } else if (config.getBoolean("glow")) { meta.addEnchant(XEnchantment.UNBREAKING.getEnchant(), 1, false); meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); // HIDE_UNBREAKABLE is not for UNBREAKING enchant. } // Enchanted Books ConfigurationSection enchantment = config.getConfigurationSection("stored-enchants"); if (enchantment != null) { for (String ench : enchantment.getKeys(false)) { Optional enchant = XEnchantment.matchXEnchantment(ench); EnchantmentStorageMeta book = (EnchantmentStorageMeta) meta; enchant.ifPresent(xEnchantment -> book.addStoredEnchant(xEnchantment.getEnchant(), enchantment.getInt(ench), true)); } } // Flags List flags = config.getStringList("flags"); if (!flags.isEmpty()) { for (String flag : flags) { flag = flag.toUpperCase(Locale.ENGLISH); if (flag.equals("ALL")) { meta.addItemFlags(ITEM_FLAGS); break; } ItemFlag itemFlag = Enums.getIfPresent(ItemFlag.class, flag).orNull(); if (itemFlag != null) meta.addItemFlags(itemFlag); } } else { String allFlags = config.getString("flags"); if (!Strings.isNullOrEmpty(allFlags) && allFlags.equalsIgnoreCase("ALL")) meta.addItemFlags(ITEM_FLAGS); } // Atrributes - https://minecraft.wiki/w/Attribute if (supports(13)) { ConfigurationSection attributes = config.getConfigurationSection("attributes"); if (attributes != null) { for (String attribute : attributes.getKeys(false)) { Attribute attributeInst = Enums.getIfPresent(Attribute.class, attribute.toUpperCase(Locale.ENGLISH)).orNull(); if (attributeInst == null) continue; ConfigurationSection section = attributes.getConfigurationSection(attribute); if (section == null) continue; String attribId = section.getString("id"); UUID id = attribId != null ? UUID.fromString(attribId) : UUID.randomUUID(); EquipmentSlot slot = section.getString("slot") != null ? Enums.getIfPresent(EquipmentSlot.class, section.getString("slot")).or(EquipmentSlot.HAND) : null; AttributeModifier modifier = new AttributeModifier( id, section.getString("name"), section.getDouble("amount"), Enums.getIfPresent(AttributeModifier.Operation.class, section.getString("operation")) .or(AttributeModifier.Operation.ADD_NUMBER), slot); meta.addAttributeModifier(attributeInst, modifier); } } } item.setItemMeta(meta); return item; } /** * Converts a {@code Map} into a {@code ConfigurationSection}. * * @param map the map to convert. * @return a {@code ConfigurationSection} containing the map values. */ @Nonnull private static ConfigurationSection mapToConfigSection(@Nonnull Map map) { ConfigurationSection config = new MemoryConfiguration(); for (Map.Entry entry : map.entrySet()) { String key = entry.getKey().toString(); Object value = entry.getValue(); if (value == null) continue; if (value instanceof Map) { value = mapToConfigSection((Map) value); } config.set(key, value); } return config; } /** * Converts a {@code ConfigurationSection} into a {@code Map}. * * @param config the configuration section to convert. * @return a {@code Map} containing the configuration section values. */ @Nonnull private static Map configSectionToMap(@Nonnull ConfigurationSection config) { Map map = new LinkedHashMap<>(); for (String key : config.getKeys(false)) { Object value = config.get(key); if (value == null) continue; if (value instanceof ConfigurationSection) { value = configSectionToMap((ConfigurationSection) value); } map.put(key, value); } return map; } /** * Parses RGB color codes from a string. * This only works for 1.13 and above. * * @param str the RGB string. * @return a color based on the RGB. * @since 1.1.0 */ @Nonnull public static Color parseColor(@Nullable String str) { if (Strings.isNullOrEmpty(str)) return Color.BLACK; List rgb = split(str.replace(" ", ""), ','); if (rgb.size() < 3) return Color.WHITE; return Color.fromRGB(toInt(rgb.get(0), 0), toInt(rgb.get(1), 0), toInt(rgb.get(2), 0)); } /** * Adds a list of items to the player's inventory and drop the items that did not fit. * * @param player the player to give the items to. * @param items the items to give. * @return the items that did not fit and were dropped. * @since 2.0.1 */ @Nonnull public static List giveOrDrop(@Nonnull Player player, @Nullable ItemStack... items) { return giveOrDrop(player, false, items); } /** * Adds a list of items to the player's inventory and drop the items that did not fit. * * @param player the player to give the items to. * @param items the items to give. * @param split same as {@link #addItems(Inventory, boolean, ItemStack...)} * @return the items that did not fit and were dropped. * @since 2.0.1 */ @Nonnull public static List giveOrDrop(@Nonnull Player player, boolean split, @Nullable ItemStack... items) { if (items == null || items.length == 0) return new ArrayList<>(); List leftOvers = addItems(player.getInventory(), split, items); World world = player.getWorld(); Location location = player.getLocation(); for (ItemStack drop : leftOvers) world.dropItemNaturally(location, drop); return leftOvers; } public static List addItems(@Nonnull Inventory inventory, boolean split, @Nonnull ItemStack... items) { return addItems(inventory, split, null, items); } /** * Optimized version of {@link Inventory#addItem(ItemStack...)} * CraftInventory * * @param inventory the inventory to add the items to. * @param split false if it should check for the inventory stack size {@link Inventory#getMaxStackSize()} or * true for item's max stack size {@link ItemStack#getMaxStackSize()} when putting items. This is useful when * you're adding stacked tools such as swords that you'd like to split them to other slots. * @param modifiableSlots the slots that are allowed to be used for adding the items, otherwise null to allow all slots. * @param items the items to add. * @return items that didn't fit in the inventory. * @since 4.0.0 */ @Nonnull public static List addItems(@Nonnull Inventory inventory, boolean split, @Nullable Predicate modifiableSlots, @Nonnull ItemStack... items) { Objects.requireNonNull(inventory, "Cannot add items to null inventory"); Objects.requireNonNull(items, "Cannot add null items to inventory"); List leftOvers = new ArrayList<>(items.length); // No other optimized way to access this using Bukkit API... // We could pass the length to individual methods, so they could also use getItem() which // skips parsing all the items in the inventory if not needed, but that's just too much. // Note: This is not the same as Inventory#getSize() int invSize = inventory.getStorageContents().length; int lastEmpty = 0; for (ItemStack item : items) { int lastPartial = 0; int maxAmount = split ? item.getMaxStackSize() : inventory.getMaxStackSize(); while (true) { // Check if there is a similar item that can be stacked before using free slots. int firstPartial = lastPartial >= invSize ? -1 : firstPartial(inventory, item, lastPartial, modifiableSlots); if (firstPartial == -1) { // No partial items found // Start adding items to leftovers if there are no partial and empty slots // -1 means that there are no empty slots left. if (lastEmpty != -1) lastEmpty = firstEmpty(inventory, lastEmpty, modifiableSlots); if (lastEmpty == -1) { leftOvers.add(item); break; } // Avoid firstPartial() for checking again for no reason, since if we're already checking // for free slots, that means there are no partials even left. lastPartial = Integer.MAX_VALUE; int amount = item.getAmount(); if (amount <= maxAmount) { inventory.setItem(lastEmpty, item); break; } else { ItemStack copy = item.clone(); copy.setAmount(maxAmount); inventory.setItem(lastEmpty, copy); item.setAmount(amount - maxAmount); } if (++lastEmpty == invSize) lastEmpty = -1; } else { ItemStack partialItem = inventory.getItem(firstPartial); int sum = item.getAmount() + partialItem.getAmount(); if (sum <= maxAmount) { partialItem.setAmount(sum); inventory.setItem(firstPartial, partialItem); break; } else { partialItem.setAmount(maxAmount); inventory.setItem(firstPartial, partialItem); item.setAmount(sum - maxAmount); } lastPartial = firstPartial + 1; } } } return leftOvers; } public static int firstPartial(@Nonnull Inventory inventory, @Nullable ItemStack item, int beginIndex) { return firstPartial(inventory, item, beginIndex, null); } /** * Gets the item slot in the inventory that matches the given item argument. * The matched item must be {@link ItemStack#isSimilar(ItemStack)} and has not * reached its {@link ItemStack#getMaxStackSize()} for the inventory. * * @param inventory the inventory to match the item from. * @param item the item to match. * @param beginIndex the index which to start the search from in the inventory. * @param modifiableSlots the slots that can be used to share items. * @return the first matched item slot, otherwise -1 * @throws IndexOutOfBoundsException if the beginning index is less than 0 or greater than the inventory storage size. * @since 4.0.0 */ public static int firstPartial(@Nonnull Inventory inventory, @Nullable ItemStack item, int beginIndex, @Nullable Predicate modifiableSlots) { if (item != null) { ItemStack[] items = inventory.getStorageContents(); int invSize = items.length; if (beginIndex < 0 || beginIndex >= invSize) throw new IndexOutOfBoundsException("Begin Index: " + beginIndex + ", Inventory storage content size: " + invSize); for (; beginIndex < invSize; beginIndex++) { if (modifiableSlots != null && !modifiableSlots.test(beginIndex)) continue; ItemStack cItem = items[beginIndex]; if (cItem != null && cItem.getAmount() < cItem.getMaxStackSize() && cItem.isSimilar(item)) return beginIndex; } } return -1; } public static List stack(@Nonnull Collection items) { return stack(items, ItemStack::isSimilar); } /** * Stacks up the items in the given item collection that are pass the similarity check. * This means that if you have a collection that consists of separate items with the same material, you can reduce them using the following: *
{@code
     *   List items = Arrays.asList(XMaterial.STONE.parseItem(), XMaterial.STONE.parseItem(), XMaterial.AIR.parseItem());
     *   items = XItemStack.stack(items, (first, second) -> first.getType == second.getType());
     *   // items -> [STONE x2, AIR x1]
     * }
* * @param items the items to stack. * @return stacked up items. * @since 4.0.0 */ @Nonnull public static List stack(@Nonnull Collection items, @Nonnull BiPredicate similarity) { Objects.requireNonNull(items, "Cannot stack null items"); Objects.requireNonNull(similarity, "Similarity check cannot be null"); List stacked = new ArrayList<>(items.size()); for (ItemStack item : items) { if (item == null) continue; boolean add = true; for (ItemStack stack : stacked) { if (similarity.test(item, stack)) { stack.setAmount(stack.getAmount() + item.getAmount()); add = false; break; } } if (add) stacked.add(item.clone()); } return stacked; } public static int firstEmpty(@Nonnull Inventory inventory, int beginIndex) { return firstEmpty(inventory, beginIndex, null); } /** * Gets the first item slot in the inventory that is empty or matches the given item argument. * The matched item must be {@link ItemStack#isSimilar(ItemStack)} and has not * reached its {@link ItemStack#getMaxStackSize()} for the inventory. * * @param inventory the inventory to search from. * @param beginIndex the item slot to start the search from in the inventory. * @param modifiableSlots the slots that can be used. * @return first empty item slot, otherwise -1 * @throws IndexOutOfBoundsException if the beginning index is less than 0 or greater than the inventory storage size. * @since 4.0.0 */ public static int firstEmpty(@Nonnull Inventory inventory, int beginIndex, @Nullable Predicate modifiableSlots) { ItemStack[] items = inventory.getStorageContents(); int invSize = items.length; if (beginIndex < 0 || beginIndex >= invSize) throw new IndexOutOfBoundsException("Begin Index: " + beginIndex + ", Inventory storage content size: " + invSize); for (; beginIndex < invSize; beginIndex++) { if (modifiableSlots != null && !modifiableSlots.test(beginIndex)) continue; if (items[beginIndex] == null) return beginIndex; } return -1; } /** * Gets the first empty slot or partial item in the inventory from an index. * * @param inventory the inventory to search from. * @param beginIndex the item slot to start the search from in the inventory. * @return first empty or partial item slot, otherwise -1 * @throws IndexOutOfBoundsException if the beginning index is less than 0 or greater than the inventory storage size. * @see #firstEmpty(Inventory, int) * @see #firstPartial(Inventory, ItemStack, int) * @since 4.2.0 */ public static int firstPartialOrEmpty(@Nonnull Inventory inventory, @Nullable ItemStack item, int beginIndex) { if (item != null) { ItemStack[] items = inventory.getStorageContents(); int len = items.length; if (beginIndex < 0 || beginIndex >= len) throw new IndexOutOfBoundsException("Begin Index: " + beginIndex + ", Size: " + len); for (; beginIndex < len; beginIndex++) { ItemStack cItem = items[beginIndex]; if (cItem == null || (cItem.getAmount() < cItem.getMaxStackSize() && cItem.isSimilar(item))) return beginIndex; } } return -1; } public static class MaterialCondition extends RuntimeException { protected XMaterial solution; public MaterialCondition(String message) { super(message); } public void setSolution(XMaterial solution) { this.solution = solution; } public boolean hasSolution() { return this.solution != null; } } public static final class UnknownMaterialCondition extends MaterialCondition { private final String material; public UnknownMaterialCondition(String material) { super("Unknown material: " + material); this.material = material; } public String getMaterial() { return material; } } public static final class UnAcceptableMaterialCondition extends MaterialCondition { private final XMaterial material; private final Reason reason; public UnAcceptableMaterialCondition(XMaterial material, Reason reason) { super("Unacceptable material: " + material.name() + " (" + reason.name() + ')'); this.material = material; this.reason = reason; } public Reason getReason() { return reason; } public XMaterial getMaterial() { return material; } public enum Reason {UNSUPPORTED, NOT_DISPLAYABLE} } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy