/* * Copyright (C) 2022-2023 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; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.github.moulberry.notenoughupdates.auction.APIManager; import io.github.moulberry.notenoughupdates.core.config.KeybindHelper; import io.github.moulberry.notenoughupdates.miscfeatures.inventory.MuseumTooltipManager; import io.github.moulberry.notenoughupdates.util.Constants; import io.github.moulberry.notenoughupdates.util.Utils; import io.github.moulberry.notenoughupdates.util.hypixelapi.HypixelItemAPI; import lombok.val; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import org.lwjgl.input.Keyboard; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; public class ItemPriceInformation { private static File file; private static HashSet auctionableItems; private static Gson gson; private static final NumberFormat format = new DecimalFormat("#,##0.#", new DecimalFormatSymbols(Locale.US)); public static String STACKSIZE_OVERRIDE = "NEU_STACKSIZE_OVERRIDE"; private static final Pattern SACK_STORED_AMOUNT = Pattern.compile( ".*Stored: §.(?[\\d,]+)§.\\/.*"); private static final Pattern GEMSTONE_STORED_AMOUNT = Pattern.compile( ".*Amount: §.(?[\\d,]+)"); private static final Pattern COMPOSTER_STORED_AMOUNT = Pattern.compile( ".*Compost Available: §.(?[\\d,]+)"); private static final Pattern BAZAAR_STORED_AMOUNT = Pattern.compile( ".*(?:Offer|Order) amount: §.(?[\\d,]+)§.x"); public static void addToTooltip(List tooltip, String internalName, ItemStack stack) { addToTooltip(tooltip, internalName, stack, true); } public static void init(File saveLocation, Gson neuGson) { file = saveLocation; gson = neuGson; if (file.exists()) { try ( BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(file), StandardCharsets.UTF_8 )) ) { auctionableItems = gson.fromJson(reader, HashSet.class); } catch (Exception ignored) { } } } public static void updateAuctionableItemsList() { Set items = NotEnoughUpdates.INSTANCE.manager.auctionManager.getItemAuctionInfoKeySet(); if (!items.isEmpty()) { auctionableItems = (HashSet) items; try ( BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8 )) ) { //noinspection ResultOfMethodCallIgnored file.createNewFile(); writer.write(gson.toJson(items)); } catch (IOException ignored) { } } } public static void addToTooltip(List tooltip, String internalname, ItemStack stack, boolean useStackSize) { if (stack.getTagCompound().hasKey("disableNeuTooltip") && stack.getTagCompound().getBoolean("disableNeuTooltip")) { return; } if (NotEnoughUpdates.INSTANCE.config.tooltipTweaks.disablePriceKey && !KeybindHelper.isKeyDown(NotEnoughUpdates.INSTANCE.config.tooltipTweaks.disablePriceKeyKeybind)) { return; } if (internalname.equals("SKYBLOCK_MENU")) { return; } JsonObject auctionInfo = NotEnoughUpdates.INSTANCE.manager.auctionManager.getItemAuctionInfo(internalname); JsonObject bazaarInfo = NotEnoughUpdates.INSTANCE.manager.auctionManager.getBazaarInfo(internalname); double lowestBinAvg = NotEnoughUpdates.INSTANCE.manager.auctionManager.getItemAvgBin(internalname); double lowestBin = NotEnoughUpdates.INSTANCE.manager.auctionManager.getLowestBin(internalname); APIManager.CraftInfo craftCost = NotEnoughUpdates.INSTANCE.manager.auctionManager.getCraftCost(internalname); boolean bazaarItem = bazaarInfo != null; boolean auctionItem = !bazaarItem; boolean auctionInfoErrored = auctionInfo == null && lowestBin < 0; if (auctionItem) { long currentTime = System.currentTimeMillis(); long lastUpdate = NotEnoughUpdates.INSTANCE.manager.auctionManager.getLastLowestBinUpdateTime(); //check if info is older than 10 minutes if (currentTime - lastUpdate > 600 * 1000 && NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) { tooltip.add(EnumChatFormatting.RED + "[NEU] Price info is outdated."); tooltip.add(EnumChatFormatting.RED + "It will be updated again as soon as possible."); } } int shiftStackMultiplier = useStackSize && stack.stackSize > 1 ? stack.stackSize : stack.getItem().getItemStackLimit(stack); if (stack.getTagCompound() != null && stack.getTagCompound().hasKey(STACKSIZE_OVERRIDE)) { shiftStackMultiplier = stack.getTagCompound().getInteger(STACKSIZE_OVERRIDE); } int stackMultiplier = 1; boolean foundMulti = false; for (int i = 1; i < tooltip.size(); i++) { if (foundMulti) break; for (Pattern pattern : new Pattern[]{SACK_STORED_AMOUNT, GEMSTONE_STORED_AMOUNT, COMPOSTER_STORED_AMOUNT, BAZAAR_STORED_AMOUNT}) { val matcher = pattern.matcher(tooltip.get(i)); if (matcher.matches()) { String amountString = matcher.group("amount").replace(",", ""); try { int parsedValue = Integer.parseInt(amountString); if (parsedValue != 0) { shiftStackMultiplier = parsedValue; foundMulti = true; } } catch (NumberFormatException e) { e.printStackTrace(); } } } } boolean shiftPressed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT); if (shiftPressed) { stackMultiplier = shiftStackMultiplier; } boolean added = false; if (bazaarItem) { List lines = NotEnoughUpdates.INSTANCE.config.tooltipTweaks.priceInfoBaz; //values = {"", "Buy", "Sell", "Buy (Insta)", "Sell (Insta)", "Raw Craft Cost", "Instabuys (Hourly)", "Instasells (Hourly)", "Instabuys (Daily)", "Instasells (Daily)", "Instabuys (Weekly)", "Instasells (Weekly)"} for (int lineId : lines) { switch (lineId) { case 0: if (bazaarInfo.has("avg_buy")) { if (!added) { tooltip.add(""); if (!shiftPressed) tooltip.add(EnumChatFormatting.DARK_GRAY + "[SHIFT show x" + shiftStackMultiplier + "]"); added = true; } double bazaarBuyPrice = bazaarInfo.get("avg_buy").getAsFloat() * stackMultiplier; tooltip.add(formatPrice("Bazaar Buy: ", bazaarBuyPrice)); } break; case 1: if (bazaarInfo.has("avg_sell")) { if (!added) { tooltip.add(""); if (!shiftPressed) tooltip.add(EnumChatFormatting.DARK_GRAY + "[SHIFT show x" + shiftStackMultiplier + "]"); added = true; } double bazaarSellPrice = bazaarInfo.get("avg_sell").getAsDouble() * stackMultiplier; tooltip.add(formatPrice("Bazaar Sell: ", bazaarSellPrice)); } break; case 2: if (bazaarInfo.has("curr_buy")) { if (!added) { tooltip.add(""); if (!shiftPressed) tooltip.add(EnumChatFormatting.DARK_GRAY + "[SHIFT show x" + shiftStackMultiplier + "]"); added = true; } double bazaarInstantBuyPrice = bazaarInfo.get("curr_buy").getAsFloat() * stackMultiplier; tooltip.add(formatPrice("Bazaar Insta-Buy: ", bazaarInstantBuyPrice)); } break; case 3: if (bazaarInfo.has("curr_sell")) { if (!added) { tooltip.add(""); if (!shiftPressed) tooltip.add(EnumChatFormatting.DARK_GRAY + "[SHIFT show x" + shiftStackMultiplier + "]"); added = true; } double bazaarInstantSellPrice = bazaarInfo.get("curr_sell").getAsFloat() * stackMultiplier; tooltip.add(formatPrice("Bazaar Insta-Sell: ", bazaarInstantSellPrice)); } break; case 4: if (craftCost != null && craftCost.fromRecipe) { if (craftCost.craftCost == 0) { continue; } if (!added) { tooltip.add(""); added = true; } double cost = craftCost.craftCost; if (shiftPressed) cost = cost * shiftStackMultiplier; tooltip.add(formatPrice("Raw Craft Cost: ", cost)); } break; case 5: if (bazaarInfo.has("instabuys_hourly")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Buys (Hourly): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instabuys_hourly").getAsFloat())); } break; case 6: if (bazaarInfo.has("instasells_hourly")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Sells (Hourly): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instasells_hourly").getAsFloat())); } break; case 7: if (bazaarInfo.has("instabuys_daily")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Buys (Daily): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instabuys_daily").getAsFloat())); } break; case 8: if (bazaarInfo.has("instasells_daily")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Sells (Daily): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instasells_daily").getAsFloat())); } break; case 9: if (bazaarInfo.has("instabuys_weekly")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Buys (Weekly): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instabuys_weekly").getAsFloat())); } break; case 10: if (bazaarInfo.has("instasells_weekly")) { if (!added) { tooltip.add(""); added = true; } tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Insta-Sells (Weekly): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + format.format(bazaarInfo.get("instasells_weekly").getAsFloat())); } break; } } } else if (auctionItem && !auctionInfoErrored) { List lines = NotEnoughUpdates.INSTANCE.config.tooltipTweaks.priceInfoAuc; for (int lineId : lines) { switch (lineId) { case 0: if (lowestBin > 0) { if (!added) { tooltip.add(""); added = true; } tooltip.add(formatPrice("Lowest BIN: ", lowestBin)); } break; case 1: if (auctionInfo != null) { if (!added) { tooltip.add(""); added = true; } if (auctionInfo.has("clean_price")) { double cleanPrice = auctionInfo.get("clean_price").getAsDouble(); tooltip.add(formatPrice("AH Price (Clean): ", cleanPrice)); } else { double auctionPrice = auctionInfo.get("price").getAsDouble() / auctionInfo.get("count").getAsFloat(); tooltip.add(formatPrice("AH Price: ", auctionPrice)); } } break; case 2: if (auctionInfo != null) { if (!added) { tooltip.add(""); added = true; } if (auctionInfo.has("clean_price")) { tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "AH Sales (Clean): " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + (auctionInfo.get("clean_sales").getAsFloat() < 2 ? format.format(auctionInfo.get("clean_sales").getAsFloat()) + " sale/day" : format.format(auctionInfo.get("clean_sales").getAsFloat()) + " sales/day")); } else { tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "AH Sales: " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + (auctionInfo.get("sales").getAsFloat() < 2 ? format.format(auctionInfo.get("sales").getAsFloat()) + " sale/day" : format.format(auctionInfo.get("sales").getAsFloat()) + " sales/day")); } } break; case 3: if (craftCost != null && craftCost.fromRecipe) { if (craftCost.craftCost == 0) { continue; } if (!added) { tooltip.add(""); added = true; } tooltip.add(formatPrice("Raw Craft Cost: ", craftCost.craftCost)); } break; case 4: if (lowestBinAvg > 0) { if (!added) { tooltip.add(""); added = true; } tooltip.add(formatPrice("AVG Lowest BIN: ", lowestBinAvg)); } break; case 5: if (Constants.ESSENCECOSTS == null) break; JsonObject essenceCosts = Constants.ESSENCECOSTS; if (!essenceCosts.has(internalname)) { break; } JsonObject itemCosts = essenceCosts.get(internalname).getAsJsonObject(); String essenceType = itemCosts.get("type").getAsString(); boolean requiresItems = false; JsonObject itemsObject = null; if (itemCosts.has("items")) { requiresItems = true; itemsObject = itemCosts.get("items").getAsJsonObject(); } int dungeonItemLevel = Utils.getNumberOfStars(stack); if (dungeonItemLevel == -1) { int dungeonizeCost = 0; if (itemCosts.has("dungeonize")) { dungeonizeCost = itemCosts.get("dungeonize").getAsInt(); } if (dungeonizeCost > 0) { tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Dungeonize Cost: " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + dungeonizeCost + " " + essenceType); } } else if (dungeonItemLevel >= 0) { String nextStarLevelString = (dungeonItemLevel + 1) + ""; int nextStarLevelInt = Integer.parseInt(nextStarLevelString); if (itemCosts.has(nextStarLevelString)) { int upgradeCost = itemCosts.get(nextStarLevelString).getAsInt(); String starString = Utils.getStarsString(nextStarLevelInt); tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Upgrade to " + starString + EnumChatFormatting.YELLOW + EnumChatFormatting.BOLD + ": " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + upgradeCost + " " + essenceType); if (requiresItems && itemsObject.has(nextStarLevelString)) { boolean shouldShow = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || NotEnoughUpdates.INSTANCE.config.tooltipTweaks.alwaysShowRequiredItems; if (shouldShow) { tooltip.add(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD + "Required Items:"); for (JsonElement item : itemsObject.get(nextStarLevelString).getAsJsonArray()) { if (item.getAsString().contains("§")) { //TODO show outdated repo notification when 2.1.1 releases tooltip.add(" - " + item.getAsString()); continue; } String itemString = item.getAsString(); int colon = itemString.indexOf(':'); if (colon != -1) { String amount = itemString.substring(colon + 1); String requiredItem = itemString.substring(0, colon); if (requiredItem.equals("SKYBLOCK_COIN")) { tooltip.add(" - " + EnumChatFormatting.GOLD + amount + " Coins"); } if (NotEnoughUpdates.INSTANCE.manager.isValidInternalName(requiredItem)) { JsonObject itemObject = NotEnoughUpdates.INSTANCE.manager. createItemResolutionQuery(). withKnownInternalName(requiredItem). resolveToItemListJson(); if (itemObject != null && itemObject.has("displayname")) { String displayName = itemObject.get("displayname").getAsString(); tooltip.add(" - " + displayName + EnumChatFormatting.DARK_GRAY + " x" + amount); } } } } } else { tooltip.add(EnumChatFormatting.DARK_GRAY + "[CTRL to show required items]"); } } } } break; } } } else if (NotEnoughUpdates.INSTANCE.config.tooltipTweaks.rawCraft && craftCost != null && craftCost.fromRecipe) { if (craftCost.craftCost != 0) { double cost = craftCost.craftCost; cost = cost * stackMultiplier; added = true; tooltip.add(""); tooltip.add(formatPrice("Raw Craft Cost: ", cost)); } } else if (auctionInfoErrored && NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) { String message = EnumChatFormatting.RED.toString() + EnumChatFormatting.BOLD + "[NEU] API is down"; if (auctionableItems != null && !auctionableItems.isEmpty()) { if (auctionableItems.contains(internalname)) { tooltip.add(message); } } else { tooltip.add(message + " and no item data is cached"); } } Double npcSellPrice = HypixelItemAPI.getNPCSellPrice(internalname); if (NotEnoughUpdates.INSTANCE.config.tooltipTweaks.npcSellPrice && npcSellPrice != null && npcSellPrice != 0) { if (!added) tooltip.add(""); tooltip.add(formatPrice("NPC Sell Price: ", npcSellPrice * stackMultiplier)); } if (NotEnoughUpdates.INSTANCE.config.tooltipTweaks.museumDonationStatus) { if (!MuseumTooltipManager.INSTANCE.hasPlayerVisitedMuseum()) { tooltip.add(EnumChatFormatting.RED + EnumChatFormatting.BOLD.toString() + "[NEU] Visit your Museum to display donation status"); } if (MuseumTooltipManager.INSTANCE.isItemDonated(internalname)) { tooltip.add( EnumChatFormatting.YELLOW + "Item already donated to museum"); } } } private static String formatPrice(String label, double price) { boolean shortNumber = NotEnoughUpdates.INSTANCE.config.tooltipTweaks.shortNumberFormatPrices; String number = (shortNumber && price > 1000 ? Utils.shortNumberFormat(price, 0) : price > 5 && Integer.MAX_VALUE > price ? format.format((int) price) : format.format(price)); return "§e§l" + label + "§6§l" + number + " coins"; } }