aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java
diff options
context:
space:
mode:
authorYasin <a.piri@hotmail.de>2023-10-09 12:58:02 +0200
committerYasin <a.piri@hotmail.de>2023-10-09 12:58:02 +0200
commitbd3f0329d0e391bd84b5f9e3ff207d9dd9815853 (patch)
tree2fd1d1ef625f57acc2e4916c967d8d2393844798 /src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java
parent2315b90da8117f28f66348927afdb621ee4fc815 (diff)
downloadSkyblocker-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.java443
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