diff --git a/src/main/java/nl/pim16aap2/armoredElytra/ArmoredElytra.java b/src/main/java/nl/pim16aap2/armoredElytra/ArmoredElytra.java index 6c03aa5..c58e4f3 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/ArmoredElytra.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/ArmoredElytra.java @@ -57,7 +57,7 @@ public class ArmoredElytra extends JavaPlugin implements Listener myLogger(Level.SEVERE, "Trying to run this plugin on an unsupported version... ABORT!"); return; } - + nbtEditor = minecraftVersion.isNewerThan(MinecraftVersion.v1_15) ? new NBTEditor() : new NBTEditor_legacy(); config = new ConfigLoader(this); @@ -82,7 +82,8 @@ public class ArmoredElytra extends JavaPlugin implements Listener // Load the files for the correct version of Minecraft. if (compatibleMCVer()) { - Bukkit.getPluginManager().registerEvents(new EventHandlers(this, is1_9), this); + Bukkit.getPluginManager() + .registerEvents(new EventHandlers(this, is1_9, config.craftingInSmithingTable()), this); getCommand("ArmoredElytra").setExecutor(new CommandHandler(this)); } else @@ -142,6 +143,8 @@ public class ArmoredElytra extends JavaPlugin implements Listener messages.getString(Message.TIER_SHORT_IRON))); armorTierNames.put(ArmorTier.DIAMOND, new ArmorTierName(messages.getString(Message.TIER_DIAMOND), messages.getString(Message.TIER_SHORT_DIAMOND))); + armorTierNames.put(ArmorTier.NETHERITE, new ArmorTierName(messages.getString(Message.TIER_NETHERITE), + messages.getString(Message.TIER_SHORT_NETHERITE))); } public boolean playerHasCraftPerm(Player player, ArmorTier armorTier) diff --git a/src/main/java/nl/pim16aap2/armoredElytra/handlers/EventHandlers.java b/src/main/java/nl/pim16aap2/armoredElytra/handlers/EventHandlers.java index 4c94b88..03ca08b 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/handlers/EventHandlers.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/handlers/EventHandlers.java @@ -39,10 +39,12 @@ public class EventHandlers implements Listener private final Consumer cleanAnvilInventory; private final Consumer moveChestplateToInventory; + private final boolean creationEnabled; - public EventHandlers(ArmoredElytra plugin, boolean is1_9) + public EventHandlers(ArmoredElytra plugin, boolean is1_9, boolean creationEnabled) { this.plugin = plugin; + this.creationEnabled = creationEnabled; initializeArmorEquipEvent(); if (is1_9) { @@ -197,6 +199,9 @@ public class EventHandlers implements Listener else if (repairItem.getType().equals(Material.DIAMOND)) mult *= (100.0f / plugin.getConfigLoader().DIAMONDS_TO_FULL()); + else if (repairItem.getType().equals(XMaterial.NETHERITE_INGOT.parseMaterial())) + mult *= (100.0f / plugin.getConfigLoader().NETHERITE_TO_FULL()); + int maxDurability = Material.ELYTRA.getMaxDurability(); int newDurability = (int) (curDur - (maxDurability * mult)); return (short) (newDurability <= 0 ? 0 : newDurability); @@ -264,12 +269,13 @@ public class EventHandlers implements Listener // If the armored elytra is to be repaired using its repair item... if (ArmorTier.getRepairItem(tier) == matTwo) - return Action.REPAIR; + return itemOne.getDurability() == 0 ? Action.NONE : Action.REPAIR; // If the armored elytra is to be combined with another armored elytra of the // same tier... + // TODO: Should this also be disabled by "creationEnabled"? if (ArmoredElytra.getInstance().getNbtEditor().getArmorTier(itemTwo) == tier) - return Action.COMBINE; + return creationEnabled ? Action.COMBINE : Action.NONE; // If the armored elytra is not of the leather tier, but itemTwo is leather, // Pick the block action, as that would repair the elytra by default (vanilla). @@ -290,6 +296,11 @@ public class EventHandlers implements Listener ItemStack itemB = event.getInventory().getItem(1); ItemStack result = null; +// if (itemA != null && itemB == null) +// { +// +// } + if (itemA != null && itemB != null) // If itemB is the elytra, while itemA isn't, switch itemA and itemB. if (itemB.getType() == Material.ELYTRA && itemA.getType() != Material.ELYTRA) @@ -361,11 +372,11 @@ public class EventHandlers implements Listener } } - // Check if either itemA or itemB is unoccupied. - if ((itemA == null || itemB == null) && - ArmoredElytra.getInstance().getNbtEditor().getArmorTier(event.getInventory().getItem(2)) != ArmorTier.NONE) - // If Item2 is occupied despite itemA or itemB not being occupied. (only for - // armored elytra)/ + // If one of the input items is null and the other an armored elytra, remove the result. + // This prevent some naming issues. + // TODO: Allow renaming armored elytras. + if ((itemA == null ^ itemB == null) && + ArmoredElytra.getInstance().getNbtEditor().getArmorTier(itemA == null ? itemB : itemA) != ArmorTier.NONE) event.setResult(null); player.updateInventory(); } diff --git a/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/INBTEditor.java b/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/INBTEditor.java index 3b91929..00fdbd0 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/INBTEditor.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/INBTEditor.java @@ -6,7 +6,8 @@ import org.bukkit.inventory.ItemStack; public interface INBTEditor { /** - * Adds a given {@link ArmorTier} to an item. The item will be cloned. + * Adds a given {@link ArmorTier} to an item. The item will be cloned. Note that setting the armor tier to {@link + * ArmorTier#NONE} has no effect (besides making a copy of the item). * * @param item The item. * @param armorTier The {@link ArmorTier} that will be added to it. diff --git a/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/NBTEditor.java b/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/NBTEditor.java index eef9402..e465aa5 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/NBTEditor.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/nbtEditor/NBTEditor.java @@ -3,17 +3,33 @@ package nl.pim16aap2.armoredElytra.nbtEditor; import nl.pim16aap2.armoredElytra.ArmoredElytra; import nl.pim16aap2.armoredElytra.util.ArmorTier; import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; import java.util.Collection; import java.util.UUID; +// TODO: Consider using static UUIDs, to ensure attributes aren't stacked. public class NBTEditor implements INBTEditor { +// private static final Map namespaceKeys; +// +// static +// { +// final Map namespaceKeysTmp = new EnumMap(ArmorTier.class); +// for (final ArmorTier tier : ArmorTier.values()) +// namespaceKeysTmp.put(tier, new NamespacedKey(ArmoredElytra.getInstance(), "ARMORTIER_" + tier.name())); +// namespaceKeys = Collections.unmodifiableMap(namespaceKeysTmp); +// } + + private static final NamespacedKey armorTierKey = new NamespacedKey(ArmoredElytra.getInstance(), + "ARMOR_TIER_LEVEL"); + public NBTEditor() { } @@ -24,16 +40,23 @@ public class NBTEditor implements INBTEditor @Override public ItemStack addArmorNBTTags(ItemStack item, ArmorTier armorTier, boolean unbreakable) { + if (armorTier == null || armorTier == ArmorTier.NONE) + return new ItemStack(item); + ItemStack ret = new ItemStack(item); ItemMeta meta = ret.hasItemMeta() ? ret.getItemMeta() : Bukkit.getItemFactory().getItemMeta(ret.getType()); if (meta == null) throw new IllegalArgumentException("Tried to add armor to invalid item: " + item); + meta.getPersistentDataContainer().set(armorTierKey, PersistentDataType.INTEGER, ArmorTier.getTierID(armorTier)); overwriteNBTValue(meta, Attribute.GENERIC_ARMOR, ArmorTier.getArmor(armorTier), "generic.armor"); - if (ArmorTier.getToughness(armorTier) > 0) overwriteNBTValue(meta, Attribute.GENERIC_ARMOR_TOUGHNESS, ArmorTier.getToughness(armorTier), - "generic.armorToughness"); + "generic.armor_toughness"); + + if (ArmorTier.getKnockbackResistance(armorTier) > 0) + overwriteNBTValue(meta, Attribute.GENERIC_KNOCKBACK_RESISTANCE, ArmorTier.getKnockbackResistance(armorTier), + "generic.knockback_resistance"); meta.setUnbreakable(unbreakable); meta.setDisplayName(ArmoredElytra.getInstance().getArmoredElytraName(armorTier)); @@ -48,7 +71,8 @@ public class NBTEditor implements INBTEditor meta.removeAttributeModifier(attribute); AttributeModifier attributeModifier = new AttributeModifier(UUID.randomUUID(), modifierName, value, - AttributeModifier.Operation.ADD_NUMBER, EquipmentSlot.CHEST); + AttributeModifier.Operation.ADD_NUMBER, + EquipmentSlot.CHEST); meta.addAttributeModifier(attribute, attributeModifier); } @@ -62,16 +86,20 @@ public class NBTEditor implements INBTEditor return ArmorTier.NONE; ItemMeta meta = item.getItemMeta(); - if (meta == null) + if (meta == null || !meta.hasAttributeModifiers()) return ArmorTier.NONE; + Integer tierID = meta.getPersistentDataContainer().get(armorTierKey, PersistentDataType.INTEGER); + if (tierID != null) + return ArmorTier.getArmorTierFromID(tierID); + Collection attributeModifiers = meta.getAttributeModifiers(Attribute.GENERIC_ARMOR); if (attributeModifiers == null) return ArmorTier.NONE; for (final AttributeModifier attributeModifier : attributeModifiers) { - ArmorTier armorTier = ArmorTier.getArmorTier((int) attributeModifier.getAmount()); + ArmorTier armorTier = ArmorTier.getArmorTierFromArmor((int) attributeModifier.getAmount()); if (armorTier != ArmorTier.NONE) return armorTier; } diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/ArmorTier.java b/src/main/java/nl/pim16aap2/armoredElytra/util/ArmorTier.java index 042a2a0..d74a53d 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/ArmorTier.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/ArmorTier.java @@ -7,54 +7,100 @@ import java.util.Map; public enum ArmorTier { - // Tier: armor-value, armor-toughness, repair - NONE (0 , 0 , null , ""), - LEATHER (3 , 0 , Material.LEATHER , "leather"), - GOLD (5 , 0 , Material.GOLD_INGOT, "gold"), - CHAIN (5 , 0 , Material.IRON_INGOT, "chain"), - IRON (6 , 0 , Material.IRON_INGOT, "iron"), - DIAMOND (8 , 2 , Material.DIAMOND , "diamond"); + // Tier: TierID, armor-value, armor-toughness, knockbackResistance, repair + NONE(0, 0, 0, 0, null, ""), + LEATHER(1, 3, 0, 0, Material.LEATHER, "leather"), + GOLD(2, 5, 0, 0, Material.GOLD_INGOT, "gold"), + CHAIN(3, 5, 0, 0, Material.IRON_INGOT, "chain"), + IRON(4, 6, 0, 0, Material.IRON_INGOT, "iron"), + DIAMOND(5, 8, 2, 0, Material.DIAMOND, "diamond"), + NETHERITE(6, 8, 2, 0.1, XMaterial.NETHERITE_INGOT.parseMaterial(), "netherite"), + ; - private final int armor; - private final int toughness; - private final Material repair; - private final String name; - private static Map map = new HashMap<>(); + private final int tierID; + private final int armor; + private final int toughness; + private final double knockbackResistance; + private final Material repair; + private final String name; + private static Map map = new HashMap<>(); private static Map armorValueMap = new HashMap<>(); + private static Map armorIDMap = new HashMap<>(); - private ArmorTier (int armor, int toughness, Material repair, String name) + ArmorTier(int tierID, int armor, int toughness, double knockbackResistance, Material repair, String name) { - this.armor = armor; - this.toughness = toughness; - this.repair = repair; - this.name = name; + this.tierID = tierID; + this.armor = armor; + this.toughness = toughness; + this.knockbackResistance = knockbackResistance; + this.repair = repair; + this.name = name; } // return the armor value of a tier. - public static int getArmor (ArmorTier tier) { return tier.armor; } + public static int getArmor(ArmorTier tier) + { + return tier.armor; + } + + // return the armor value of a tier. + public static int getTierID(ArmorTier tier) + { + return tier.tierID; + } // return the armor toughness of a tier. - public static int getToughness (ArmorTier tier) { return tier.toughness; } + public static int getToughness(ArmorTier tier) + { + return tier.toughness; + } + + // return the armor toughness of a tier. + public static double getKnockbackResistance(ArmorTier tier) + { + return tier.knockbackResistance; + } // return the repair item of a tier - public static Material getRepairItem (ArmorTier tier) { return tier.repair; } + public static Material getRepairItem(ArmorTier tier) + { + return tier.repair; + } - public static String getName (ArmorTier tier) { return tier.name; } + public static String getName(ArmorTier tier) + { + return tier.name; + } - public static ArmorTier valueOfName (String name) { return map.get(name); } + public static ArmorTier valueOfName(String name) + { + return map.get(name); + } - public static ArmorTier getArmorTier(int armor) + public static ArmorTier getArmorTierFromArmor(int armor) { ArmorTier tier = armorValueMap.get(armor); return tier == null ? ArmorTier.NONE : tier; } + public static ArmorTier getArmorTierFromID(int tierID) + { + ArmorTier tier = armorIDMap.get(tierID); + return tier == null ? ArmorTier.NONE : tier; + } + static { for (ArmorTier tier : ArmorTier.values()) { map.put(tier.name, tier); armorValueMap.put(tier.armor, tier); + armorIDMap.put(tier.tierID, tier); } + // Overwrite the index for diamond-tier armor value. + // This value is the same as netherite's tier. However, with the introduction of the NETHERITE armor tier, + // a new system was introduced that doesn't rely on the armor value for determining the armortier. + // Therefore, when using the old backup system, it is always going to be the diamond tier instead. + armorValueMap.put(ArmorTier.DIAMOND.armor, ArmorTier.DIAMOND); } } diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/ConfigLoader.java b/src/main/java/nl/pim16aap2/armoredElytra/util/ConfigLoader.java index ded2bff..f595674 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/ConfigLoader.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/ConfigLoader.java @@ -27,14 +27,16 @@ public class ConfigLoader private boolean autoDLUpdate; private int LEATHER_TO_FULL; private int DIAMONDS_TO_FULL; + private int NETHERITE_TO_FULL; private boolean noFlightDurability; private List allowedEnchantments; private boolean allowMultipleProtectionEnchantments; - public boolean bypassWearPerm; - public boolean bypassCraftPerm; + private boolean craftingInSmithingTable; + private boolean bypassWearPerm; + private boolean bypassCraftPerm; - private ArrayList> configOptionsList; - private ArmoredElytra plugin; + private final ArrayList> configOptionsList; + private final ArmoredElytra plugin; public ConfigLoader(ArmoredElytra plugin) { @@ -113,14 +115,17 @@ public class ConfigLoader { "Globally bypass permissions for wearing and/or crafting amored elytras. Useful if permissions are unavailable." }; + String[] craftingInSmithingTableComment = + { + "This option only works on 1.16+! When enabled, armored elytra creation in anvils is disabled. ", + "Instead, you will have to craft them in a smithy. Enchanting/repairing them still works via the anvil." + }; // Set default list of allowed enchantments. allowedEnchantments = new ArrayList<>(Arrays.asList("DURABILITY", "PROTECTION_FIRE", "PROTECTION_EXPLOSIONS", "PROTECTION_PROJECTILE", "PROTECTION_ENVIRONMENTAL", - "THORNS", - "BINDING_CURSE", "VANISHING_CURSE", "MENDING")); - + "THORNS", "BINDING_CURSE", "VANISHING_CURSE", "MENDING")); FileConfiguration config = plugin.getConfig(); unbreakable = addNewConfigOption(config, "unbreakable", false, unbreakableComment); @@ -129,6 +134,11 @@ public class ConfigLoader GOLD_TO_FULL = addNewConfigOption(config, "goldRepair", 5, null); IRON_TO_FULL = addNewConfigOption(config, "ironRepair", 4, null); DIAMONDS_TO_FULL = addNewConfigOption(config, "diamondsRepair", 3, null); + NETHERITE_TO_FULL = addNewConfigOption(config, "netheriteIngotsRepair", 3, null); + +// craftingInSmithingTable = addNewConfigOption(config, "craftingInSmithingTable", true, craftingInSmithingTableComment); + craftingInSmithingTable = false; + allowedEnchantments = addNewConfigOption(config, "allowedEnchantments", allowedEnchantments, enchantmentsComment); allowMultipleProtectionEnchantments = addNewConfigOption(config, "allowMultipleProtectionEnchantments", false, @@ -200,6 +210,12 @@ public class ConfigLoader return allowStats; } + public boolean craftingInSmithingTable() + { + return ArmoredElytra.getInstance().getMinecraftVersion().isNewerThan(MinecraftVersion.v1_15) && + craftingInSmithingTable; + } + public boolean unbreakable() { return unbreakable; @@ -235,6 +251,11 @@ public class ConfigLoader return DIAMONDS_TO_FULL; } + public int NETHERITE_TO_FULL() + { + return NETHERITE_TO_FULL; + } + public boolean allowMultipleProtectionEnchantments() { return allowMultipleProtectionEnchantments; diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/Util.java b/src/main/java/nl/pim16aap2/armoredElytra/util/Util.java index 9d8369b..d6315d9 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/Util.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/Util.java @@ -1,13 +1,13 @@ package nl.pim16aap2.armoredElytra.util; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Map; - import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + public class Util { public static String errorToString(Error e) @@ -40,23 +40,26 @@ public class Util switch (xmat) { - case LEATHER_CHESTPLATE: - ret = ArmorTier.LEATHER; - break; - case GOLDEN_CHESTPLATE: - ret = ArmorTier.GOLD; - break; - case CHAINMAIL_CHESTPLATE: - ret = ArmorTier.CHAIN; - break; - case IRON_CHESTPLATE: - ret = ArmorTier.IRON; - break; - case DIAMOND_CHESTPLATE: - ret = ArmorTier.DIAMOND; - break; - default: - break; + case LEATHER_CHESTPLATE: + ret = ArmorTier.LEATHER; + break; + case GOLDEN_CHESTPLATE: + ret = ArmorTier.GOLD; + break; + case CHAINMAIL_CHESTPLATE: + ret = ArmorTier.CHAIN; + break; + case IRON_CHESTPLATE: + ret = ArmorTier.IRON; + break; + case DIAMOND_CHESTPLATE: + ret = ArmorTier.DIAMOND; + break; + case NETHERITE_CHESTPLATE: + ret = ArmorTier.NETHERITE; + break; + default: + break; } return ret; } @@ -64,29 +67,37 @@ public class Util // Check if mat is a chest plate. public static boolean isChestPlate(Material mat) { - XMaterial xmat = XMaterial.matchXMaterial(mat); - if (xmat == null) + try + { + XMaterial xmat = XMaterial.matchXMaterial(mat); + if (xmat == null) + return false; + if (xmat == XMaterial.LEATHER_CHESTPLATE || xmat == XMaterial.GOLDEN_CHESTPLATE || + xmat == XMaterial.CHAINMAIL_CHESTPLATE || xmat == XMaterial.IRON_CHESTPLATE || + xmat == XMaterial.DIAMOND_CHESTPLATE || xmat == XMaterial.NETHERITE_CHESTPLATE) + return true; return false; - if (xmat == XMaterial.LEATHER_CHESTPLATE || xmat == XMaterial.GOLDEN_CHESTPLATE || - xmat == XMaterial.CHAINMAIL_CHESTPLATE || xmat == XMaterial.IRON_CHESTPLATE || - xmat == XMaterial.DIAMOND_CHESTPLATE) - return true; - return false; + } + catch (IllegalArgumentException e) + { + // No need to handle this, this is just XMaterial complaining the material doesn't exist. + return false; + } } // Function that returns which/how many protection enchantments there are. // TODO: Use bit flags for this. public static int getProtectionEnchantmentsVal(Map enchantments) { - int ret = 0; + int ret = 0; if (enchantments.containsKey(Enchantment.PROTECTION_ENVIRONMENTAL)) - ret += 1; + ret += 1; if (enchantments.containsKey(Enchantment.PROTECTION_EXPLOSIONS)) - ret += 2; + ret += 2; if (enchantments.containsKey(Enchantment.PROTECTION_FALL)) - ret += 4; + ret += 4; if (enchantments.containsKey(Enchantment.PROTECTION_FIRE)) - ret += 8; + ret += 8; if (enchantments.containsKey(Enchantment.PROTECTION_PROJECTILE)) ret += 16; return ret; diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/XMaterial.java b/src/main/java/nl/pim16aap2/armoredElytra/util/XMaterial.java index 09354c2..0dfc7f0 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/XMaterial.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/XMaterial.java @@ -1,87 +1,175 @@ package nl.pim16aap2.armoredElytra.util; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Objects; -import java.util.Optional; -import java.util.regex.Pattern; - /* * The MIT License (MIT) * - * Original work Copyright (c) 2018 Hex_27 - * v2.0 Copyright (c) 2019 Crypto Morin + * Copyright (c) 2018 Hex_27 + * Copyright (c) 2020 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: + * 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 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. + * 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. */ +import com.google.common.base.Enums; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; +import org.apache.commons.lang.WordUtils; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; -/* - * References - * - * * * GitHub: https://github.com/CryptoMorin/XMaterial/blob/master/XMaterial.java - * * Thread: https://www.spigotmc.org/threads/378136/ - * https://minecraft.gamepedia.com/Java_Edition_data_values/Pre-flattening - * https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html - * http://docs.codelanx.com/Bukkit/1.8/org/bukkit/Material.html - * https://www.spigotmc.org/threads/1-8-to-1-13-itemstack-material-version-support.329630/ - * https://minecraft-ids.grahamedgecombe.com/ - * v1: https://pastebin.com/Fe65HZnN - * v2: 6/15/2019 - */ +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; /** - * XMaterial v2.2 - Data Values/Pre-flattening Supports 1.8-1.14 1.13 and above - * as priority. + * XMaterial - Data Values/Pre-flattening
+ * 1.13 and above as priority. + *

