package tc.oc.pgm.kits; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.base.Splitter; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import org.bukkit.Color; import org.bukkit.Material; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; import org.bukkit.attribute.ItemAttributeModifier; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.LeatherArmorMeta; import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.potion.Potion; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.jdom2.Element; import tc.oc.commons.bukkit.util.BukkitUtils; import tc.oc.commons.core.formatting.StringUtils; import tc.oc.commons.core.util.Pair; import tc.oc.pgm.kits.tag.Grenade; import tc.oc.pgm.kits.tag.ItemTags; import tc.oc.pgm.utils.XMLUtils; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; import tc.oc.pgm.xml.parser.ElementParser; import tc.oc.pgm.xml.parser.Parser; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowFunction; /** * Item parser with no MapScoped dependencies, so it can be used outside of any specific map. * * May be missing some features that require a map. */ public class GlobalItemParser implements ElementParser<ItemStack> { private final Parser<Material> materialParser; @Inject protected GlobalItemParser(Parser<Material> materialParser) { this.materialParser = materialParser; } @Override public @Nullable ItemStack parseElement(@Nullable Element element) throws InvalidXMLException { return parseItem(element, true); } public ItemStack parseRequiredItem(Element parent) throws InvalidXMLException { final Element el = XMLUtils.getRequiredUniqueChild(parent); switch(el.getName()) { case "item": return parseItem(el, false); case "head": return parseItem(el, Material.SKULL_ITEM, (short) 3); case "book": return parseItem(el, Material.WRITTEN_BOOK); } throw new InvalidXMLException("Item expected", el); } public ItemStack parseBook(Element el) throws InvalidXMLException { return parseItem(el, Material.WRITTEN_BOOK); } public ItemStack parseHead(Element el) throws InvalidXMLException { return parseItem(el, Material.SKULL_ITEM, (short) 3); } public @Nullable ItemStack parseItem(@Nullable Element el, boolean allowAir) throws InvalidXMLException { if (el == null) return null; final Node materialNode = Optional.ofNullable(el.getAttribute("material")) .map(Node::of) .orElseGet(() -> Node.of(el)); final Material material = materialParser.parse(materialNode); if(material == Material.AIR && !allowAir) { throw new InvalidXMLException("Material AIR is not allowed here", materialNode); } return parseItem(el, material); } public ItemStack parseItem(Element el, Material type) throws InvalidXMLException { return parseItem(el, type, XMLUtils.parseNumber(el.getAttribute("damage"), Short.class, (short) 0)); } public ItemStack parseItem(Element el, Material type, short damage) throws InvalidXMLException { int amount = XMLUtils.parseNumber(el.getAttribute("amount"), Integer.class, 1); // If the item is a potion with non-zero damage, and there is // no modern potion ID, decode the legacy damage value. final Potion legacyPotion; if(type == Material.POTION && damage > 0 && el.getAttribute("potion") == null) { try { legacyPotion = Potion.fromDamage(damage); } catch(IllegalArgumentException e) { throw new InvalidXMLException("Invalid legacy potion damage value " + damage + ": " + e.getMessage(), el, e); } // If the legacy splash bit is set, convert to a splash potion if(legacyPotion.isSplash()) { type = Material.SPLASH_POTION; legacyPotion.setSplash(false); } // Potions always have damage 0 damage = 0; } else { legacyPotion = null; } ItemStack itemStack = new ItemStack(type, amount, damage); if(itemStack.getType() != type) { throw new InvalidXMLException("Invalid item/block", el); } final ItemMeta meta = itemStack.getItemMeta(); if(meta != null) { // This happens if the item is "air" parseItemMeta(el, meta); // If we decoded a legacy potion, apply it now, but only if there are no custom effects. // This emulates the old behavior of custom effects overriding default effects. if(legacyPotion != null) { final PotionMeta potionMeta = (PotionMeta) meta; if(!potionMeta.hasCustomEffects()) { potionMeta.setBasePotionData(new PotionData(legacyPotion.getType(), legacyPotion.hasExtendedDuration(), legacyPotion.getLevel() == 2)); } } itemStack.setItemMeta(meta); } return itemStack; } public void parseItemMeta(Element el, ItemMeta meta) throws InvalidXMLException { parseEnchantments(el, "enchantment").forEach( (enchantment, level) -> meta.addEnchant(enchantment, level, true) ); if(meta instanceof EnchantmentStorageMeta) { parseEnchantments(el, "stored-enchantment").forEach( (enchantment, level) -> ((EnchantmentStorageMeta) meta).addStoredEnchant(enchantment, level, true) ); } if(meta instanceof PotionMeta) { final PotionMeta potionMeta = (PotionMeta) meta; final Node potionAttr = Node.fromAttr(el, "potion"); if(potionAttr != null) { potionMeta.setPotionBrew(XMLUtils.parsePotion(potionAttr)); } final List<PotionEffect> effects = parsePotionEffects(el); for(PotionEffect effect : potionMeta.getCustomEffects()) { potionMeta.removeCustomEffect(effect.getType()); } for(PotionEffect effect : effects) { potionMeta.addCustomEffect(effect, false); } } for(Map.Entry<Attribute, ItemAttributeModifier> entry : parseItemAttributeModifiers(el).entries()) { meta.addAttributeModifier(entry.getKey(), entry.getValue()); } String customName = el.getAttributeValue("name"); if(customName != null) { meta.setDisplayName(BukkitUtils.colorize(customName)); } else if (XMLUtils.parseBoolean(el.getAttribute("grenade"), false)) { meta.setDisplayName("Grenade"); } if(meta instanceof LeatherArmorMeta) { LeatherArmorMeta armorMeta = (LeatherArmorMeta) meta; org.jdom2.Attribute attrColor = el.getAttribute("color"); if(attrColor != null) { String raw = attrColor.getValue(); if(!raw.matches("[a-fA-F0-9]{6}")) { throw new InvalidXMLException("Invalid color format", attrColor); } armorMeta.setColor(Color.fromRGB(Integer.parseInt(attrColor.getValue(), 16))); } } String loreText = el.getAttributeValue("lore"); if(loreText != null) { List<String> lore = ImmutableList.copyOf(Splitter.on('|').split(BukkitUtils.colorize(loreText))); meta.setLore(lore); } for(ItemFlag flag : ItemFlag.values()) { if(!XMLUtils.parseBoolean(Node.fromAttr(el, "show-" + itemFlagName(flag)), true)) { meta.addItemFlags(flag); } } if(XMLUtils.parseBoolean(el.getAttribute("unbreakable"), false)) { meta.setUnbreakable(true); } Element elCanDestroy = el.getChild("can-destroy"); if(elCanDestroy != null) { meta.setCanDestroy(XMLUtils.parseMaterialMatcher(elCanDestroy).getMaterials()); } Element elCanPlaceOn = el.getChild("can-place-on"); if(elCanPlaceOn != null) { meta.setCanPlaceOn(XMLUtils.parseMaterialMatcher(elCanPlaceOn).getMaterials()); } if(meta instanceof SkullMeta) { final Node skin = Node.fromChildOrAttr(el, "skin"); if(skin != null) { ((SkullMeta) meta).setOwner(XMLUtils.parseUsername(Node.fromChildOrAttr(el, "username")), Node.childOrAttr(el, "uuid") .map(rethrowFunction(XMLUtils::parseUuid)) .orElseGet(UUID::randomUUID), XMLUtils.parseUnsignedSkin(Node.fromRequiredChildOrAttr(el, "skin"))); } } if(meta instanceof BookMeta) { final BookMeta book = (BookMeta) meta; Node.childOrAttr(el, "title").ifPresent( node -> book.setTitle(BukkitUtils.colorize(node.getValue())) ); Node.childOrAttr(el, "author").ifPresent( node -> book.setAuthor(BukkitUtils.colorize(node.getValue())) ); Element elPages = el.getChild("pages"); if(elPages != null) { for(Element elPage : elPages.getChildren("page")) { String text = elPage.getText(); text = text.trim(); // Remove leading and trailing whitespace text = Pattern.compile("^[ \\t]+", Pattern.MULTILINE).matcher(text).replaceAll(""); // Remove indentation on each line text = Pattern.compile("^\\n", Pattern.MULTILINE).matcher(text).replaceAll(" \n"); // Add a space to blank lines, otherwise they vanish for unknown reasons text = BukkitUtils.colorize(text); // Color codes book.addPage(text); } } } parseCustomNBT(el, meta); } String itemFlagName(ItemFlag flag) { switch(flag) { case HIDE_ATTRIBUTES: return "attributes"; case HIDE_ENCHANTS: return "enchantments"; case HIDE_UNBREAKABLE: return "unbreakable"; case HIDE_DESTROYS: return "can-destroy"; case HIDE_PLACED_ON: return "can-place-on"; case HIDE_POTION_EFFECTS: return "other"; } throw new IllegalStateException("Unknown item flag " + flag); } public void parseCustomNBT(Element el, ItemMeta meta) throws InvalidXMLException { if (XMLUtils.parseBoolean(el.getAttribute("grenade"), false)) { Grenade.ITEM_TAG.set(meta, new Grenade( XMLUtils.parseNumber(el.getAttribute("grenade-power"), Float.class, 1f), XMLUtils.parseBoolean(el.getAttribute("grenade-fire"), false), XMLUtils.parseBoolean(el.getAttribute("grenade-destroy"), true) )); } if(XMLUtils.parseBoolean(el.getAttribute("prevent-sharing"), false)) { ItemTags.PREVENT_SHARING.set(meta, true); } if(XMLUtils.parseBoolean(el.getAttribute("locked"), false)) { ItemTags.LOCKED.set(meta, true); } } public Pair<Enchantment, Integer> parseEnchantment(Element el) throws InvalidXMLException { return Pair.create(XMLUtils.parseEnchantment(new Node(el)), XMLUtils.parseNumber(Node.fromAttr(el, "level"), Integer.class, 1)); } public Map<Enchantment, Integer> parseEnchantments(Element el, String name) throws InvalidXMLException { Map<Enchantment, Integer> enchantments = Maps.newHashMap(); Node attr = Node.fromAttr(el, name, StringUtils.pluralize(name)); if(attr != null) { Iterable<String> enchantmentTexts = Splitter.on(";").split(attr.getValue()); for(String enchantmentText : enchantmentTexts) { int level = 1; List<String> parts = Lists.newArrayList(Splitter.on(":").limit(2).split(enchantmentText)); Enchantment enchant = XMLUtils.parseEnchantment(attr, parts.get(0)); if(parts.size() > 1) { level = XMLUtils.parseNumber(attr, parts.get(1), Integer.class); } enchantments.put(enchant, level); } } for(Element elEnchantment : el.getChildren(name)) { Pair<Enchantment, Integer> entry = parseEnchantment(elEnchantment); enchantments.put(entry.first, entry.second); } return enchantments; } public SetMultimap<Attribute, AttributeModifier> parseAttributeModifiers(Element el) throws InvalidXMLException { SetMultimap<Attribute, AttributeModifier> modifiers = HashMultimap.create(); Node attr = Node.fromAttr(el, "attribute", "attributes"); if(attr != null) { for(String modifierText : Splitter.on(";").split(attr.getValue())) { Pair<Attribute, AttributeModifier> mod = XMLUtils.parseCompactAttributeModifier(attr, modifierText); modifiers.put(mod.first, mod.second); } } for(Element elAttribute : el.getChildren("attribute")) { Pair<Attribute, AttributeModifier> mod = XMLUtils.parseAttributeModifier(elAttribute); modifiers.put(mod.first, mod.second); } return modifiers; } public SetMultimap<Attribute, ItemAttributeModifier> parseItemAttributeModifiers(Element el) throws InvalidXMLException { SetMultimap<Attribute, ItemAttributeModifier> modifiers = HashMultimap.create(); Node attr = Node.fromAttr(el, "attribute", "attributes"); if(attr != null) { for(String modifierText : Splitter.on(";").split(attr.getValue())) { Pair<Attribute, AttributeModifier> mod = XMLUtils.parseCompactAttributeModifier(attr, modifierText); modifiers.put(mod.first, new ItemAttributeModifier(null, mod.second)); } } for(Element elAttribute : el.getChildren("attribute")) { Pair<Attribute, ItemAttributeModifier> mod = XMLUtils.parseItemAttributeModifier(elAttribute); modifiers.put(mod.first, mod.second); } return modifiers; } public List<PotionEffect> parsePotionEffects(Element el) throws InvalidXMLException { List<PotionEffect> effects = new ArrayList<>(); Node attr = Node.fromAttr(el, "effect", "effects", "potions"); if(attr != null) { for(String piece : attr.getValue().split(";")) { effects.add(checkPotionEffect(XMLUtils.parseCompactPotionEffect(attr, piece), attr)); } } for(Element elPotion : XMLUtils.getChildren(el, "effect", "potion")) { effects.add(parsePotionEffect(elPotion)); } return effects; } public PotionEffect parsePotionEffect(Element el) throws InvalidXMLException { return checkPotionEffect(XMLUtils.parsePotionEffect(el), new Node(el)); } private PotionEffect checkPotionEffect(PotionEffect effect, Node node) throws InvalidXMLException { if(effect.getType().equals(PotionEffectType.HEALTH_BOOST) && effect.getAmplifier() < 0) { if(effect.getDuration() != Integer.MAX_VALUE) { // TODO: enable this check after existing maps are fixed // throw new InvalidXMLException("Negative health boost effect must have infinite duration (use max-health instead)", node); } } return effect; } }