From c671d5c03fb5d0410645d8b6edc56adeda3d94a7 Mon Sep 17 00:00:00 2001
From: Roman / Linnea Gräf <roman.graef@gmail.com>
Date: Thu, 8 Sep 2022 14:12:03 +0200
Subject: Add item ids to Books in more places (#256)

* Add item ids to Books in more places

Add item ids to Enchanted Books in the Bazaar and in the Experimentation
Table.

Add ItemResolutionQuery as a new way to query items and item ids.

Co-authored-by: romangraef <roman.graef@gmail.com>

* Update 2.1.md

* Infer found a bug that already existed before.

* add support for animated crab hats

Co-authored-by: hannibal00212 <24389977+hannibal00212@users.noreply.github.com>
Co-authored-by: nopo <noahogno@gmail.com>
---
 .../moulberry/notenoughupdates/NEUManager.java     | 108 +++-------
 .../notenoughupdates/auction/APIManager.java       |   1 +
 .../notenoughupdates/core/util/StringUtils.java    |   8 +
 .../listener/ItemTooltipListener.java              |  31 +--
 .../miscfeatures/EnchantingSolvers.java            |   4 +-
 .../notenoughupdates/util/ItemResolutionQuery.java | 239 +++++++++++++++++++++
 .../moulberry/notenoughupdates/util/ItemUtils.java |  14 +-
 7 files changed, 311 insertions(+), 94 deletions(-)
 create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/util/ItemResolutionQuery.java

(limited to 'src')

diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
index 4e5c5b7d..35385f38 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
@@ -36,6 +36,8 @@ import io.github.moulberry.notenoughupdates.recipes.NeuRecipe;
 import io.github.moulberry.notenoughupdates.util.Constants;
 import io.github.moulberry.notenoughupdates.util.HotmInformation;
 import io.github.moulberry.notenoughupdates.util.HypixelApi;
+import io.github.moulberry.notenoughupdates.util.ItemResolutionQuery;
+import io.github.moulberry.notenoughupdates.util.ItemUtils;
 import io.github.moulberry.notenoughupdates.util.SBInfo;
 import io.github.moulberry.notenoughupdates.util.Utils;
 import net.minecraft.client.Minecraft;
@@ -679,82 +681,18 @@ public class NEUManager {
 		return null;
 	}
 
+	/**
+	 * Replaced with {@link #createItemResolutionQuery()}
+	 */
+	@Deprecated
 	public String getInternalnameFromNBT(NBTTagCompound tag) {
-		String internalname = null;
-		if (tag != null && tag.hasKey("ExtraAttributes", 10)) {
-			NBTTagCompound ea = tag.getCompoundTag("ExtraAttributes");
-
-			if (ea.hasKey("id", 8)) {
-				internalname = ea.getString("id").replaceAll(":", "-");
-			} else {
-				return null;
-			}
-
-			if ("PET".equals(internalname)) {
-				String petInfo = ea.getString("petInfo");
-				if (petInfo.length() > 0) {
-					JsonObject petInfoObject = gson.fromJson(petInfo, JsonObject.class);
-					internalname = petInfoObject.get("type").getAsString();
-					String tier = petInfoObject.get("tier").getAsString();
-					switch (tier) {
-						case "COMMON":
-							internalname += ";0";
-							break;
-						case "UNCOMMON":
-							internalname += ";1";
-							break;
-						case "RARE":
-							internalname += ";2";
-							break;
-						case "EPIC":
-							internalname += ";3";
-							break;
-						case "LEGENDARY":
-							internalname += ";4";
-							break;
-						case "MYTHIC":
-							internalname += ";5";
-							break;
-					}
-				}
-			}
-			if ("ENCHANTED_BOOK".equals(internalname) && ea.hasKey("enchantments", 10)) {
-				NBTTagCompound enchants = ea.getCompoundTag("enchantments");
-
-				for (String enchname : enchants.getKeySet()) {
-					internalname = enchname.toUpperCase() + ";" + enchants.getInteger(enchname);
-					break;
-				}
-			}
-			if ("RUNE".equals(internalname) && ea.hasKey("runes", 10)) {
-				NBTTagCompound rune = ea.getCompoundTag("runes");
-
-				for (String runename : rune.getKeySet()) {
-					internalname = runename.toUpperCase() + "_RUNE" + ";" + rune.getInteger(runename);
-					break;
-				}
-			}
-			if ("PARTY_HAT_CRAB".equals(internalname) && (ea.getString("party_hat_color") != null)) {
-				String crabhat = ea.getString("party_hat_color");
-				internalname = "PARTY_HAT_CRAB" + "_" + crabhat.toUpperCase();
-			}
-		}
-
-		return internalname;
+		return createItemResolutionQuery()
+			.withItemNBT(tag)
+			.resolveInternalName();
 	}
 
 	public String[] getLoreFromNBT(NBTTagCompound tag) {
-		String[] lore = new String[0];
-		NBTTagCompound display = tag.getCompoundTag("display");
-
-		if (display.hasKey("Lore", 9)) {
-			NBTTagList list = display.getTagList("Lore", 8);
-			lore = new String[list.tagCount()];
-			for (int k = 0; k < list.tagCount(); k++) {
-				lore[k] = list.getStringTagAt(k);
-			}
-		}
-		return lore;
+		return ItemUtils.getLore(tag).toArray(new String[0]);
 	}
 
 	public JsonObject getJsonFromNBT(NBTTagCompound tag) {
@@ -914,10 +852,18 @@ public class NEUManager {
 		return getSkullValueFromNBT(tag);
 	}
 
+	public ItemResolutionQuery createItemResolutionQuery() {
+		return new ItemResolutionQuery(this);
+	}
+
+	/**
+	 * Replaced with {@link #createItemResolutionQuery()}
+	 */
+	@Deprecated
 	public String getInternalNameForItem(ItemStack stack) {
-		if (stack == null) return null;
-		NBTTagCompound tag = stack.getTagCompound();
-		return getInternalnameFromNBT(tag);
+		return createItemResolutionQuery()
+			.withItemStack(stack)
+			.resolveInternalName();
 	}
 
 	public String getUUIDForItem(ItemStack stack) {
@@ -1573,9 +1519,13 @@ public class NEUManager {
 		return comp;
 	}
 
-	public ItemStack createItem(String internalname) {
-		JsonObject jsonObject = itemMap.get(internalname);
-		if (jsonObject == null) return null;
-		return jsonToStack(jsonObject);
+	public ItemStack createItem(String internalName) {
+		return createItemResolutionQuery()
+			.withKnownInternalName(internalName)
+			.resolveToItemStack();
+	}
+
+	public boolean isValidInternalName(String internalName) {
+		return itemMap.containsKey(internalName);
 	}
 }
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java
index c3999fb6..74ef2483 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java
@@ -168,6 +168,7 @@ public class APIManager {
 				return stack;
 			} else {
 				JsonObject item = manager.getJsonFromNBT(item_tag);
+				if (item == null) return null;
 				ItemStack stack = manager.jsonToStack(item, false);
 
 				JsonObject itemDefault = manager.getItemInformation().get(item.get("internalname").getAsString());
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/core/util/StringUtils.java b/src/main/java/io/github/moulberry/notenoughupdates/core/util/StringUtils.java
index ecb4cd8b..6143085c 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/core/util/StringUtils.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/core/util/StringUtils.java
@@ -74,4 +74,12 @@ public class StringUtils {
 			throw new RuntimeException(e); // UTF 8 should always be present
 		}
 	}
+
+	/**
+	 * taken and modified from https://stackoverflow.com/a/23326014/5507634
+	 */
+	public static String replaceLast(String string, String toReplace, String replacement) {
+		int start = string.lastIndexOf(toReplace);
+		return string.substring(0, start) + replacement + string.substring(start + toReplace.length());
+	}
 }
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/listener/ItemTooltipListener.java b/src/main/java/io/github/moulberry/notenoughupdates/listener/ItemTooltipListener.java
index f3ed8e7c..e5c3324f 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/listener/ItemTooltipListener.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/listener/ItemTooltipListener.java
@@ -28,6 +28,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture;
 import io.github.moulberry.notenoughupdates.ItemPriceInformation;
 import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
 import io.github.moulberry.notenoughupdates.core.util.MiscUtils;
+import io.github.moulberry.notenoughupdates.core.util.StringUtils;
 import io.github.moulberry.notenoughupdates.miscfeatures.PetInfoOverlay;
 import io.github.moulberry.notenoughupdates.miscgui.GuiEnchantColour;
 import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewer;
@@ -42,7 +43,6 @@ import net.minecraft.nbt.NBTTagCompound;
 import net.minecraft.nbt.NBTTagList;
 import net.minecraft.nbt.NBTUtil;
 import net.minecraft.util.EnumChatFormatting;
-import net.minecraft.util.StringUtils;
 import net.minecraftforge.event.entity.player.ItemTooltipEvent;
 import net.minecraftforge.fml.common.Loader;
 import net.minecraftforge.fml.common.eventhandler.EventPriority;
@@ -104,8 +104,13 @@ public class ItemTooltipListener {
 	public void onItemTooltipLow(ItemTooltipEvent event) {
 		if (!NotEnoughUpdates.INSTANCE.isOnSkyblock()) return;
 
-		String internalname = NotEnoughUpdates.INSTANCE.manager.getInternalNameForItem(event.itemStack);
-		if (internalname == null) {
+		String internalName = NotEnoughUpdates.INSTANCE.manager
+			.createItemResolutionQuery()
+			.withCurrentGuiContext()
+			.withItemStack(event.itemStack)
+			.resolveInternalName();
+
+		if (internalName == null) {
 			return;
 		}
 		petToolTipXPExtendPetMenu(event);
@@ -155,7 +160,8 @@ public class ItemTooltipListener {
 							for (int j = 0; j < Utils.rarityArrC.length; j++) {
 								for (Map.Entry<String, JsonElement> entry : enchantsObj.entrySet()) {
 									if (line.contains(Utils.rarityArrC[j] + " " + entry.getKey()) || line.contains(
-										Utils.rarityArrC[j] + " DUNGEON " + entry.getKey()) || line.contains("SHINY " + Utils.rarityArrC[j].replaceAll("§.§.","") + " DUNGEON " + entry.getKey())) {
+										Utils.rarityArrC[j] + " DUNGEON " + entry.getKey()) || line.contains(
+										"SHINY " + Utils.rarityArrC[j].replaceAll("§.§.", "") + " DUNGEON " + entry.getKey())) {
 										allItemEnchs = entry.getValue().getAsJsonArray();
 										break out;
 									}
@@ -180,7 +186,7 @@ public class ItemTooltipListener {
 				NotEnoughUpdates.INSTANCE.config.tooltipTweaks.showReforgeStats) {
 				JsonObject reforgeStones = Constants.REFORGESTONES;
 
-				if (reforgeStones != null && reforgeStones.has(internalname)) {
+				if (reforgeStones != null && reforgeStones.has(internalName)) {
 					boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT);
 					if (!pressedShiftLast && shift) {
 						showReforgeStoneStats = !showReforgeStoneStats;
@@ -195,7 +201,7 @@ public class ItemTooltipListener {
 						newTooltip.add(EnumChatFormatting.DARK_GRAY + "[Press SHIFT to hide extra info]");
 					}
 
-					JsonObject reforgeInfo = reforgeStones.get(internalname).getAsJsonObject();
+					JsonObject reforgeInfo = reforgeStones.get(internalName).getAsJsonObject();
 					JsonArray requiredRaritiesArray = reforgeInfo.get("requiredRarities").getAsJsonArray();
 
 					if (showReforgeStoneStats && requiredRaritiesArray.size() > 0) {
@@ -526,7 +532,7 @@ public class ItemTooltipListener {
 						newTooltip.add("");
 						newTooltip.add(EnumChatFormatting.GRAY + "[SHIFT for Price Info]");
 					} else {
-						ItemPriceInformation.addToTooltip(newTooltip, internalname, event.itemStack);
+						ItemPriceInformation.addToTooltip(newTooltip, internalName, event.itemStack);
 					}
 				}
 			}
@@ -583,9 +589,10 @@ public class ItemTooltipListener {
 										JsonObject auctionInfo = neu.manager.auctionManager.getItemAuctionInfo(internal);
 										if (auctionInfo != null) {
 											if (auctionInfo.has("clean_price")) {
-												worth = (long)auctionInfo.get("clean_price").getAsDouble();
+												worth = (long) auctionInfo.get("clean_price").getAsDouble();
 											} else {
-												worth = (long) (auctionInfo.get("price").getAsDouble() / auctionInfo.get("count").getAsDouble());
+												worth =
+													(long) (auctionInfo.get("price").getAsDouble() / auctionInfo.get("count").getAsDouble());
 											}
 										}
 										break;
@@ -703,7 +710,7 @@ public class ItemTooltipListener {
 		event.toolTip.addAll(newTooltip);
 
 		if (NotEnoughUpdates.INSTANCE.config.tooltipTweaks.showPriceInfoInvItem) {
-			ItemPriceInformation.addToTooltip(event.toolTip, internalname, event.itemStack);
+			ItemPriceInformation.addToTooltip(event.toolTip, internalName, event.itemStack);
 		}
 
 		if (event.itemStack.getTagCompound() != null && event.itemStack.getTagCompound().getBoolean("NEUHIDEPETTOOLTIP") &&
@@ -824,14 +831,14 @@ public class ItemTooltipListener {
 		if (event.toolTip == null) return;
 
 		if (event.toolTip.size() > 2 && NotEnoughUpdates.INSTANCE.config.tooltipTweaks.hideDefaultReforgeStats) {
-			String secondLine = StringUtils.stripControlCodes(event.toolTip.get(1));
+			String secondLine = StringUtils.cleanColour(event.toolTip.get(1));
 			if (secondLine.equals("Reforge Stone")) {
 				Integer startIndex = null;
 				Integer cutoffIndex = null;
 				//loop from the back of the List to find the wanted index sooner
 				for (int i = event.toolTip.size() - 1; i >= 0; i--) {
 					//rarity or mining level requirement
-					String line = StringUtils.stripControlCodes(event.toolTip.get(i));
+					String line = StringUtils.cleanColour(event.toolTip.get(i));
 					if (line.contains("REFORGE STONE") || line.contains("Requires Mining Skill Level")) {
 						cutoffIndex = i;
 					}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/EnchantingSolvers.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/EnchantingSolvers.java
index 6b541256..558917fe 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/EnchantingSolvers.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/EnchantingSolvers.java
@@ -45,9 +45,9 @@ import java.util.List;
 import java.util.Map;
 
 public class EnchantingSolvers {
-	private static SolverType currentSolver = SolverType.NONE;
+	public static SolverType currentSolver = SolverType.NONE;
 
-	private enum SolverType {
+	public enum SolverType {
 		NONE,
 		CHRONOMATRON,
 		ULTRASEQUENCER,
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ItemResolutionQuery.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ItemResolutionQuery.java
new file mode 100644
index 00000000..1ff659ac
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ItemResolutionQuery.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2022 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util;
+
+import com.google.common.collect.Iterables;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import io.github.moulberry.notenoughupdates.NEUManager;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Gui;
+import net.minecraft.client.gui.inventory.GuiChest;
+import net.minecraft.init.Items;
+import net.minecraft.inventory.ContainerChest;
+import net.minecraft.inventory.IInventory;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ItemResolutionQuery {
+
+	private static final Pattern ENCHANTED_BOOK_NAME_PATTERN = Pattern.compile("^((?:§.)+)([^§]+) ([IVXL]+)$");
+	private static final String EXTRA_ATTRIBUTES = "ExtraAttributes";
+	private static final List<String> PET_RARITIES = Arrays.asList(
+		"COMMON",
+		"UNCOMMON",
+		"RARE",
+		"EPIC",
+		"LEGENDARY",
+		"MYTHIC"
+	);
+	private final NEUManager manager;
+	private NBTTagCompound compound;
+	private Item itemType;
+	private int stackSize = -1;
+	private Gui guiContext;
+	private String knownInternalName;
+
+	public ItemResolutionQuery(NEUManager manager) {
+		this.manager = manager;
+	}
+
+	public ItemResolutionQuery withItemNBT(NBTTagCompound compound) {
+		this.compound = compound;
+		return this;
+	}
+
+	public ItemResolutionQuery withItemStack(ItemStack stack) {
+		if (stack == null) return this;
+		this.itemType = stack.getItem();
+		this.compound = stack.getTagCompound();
+		this.stackSize = stack.stackSize;
+		return this;
+	}
+
+	public ItemResolutionQuery withGuiContext(Gui gui) {
+		this.guiContext = gui;
+		return this;
+	}
+
+	public ItemResolutionQuery withCurrentGuiContext() {
+		this.guiContext = Minecraft.getMinecraft().currentScreen;
+		return this;
+	}
+
+	public ItemResolutionQuery withKnownInternalName(String knownInternalName) {
+		this.knownInternalName = knownInternalName;
+		return this;
+	}
+
+	@Nullable
+	public String resolveInternalName() {
+		if (knownInternalName != null) {
+			return knownInternalName;
+		}
+		String resolvedName = resolveFromSkyblock();
+		if (resolvedName == null) {
+			resolvedName = resolveContextualName();
+		} else {
+			switch (resolvedName.intern()) {
+				case "PET":
+					resolvedName = resolvePetName();
+					break;
+				case "RUNE":
+					resolvedName = resolveRuneName();
+					break;
+				case "ENCHANTED_BOOK":
+					resolvedName = resolveEnchantedBookNameFromNBT();
+					break;
+				case "PARTY_HAT_CRAB":
+				case "PARTY_HAT_CRAB_ANIMATED":
+					resolvedName = resolveCrabHatName();
+					break;
+			}
+		}
+
+		return resolvedName;
+	}
+
+	@Nullable
+	public JsonObject resolveToItemListJson() {
+		String internalName = resolveInternalName();
+		if (internalName == null) {
+			return null;
+		}
+		return manager.getItemInformation().get(internalName);
+	}
+
+	@Nullable
+	public ItemStack resolveToItemStack() {
+		JsonObject jsonObject = resolveToItemListJson();
+		if (jsonObject == null) return null;
+		return manager.jsonToStack(jsonObject);
+	}
+
+	// <editor-fold desc="Resolution Helpers">
+	private boolean isBazaar(IInventory chest) {
+		if (chest.getDisplayName().getFormattedText().startsWith("Bazaar ➜ ")) {
+			return true;
+		}
+		int bazaarSlot = chest.getSizeInventory() - 5;
+		if (bazaarSlot < 0) return false;
+		ItemStack stackInSlot = chest.getStackInSlot(bazaarSlot);
+		if (stackInSlot == null || stackInSlot.stackSize == 0) return false;
+		// NBT lore, we do not care about rendered lore
+		List<String> lore = ItemUtils.getLore(stackInSlot);
+		return lore.contains("§7To Bazaar");
+	}
+
+	private String resolveContextualName() {
+		if (!(guiContext instanceof GuiChest)) {
+			return null;
+		}
+		GuiChest chest = (GuiChest) guiContext;
+		ContainerChest inventorySlots = (ContainerChest) chest.inventorySlots;
+		String guiName = inventorySlots.getLowerChestInventory().getDisplayName().getUnformattedText();
+		boolean isOnBazaar = isBazaar(inventorySlots.getLowerChestInventory());
+		String displayName = ItemUtils.getDisplayName(compound);
+		if (displayName == null) return null;
+		if (itemType == Items.enchanted_book && isOnBazaar && compound != null) {
+			return resolveEnchantmentByName(displayName);
+		}
+		if (displayName.endsWith("Enchanted Book") && guiName.startsWith("Superpairs")) {
+			for (String loreLine : ItemUtils.getLore(compound)) {
+				String enchantmentIdCandidate = resolveEnchantmentByName(loreLine);
+				if (enchantmentIdCandidate != null) return enchantmentIdCandidate;
+			}
+			return null;
+		}
+		return null;
+	}
+
+	private String resolveEnchantmentByName(String name) {
+		Matcher matcher = ENCHANTED_BOOK_NAME_PATTERN.matcher(name);
+		if (!matcher.matches()) return null;
+		String format = matcher.group(1).toLowerCase(Locale.ROOT);
+		String enchantmentName = matcher.group(2).trim();
+		String romanLevel = matcher.group(3);
+		boolean ultimate = (format.contains("§l"));
+
+		return (ultimate ? "ULTIMATE_" : "")
+			+ enchantmentName.replace(" ", "_").toUpperCase(Locale.ROOT)
+			+ ";" + Utils.parseRomanNumeral(romanLevel);
+	}
+
+	private String resolveCrabHatName() {
+		int crabHatYear = getExtraAttributes().getInteger("party_hat_year");
+		String color = getExtraAttributes().getString("party_hat_color");
+		return "PARTY_HAT_CRAB_" + color.toUpperCase(Locale.ROOT) + (crabHatYear == 2022 ? "_ANIMATED" : "");
+	}
+
+	private String resolveEnchantedBookNameFromNBT() {
+		NBTTagCompound enchantments = getExtraAttributes().getCompoundTag("enchantments");
+		String enchantName = Iterables.getOnlyElement(enchantments.getKeySet(), null);
+		if (enchantName == null || enchantName.isEmpty()) return null;
+		return enchantName.toUpperCase(Locale.ROOT) + ";" + enchantments.getInteger(enchantName);
+	}
+
+	private String resolveRuneName() {
+		NBTTagCompound runes = getExtraAttributes().getCompoundTag("runes");
+		String runeName = Iterables.getOnlyElement(runes.getKeySet(), null);
+		if (runeName == null || runeName.isEmpty()) return null;
+		return runeName.toUpperCase(Locale.ROOT) + "_RUNE;" + runes.getInteger(runeName);
+	}
+
+	private String resolvePetName() {
+		String petInfo = getExtraAttributes().getString("petInfo");
+		if (petInfo == null || petInfo.isEmpty()) return null;
+		try {
+			JsonObject petInfoObject = manager.gson.fromJson(petInfo, JsonObject.class);
+			String petId = petInfoObject.get("type").getAsString();
+			String petTier = petInfoObject.get("tier").getAsString();
+			int rarityIndex = PET_RARITIES.indexOf(petTier);
+			return petId.toUpperCase(Locale.ROOT) + ";" + rarityIndex;
+		} catch (JsonParseException | ClassCastException ex) {
+			/* This happens if Hypixel changed the pet json format;
+				 I still log this exception, since this case *is* exceptional and cannot easily be recovered from */
+			ex.printStackTrace();
+			return null;
+		}
+	}
+
+	private NBTTagCompound getExtraAttributes() {
+		if (compound == null) return new NBTTagCompound();
+		return compound.getCompoundTag(EXTRA_ATTRIBUTES);
+	}
+
+	private String resolveFromSkyblock() {
+		String internalName = getExtraAttributes().getString("id");
+		if (internalName == null || internalName.isEmpty()) return null;
+		return internalName.toUpperCase(Locale.ROOT);
+	}
+
+	// </editor-fold>
+
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ItemUtils.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ItemUtils.java
index 3a2db0fb..cffdd164 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/util/ItemUtils.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ItemUtils.java
@@ -73,7 +73,10 @@ public class ItemUtils {
 	}
 
 	public static List<String> getLore(ItemStack is) {
-		NBTTagCompound tagCompound = is.getTagCompound();
+		return getLore(is.getTagCompound());
+	}
+
+	public static List<String> getLore(NBTTagCompound tagCompound) {
 		if (tagCompound == null) {
 			return Collections.emptyList();
 		}
@@ -84,4 +87,13 @@ public class ItemUtils {
 		}
 		return list;
 	}
+
+	public static String getDisplayName(NBTTagCompound compound) {
+		if (compound == null) return null;
+		String string = compound.getCompoundTag("display").getString("Name");
+		if (string == null || string.isEmpty())
+			return null;
+		return string;
+	}
+
 }
-- 
cgit