+ * This class is mainly designed to support ItemStacks. If you want to use it on blocks you'll have to use XBlock + *

+ * Pre-flattening: https://minecraft.gamepedia.com/Java_Edition_data_values/Pre-flattening Materials: + * https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html Materials (1.12): + * https://helpch.at/docs/1.12.2/index.html?org/bukkit/Material.html Material IDs: + * https://minecraft-ids.grahamedgecombe.com/ Material Source Code: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/browse/src/main/java/org/bukkit/Material.java + * XMaterial v1: https://www.spigotmc.org/threads/329630/ + * + * @author Crypto Morin + * @version 5.0.0 + * @see Material + * @see ItemStack */ public enum XMaterial { - - CHAINMAIL_CHESTPLATE(0, ""), - DIAMOND_CHESTPLATE(0, ""), - GOLDEN_CHESTPLATE(0, "GOLD_CHESTPLATE"), - IRON_CHESTPLATE(0, ""), - LEATHER_CHESTPLATE(0, ""), - PHANTOM_MEMBRANE(0, "1.13"), - - AIR(0, ""), + CHAINMAIL_CHESTPLATE, + DIAMOND_CHESTPLATE, + GOLDEN_CHESTPLATE("GOLD_CHESTPLATE"), + IRON_CHESTPLATE, + LEATHER_CHESTPLATE, + NETHERITE_CHESTPLATE("1.16"), + PHANTOM_MEMBRANE("1.13"), + AIR, + NETHERITE_INGOT("1.16"), ; /** - * A list of material names that can be damaged. Some names are not complete as - * this list needs to be checked with {@link String#contains}. + * An immutable cached set of {@link XMaterial#values()} to avoid allocating memory for calling the method every + * time. + * + * @since 2.0.0 */ - public static final String[] DAMAGEABLE = { "HELMET", "CHESTPLATE", "LEGGINGS", "BOOTS", "SWORD", "AXE", "PICKAXE", - "SHOVEL", "HOE", "ELYTRA", "TRIDENT", "HORSE_ARMOR", "BARDING", - "SHEARS", "FLINT_AND_STEEL", "BOW", "FISHING_ROD", "CARROT_ON_A_STICK", - "CARROT_STICK" }; + public static final EnumSet VALUES = EnumSet.allOf(XMaterial.class); + /** + * A set of material names that can be damaged. + *

