From 9ddc4f9f61dfd7c503313cffc002b102aceda7a9 Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Sat, 27 Jun 2020 15:40:41 +0200 Subject: [PATCH] Add support for Netherite, improve tier retrieval - Added support for creation and general usage of NETHERITE ArmoredElytras. - Improved ArmorTier retrieval on 1.16+. A PersistentDataContainer is now stored in the item meta to keep track of the armortier of an item. Instead of using the armorvalue of an item to figure out which armortier an item is (which is how it was done before), this container is used instead. This is not only simpler (and probably more reliable), but it also solves the issue where armor values caused collisions between two types (e.g. gold and chain or diamond and netherite). - Added Netherite tier to translation system. --- .../armoredElytra/ArmoredElytra.java | 7 +- .../armoredElytra/handlers/EventHandlers.java | 27 +- .../armoredElytra/nbtEditor/INBTEditor.java | 3 +- .../armoredElytra/nbtEditor/NBTEditor.java | 38 +- .../armoredElytra/util/ArmorTier.java | 92 +- .../armoredElytra/util/ConfigLoader.java | 35 +- .../nl/pim16aap2/armoredElytra/util/Util.java | 77 +- .../armoredElytra/util/XMaterial.java | 1186 +++++++++++------ .../armoredElytra/util/messages/Message.java | 2 + .../armoredElytra/util/messages/Messages.java | 3 +- src/main/resources/en_US.txt | 2 + 11 files changed, 979 insertions(+), 493 deletions(-) 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.