/* * 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 . */ package io.github.moulberry.notenoughupdates.util; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import io.github.moulberry.notenoughupdates.NEUManager; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.core.util.StringUtils; import lombok.var; 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.Collection; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; 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 Pattern PET_PATTERN = Pattern.compile(".*(\\[Lvl .*\\] )§(.).*"); private static final String EXTRA_ATTRIBUTES = "ExtraAttributes"; private static final List 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": case "UNIQUE_RUNE": resolvedName = resolveRuneName(); break; case "ENCHANTED_BOOK": resolvedName = resolveEnchantedBookNameFromNBT(); break; case "PARTY_HAT_CRAB": case "PARTY_HAT_CRAB_ANIMATED": resolvedName = resolveCrabHatName(); break; case "ABICASE": resolvedName = resolvePhoneCase(); break; case "PARTY_HAT_SLOTH": resolvedName = resolveSlothHatName(); break; case "POTION": resolvedName = resolvePotionName(); break; case "BALLOON_HAT_2024": resolvedName = resolveBalloonHatName(); 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); } @Nullable public ItemStack resolveToItemStack(boolean useReplacements) { JsonObject jsonObject = resolveToItemListJson(); if (jsonObject == null) return null; return manager.jsonToStack(jsonObject, false, useReplacements); } // 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 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; } if (guiName.equals("Catacombs RNG Meter")) { return resolveItemInCatacombsRngMeter(); } if (guiName.startsWith("Choose Pet")) { return findInternalNameByDisplayName(displayName, false); } return null; } /** * Search for an item by the display name * * @param displayName The display name of the item we are searching * @param mayBeMangled Whether the item name may be mangled (for example: reforges, stars) * @return the internal neu item id of that item, or null */ public static String findInternalNameByDisplayName(String displayName, boolean mayBeMangled) { return filterInternalNameCandidates( findInternalNameCandidatesForDisplayName(displayName), displayName, mayBeMangled ); } public static String filterInternalNameCandidates( Collection candidateInternalNames, String displayName, boolean mayBeMangled ) { boolean isPet = displayName.contains("[Lvl "); String petRarity = null; if (isPet) { Matcher matcher = PET_PATTERN.matcher(displayName); if (matcher.matches()) { displayName = displayName.replace(matcher.group(1), ""); petRarity = matcher.group(2); } } var cleanDisplayName = StringUtils.cleanColour(displayName); var manager = NotEnoughUpdates.INSTANCE.manager; String bestMatch = null; int bestMatchLength = -1; for (String internalName : candidateInternalNames) { String unCleanItemDisplayName = manager.getDisplayName(internalName); var cleanItemDisplayName = StringUtils.cleanColour(unCleanItemDisplayName); if (cleanItemDisplayName.length() == 0) continue; if (isPet) { if (!cleanItemDisplayName.contains("[Lvl {LVL}] ")) continue; cleanItemDisplayName = cleanItemDisplayName.replace("[Lvl {LVL}] ", ""); Matcher matcher = PET_PATTERN.matcher(unCleanItemDisplayName); if (matcher.matches()) { if (!matcher.group(2).equals(petRarity)) { continue; } } } if (mayBeMangled ? !cleanDisplayName.contains(cleanItemDisplayName) : !cleanItemDisplayName.equals(cleanDisplayName)) { continue; } if (cleanItemDisplayName.length() > bestMatchLength) { bestMatchLength = cleanItemDisplayName.length(); bestMatch = internalName; } } return bestMatch; } /** * Find potential item ids for a given display name. This function is over eager to give results, * and may give invalid results, but if there is a matching item in the repository it will return at least * that item. This should be used as a first filtering pass. Use {@link #findInternalNameByDisplayName} for a more * user-friendly API. * * @param displayName The display name of the item we are searching * @return a list of internal neu item ids some of which may have a matching display name */ public static Set findInternalNameCandidatesForDisplayName(String displayName) { boolean isPet = displayName.contains("[Lvl "); var cleanDisplayName = NEUManager.cleanForTitleMapSearch(displayName); var titleWordMap = NotEnoughUpdates.INSTANCE.manager.titleWordMap; var candidates = new HashSet(); for (var partialDisplayName : cleanDisplayName.split(" ")) { if ("".equals(partialDisplayName)) continue; if (!titleWordMap.containsKey(partialDisplayName)) continue; Set c = titleWordMap.get(partialDisplayName).keySet(); for (String s : c) { if (isPet && !s.contains(";")) continue; candidates.add(s); } } return candidates; } private String resolveItemInCatacombsRngMeter() { List lore = ItemUtils.getLore(compound); if (lore.size() > 16) { String s = lore.get(15); if (s.equals("§7Selected Drop")) { String displayName = lore.get(16); return findInternalNameByDisplayName(displayName, false); } } return null; } public static 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 && !enchantmentName.equals("Ultimate Wise")) ? "ULTIMATE_" : "") + turboCheck(enchantmentName).replace(" ", "_").replace("-", "_").toUpperCase(Locale.ROOT) + ";" + Utils.parseRomanNumeral(romanLevel); } private static String turboCheck(String text) { if (text.equals("Turbo-Cocoa")) return "Turbo-Coco"; if (text.equals("Turbo-Cacti")) return "Turbo-Cactus"; return text; } 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 resolveSlothHatName() { String emoji = getExtraAttributes().getString("party_hat_emoji"); return "PARTY_HAT_SLOTH_" + emoji.toUpperCase(Locale.ROOT); } private String resolvePhoneCase() { String model = getExtraAttributes().getString("model"); return "ABICASE_" + model.toUpperCase(Locale.ROOT); } private String resolveEnchantedBookNameFromNBT() { NBTTagCompound enchantments = getExtraAttributes().getCompoundTag("enchantments"); String enchantName = IteratorUtils.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 = IteratorUtils.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 String resolvePotionName() { String potion = getExtraAttributes().getString("potion"); int potionLvl = getExtraAttributes().getInteger("potion_level"); String potionName = getExtraAttributes().getString("potion_name").replace(" ", "_"); String potionType = getExtraAttributes().getString("potion_type"); if (potionName != null && !potionName.isEmpty()) { return "POTION_" + potionName.toUpperCase(Locale.ROOT) + ";" + potionLvl; } else if (potion != null && !potion.isEmpty()) { return "POTION_" + potion.toUpperCase(Locale.ROOT) + ";" + potionLvl; } else if (potionType != null && !potionType.isEmpty()) { return "POTION_" + potionType.toUpperCase(Locale.ROOT); } else { return "WATER_BOTTLE"; } } private String resolveBalloonHatName() { String color = getExtraAttributes().getString("party_hat_color"); return "BALLOON_HAT_2024_" + color.toUpperCase(Locale.ROOT); } 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).replace(':', '-'); } // }