+ * Most of the names are not complete as this list is intended to be checked with {@link String#contains} for memory + * usage. + * + * @since 1.0.0 + */ + private static final ImmutableSet DAMAGEABLE = ImmutableSet.of( + "HELMET", "CHESTPLATE", "LEGGINGS", "BOOTS", + "SWORD", "AXE", "PICKAXE", "SHOVEL", "HOE", + "ELYTRA", "TRIDENT", "HORSE_ARMOR", "BARDING", + "SHEARS", "FLINT_AND_STEEL", "BOW", "FISHING_ROD", + "CARROT_ON_A_STICK", "CARROT_STICK", "SPADE", "SHIELD" + ); - public static final XMaterial[] VALUES = XMaterial.values(); - private static final HashMap CACHED_SEARCH = new HashMap<>(); - private static MinecraftVersion version; - private static Boolean isNewVersion; + /* + * A set of all the legacy names without duplicates. + *

+ * It'll help to free up a lot of memory if it's not used. + * Add it back if you need it. + * + * @see #containsLegacy(String) + * @since 2.2.0 + * + private static final ImmutableSet LEGACY_VALUES = VALUES.stream().map(XMaterial::getLegacy) + .flatMap(Arrays::stream) + .filter(m -> m.charAt(1) == '.') + .collect(Collectors.collectingAndThen(Collectors.toSet(), ImmutableSet::copyOf)); + */ + + /** + * Guava (Google Core Libraries for Java)'s cache for performance and timed caches. For strings that match a certain + * XMaterial. Mostly cached for configs. + * + * @since 1.0.0 + */ + private static final Cache NAME_CACHE + = CacheBuilder.newBuilder() + .softValues() + .expireAfterAccess(15, TimeUnit.MINUTES) + .build(); + /** + * Guava (Google Core Libraries for Java)'s cache for performance and timed caches. For XMaterials that are already + * parsed once. + * + * @since 3.0.0 + */ + private static final Cache> PARSED_CACHE + = CacheBuilder.newBuilder() + .softValues() + .expireAfterAccess(10, TimeUnit.MINUTES) + .concurrencyLevel(Runtime.getRuntime().availableProcessors()) + .build(); + + /** + * Pre-compiled RegEx pattern. Include both replacements to avoid recreating string multiple times with multiple + * RegEx checks. + * + * @since 3.0.0 + */ + private static final Pattern FORMAT_PATTERN = Pattern.compile("\\W+"); + /** + * The current version of the server in the a form of a major version. + * + * @since 1.0.0 + */ + private static final int VERSION = Integer.parseInt(getMajorVersion(Bukkit.getVersion()).substring(2)); + /** + * Cached result if the server version is after the v1.13 flattening update. Please don't mistake this with + * flat-chested people. It happened. + * + * @since 3.0.0 + */ + private static final boolean ISFLAT = supports(13); + /** + * The data value of this material https://minecraft.gamepedia.com/Java_Edition_data_values/Pre-flattening + * + * @see #getData() + */ private final byte data; + /** + * A list of material names that was being used for older verions. + * + * @see #getLegacy() + */ private final String[] legacy; XMaterial(int data, String... legacy) @@ -90,568 +178,840 @@ public enum XMaterial this.legacy = legacy; } + XMaterial() + { + this(0); + } + + XMaterial(String... legacy) + { + this(0, legacy); + } + /** - * Checks if the version is 1.13 (Aquatic Update) or higher. + * Checks if the version is 1.13 Aquatic Update or higher. An invocation of this method yields the cached result + * from the expression: + *

+ *

+ * {@link #supports(int) 13}} + *
* * @return true if 1.13 or higher. + * + * @see #getVersion() + * @see #supports(int) + * @since 1.0.0 */ public static boolean isNewVersion() { - if (isNewVersion != null) - return isNewVersion; - return isNewVersion = isVersionOrHigher(MinecraftVersion.VERSION_1_13); + return ISFLAT; } + /** + * This is just an extra method that method that can be used for many cases. It can be used in {@link + * org.bukkit.event.player.PlayerInteractEvent} or when accessing {@link org.bukkit.entity.Player#getMainHand()}, or + * other compatibility related methods. + *

+ * An invocation of this method yields exactly the same result as the expression: + *

+ *

+ * {@link #getVersion()} == 1.8 + *
+ * + * @since 2.0.0 + */ public static boolean isOneEight() { - return getVersion() == MinecraftVersion.VERSION_1_8; + return !supports(9); } /** - * Uses newly added materials to minecraft to detect the server version. + * The current version of the server. * - * @return the current server version. + * @return the current server version or 0.0 if unknown. + * + * @see #isNewVersion() + * @since 2.0.0 */ - public static MinecraftVersion getVersion() + public static double getVersion() { - if (version != null) - return version; - return version = valueOfVersion(Bukkit.getVersion()); + return VERSION; } /** - * When using newer versions of Minecraft {@link #isNewVersion()} this helps to - * find the old material name with its data using a cached search for - * optimization. + * When using newer versions of Minecraft ({@link #isNewVersion()}), helps to find the old material name with its + * data value using a cached search for optimization. * - * @see #matchXMaterial(String, byte) + * @see #matchDefinedXMaterial(String, byte) + * @since 1.0.0 */ - private static XMaterial requestOldXMaterial(String name, byte data) + @Nullable + private static XMaterial requestOldXMaterial(@Nonnull String name, byte data) { - XMaterial cached = CACHED_SEARCH.get(name + "," + data); + String holder = name + data; + XMaterial cache = NAME_CACHE.getIfPresent(holder); + if (cache != null) + return cache; - if (cached != null) - return cached; - Optional search = data == -1 ? - Arrays.stream(XMaterial.VALUES).filter(mat -> mat.matchAnyLegacy(name)).findFirst() : - Arrays.stream(XMaterial.VALUES).filter(mat -> mat.matchAnyLegacy(name) && mat.data == data).findFirst(); - - if (search.isPresent()) + for (XMaterial material : VALUES) { - XMaterial found = search.get(); - CACHED_SEARCH.put(found.legacy[0] + "," + found.getData(), found); - return found; + // Not using material.name().equals(name) check is intended. + if ((data == -1 || data == material.data) && material.anyMatchLegacy(name)) + { + NAME_CACHE.put(holder, material); + return material; + } } + return null; } /** - * Checks if XMaterial enum contains a material with this name. + * Checks if XMaterial enum contains a material with the given name. + *

+ * You should use {@link #matchXMaterial(String)} instead if you're going to get the XMaterial object after checking + * if it's available in the list by doing a simple {@link Optional#isPresent()} check. This is just to avoid + * multiple loops for maximum performance. * - * @param name name of the material + * @param name name of the material. * @return true if XMaterial enum has this material. - */ - public static boolean contains(String name) - { - String formatted = format(name); - return Arrays.stream(XMaterial.VALUES).anyMatch(mat -> mat.name().equals(formatted)); - } - - /** - * Checks if the given material matches any of the legacy names. * - * @param name the material name. - * @return true if it's a legacy name. + * @since 1.0.0 */ - public static boolean containsLegacy(String name) + public static boolean contains(@Nonnull String name) { - String formatted = format(name); - return Arrays.stream(Arrays.stream(XMaterial.VALUES).map(m -> m.legacy).toArray(String[]::new)) - .anyMatch(mat -> parseLegacyVersionMaterialName(mat).equals(formatted)); - } - - /** - * @see #matchXMaterial(String, byte) - */ - public static XMaterial matchXMaterial(Material material) - { - return matchXMaterial(material.name()); - } - - /** - * @see #matchXMaterial(String, byte) - */ - public static XMaterial matchXMaterial(String name) - { - // -1 Determines whether the item's data is unknown and only the name is given. - // Checking if the item is damageable won't do anything as the data is not going - // to be checked in requestOldMaterial anyway. - return matchXMaterial(name, (byte) -1); - } - - /** - * Parses the material name and data argument as a {@link Material}. - * - * @param name name of the material - * @param data data of the material - */ - public static Material parseMaterial(String name, byte data) - { - return matchXMaterial(name, data).parseMaterial(); - } - - /** - * @param item the ItemStack to match its material and data. - * @see #matchXMaterial(String, byte) - */ - @SuppressWarnings("deprecation") - public static XMaterial matchXMaterial(ItemStack item) - { - return isDamageable(item.getType().name()) ? matchXMaterial(item.getType().name(), (byte) 0) : - matchXMaterial(item.getType().name(), (byte) item.getDurability()); - } - - /** - * Matches the argument string and its data with a XMaterial. - * - * @param name the name of the material - * @param data the data byte of the material - * @return a XMaterial from the enum (with the same legacy name and data if in - * older versions.) - */ - public static XMaterial matchXMaterial(String name, byte data) - { - Validate.notEmpty(name, "Material name cannot be null or empty"); + Validate.notEmpty(name, "Cannot check for null or empty material name"); name = format(name); - if ((contains(name) && data <= 0)) - return valueOf(name); - return requestOldXMaterial(name, data); + for (XMaterial materials : VALUES) + if (materials.name().equals(name)) + return true; + return false; } /** - * Gets the XMaterial based on the Material's ID and data. You should avoid - * using this for performance reasons. + * Parses the given material name as an XMaterial with unspecified data value. + * + * @see #matchXMaterialWithData(String) + * @since 2.0.0 + */ + @Nonnull + public static Optional matchXMaterial(@Nonnull String name) + { + Validate.notEmpty(name, "Cannot match a material with null or empty material name"); + Optional oldMatch = matchXMaterialWithData(name); + if (oldMatch.isPresent()) + return oldMatch; + return matchDefinedXMaterial(format(name), (byte) -1); + } + + /** + * Parses material name and data value from the specified string. The seperators are: , or : Spaces are + * allowed. Mostly used when getting materials from config for old school minecrafters. + *

+ * Examples + *

+     *     {@code INK_SACK:1 -> RED_DYE}
+     *     {@code WOOL, 14  -> RED_WOOL}
+     * 
+ * + * @param name the material string that consists of the material name, data and separator character. + * @return the parsed XMaterial. + * + * @see #matchXMaterial(String) + * @since 3.0.0 + */ + private static Optional matchXMaterialWithData(String name) + { + for (char separator : new char[]{',', ':'}) + { + int index = name.indexOf(separator); + if (index == -1) + continue; + + String mat = format(name.substring(0, index)); + byte data = Byte.parseByte(StringUtils.deleteWhitespace(name.substring(index + 1))); + return matchDefinedXMaterial(mat, data); + } + + return Optional.empty(); + } + + /** + * Parses the given material as an XMaterial. + * + * @throws IllegalArgumentException may be thrown as an unexpected exception. + * @see #matchDefinedXMaterial(String, byte) + * @see #matchXMaterial(ItemStack) + * @since 2.0.0 + */ + @Nonnull + public static XMaterial matchXMaterial(@Nonnull Material material) + { + Objects.requireNonNull(material, "Cannot match null material"); + return matchDefinedXMaterial(material.name(), (byte) -1) + .orElseThrow(() -> new IllegalArgumentException("Unsupported Material With No Bytes: " + material.name())); + } + + /** + * Parses the given item as an XMaterial using its material and data value (durability). + * + * @param item the ItemStack to match. + * @return an XMaterial if matched any. + * + * @throws IllegalArgumentException may be thrown as an unexpected exception. + * @see #matchDefinedXMaterial(String, byte) + * @since 2.0.0 + */ + @Nonnull + @SuppressWarnings("deprecation") + public static XMaterial matchXMaterial(@Nonnull ItemStack item) + { + Objects.requireNonNull(item, "Cannot match null ItemStack"); + String material = item.getType().name(); + byte data = (byte) (ISFLAT || isDamageable(material) ? 0 : item.getDurability()); + + return matchDefinedXMaterial(material, data) + .orElseThrow(() -> new IllegalArgumentException("Unsupported Material: " + material + " (" + data + ')')); + } + + /** + * Parses the given material name and data value as an XMaterial. All the values passed to this method will not be + * null or empty and are formatted correctly. + * + * @param name the formatted name of the material. + * @param data the data value of the material. + * @return an XMaterial (with the same data value if specified) + * + * @see #matchXMaterial(Material) + * @see #matchXMaterial(int, byte) + * @see #matchXMaterial(ItemStack) + * @since 3.0.0 + */ + @Nonnull + private static Optional matchDefinedXMaterial(@Nonnull String name, byte data) + { + boolean duplicated = isDuplicated(name); + + // Do basic number and boolean checks before accessing more complex enum stuff. + // Maybe we can simplify (ISFLAT || !duplicated) with the (!ISFLAT && duplicated) under it to save a few nanoseconds? + // if (!Boolean.valueOf(Boolean.getBoolean(Boolean.TRUE.toString())).equals(Boolean.FALSE.booleanValue())) return null; + if (data <= 0 && !duplicated) + { + // Apparently the transform method is more efficient than toJavaUtil() + // toJavaUtil isn't even supported in older versions. + Optional xMat = + Enums.getIfPresent(XMaterial.class, name).transform(Optional::of).or(Optional.empty()); + + if (xMat.isPresent()) + return xMat; + } + + // XMaterial Paradox (Duplication Check) + // I've concluded that this is just an infinite loop that keeps + // going around the Singular Form and the Plural Form materials. A waste of brain cells and a waste of time. + // This solution works just fine anyway. + XMaterial xMat = requestOldXMaterial(name, data); + if (xMat == null) + return Optional.empty(); + + if (!ISFLAT && duplicated && xMat.name().charAt(xMat.name().length() - 1) == 'S') + { + // A solution for XMaterial Paradox. + // Manually parses the duplicated materials to find the exact material based on the server version. + // If ends with "S" -> Plural Form Material + return Enums.getIfPresent(XMaterial.class, name).transform(Optional::of).or(Optional.empty()); + } + return Optional.ofNullable(xMat); + } + + /** + * XMaterial Paradox (Duplication Check) + * Checks if the material has any duplicates. + *

+ * Example: + *

{@code MELON, CARROT, POTATO, BEETROOT -> true} + * + * @param name the name of the material to check. + * @return true if there's a duplicated material for this material, otherwise false. + * + * @see #isDuplicated() + * @since 2.0.0 + */ + private static boolean isDuplicated(@Nonnull String name) + { + return false; + } + + /** + * Gets the XMaterial based on the material's ID (Magic Value) and data value.
You should avoid using this for + * performance issues. * * @param id the ID (Magic value) of the material. - * @param data the data of the material. - * @return some XMaterial, or null. + * @param data the data value of the material. + * @return a parsed XMaterial with the same ID and data value. + * + * @see #matchXMaterial(ItemStack) + * @since 2.0.0 */ - public static XMaterial matchXMaterial(int id, byte data) + @Nonnull + public static Optional matchXMaterial(int id, byte data) { + if (id < 0 || data < 0) return Optional.empty(); + // Looping through Material.values() will take longer. - return Arrays.stream(XMaterial.VALUES).filter(mat -> mat.getId() == id && mat.data == data).findFirst() - .orElse(null); + for (XMaterial materials : VALUES) + if (materials.data == data && materials.getId() == id) return Optional.of(materials); + return Optional.empty(); } /** - * Attempts to build the string like an enum name. + * Attempts to build the string like an enum name. Removes all the spaces, numbers and extra non-English characters. + * Also removes some config/in-game based strings. * * @param name the material name to modify. * @return a Material enum name. + * + * @since 2.0.0 */ - private static String format(String name) + @Nonnull + private static String format(@Nonnull String name) { - return name.toUpperCase().replace("MINECRAFT:", "").replace('-', '_').replaceAll("\\s+", "_").replaceAll("\\W", - ""); + return FORMAT_PATTERN.matcher( + name.trim().replace('-', '_').replace(' ', '_')).replaceAll("").toUpperCase(Locale.ENGLISH); } /** - * Parses the material name if the legacy name has a version attached to it. + * Checks if the specified version is the same version or higher than the current server version. * - * @param name the material name to parse. - * @return the material name with the version removed. - */ - private static String parseLegacyVersionMaterialName(String name) - { - if (!name.contains("/")) - return name; - return name.substring(0, name.indexOf('/')); - } - - /** - * Checks if the version argument is the same or higher than the current server - * version. - * - * @param version the version to be checked. + * @param version the major version to be checked. "1." is ignored. E.g. 1.12 = 12 | 1.9 = 9 * @return true of the version is equal or higher than the current version. + * + * @since 2.0.0 */ - public static boolean isVersionOrHigher(MinecraftVersion version) + public static boolean supports(int version) { - MinecraftVersion current = getVersion(); - - if (version == current) - return true; - if (version == MinecraftVersion.UNKNOWN) - return false; - if (current == MinecraftVersion.UNKNOWN) - return true; - - int ver = Integer.parseInt(version.name().replace("VERSION_", "").replace("_", "")); - int currentVer = Integer.parseInt(current.name().replace("VERSION_", "").replace("_", "")); - - return currentVer >= ver; + return VERSION >= version; } /** - * Converts the material's name to a string with the first letter uppercase. + * Converts the enum names to a more friendly and readable string. * - * @return a converted string. + * @return a formatted string. + * + * @see #toWord(String) + * @since 2.1.0 */ - public static String toWord(Material material) + @Nonnull + public static String toWord(@Nonnull Material material) { - return material.name().charAt(0) + material.name().substring(1).toLowerCase(); + Objects.requireNonNull(material, "Cannot translate a null material to a word"); + return toWord(material.name()); } /** - * Compares two major versions. The current server version and the given - * version. + * Parses an enum name to a normal word. Normal names have underlines removed and each word capitalized. + *

+ * Examples: + *

+     *     EMERALD                 -> Emerald
+     *     EMERALD_BLOCK           -> Emerald Block
+     *     ENCHANTED_GOLDEN_APPLE  -> Enchanted Golden Apple
+     * 
* - * @param version the version to check. - * @return true if is the same as the current version or higher. + * @param name the name of the enum. + * @return a cleaned more readable enum name. + * + * @since 2.1.0 */ - public static boolean isVersionOrHigher(String version) + @Nonnull + private static String toWord(@Nonnull String name) { - int currentVer = Integer.parseInt(getExactMajorVersion(Bukkit.getVersion()).replace(".", "")); - int versionNumber = Integer.parseInt(version.replace(".", "")); - - return currentVer >= versionNumber; + return WordUtils.capitalize(name.replace('_', ' ').toLowerCase(Locale.ENGLISH)); } /** * Gets the exact major version (..., 1.9, 1.10, ..., 1.14) * - * @param version Supports {@link Bukkit#getVersion()}, - * {@link Bukkit#getBukkitVersion()} and normal formats such as + * @param version Supports {@link Bukkit#getVersion()}, {@link Bukkit#getBukkitVersion()} and normal formats such as * "1.14" * @return the exact major version. + * + * @since 2.0.0 */ - public static String getExactMajorVersion(String version) + @Nonnull + public static String getMajorVersion(@Nonnull String version) { - // getBukkitVersion() - if (version.contains("SNAPSHOT") || version.contains("-R")) - version = version.substring(0, version.indexOf("-")); + Validate.notEmpty(version, "Cannot get major Minecraft version from null or empty string"); + // getVersion() - if (version.contains("git")) - version = version.substring(version.indexOf("MC:") + 4).replace(")", ""); - if (version.split(Pattern.quote(".")).length > 2) - version = version.substring(0, version.lastIndexOf(".")); + int index = version.lastIndexOf("MC:"); + if (index != -1) + version = version.substring(index + 4, version.length() - 1); + else if (version.endsWith("SNAPSHOT")) + { + // getBukkitVersion() + index = version.indexOf('-'); + version = version.substring(0, index); + } + + // 1.13.2, 1.14.4, etc... + int lastDot = version.lastIndexOf('.'); + if (version.indexOf('.') != lastDot) + version = version.substring(0, lastDot); + return version; } /** - * Parses the string arugment to a version. Supports - * {@link Bukkit#getVersion()}, {@link Bukkit#getBukkitVersion()} and normal - * formats such as "1.14" - * - * @param version the server version. - * @return the Minecraft version represented by the string. - */ - private static MinecraftVersion valueOfVersion(String version) - { - version = getExactMajorVersion(version); - if (version.equals("1.10") || version.equals("1.11") || version.equals("1.12")) - return MinecraftVersion.VERSION_1_9; - version = version.replace(".", "_"); - if (!version.startsWith("VERSION_")) - version = "VERSION_" + version; - String check = version; - return Arrays.stream(MinecraftVersion.VALUES).anyMatch(v -> v.name().equals(check)) ? - MinecraftVersion.valueOf(version) : MinecraftVersion.UNKNOWN; - } - - /** - * Checks if the material can be damaged from {@link #DAMAGEABLE}. + * Checks if the material can be damaged by using it. Names going through this method are not formatted. * * @param name the name of the material. * @return true of the material can be damaged. + * + * @see #isDamageable() + * @since 1.0.0 */ - public static boolean isDamageable(String name) + public static boolean isDamageable(@Nonnull String name) { - return Arrays.stream(DAMAGEABLE).anyMatch(name::contains); + Objects.requireNonNull(name, "Material name cannot be null"); + for (String damageable : DAMAGEABLE) + if (name.contains(damageable)) + return true; + return false; } /** - * Gets the ID (Magic value) of the material. If an - * {@link IllegalArgumentException} was thrown from this method, you should - * definitely report it. + * Checks if the list of given material names matches the given base material. Mostly used for configs. + *

+ * Supports {@link String#contains} {@code CONTAINS:NAME} and Regular Expression {@code REGEX:PATTERN} formats. + *

+ * Example: + *

+     *     XMaterial material = {@link #matchXMaterial(ItemStack)};
+     *     if (material.isOneOf(plugin.getConfig().getStringList("disabled-items")) return;
+     * 
+ *
+ * {@code CONTAINS} Examples: + *
+     *     {@code "CONTAINS:CHEST" -> CHEST, ENDERCHEST, TRAPPED_CHEST -> true}
+     *     {@code "cOnTaINS:dYe" -> GREEN_DYE, YELLOW_DYE, BLUE_DYE, INK_SACK -> true}
+     * 
+ *

+ * {@code REGEX} Examples + *

+     *     {@code "REGEX:^.+_.+_.+$" -> Every Material with 3 underlines or more: SHULKER_SPAWN_EGG, SILVERFISH_SPAWN_EGG, SKELETON_HORSE_SPAWN_EGG}
+     *     {@code "REGEX:^.{1,3}$" -> Material names that have 3 letters only: BED, MAP, AIR}
+     * 
+ *

+ * The reason that there are tags for {@code CONTAINS} and {@code REGEX} is for the performance. Please avoid using + * the {@code REGEX} tag if you can use the {@code CONTAINS} tag. It'll have a huge impact on performance. Please + * avoid using {@code (capturing groups)} there's no use for them in this case. If you want to use groups, use + * {@code (?: non-capturing groups)}. It's faster. + *

+ * You can make a cache for pre-compiled RegEx patterns from your config. It's better, but not much faster since + * these patterns are not that complex. + *

+ * Want to learn RegEx? You can mess around in RegExr website. * - * @return the ID of the material. -1 if it's a new block. + * @param material the base material to match other materials with. + * @param materials the material names to check base material on. + * @return true if one of the given material names is similar to the base material. + * + * @since 3.1.1 + */ + public static boolean isOneOf(@Nonnull Material material, @Nullable List materials) + { + if (materials == null || materials.isEmpty()) + return false; + Objects.requireNonNull(material, "Cannot match materials with a null material"); + String name = material.name(); + + for (String comp : materials) + { + comp = comp.toUpperCase(); + if (comp.startsWith("CONTAINS:")) + { + comp = format(comp.substring(9)); + if (name.contains(comp)) + return true; + continue; + } + if (comp.startsWith("REGEX:")) + { + comp = comp.substring(6); + if (name.matches(comp)) + return true; + continue; + } + + // Direct Object Equals + Optional mat = matchXMaterial(comp); + if (mat.isPresent() && mat.get().parseMaterial() == material) + return true; + } + return false; + } + + /** + * Gets the version which this material was added in. If the material doesn't have a version it'll return 0; + * + * @return the Minecraft version which tihs material was added in. + * + * @since 3.0.0 + */ + public int getMaterialVersion() + { + if (this.legacy.length == 0) + return 0; + String version = this.legacy[0]; + if (version.charAt(1) != '.') + return 0; + + return Integer.parseInt(version.substring(2)); + } + + /** + * Sets the {@link Material} (and data value on older versions) of an item. Damageable materials will not have their + * durability changed. + *

+ * Use {@link #parseItem()} instead when creating new ItemStacks. + * + * @param item the item to change its type. + * @see #parseItem() + * @since 3.0.0 + */ + @Nonnull + @SuppressWarnings("deprecation") + public ItemStack setType(@Nonnull ItemStack item) + { + Objects.requireNonNull(item, "Cannot set material for null ItemStack"); + + item.setType(this.parseMaterial()); + if (!ISFLAT && !this.isDamageable()) + item.setDurability(this.data); + return item; + } + + /** + * Checks if the list of given material names matches the given base material. Mostly used for configs. + * + * @param materials the material names to check base material on. + * @return true if one of the given material names is similar to the base material. + * + * @see #isOneOf(Material, List) + * @since 3.0.0 + */ + public boolean isOneOf(@Nullable List materials) + { + Material material = this.parseMaterial(); + if (material == null) + return false; + return isOneOf(material, materials); + } + + /** + * Checks if the given string matches any of this material's legacy material names. All the values passed to this + * method will not be null or empty and are formatted correctly. + * + * @param name the name to check + * @return true if it's one of the legacy names. + * + * @since 2.0.0 + */ + private boolean anyMatchLegacy(@Nonnull String name) + { + for (String legacy : this.legacy) + { + if (legacy.isEmpty()) + break; // Left-side suggestion list + if (name.equals(legacy)) + return true; + } + return false; + } + + /** + * User-friendly readable name for this material In most cases you should be using {@link #name()} instead. + * + * @return string of this object. + * + * @see #toWord(String) + * @since 3.0.0 + */ + @Override + public String toString() + { + return toWord(this.name()); + } + + /** + * Gets the ID (Magic value) of the material. + * + * @return the ID of the material or -1 if it's a new block or the material is not supported. + * + * @see #matchXMaterial(int, byte) + * @since 2.2.0 */ @SuppressWarnings("deprecation") public int getId() { - return isNew() ? -1 : this.parseMaterial().getId(); + if (this.data != 0 || (this.legacy.length != 0 && Integer.parseInt(this.legacy[0].substring(2)) >= 13)) + return -1; + Material material = this.parseMaterial(); + return material == null ? -1 : material.getId(); } /** - * Checks if the given string matches any of this material's legacy material - * names. + * Checks if the material has any duplicates. * - * @param name the name to check - * @return true if it's one of the legacy names. - */ - public boolean matchAnyLegacy(String name) - { - String formatted = format(name); - return Arrays.asList(legacy).contains(formatted); - } - - /** - * Converts the material's name to a string with the first letter uppercase. + * @return true if there is a duplicated name for this material, otherwise false. * - * @return a converted string. + * @see #isDuplicated(String) + * @since 2.0.0 */ - public String toWord() + public boolean isDuplicated() { - return name().charAt(0) + name().substring(1).toLowerCase(); + return false; } /** - * @return true if the item can be damaged. + * Checks if the material can be damaged by using it. Names going through this method are not formatted. + * + * @return true if the item can be damaged (have its durability changed), otherwise false. + * * @see #isDamageable(String) + * @since 1.0.0 */ public boolean isDamageable() { - return isDamageable(name()); + return isDamageable(this.name()); } /** - * Get the {@link ItemStack} data of this material in older versions. Which can - * be accessed with {@link ItemStack#getData()} then MaterialData#getData() or - * {@link ItemStack#getDurability()} if not damageable. + * The data value of this material pre-flattening. + *

+ * Can be accessed with {@link ItemStack#getData()} then {@code MaterialData#getData()} or {@link + * ItemStack#getDurability()} if not damageable. * - * @return data of this material. + * @return data of this material, or 0 if none. + * + * @since 1.0.0 */ - public int getData() + @SuppressWarnings("deprecation") + public byte getData() { return data; } /** - * Get a list of materials names that was previously used by older versions. + * Get a list of materials names that was previously used by older versions. If the material was added in a new + * version {@link #isNewVersion()}, then the first element will indicate which version the material was added in. * - * @return a list of string of legacy material names. + * @return a list of legacy material names and the first element as the version the material was added in if new. + * + * @since 1.0.0 */ + @Nonnull public String[] getLegacy() { return legacy; } /** - * Parses the XMaterial as an {@link ItemStack}. + * Parses an item from this XMaterial. Uses data values on older versions. * - * @return an ItemStack with the same material (and data if in older versions.) + * @return an ItemStack with the same material (and data value if in older versions.) + * + * @see #parseItem(boolean) + * @see #setType(ItemStack) + * @since 1.0.0 */ + @Nullable public ItemStack parseItem() { return parseItem(false); } /** - * Parses the XMaterial as an {@link ItemStack}. + * Parses an item from this XMaterial. Uses data values on older versions. * - * @param suggest if true {@link #parseMaterial(boolean)} - * @return an ItemStack with the same material (and data if in older versions.) + * @param suggest if true {@link #parseMaterial(boolean)} true will be used. + * @return an ItemStack with the same material (and data value if in older versions.) + * + * @see #setType(ItemStack) + * @since 2.0.0 */ + @Nullable @SuppressWarnings("deprecation") public ItemStack parseItem(boolean suggest) { Material material = this.parseMaterial(suggest); - return isNewVersion() ? new ItemStack(material) : new ItemStack(material, 1, data); + if (material == null) + return null; + return ISFLAT ? new ItemStack(material) : new ItemStack(material, 1, this.data); } /** - * Parses the XMaterial as a {@link Material}. + * Parses the material of this XMaterial. * - * @return the Material related to this XMaterial based on the server version. + * @return the material related to this XMaterial based on the server version. + * + * @see #parseMaterial(boolean) + * @since 1.0.0 */ + @Nullable public Material parseMaterial() { return parseMaterial(false); } /** - * Parses the XMaterial as a {@link Material}. + * Parses the material of this XMaterial and accepts suggestions. * - * @param suggest Use a suggested material if the material is added in the new - * version. - * @return the Material related to this XMaterial based on the server version. - * @see #matchXMaterial(String, byte) + * @param suggest use a suggested material (from older materials) if the material is added in a later version of + * Minecraft. + * @return the material related to this XMaterial based on the server version. + * + * @since 2.0.0 */ + @SuppressWarnings("OptionalAssignedToNull") + @Nullable public Material parseMaterial(boolean suggest) { - Material newMat = Material.getMaterial(name()); + Optional cache = PARSED_CACHE.getIfPresent(this); + if (cache != null) + return cache.orElse(null); + Material mat; - // If the name is not null it's probably the new version. - // So you can still use this name even if it's a duplicated name. - // Since duplicated names only apply to older versions. - if (newMat != null && (isNewVersion())) - return newMat; - return requestOldMaterial(suggest); + if (!ISFLAT && this.isDuplicated()) + mat = requestOldMaterial(suggest); + else + { + mat = Material.getMaterial(this.name()); + if (mat == null) + mat = requestOldMaterial(suggest); + } + + if (mat != null) + PARSED_CACHE.put(this, Optional.ofNullable(mat)); + return mat; } /** - * Parses from old material names and can accept suggestions. + * Parses a material for older versions of Minecraft. Accepts suggestions if specified. * - * @param suggest Accept suggestions for newly added blocks - * @return A parsed Material suitable for this minecraft version. + * @param suggest if true suggested materials will be considered for old versions. + * @return a parsed material suitable for the current Minecraft version. + * + * @see #parseMaterial(boolean) + * @since 2.0.0 */ + @Nullable private Material requestOldMaterial(boolean suggest) { - Material oldMat; - boolean isNew = getVersionIfNew() != MinecraftVersion.UNKNOWN; - for (int i = legacy.length - 1; i >= 0; i--) + for (int i = this.legacy.length - 1; i >= 0; i--) { - String legacyName = legacy[i]; - // Slash means it's just another name for the material in another version. - if (legacyName.contains("/")) - { - oldMat = Material.getMaterial(parseLegacyVersionMaterialName(legacyName)); + String legacy = this.legacy[i]; - if (oldMat != null) - return oldMat; - else - continue; - } - if (isNew) + // Check if we've reached the end and the last string is our + // material version. + if (i == 0 && legacy.charAt(1) == '.') + return null; + + // According to the suggestion list format, all the other names continuing + // from here are considered as a "suggestion" + // The empty string is an indicator for suggestion list on the left side. + if (legacy.isEmpty()) { - if (suggest) - { - oldMat = Material.getMaterial(legacyName); - if (oldMat != null) - return oldMat; - } - else - return null; - // According to the suggestion format list, all the other names continuing - // from here are considered as a "suggestion" if there's no slash anymore. + if (suggest) continue; + break; } - oldMat = Material.getMaterial(legacyName); - if (oldMat != null) - return oldMat; + + Material material = Material.getMaterial(legacy); + if (material != null) + return material; } return null; } /** - * Checks if an item is similar to the material and its data (if in older - * versions.) + * Checks if an item has the same material (and data value on older versions). * * @param item item to check. - * @return true if the material is the same as the item's material (and data if - * in older versions.) + * @return true if the material is the same as the item's material (and data value if on older versions), otherwise + * false. + * + * @since 1.0.0 */ @SuppressWarnings("deprecation") - public boolean isSimilar(ItemStack item) + public boolean isSimilar(@Nonnull ItemStack item) { - Objects.requireNonNull(item, "ItemStack cannot be null"); - Objects.requireNonNull(item.getType(), "ItemStack's material cannot be null"); - return (isNewVersion() || this.isDamageable()) ? item.getType() == this.parseMaterial() : - item.getType() == this.parseMaterial() && item.getDurability() == data; + Objects.requireNonNull(item, "Cannot compare with null ItemStack"); + if (item.getType() != this.parseMaterial()) + return false; + return ISFLAT || this.isDamageable() || item.getDurability() == this.data; } /** - * Get the suggested material names that can be used instead of this material. + * Gets the suggested material names that can be used if the material is not supported in the current version. * * @return a list of suggested material names. + * + * @see #parseMaterial(boolean) + * @since 2.0.0 */ - public String[] getSuggestions() + @Nonnull + public List getSuggestions() { - if (!legacy[0].contains(".")) - return new String[0]; - return Arrays.stream(legacy).filter(mat -> !mat.contains(".")).toArray(String[]::new); + if (this.legacy.length == 0 || this.legacy[0].charAt(1) != '.') + return new ArrayList<>(); + List suggestions = new ArrayList<>(); + for (String legacy : this.legacy) + { + if (legacy.isEmpty()) + break; + suggestions.add(legacy); + } + return suggestions; } /** - * Checks if this material is supported in the current version. It'll check both - * the newest matetrial name and for legacy names. + * Checks if this material is supported in the current version. Suggested materials will be ignored. + *

+ * Note that you should use {@link #parseMaterial()} and check if it's null if you're going to parse and use the + * material later. * * @return true if the material exists in {@link Material} list. + * + * @since 2.0.0 */ public boolean isSupported() { - return Arrays.stream(Material.values()) - .anyMatch(mat -> mat.name().equals(name()) || matchAnyLegacy(mat.name())); - } + int version = this.getMaterialVersion(); + if (version != 0) + return supports(version); - /** - * Gets the added version if the material is newly added after the 1.13 Aquatic - * Update and higher. - * - * @return the version which the material was added in. - * {@link MinecraftVersion#UNKNOWN} if not new. - * @see #isNew() - */ - public MinecraftVersion getVersionIfNew() - { - return isNew() ? valueOfVersion(legacy[0]) : MinecraftVersion.UNKNOWN; + Material material = Material.getMaterial(this.name()); + if (material != null) + return true; + return requestOldMaterial(false) != null; } /** * Checks if the material is newly added after the 1.13 Aquatic Update. * - * @return true if it was newly added. - */ - public boolean isNew() - { - return legacy[0].contains("."); - } - - /** - * Gets the suggested material instead of returning null for unsupported - * versions. This is somehow similar to what ProtcolSupport and ViaVersion are - * doing to new materials. Don't use this if you want to parse to a - * {@link Material} + * @return true if the material was newly added, otherwise false. * - * @return The suggested material that is similar. - * @see #parseMaterial() + * @see #getMaterialVersion() + * @since 2.0.0 */ - public XMaterial suggestOldMaterialIfNew() + public boolean isFromNewSystem() { - if (getVersionIfNew() == MinecraftVersion.UNKNOWN || legacy.length == 1) - return null; - - // We need a loop because: Newest -> Oldest - for (int i = legacy.length - 1; i >= 0; i--) - { - String legacyName = legacy[i]; - - if (legacyName.contains("/")) - continue; - XMaterial mat = matchXMaterial(parseLegacyVersionMaterialName(legacyName), data); - if (mat != null && this != mat) - return mat; - } - return null; + return this.legacy.length != 0 && Integer.parseInt(this.legacy[0].substring(2)) > 13; } +} + + - /** - * Only major versions related to material changes. - */ - public enum MinecraftVersion - { - /** - * Bountiful Update - */ - VERSION_1_8, - /** - * Combat Update (Pitiful Update?) - */ - VERSION_1_9, - /** - * Aquatic Update - */ - VERSION_1_13, - /** - * Village Pillage Update - */ - VERSION_1_14, - /** - * 1.7 or below. Using {@link #getVersionIfNew()} it means 1.12 or below. - */ - UNKNOWN; - public static final MinecraftVersion[] VALUES = MinecraftVersion.values(); - } -} \ No newline at end of file diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Message.java b/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Message.java index ea03158..01d249d 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Message.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Message.java @@ -14,12 +14,14 @@ public enum Message implements IMessageVariable TIER_CHAIN(), TIER_IRON(), TIER_DIAMOND(), + TIER_NETHERITE(), TIER_SHORT_LEATHER(), TIER_SHORT_GOLD(), TIER_SHORT_CHAIN(), TIER_SHORT_IRON(), TIER_SHORT_DIAMOND(), + TIER_SHORT_NETHERITE(), MESSAGES_UNINSTALLMODE(), MESSAGES_UNSUPPORTEDTIER(), diff --git a/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Messages.java b/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Messages.java index f81f17a..5571d26 100644 --- a/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Messages.java +++ b/src/main/java/nl/pim16aap2/armoredElytra/util/messages/Messages.java @@ -42,6 +42,7 @@ public class Messages public Messages(final ArmoredElytra plugin) { this.plugin = plugin; + writeDefaultFile(); final String fileName = plugin.getConfigLoader().languageFile(); // Only append .txt if the provided name doesn't already have it. textFile = new File(plugin.getDataFolder(), fileName.endsWith(".txt") ? fileName : (fileName + ".txt")); @@ -49,7 +50,7 @@ public class Messages if (!textFile.exists()) { plugin.myLogger(Level.WARNING, "Failed to load language file: \"" + textFile + - "\": File not found! Using default file instead!"); + "\": File not found! Using default file (\"" + DEFAULTFILENAME + "\") instead!"); textFile = new File(plugin.getDataFolder(), DEFAULTFILENAME); } populateMessageMap(); diff --git a/src/main/resources/en_US.txt b/src/main/resources/en_US.txt index b8915e0..444d763 100644 --- a/src/main/resources/en_US.txt +++ b/src/main/resources/en_US.txt @@ -9,11 +9,13 @@ TIER.Gold=&EGolden Armored Elytra TIER.Chain=&8Chain Armored Elytra TIER.Iron=&7Iron Armored Elytra TIER.Diamond=&BDiamond Armored Elytra +TIER.Netherite=&4Netherite Armored Elytra TIER.SHORT.Leather=&2Leather TIER.SHORT.Gold=&EGold TIER.SHORT.Chain=&8Chain TIER.SHORT.Iron=&7Iron TIER.SHORT.Diamond=&BDiamond +TIER.SHORT.Netherite=&4Netherite MESSAGES.UninstallMode=&cPlugin in uninstall mode! New Armored Elytras are not allowed! MESSAGES.UnsupportedTier=&cNot a supported armor tier! Try one of these: leather, gold, chain, iron, diamond. MESSAGES.RepairNeeded=&cYou cannot equip this elytra! Please repair it in an anvil first.