aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip
diff options
context:
space:
mode:
authorKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-10-29 01:12:49 -0400
committerKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-10-30 00:25:40 -0400
commit14074ff48e377d26502047ca07b6e2b14bd2ca2d (patch)
treefd804463de81439657ff4dc24d221732c0993d36 /src/main/java/de/hysky/skyblocker/skyblock/item/tooltip
parent3a68bf3d435e1288a050f3db18c9fc61b7e23172 (diff)
downloadSkyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.tar.gz
Skyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.tar.bz2
Skyblocker-14074ff48e377d26502047ca07b6e2b14bd2ca2d.zip
Update ItemTooltip
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/item/tooltip')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java204
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java90
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java56
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java98
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java485
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