diff options
author | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2023-10-29 01:12:49 -0400 |
---|---|---|
committer | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2023-10-30 00:25:40 -0400 |
commit | 14074ff48e377d26502047ca07b6e2b14bd2ca2d (patch) | |
tree | fd804463de81439657ff4dc24d221732c0993d36 /src/main/java/de/hysky/skyblocker/skyblock/item/tooltip | |
parent | 3a68bf3d435e1288a050f3db18c9fc61b7e23172 (diff) | |
download | Skyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.tar.gz Skyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.tar.bz2 Skyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.zip |
Update ItemTooltip
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/item/tooltip')
5 files changed, 933 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java new file mode 100644 index 00000000..5627b56d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java @@ -0,0 +1,204 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtInt; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtList; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Path; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BackpackPreview { + private static final Logger LOGGER = LoggerFactory.getLogger(BackpackPreview.class); + private static final Identifier TEXTURE = new Identifier("textures/gui/container/generic_54.png"); + private static final Pattern ECHEST_PATTERN = Pattern.compile("Ender Chest.*\\((\\d+)/\\d+\\)"); + private static final Pattern BACKPACK_PATTERN = Pattern.compile("Backpack.*\\(Slot #(\\d+)\\)"); + private static final int STORAGE_SIZE = 27; + + private static final Storage[] storages = new Storage[STORAGE_SIZE]; + + /** + * The profile id of the currently loaded backpack preview. + */ + private static String loaded; + private static Path saveDir; + + public static void init() { + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (screen instanceof HandledScreen<?> handledScreen) { + ScreenEvents.remove(screen).register(screen1 -> updateStorage(handledScreen)); + } + }); + } + + public static void tick() { + Utils.update(); // force update isOnSkyblock to prevent crash on disconnect + if (Utils.isOnSkyblock()) { + // save all dirty storages + saveStorages(); + // update save dir based on sb profile id + String id = MinecraftClient.getInstance().getSession().getUuidOrNull().toString().replaceAll("-", "") + "/" + Utils.getProfileId(); + if (!id.equals(loaded)) { + saveDir = SkyblockerMod.CONFIG_DIR.resolve("backpack-preview/" + id); + //noinspection ResultOfMethodCallIgnored + saveDir.toFile().mkdirs(); + // load storage again because profile id changed + loaded = id; + loadStorages(); + } + } + } + + private static void loadStorages() { + for (int index = 0; index < STORAGE_SIZE; ++index) { + storages[index] = null; + File storageFile = saveDir.resolve(index + ".nbt").toFile(); + if (storageFile.isFile()) { + try { + storages[index] = Storage.fromNbt(Objects.requireNonNull(NbtIo.read(storageFile))); + } catch (Exception e) { + LOGGER.error("Failed to load backpack preview file: " + storageFile.getName(), e); + } + } + } + } + + private static void saveStorages() { + for (int index = 0; index < STORAGE_SIZE; ++index) { + if (storages[index] != null && storages[index].dirty) { + saveStorage(index); + } + } + } + + private static void saveStorage(int index) { + try { + NbtIo.write(storages[index].toNbt(), saveDir.resolve(index + ".nbt").toFile()); + storages[index].markClean(); + } catch (Exception e) { + LOGGER.error("Failed to save backpack preview file: " + index + ".nbt", e); + } + } + + private static void updateStorage(HandledScreen<?> handledScreen) { + String title = handledScreen.getTitle().getString(); + int index = getStorageIndexFromTitle(title); + if (index != -1) { + storages[index] = new Storage(handledScreen.getScreenHandler().slots.get(0).inventory, title, true); + } + } + + public static boolean renderPreview(DrawContext context, Screen screen, int index, int mouseX, int mouseY) { + if (index >= 9 && index < 18) index -= 9; + else if (index >= 27 && index < 45) index -= 18; + else return false; + + if (storages[index] == null) return false; + int rows = (storages[index].size() - 9) / 9; + + int x = mouseX + 184 >= screen.width ? mouseX - 188 : mouseX + 8; + int y = Math.max(0, mouseY - 16); + + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(0f, 0f, 400f); + + RenderSystem.enableDepthTest(); + context.drawTexture(TEXTURE, x, y, 0, 0, 176, rows * 18 + 17); + context.drawTexture(TEXTURE, x, y + rows * 18 + 17, 0, 215, 176, 7); + + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, storages[index].name, x + 8, y + 6, 0x404040, false); + + matrices.translate(0f, 0f, 200f); + for (int i = 9; i < storages[index].size(); ++i) { + ItemStack currentStack = storages[index].getStack(i); + int itemX = x + (i - 9) % 9 * 18 + 8; + int itemY = y + (i - 9) / 9 * 18 + 18; + + if (SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) { + ItemRarityBackgrounds.tryDraw(currentStack, context, itemX, itemY); + } + + context.drawItem(currentStack, itemX, itemY); + context.drawItemInSlot(textRenderer, currentStack, itemX, itemY); + } + + matrices.pop(); + + return true; + } + + private static int getStorageIndexFromTitle(String title) { + Matcher echest = ECHEST_PATTERN.matcher(title); + if (echest.find()) return Integer.parseInt(echest.group(1)) - 1; + Matcher backpack = BACKPACK_PATTERN.matcher(title); + if (backpack.find()) return Integer.parseInt(backpack.group(1)) + 8; + return -1; + } + + static class Storage { + private final Inventory inventory; + private final String name; + private boolean dirty; + + private Storage(Inventory inventory, String name, boolean dirty) { + this.inventory = inventory; + this.name = name; + this.dirty = dirty; + } + + private int size() { + return inventory.size(); + } + + private ItemStack getStack(int index) { + return inventory.getStack(index); + } + + private void markClean() { + dirty = false; + } + + @NotNull + private static Storage fromNbt(NbtCompound root) { + SimpleInventory inventory = new SimpleInventory(root.getList("list", NbtCompound.COMPOUND_TYPE).stream().map(NbtCompound.class::cast).map(ItemStack::fromNbt).toArray(ItemStack[]::new)); + return new Storage(inventory, root.getString("name"), false); + } + + @NotNull + private NbtCompound toNbt() { + NbtCompound root = new NbtCompound(); + NbtList list = new NbtList(); + for (int i = 0; i < size(); ++i) { + list.add(getStack(i).writeNbt(new NbtCompound())); + } + root.put("list", list); + root.put("size", NbtInt.of(size())); + root.putString("name", name); + return root; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java new file mode 100644 index 00000000..9cf0356b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java @@ -0,0 +1,90 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import de.hysky.skyblocker.mixin.accessor.DrawContextInvoker; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.utils.ItemUtils; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import it.unimi.dsi.fastutil.ints.IntObjectPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class CompactorDeletorPreview { + /** + * The width and height in slots of the compactor/deletor + */ + private static final Map<String, IntIntPair> DIMENSIONS = Map.of( + "4000", IntIntPair.of(1, 1), + "5000", IntIntPair.of(1, 3), + "6000", IntIntPair.of(1, 7), + "7000", IntIntPair.of(2, 6) + ); + private static final IntIntPair DEFAULT_DIMENSION = IntIntPair.of(1, 6); + public static final Pattern NAME = Pattern.compile("PERSONAL_(?<type>COMPACTOR|DELETOR)_(?<size>\\d+)"); + private static final MinecraftClient client = MinecraftClient.getInstance(); + + public static boolean drawPreview(DrawContext context, ItemStack stack, String type, String size, int x, int y) { + List<Text> tooltips = Screen.getTooltipFromItem(client, stack); + int targetIndex = getTargetIndex(tooltips); + if (targetIndex == -1) return false; + + // Get items in compactor or deletor + NbtCompound extraAttributes = ItemUtils.getExtraAttributes(stack); + if (extraAttributes == null) return false; + // Get the slots and their items from the nbt, which is in the format personal_compact_<slot_number> or personal_deletor_<slot_number> + List<IntObjectPair<ItemStack>> slots = extraAttributes.getKeys().stream().filter(slot -> slot.contains(type.toLowerCase().substring(0, 7))).map(slot -> IntObjectPair.of(Integer.parseInt(slot.substring(17)), ItemRepository.getItemStack(extraAttributes.getString(slot)))).toList(); + + List<TooltipComponent> components = tooltips.stream().map(Text::asOrderedText).map(TooltipComponent::of).collect(Collectors.toList()); + IntIntPair dimensions = DIMENSIONS.getOrDefault(size, DEFAULT_DIMENSION); + + // If there are no items in compactor or deletor + if (slots.isEmpty()) { + int slotsCount = dimensions.leftInt() * dimensions.rightInt(); + components.add(targetIndex, TooltipComponent.of(Text.literal(slotsCount + (slotsCount == 1 ? " slot" : " slots")).formatted(Formatting.GRAY).asOrderedText())); + + ((DrawContextInvoker) context).invokeDrawTooltip(client.textRenderer, components, x, y, HoveredTooltipPositioner.INSTANCE); + return true; + } + + // Add the preview tooltip component + components.add(targetIndex, new CompactorPreviewTooltipComponent(slots, dimensions)); + + // Render accompanying text + components.add(targetIndex, TooltipComponent.of(Text.literal("Contents:").asOrderedText())); + if (extraAttributes.contains("PERSONAL_DELETOR_ACTIVE")) { + components.add(targetIndex, TooltipComponent.of(Text.literal("Active: ") + .append(extraAttributes.getBoolean("PERSONAL_DELETOR_ACTIVE") ? Text.literal("YES").formatted(Formatting.BOLD).formatted(Formatting.GREEN) : Text.literal("NO").formatted(Formatting.BOLD).formatted(Formatting.RED)).asOrderedText())); + } + ((DrawContextInvoker) context).invokeDrawTooltip(client.textRenderer, components, x, y, HoveredTooltipPositioner.INSTANCE); + return true; + } + + /** + * Finds the target index to insert the preview component, which is the second empty line + */ + private static int getTargetIndex(List<Text> tooltips) { + int targetIndex = -1; + int lineCount = 0; + for (int i = 0; i < tooltips.size(); i++) { + if (tooltips.get(i).getString().isEmpty()) { + lineCount += 1; + } + if (lineCount == 2) { + targetIndex = i; + break; + } + } + return targetIndex; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java new file mode 100644 index 00000000..22498c02 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java @@ -0,0 +1,56 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import de.hysky.skyblocker.SkyblockerMod; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import it.unimi.dsi.fastutil.ints.IntObjectPair; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +public class CompactorPreviewTooltipComponent implements TooltipComponent { + private static final Identifier INVENTORY_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/inventory_background.png"); + private final Iterable<IntObjectPair<ItemStack>> items; + private final IntIntPair dimensions; + + public CompactorPreviewTooltipComponent(Iterable<IntObjectPair<ItemStack>> items, IntIntPair dimensions) { + this.items = items; + this.dimensions = dimensions; + } + + @Override + public int getHeight() { + return dimensions.leftInt() * 18 + 14; + } + + @Override + public int getWidth(TextRenderer textRenderer) { + return dimensions.rightInt() * 18 + 14; + } + + @Override + public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) { + context.drawTexture(INVENTORY_TEXTURE, x, y, 0, 0, 7 + dimensions.rightInt() * 18, 7); + context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y, 169, 0, 7, 7); + + for (int i = 0; i < dimensions.leftInt(); i++) { + context.drawTexture(INVENTORY_TEXTURE, x, y + 7 + i * 18, 0, 7, 7, 18); + for (int j = 0; j < dimensions.rightInt(); j++) { + context.drawTexture(INVENTORY_TEXTURE, x + 7 + j * 18, y + 7 + i * 18, 7, 7, 18, 18); + } + context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y + 7 + i * 18, 169, 7, 7, 18); + } + context.drawTexture(INVENTORY_TEXTURE, x, y + 7 + dimensions.leftInt() * 18, 0, 25, 7 + dimensions.rightInt() * 18, 7); + context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y + 7 + dimensions.leftInt() * 18, 169, 25, 7, 7); + + for (IntObjectPair<ItemStack> entry : items) { + if (entry.right() != null) { + int itemX = x + entry.leftInt() % dimensions.rightInt() * 18 + 8; + int itemY = y + entry.leftInt() / dimensions.rightInt() * 18 + 8; + context.drawItem(entry.right(), itemX, itemY); + context.drawItemInSlot(textRenderer, entry.right(), itemX, itemY); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java new file mode 100644 index 00000000..8f701758 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java @@ -0,0 +1,98 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import de.hysky.skyblocker.utils.Constants; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.StringIdentifiable; + +public class ExoticTooltip { + public static String getExpectedHex(String id) { + if (!ItemTooltip.colorJson.has(id)) { + return null; + } + String color = ItemTooltip.colorJson.get(id).getAsString(); + if (color != null) { + String[] RGBValues = color.split(","); + return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2])); + } else { + ItemTooltip.LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id); + return null; + } + } + + public static boolean isException(String id, String hex) { + if (id.startsWith("LEATHER") || id.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(id)) { + return true; + } + if (id.startsWith("RANCHER")) { + return Constants.RANCHERS.contains(hex); + } + if (id.contains("ADAPTIVE_CHESTPLATE")) { + return Constants.ADAPTIVE_CHEST.contains(hex); + } else if (id.contains("ADAPTIVE")) { + return Constants.ADAPTIVE.contains(hex); + } + if (id.startsWith("REAPER")) { + return Constants.REAPER.contains(hex); + } + if (id.startsWith("FAIRY")) { + return Constants.FAIRY_HEXES.contains(hex); + } + if (id.startsWith("CRYSTAL")) { + return Constants.CRYSTAL_HEXES.contains(hex); + } + if (id.contains("SPOOK")) { + return Constants.SPOOK.contains(hex); + } + return false; + } + + public static DyeType checkDyeType(String hex) { + if (Constants.CRYSTAL_HEXES.contains(hex)) { + return DyeType.CRYSTAL; + } + if (Constants.FAIRY_HEXES.contains(hex)) { + return DyeType.FAIRY; + } + if (Constants.OG_FAIRY_HEXES.contains(hex)) { + return DyeType.OG_FAIRY; + } + if (Constants.SPOOK.contains(hex)) { + return DyeType.SPOOK; + } + if (Constants.GLITCHED.contains(hex)) { + return DyeType.GLITCHED; + } + return DyeType.EXOTIC; + } + + public static boolean intendedDyed(NbtCompound ItemData) { + return ItemData.getCompound("ExtraAttributes").contains("dye_item"); + } + + public enum DyeType implements StringIdentifiable { + CRYSTAL("crystal", Formatting.AQUA), + FAIRY("fairy", Formatting.LIGHT_PURPLE), + OG_FAIRY("og_fairy", Formatting.DARK_PURPLE), + SPOOK("spook", Formatting.RED), + GLITCHED("glitched", Formatting.BLUE), + EXOTIC("exotic", Formatting.GOLD); + private final String name; + private final Formatting formatting; + DyeType(String name, Formatting formatting) { + this.name = name; + this.formatting = formatting; + } + + @Override + public String asString() { + return name; + } + + public MutableText getTranslatedText() { + return Text.translatable("skyblocker.exotic." + name).formatted(formatting); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java new file mode 100644 index 00000000..3c5e0b33 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java @@ -0,0 +1,485 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.ItemUtils; +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.nbt.NbtElement; +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.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +public class ItemTooltip { + protected static final Logger LOGGER = LoggerFactory.getLogger(ItemTooltip.class.getName()); + private static final MinecraftClient client = MinecraftClient.getInstance(); + private static final SkyblockerConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip; + private static final Map<InfoType, String> API_ADDRESSES = Map.of( + InfoType.NPC, "https://hysky.de/api/npcprice", + InfoType.BAZAAR, "https://hysky.de/api/bazaar", + InfoType.LOWEST_BINS, "https://hysky.de/api/auctions/lowestbins", + InfoType.ONE_DAY_AVERAGE, "https://moulberry.codes/auction_averages_lbin/1day.json", + InfoType.THREE_DAY_AVERAGE, "https://moulberry.codes/auction_averages_lbin/3day.json", + InfoType.MOTES, "https://hysky.de/api/motesprice", + InfoType.MUSEUM, "https://hysky.de/api/museum", + InfoType.COLOR, "https://hysky.de/api/color" + ); + private static volatile boolean sentNullWarning = false; + + public static void getTooltip(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; + } + + if (lines.isEmpty()) { + return; + } + + int count = stack.getCount(); + boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:")); + + if (InfoType.NPC.isEnabled(config)) { + if (InfoType.NPC.hasOrNullWarning(internalID)) { + lines.add(Text.literal(String.format("%-21s", "NPC Price:")) + .formatted(Formatting.YELLOW) + .append(getCoinsMessage(InfoType.NPC.data.get(internalID).getAsDouble(), count))); + } + } + + boolean bazaarExist = false; + + if (InfoType.BAZAAR.isEnabled(config) && !bazaarOpened) { + if (InfoType.BAZAAR.hasOrNullWarning(name)) { + JsonObject getItem = InfoType.BAZAAR.data.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 (InfoType.LOWEST_BINS.isEnabled(config) && !bazaarOpened && !bazaarExist) { + if (InfoType.LOWEST_BINS.hasOrNullWarning(name)) { + lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:")) + .formatted(Formatting.GOLD) + .append(getCoinsMessage(InfoType.LOWEST_BINS.data.get(name).getAsDouble(), count))); + lbinExist = true; + } + } + + if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) { + if (InfoType.ONE_DAY_AVERAGE.data == null || InfoType.THREE_DAY_AVERAGE.data == 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 = config.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(InfoType.ONE_DAY_AVERAGE.data.get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(InfoType.ONE_DAY_AVERAGE.data.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(InfoType.THREE_DAY_AVERAGE.data.get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : getCoinsMessage(InfoType.THREE_DAY_AVERAGE.data.get(neuName).getAsDouble(), count) + ) + ); + } + } + } + } + + if (InfoType.MOTES.isEnabled(config) && Utils.isInTheRift()) { + if (InfoType.MOTES.hasOrNullWarning(internalID)) { + lines.add(Text.literal(String.format("%-20s", "Motes Price:")) + .formatted(Formatting.LIGHT_PURPLE) + .append(getMotesMessage(InfoType.MOTES.data.get(internalID).getAsInt(), count))); + } + } + + if (InfoType.MUSEUM.isEnabled(config) && !bazaarOpened) { + String timestamp = getTimestamp(stack); + + if (InfoType.MUSEUM.hasOrNullWarning(internalID)) { + String itemCategory = InfoType.MUSEUM.data.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))); + } + } + + if (InfoType.COLOR.isEnabled(config)) { + if (InfoType.COLOR.data == null) { + nullWarning(); + } else if (stack.getNbt() != null) { + final NbtElement color = stack.getNbt().getCompound("display").get("color"); + + if (color != null) { + String colorHex = String.format("%06X", Integer.parseInt(color.asString())); + String expectedHex = ExoticTooltip.getExpectedHex(internalID); + + boolean correctLine = false; + for (Text text : lines) { + String existingTooltip = text.getString() + " "; + if (existingTooltip.startsWith("Color: ")) { + correctLine = true; + + addExoticTooltip(lines, internalID, stack.getNbt(), colorHex, expectedHex, existingTooltip); + break; + } + } + + if (!correctLine) { + addExoticTooltip(lines, internalID, stack.getNbt(), colorHex, expectedHex, ""); + } + } + } + } + } + + private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound nbt, String colorHex, String expectedHex, String existingTooltip) { + if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !ExoticTooltip.isException(internalID, colorHex) && !ExoticTooltip.intendedDyed(nbt)) { + final ExoticTooltip.DyeType type = ExoticTooltip.checkDyeType(colorHex); + lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")")); + } + } + + private static void nullWarning() { + if (!sentNullWarning && client.player != null) { + client.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.itemTooltip.nullMessage")), false); + sentNullWarning = true; + } + } + + /** + * 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 ea = ItemUtils.getExtraAttributes(stack); + + if (ea != null && 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 ea = ItemUtils.getExtraAttributes(stack); + + if (ea == null || !ea.contains(ItemUtils.ID, 8)) { + return null; + } + String internalName = ea.getString(ItemUtils.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 = SkyblockerMod.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++) { + sentNullWarning = false; + return; + } + + List<CompletableFuture<Void>> futureList = new ArrayList<>(); + + if (InfoType.NPC.isEnabled(config) && InfoType.NPC.data == null) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.NPC))); + + if (InfoType.BAZAAR.isEnabled(config) || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.BAZAAR))); + + if (InfoType.LOWEST_BINS.isEnabled(config) || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.LOWEST_BINS))); + + if (config.enableAvgBIN) { + SkyblockerConfig.Average type = config.avg; + + if (type == SkyblockerConfig.Average.BOTH || InfoType.ONE_DAY_AVERAGE.data == null || InfoType.THREE_DAY_AVERAGE.data == null || minute % 5 == 0) { + futureList.add(CompletableFuture.runAsync(() -> { + downloadPrices(InfoType.ONE_DAY_AVERAGE); + downloadPrices(InfoType.THREE_DAY_AVERAGE); + })); + } else if (type == SkyblockerConfig.Average.ONE_DAY) { + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.ONE_DAY_AVERAGE))); + } else if (type == SkyblockerConfig.Average.THREE_DAY) { + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.THREE_DAY_AVERAGE))); + } + } + + if (InfoType.MOTES.isEnabled(config) && InfoType.MOTES.data == null) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.MOTES))); + + if (InfoType.MUSEUM.isEnabled(config) && InfoType.MUSEUM.data == null) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.MUSEUM))); + + if (InfoType.COLOR.isEnabled(config) && InfoType.COLOR.data == null) + futureList.add(CompletableFuture.runAsync(() -> downloadPrices(InfoType.COLOR))); + + minute++; + CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)) + .whenComplete((unused, throwable) -> sentNullWarning = false); + }, 1200, true); + } + + private static void downloadPrices(InfoType type) { + try { + String url = API_ADDRESSES.get(type); + + if (type.cacheable) { + HttpHeaders headers = Http.sendHeadRequest(url); + long combinedHash = Http.getEtag(headers).hashCode() + Http.getLastModified(headers).hashCode(); + + switch (type) { + case NPC, MOTES, MUSEUM, COLOR: + if (type.hash == combinedHash) return; + else type.hash = combinedHash; + } + } + + type.setData(SkyblockerMod.GSON.fromJson(Http.sendGetRequest(url), JsonObject.class)); + } catch (Exception e) { + LOGGER.warn("[Skyblocker] Failed to download " + type + " prices!", e); + } + } + + public static JsonObject getBazaarPrices() { + return InfoType.BAZAAR.data; + } + + public static JsonObject getLBINPrices() { + return InfoType.LOWEST_BINS.data; + } + + public enum InfoType { + NPC(itemTooltip -> itemTooltip.enableNPCPrice, true), + BAZAAR(itemTooltip -> itemTooltip.enableBazaarPrice, false), + LOWEST_BINS(itemTooltip -> itemTooltip.enableLowestBIN, false), + ONE_DAY_AVERAGE(itemTooltip -> itemTooltip.enableAvgBIN, false), + THREE_DAY_AVERAGE(itemTooltip -> itemTooltip.enableAvgBIN, false), + MOTES(itemTooltip -> itemTooltip.enableMotesPrice, true), + MUSEUM(itemTooltip -> itemTooltip.enableMuseumDate, true), + COLOR(itemTooltip -> itemTooltip.enableExoticCheck, true); + + private final Predicate<SkyblockerConfig.ItemTooltip> enabledPredicate; + private JsonObject data; + private final boolean cacheable; + private long hash; + + InfoType(Predicate<SkyblockerConfig.ItemTooltip> enabledPredicate, boolean cacheable) { + this(enabledPredicate, null, false); + } + + InfoType(Predicate<SkyblockerConfig.ItemTooltip> enabledPredicate, JsonObject data, boolean cacheable) { + this.enabledPredicate = enabledPredicate; + this.data = data; + this.cacheable = cacheable; + } + + public boolean isEnabled(SkyblockerConfig.ItemTooltip config) { + return enabledPredicate.test(config); + } + + public boolean hasOrNullWarning(String memberName) { + if (data == null) { + nullWarning(); + return false; + } else return data.has(memberName); + } + + public void setData(JsonObject data) { + this.data = data; + } + } +}
\ No newline at end of file |