diff options
author | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
---|---|---|
committer | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
commit | bd3f0329d0e391bd84b5f9e3ff207d9dd9815853 (patch) | |
tree | 2fd1d1ef625f57acc2e4916c967d8d2393844798 /src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java | |
parent | 2315b90da8117f28f66348927afdb621ee4fc815 (diff) | |
download | Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.gz Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.bz2 Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.zip |
new pr because fixing merge conflict would take too long
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java')
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java new file mode 100644 index 00000000..05767558 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java @@ -0,0 +1,443 @@ +package de.hysky.skyblocker.skyblock.item; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.http.HttpHeaders; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class PriceInfoTooltip { + private static final Logger LOGGER = LoggerFactory.getLogger(PriceInfoTooltip.class.getName()); + private static final MinecraftClient client = MinecraftClient.getInstance(); + private static JsonObject npcPricesJson; + private static JsonObject bazaarPricesJson; + private static JsonObject oneDayAvgPricesJson; + private static JsonObject threeDayAvgPricesJson; + private static JsonObject lowestPricesJson; + private static JsonObject isMuseumJson; + private static JsonObject motesPricesJson; + private static boolean nullMsgSend = false; + private final static Gson gson = new Gson(); + private static final Map<String, String> apiAddresses; + private static long npcHash = 0; + private static long museumHash = 0; + private static long motesHash = 0; + + public static void onInjectTooltip(ItemStack stack, TooltipContext context, List<Text> lines) { + if (!Utils.isOnSkyblock() || client.player == null) return; + + String name = getInternalNameFromNBT(stack, false); + String internalID = getInternalNameFromNBT(stack, true); + String neuName = name; + if (name == null || internalID == null) return; + + if(name.startsWith("ISSHINY_")){ + name = "SHINY_" + internalID; + neuName = internalID; + } + + int count = stack.getCount(); + boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:")); + + if (SkyblockerConfigManager.get().general.itemTooltip.enableNPCPrice) { + if (npcPricesJson == null) { + nullWarning(); + } else if (npcPricesJson.has(internalID)) { + lines.add(Text.literal(String.format("%-21s", "NPC Price:")) + .formatted(Formatting.YELLOW) + .append(getCoinsMessage(npcPricesJson.get(internalID).getAsDouble(), count))); + } + } + + if (SkyblockerConfigManager.get().general.itemTooltip.enableMotesPrice && Utils.isInTheRift()) { + if (motesPricesJson == null) { + nullWarning(); + } else if (motesPricesJson.has(internalID)) { + lines.add(Text.literal(String.format("%-20s", "Motes Price:")) + .formatted(Formatting.LIGHT_PURPLE) + .append(getMotesMessage(motesPricesJson.get(internalID).getAsInt(), count))); + } + } + + boolean bazaarExist = false; + + if (SkyblockerConfigManager.get().general.itemTooltip.enableBazaarPrice && !bazaarOpened) { + if (bazaarPricesJson == null) { + nullWarning(); + } else if (bazaarPricesJson.has(name)) { + JsonObject getItem = bazaarPricesJson.getAsJsonObject(name); + lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:")) + .formatted(Formatting.GOLD) + .append(getItem.get("buyPrice").isJsonNull() + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(getItem.get("buyPrice").getAsDouble(), count))); + lines.add(Text.literal(String.format("%-19s", "Bazaar sell Price:")) + .formatted(Formatting.GOLD) + .append(getItem.get("sellPrice").isJsonNull() + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(getItem.get("sellPrice").getAsDouble(), count))); + bazaarExist = true; + } + } + + // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api + boolean lbinExist = false; + if (SkyblockerConfigManager.get().general.itemTooltip.enableLowestBIN && !bazaarOpened && !bazaarExist) { + if (lowestPricesJson == null) { + nullWarning(); + } else if (lowestPricesJson.has(name)) { + lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:")) + .formatted(Formatting.GOLD) + .append(getCoinsMessage(lowestPricesJson.get(name).getAsDouble(), count))); + lbinExist = true; + } + } + + if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) { + if (threeDayAvgPricesJson == null || oneDayAvgPricesJson == null) { + nullWarning(); + } else { + /* + We are skipping check average prices for potions, runes + and enchanted books because there is no data for their in API. + */ + switch (internalID) { + case "PET" -> { + neuName = neuName.replaceAll("LVL_\\d*_", ""); + String[] parts = neuName.split("_"); + String type = parts[0]; + neuName = neuName.replaceAll(type + "_", ""); + neuName = neuName + "-" + type; + neuName = neuName.replace("UNCOMMON", "1") + .replace("COMMON", "0") + .replace("RARE", "2") + .replace("EPIC", "3") + .replace("LEGENDARY", "4") + .replace("MYTHIC", "5") + .replace("-", ";"); + } + case "RUNE" -> neuName = neuName.replaceAll("_(?!.*_)", ";"); + case "POTION" -> neuName = ""; + case "ATTRIBUTE_SHARD" -> + neuName = internalID + "+" + neuName.replace("SHARD-", "").replaceAll("_(?!.*_)", ";"); + default -> neuName = neuName.replace(":", "-"); + } + + if (!neuName.isEmpty() && lbinExist) { + SkyblockerConfig.Average type = SkyblockerConfigManager.get().general.itemTooltip.avg; + + // "No data" line because of API not keeping old data, it causes NullPointerException + if (type == SkyblockerConfig.Average.ONE_DAY || type == SkyblockerConfig.Average.BOTH) { + lines.add( + Text.literal(String.format("%-19s", "1 Day Avg. Price:")) + .formatted(Formatting.GOLD) + .append(oneDayAvgPricesJson.get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(oneDayAvgPricesJson.get(neuName).getAsDouble(), count) + ) + ); + } + if (type == SkyblockerConfig.Average.THREE_DAY || type == SkyblockerConfig.Average.BOTH) { + lines.add( + Text.literal(String.format("%-19s", "3 Day Avg. Price:")) + .formatted(Formatting.GOLD) + .append(threeDayAvgPricesJson.get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(threeDayAvgPricesJson.get(neuName).getAsDouble(), count) + ) + ); + } + } + } + } + + if (SkyblockerConfigManager.get().general.itemTooltip.enableMuseumDate && !bazaarOpened) { + if (isMuseumJson == null) { + nullWarning(); + } else { + String timestamp = getTimestamp(stack); + + if (isMuseumJson.has(internalID)) { + String itemCategory = isMuseumJson.get(internalID).getAsString(); + String format = switch (itemCategory) { + case "Weapons" -> "%-18s"; + case "Armor" -> "%-19s"; + default -> "%-20s"; + }; + lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")")) + .formatted(Formatting.LIGHT_PURPLE) + .append(Text.literal(timestamp).formatted(Formatting.RED))); + } else if (!timestamp.isEmpty()) { + lines.add(Text.literal(String.format("%-21s", "Obtained: ")) + .formatted(Formatting.LIGHT_PURPLE) + .append(Text.literal(timestamp).formatted(Formatting.RED))); + } + } + } + } + + private static void nullWarning() { + if (!nullMsgSend && client.player != null) { + client.player.sendMessage(Text.translatable("skyblocker.itemTooltip.nullMessage"), false); + nullMsgSend = true; + } + } + + public static NbtCompound getItemNBT(ItemStack stack) { + if (stack == null) return null; + return stack.getNbt(); + } + + /** + * this method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum. + * Currently, there are two types of timestamps the legacy which is built like this + * "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this + * "MM/dd/yy hh:mm aa" ("12/24/20 11:08 PM"). Since Hypixel transforms the two formats into one format without + * taking into account of their formats, we do the same. The final result looks like this + * "MMMM dd, yyyy" (December 24, 2020). + * Since the legacy format has a 25 as "month" SimpleDateFormat converts the 25 into 2 years and 1 month and makes + * "25/04/20 16:38" -> "January 04, 2022" instead of "April 25, 2020". + * This causes the museum rank to be much worse than it should be. + * + * @param stack the item under the pointer + * @return if the item have a "Timestamp" it will be shown formated on the tooltip + */ + public static String getTimestamp(ItemStack stack) { + NbtCompound tag = getItemNBT(stack); + + if (tag != null && tag.contains("ExtraAttributes", 10)) { + NbtCompound ea = tag.getCompound("ExtraAttributes"); + + if (ea.contains("timestamp", 8)) { + SimpleDateFormat nbtFormat = new SimpleDateFormat("MM/dd/yy"); + + try { + Date date = nbtFormat.parse(ea.getString("timestamp")); + SimpleDateFormat skyblockerFormat = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH); + return skyblockerFormat.format(date); + } catch (ParseException e) { + LOGGER.warn("[Skyblocker-tooltip] getTimestamp", e); + } + } + } + + return ""; + } + + public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) { + NbtCompound tag = getItemNBT(stack); + if (tag == null || !tag.contains("ExtraAttributes", 10)) { + return null; + } + NbtCompound ea = tag.getCompound("ExtraAttributes"); + + if (!ea.contains("id", 8)) { + return null; + } + String internalName = ea.getString("id"); + + if (internalIDOnly) { + return internalName; + } + + // Transformation to API format. + if (ea.contains("is_shiny")){ + return "ISSHINY_" + internalName; + } + + switch (internalName) { + case "ENCHANTED_BOOK" -> { + if (ea.contains("enchantments")) { + NbtCompound enchants = ea.getCompound("enchantments"); + Optional<String> firstEnchant = enchants.getKeys().stream().findFirst(); + String enchant = firstEnchant.orElse(""); + return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant); + } + } + case "PET" -> { + if (ea.contains("petInfo")) { + JsonObject petInfo = gson.fromJson(ea.getString("petInfo"), JsonObject.class); + return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString(); + } + } + case "POTION" -> { + String enhanced = ea.contains("enhanced") ? "_ENHANCED" : ""; + String extended = ea.contains("extended") ? "_EXTENDED" : ""; + String splash = ea.contains("splash") ? "_SPLASH" : ""; + if (ea.contains("potion") && ea.contains("potion_level")) { + return (ea.getString("potion") + "_" + internalName + "_" + ea.getInt("potion_level") + + enhanced + extended + splash).toUpperCase(Locale.ENGLISH); + } + } + case "RUNE" -> { + if (ea.contains("runes")) { + NbtCompound runes = ea.getCompound("runes"); + Optional<String> firstRunes = runes.getKeys().stream().findFirst(); + String rune = firstRunes.orElse(""); + return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune); + } + } + case "ATTRIBUTE_SHARD" -> { + if (ea.contains("attributes")) { + NbtCompound shards = ea.getCompound("attributes"); + Optional<String> firstShards = shards.getKeys().stream().findFirst(); + String shard = firstShards.orElse(""); + return internalName + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard); + } + } + } + return internalName; + } + + + private static Text getCoinsMessage(double price, int count) { + // Format the price string once + String priceString = String.format(Locale.ENGLISH, "%1$,.1f", price); + + // If count is 1, return a simple message + if (count == 1) { + return Text.literal(priceString + " Coins").formatted(Formatting.DARK_AQUA); + } + + // If count is greater than 1, include the "each" information + String priceStringTotal = String.format(Locale.ENGLISH, "%1$,.1f", price * count); + MutableText message = Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA); + message.append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY)); + + return message; + } + + private static Text getMotesMessage(int price, int count) { + float motesMultiplier = SkyblockerConfigManager.get().locations.rift.mcGrubberStacks * 0.05f + 1; + + // Calculate the total price + int totalPrice = price * count; + String totalPriceString = String.format(Locale.ENGLISH, "%1$,.1f", totalPrice * motesMultiplier); + + // If count is 1, return a simple message + if (count == 1) { + return Text.literal(totalPriceString.replace(".0", "") + " Motes").formatted(Formatting.DARK_AQUA); + } + + // If count is greater than 1, include the "each" information + String eachPriceString = String.format(Locale.ENGLISH, "%1$,.1f", price * motesMultiplier); + MutableText message = Text.literal(totalPriceString.replace(".0", "") + " Motes ").formatted(Formatting.DARK_AQUA); + message.append(Text.literal("(" + eachPriceString.replace(".0", "") + " each)").formatted(Formatting.GRAY)); + + return message; + } + + // If these options is true beforehand, the client will get first data of these options while loading. + // After then, it will only fetch the data if it is on Skyblock. + public static int minute = -1; + + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(() -> { + if (!Utils.isOnSkyblock() && 0 < minute++) { + nullMsgSend = false; + return; + } + + List<CompletableFuture<Void>> futureList = new ArrayList<>(); + if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) { + SkyblockerConfig.Average type = SkyblockerConfigManager.get().general.itemTooltip.avg; + + if (type == SkyblockerConfig.Average.BOTH || oneDayAvgPricesJson == null || threeDayAvgPricesJson == null || minute % 5 == 0) { + futureList.add(CompletableFuture.runAsync(() -> { + oneDayAvgPricesJson = downloadPrices("1 day avg"); + threeDayAvgPricesJson = downloadPrices("3 day avg"); + })); + } else if (type == SkyblockerConfig.Average.ONE_DAY) { + futureList.add(CompletableFuture.runAsync(() -> oneDayAvgPricesJson = downloadPrices("1 day avg"))); + } else if (type == SkyblockerConfig.Average.THREE_DAY) { + futureList.add(CompletableFuture.runAsync(() -> threeDayAvgPricesJson = downloadPrices("3 day avg"))); + } + } + if (SkyblockerConfigManager.get().general.itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator) + futureList.add(CompletableFuture.runAsync(() -> lowestPricesJson = downloadPrices("lowest bins"))); + + if (SkyblockerConfigManager.get().general.itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator) + futureList.add(CompletableFuture.runAsync(() -> bazaarPricesJson = downloadPrices("bazaar"))); + + if (SkyblockerConfigManager.get().general.itemTooltip.enableNPCPrice && npcPricesJson == null) + futureList.add(CompletableFuture.runAsync(() -> npcPricesJson = downloadPrices("npc"))); + + if (SkyblockerConfigManager.get().general.itemTooltip.enableMuseumDate && isMuseumJson == null) + futureList.add(CompletableFuture.runAsync(() -> isMuseumJson = downloadPrices("museum"))); + + if (SkyblockerConfigManager.get().general.itemTooltip.enableMotesPrice && motesPricesJson == null) + futureList.add(CompletableFuture.runAsync(() -> motesPricesJson = downloadPrices("motes"))); + + minute++; + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) + .whenComplete((unused, throwable) -> nullMsgSend = false); + }, 1200); + } + + private static JsonObject downloadPrices(String type) { + try { + String url = apiAddresses.get(type); + + if (type.equals("npc") || type.equals("museum") || type.equals("motes")) { + HttpHeaders headers = Http.sendHeadRequest(url); + long combinedHash = Http.getEtag(headers).hashCode() + Http.getLastModified(headers).hashCode(); + + switch (type) { + case "npc": if (npcHash == combinedHash) return npcPricesJson; else npcHash = combinedHash; + case "museum": if (museumHash == combinedHash) return isMuseumJson; else museumHash = combinedHash; + case "motes": if (motesHash == combinedHash) return motesPricesJson; else motesHash = combinedHash; + } + } + + String apiResponse = Http.sendGetRequest(url); + + return new Gson().fromJson(apiResponse, JsonObject.class); + } catch (Exception e) { + LOGGER.warn("[Skyblocker] Failed to download " + type + " prices!", e); + return null; + } + } + + public static JsonObject getBazaarPrices() { + return bazaarPricesJson; + } + + public static JsonObject getLBINPrices() { + return lowestPricesJson; + } + + static { + apiAddresses = new HashMap<>(); + apiAddresses.put("1 day avg", "https://moulberry.codes/auction_averages_lbin/1day.json"); + apiAddresses.put("3 day avg", "https://moulberry.codes/auction_averages_lbin/3day.json"); + apiAddresses.put("bazaar", "https://hysky.de/api/bazaar"); + apiAddresses.put("lowest bins", "https://hysky.de/api/auctions/lowestbins"); + apiAddresses.put("npc", "https://hysky.de/api/npcprice"); + apiAddresses.put("museum", "https://hysky.de/api/museum"); + apiAddresses.put("motes", "https://hysky.de/api/motesprice"); + } +}
\ No newline at end of file |