diff --git a/src/main/java/world/bentobox/bentobox/util/ItemParser.java b/src/main/java/world/bentobox/bentobox/util/ItemParser.java index f483c3d2e..c315aa897 100644 --- a/src/main/java/world/bentobox/bentobox/util/ItemParser.java +++ b/src/main/java/world/bentobox/bentobox/util/ItemParser.java @@ -1,16 +1,14 @@ package world.bentobox.bentobox.util; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.MissingFormatArgumentException; -import java.util.Optional; -import java.util.UUID; +import java.net.URL; +import java.util.*; import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.Material; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BannerMeta; import org.bukkit.inventory.meta.Damageable; @@ -19,10 +17,12 @@ import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; +import org.bukkit.profile.PlayerProfile; import org.eclipse.jdt.annotation.Nullable; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; +import com.google.common.base.Enums; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import world.bentobox.bentobox.BentoBox; @@ -55,6 +55,33 @@ public static ItemStack parse(String text) { */ @Nullable public static ItemStack parse(@Nullable String text, @Nullable ItemStack defaultItemStack) { + if (text == null || text.isBlank()) { + // Text does not exist or is empty. + return defaultItemStack; + } + + ItemStack returnValue; + + try { + // Check if item can be parsed using bukkit item factory. + returnValue = Bukkit.getItemFactory().createItemStack(text); + } + catch (IllegalArgumentException exception) { + returnValue = ItemParser.parseOld(text, defaultItemStack); + } + + return returnValue; + } + + + /** + * Parse given string to ItemStack. + * @param text String value of item stack. + * @param defaultItemStack Material that should be returned if parsing failed. + * @return ItemStack of parsed item or defaultItemStack. + */ + @Nullable + private static ItemStack parseOld(@Nullable String text, @Nullable ItemStack defaultItemStack) { if (text == null || text.isBlank()) { // Text does not exist or is empty. @@ -113,22 +140,25 @@ else if (part.length == 2) { returnValue = parseItemDurabilityAndQuantity(part); } - if (returnValue != null - // If wrapper is just for code-style null-pointer checks. - && customModelData != null) { - return customValue(returnValue, customModelData); + // Update item meta with custom data model. + if (returnValue != null && customModelData != null) { + ItemParser.setCustomModelData(returnValue, customModelData); } - } catch (Exception exception) { BentoBox.getInstance().logError("Could not parse item " + text + " " + exception.getLocalizedMessage()); returnValue = defaultItemStack; } - return returnValue; + return returnValue; } - private static @Nullable ItemStack customValue(ItemStack returnValue, Integer customModelData) { + /** + * This method assigns custom model data to the item stack. + * @param returnValue Item stack that should be updated. + * @param customModelData Integer value of custom model data. + */ + private static void setCustomModelData(ItemStack returnValue, Integer customModelData) { // We have custom data model. Now assign it to the item-stack. ItemMeta itemMeta = returnValue.getItemMeta(); @@ -138,8 +168,9 @@ else if (part.length == 2) { // Update meta to the return item. returnValue.setItemMeta(itemMeta); } - return null; } + + /** * This method parses array of 2 items into an item stack. * First array element is material, while second array element is integer, that represents item count. @@ -197,8 +228,10 @@ private static ItemStack parseItemDurabilityAndQuantity(String[] part) { * } * @param part String array that contains 6 elements. * @return Potion with given properties. + * @deprecated due to the spigot potion changes. */ - private static ItemStack parsePotion(String[] part) { + @Deprecated + private static ItemStack parsePotionOld(String[] part) { if (part.length != 6) { throw new MissingFormatArgumentException("Potion parsing requires 6 parts."); } @@ -235,6 +268,61 @@ private static ItemStack parsePotion(String[] part) { } + /** + * This method parses array of 6 items into an item stack. + * Format: + *
{@code
+     *      POTION::QTY
+     * }
+ * Example: + *
{@code
+     *      POTION:STRENGTH:1
+     * }
+ * @link Potion Type + * @param part String array that contains 3 elements. + * @return Potion with given properties. + */ + private static ItemStack parsePotion(String[] part) { + if (part.length == 6) { + BentoBox.getInstance().logWarning("The old potion parsing detected for " + part[0] + + ". Please update your configs, as SPIGOT changed potion types."); + return parsePotionOld(part); + } + + if (part.length != 3) { + throw new MissingFormatArgumentException("Potion parsing requires 3 parts."); + } + + /* + # Format POTION::QTY + # Potion Type can be found out in: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/potion/PotionType.html + # Examples: + # POTION:STRENGTH:1 + # POTION:INSTANT_DAMAGE:2 + # POTION:JUMP:1 + # POTION:WEAKNESS:1 - any weakness potion + */ + + Material material = Material.matchMaterial(part[0]); + + if (material == null) { + BentoBox.getInstance().logWarning("Could not parse potion item " + part[0] + " so using a regular potion."); + material = Material.POTION; + } + + ItemStack result = new ItemStack(material, Integer.parseInt(part[2])); + + if (result.getItemMeta() instanceof PotionMeta meta) { + PotionType potionType = Enums.getIfPresent(PotionType.class, part[1].toUpperCase(Locale.ENGLISH)). + or(PotionType.WATER); + meta.setBasePotionType(potionType); + result.setItemMeta(meta); + } + + return result; + } + + /** * This method parses array of multiple elements for the Banner. * @param part String array that contains at least 2 elements. @@ -298,39 +386,62 @@ private static ItemStack parsePlayerHead(String[] part) { // Set correct Skull texture try { - SkullMeta meta = (SkullMeta) playerHead.getItemMeta(); - - if (part[1].length() < 17) { - // Minecraft player names are in length between 3 and 16 chars. - meta.setOwner(part[1]); - } else if (part[1].length() == 32) { - // trimmed UUID length are 32 chars. - meta.setOwningPlayer(Bukkit.getOfflinePlayer( - UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")))); - } else if (part[1].length() == 36) { - // full UUID length are 36 chars. - meta.setOwningPlayer(Bukkit.getOfflinePlayer(UUID.fromString(part[1]))); - } else { - // If chars are more than 36, apparently it is base64 encoded texture. - GameProfile profile = new GameProfile(UUID.randomUUID(), ""); - profile.getProperties().put("textures", new Property("textures", part[1])); - - // Null pointer will be caught and ignored. - Field profileField = meta.getClass().getDeclaredField("profile"); - profileField.setAccessible(true); - profileField.set(meta, profile); - } + if (playerHead.getItemMeta() instanceof SkullMeta meta) + { + PlayerProfile profile; + + if (part[1].length() < 17) { + // Minecraft player names are in length between 3 and 16 chars. + profile = Bukkit.createPlayerProfile(part[1]); + } else if (part[1].length() == 32) { + // trimmed UUID length are 32 chars. + profile = Bukkit.createPlayerProfile(UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"))); + } else if (part[1].length() == 36) { + // full UUID length are 36 chars. + profile = Bukkit.createPlayerProfile(UUID.fromString(part[1])); + } else { + // If chars are more than 36, apparently it is base64 encoded texture. + profile = Bukkit.createPlayerProfile(UUID.randomUUID(), ""); + profile.getTextures().setSkin(ItemParser.getSkinURLFromBase64(part[1])); + } - // Apply new meta to the item. - playerHead.setItemMeta(meta); + // Apply item meta. + meta.setOwnerProfile(profile); + playerHead.setItemMeta(meta); + } } catch (Exception ignored) { - // Ignored + // Could not parse player head. + BentoBox.getInstance().logError("Could not parse player head item " + part[1] + " so using a Steve head."); } return playerHead; } + /** + * This method parses base64 encoded string into URL. + * @param base64 Base64 encoded string. + * @return URL of the skin. + */ + private static URL getSkinURLFromBase64(String base64) { + /* + * Base64 encoded string is in format: { "timestamp": 0, "profileId": "UUID", + * "profileName": "USERNAME", "textures": { "SKIN": { "url": + * "https://textures.minecraft.net/texture/TEXTURE_ID" }, "CAPE": { "url": + * "https://textures.minecraft.net/texture/TEXTURE_ID" } } } + */ + try { + String decoded = new String(Base64.getDecoder().decode(base64)); + JsonObject json = new Gson().fromJson(decoded, JsonObject.class); + String url = json.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString(); + return new URL(url); + } + catch (Exception e) { + return null; + } + } + + /** * Check if given sting is an integer. * @param string Value that must be checked. diff --git a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java index 739587827..b14cc32ee 100644 --- a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java +++ b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java @@ -21,8 +21,10 @@ import org.bukkit.inventory.meta.BannerMeta; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; +import org.bukkit.profile.PlayerProfile; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -50,6 +52,9 @@ public class ItemParserTest { private ItemMeta itemMeta; @Mock private ItemFactory itemFactory; + @Mock + private SkullMeta skullMeta; + private ItemStack defaultItem; @SuppressWarnings("deprecation") @@ -61,6 +66,8 @@ public void setUp() throws Exception { PowerMockito.mockStatic(Bukkit.class); when(Bukkit.getItemFactory()).thenReturn(itemFactory); + // Do not test Bukkit createItemStack method output as I assume Bukkit has their tests covered. + when(itemFactory.createItemStack(any())).thenThrow(IllegalArgumentException.class); /* when(itemFactory.getItemMeta(Mockito.eq(Material.POTION))).thenReturn(potionMeta); when(itemFactory.getItemMeta(Mockito.eq(Material.SPLASH_POTION))).thenReturn(potionMeta); @@ -106,148 +113,66 @@ public void testParseNoColons() { assertEquals(defaultItem, ItemParser.parse("NOCOLONS", defaultItem)); } - /* - * # Format POTION:NAME::::QTY - # LEVEL, EXTENDED, SPLASH, LINGER are optional. - # LEVEL is a number, 1 or 2 - # LINGER is for V1.9 servers and later - # Examples: - # POTION:STRENGTH:1:EXTENDED:SPLASH:1 - # POTION:INSTANT_DAMAGE:2::LINGER:2 - # POTION:JUMP:2:NOTEXTENDED:NOSPLASH:1 - # POTION:WEAKNESS::::1 - any weakness potion - */ - - @Ignore("Extended potions now have their own names and are not extended like this") - @Test - public void testParsePotionStrengthExtended() { - when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1:EXTENDED::5"); - assertNotNull(result); - assertEquals(Material.POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = true; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(5, result.getAmount()); - } - - @SuppressWarnings("deprecation") @Test - public void testParsePotionStrengthNotExtended() { + public void testParsePotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1:::4"); - assertNotNull(result); - assertEquals(Material.POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(4, result.getAmount()); + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - @SuppressWarnings("deprecation") @Test - public void testParsePotionStrengthNotExtendedSplash() { + public void testParseSplashPotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1::SPLASH:3"); - assertNotNull(result); - assertEquals(Material.SPLASH_POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(3, result.getAmount()); + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("SPLASH_POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.SPLASH_POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - @SuppressWarnings("deprecation") - @Ignore("Potions are no longer upgraded like this") @Test - public void testParsePotionStrengthNotExtendedUpgradedSplash() { + public void testParseLingeringPotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:2::SPLASH:3"); - assertNotNull(result); - assertEquals(Material.SPLASH_POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = true; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(3, result.getAmount()); - } - - enum extend { - NOT_EXTENDED, - EXTENDED - } - - enum type { - NO_SPLASH, - SPLASH, - LINGER + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("LINGERING_POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.LINGERING_POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - List notExtendable = Arrays.asList( - PotionType.UNCRAFTABLE, - PotionType.WATER, - PotionType.MUNDANE, - PotionType.THICK, - PotionType.AWKWARD, - PotionType.INSTANT_HEAL, - PotionType.INSTANT_DAMAGE, - PotionType.LUCK, - PotionType.NIGHT_VISION - ); - - @SuppressWarnings("deprecation") @Test - public void testParsePotion() { + public void testParseTippedArrow() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); for (PotionType type : PotionType.values()) { - if (type.name().contains("LONG") || type.name().contains("STRONG")) { - continue; - } - for (ItemParserTest.type t: ItemParserTest.type.values()) { - for (int up = 1; up < 2; up++) { - boolean isUpgraded = up > 1; - String req = "POTION:" + type.name() + ":" + up + "::"+ t.name() + ":3"; - ItemStack result = ItemParser.parse(req); - assertNotNull(result); - switch (t) { - case LINGER: - assertEquals(Material.LINGERING_POTION, result.getType()); - PotionData data = new PotionData(type, false, isUpgraded); - verify(potionMeta, times(3)).setBasePotionData(Mockito.eq(data)); - break; - case NO_SPLASH: - assertEquals(Material.POTION, result.getType()); - data = new PotionData(type, false, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - break; - case SPLASH: - assertEquals(Material.SPLASH_POTION, result.getType()); - data = new PotionData(type, false, isUpgraded); - verify(potionMeta, times(2)).setBasePotionData(Mockito.eq(data)); - break; - default: - break; - } - - assertEquals(3, result.getAmount()); - } - } + ItemStack itemStack = ItemParser.parse("TIPPED_ARROW:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.TIPPED_ARROW); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); } } @Test - public void testParseTippedArrow() { + public void testParseBadPotion() + { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("TIPPED_ARROW:WEAKNESS::::1"); - assertNotNull(result); - assertEquals(Material.TIPPED_ARROW, result.getType()); + ItemStack itemStack = ItemParser.parse("POTION::5"); + assertEquals(5, itemStack.getAmount()); + // Not sure how this can be tested + // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + itemStack = ItemParser.parse("POTION:NO_POTION:1"); + assertEquals(1, itemStack.getAmount()); + // Not sure how this can be tested + // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); } @@ -318,7 +243,6 @@ public void testParseBadThreeItem() { assertEquals(defaultItem, ItemParser.parse("WOODEN_SWORD:4:AA", defaultItem)); } - @Ignore("This doesn't work for some reason") @Test public void parseCustomModelData() { ItemStack result = ItemParser.parse("WOODEN_SWORD:CMD-23151212:2"); @@ -327,4 +251,21 @@ public void parseCustomModelData() { assertEquals(2, result.getAmount()); assertNull(ItemParser.parse("WOODEN_SWORD:CMD-23151212:2:CMD-23151212")); } + + @Test + public void parsePlayerHead() { + when(itemFactory.getItemMeta(any())).thenReturn(skullMeta); + ItemStack result = ItemParser.parse("PLAYER_HEAD:2"); + assertNotNull(result); + assertEquals(Material.PLAYER_HEAD, result.getType()); + assertEquals(2, result.getAmount()); + + result = ItemParser.parse("PLAYER_HEAD:BONNe1704"); + assertNotNull(result); + assertEquals(Material.PLAYER_HEAD, result.getType()); + assertEquals(1, result.getAmount()); + + // I do not know if it is possible to test metadata, as skull meta is not applied to player heads in testing. + //assertEquals("BONNe1704", ((SkullMeta) result.getItemMeta()).getOwnerProfile().getName()); + } }