aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/item
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
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')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java235
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java92
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java82
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java154
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java74
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java115
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java75
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java109
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java443
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java56
13 files changed, 1577 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java b/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
new file mode 100644
index 00000000..ed650e26
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+
+public class AttributeShards {
+ private static final Object2ObjectOpenHashMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
+
+ static {
+ //Weapons
+ ID_2_SHORT_NAME.put("arachno", "A");
+ ID_2_SHORT_NAME.put("attack_speed", "AS");
+ ID_2_SHORT_NAME.put("blazing", "BL");
+ ID_2_SHORT_NAME.put("combo", "C");
+ ID_2_SHORT_NAME.put("elite", "E");
+ ID_2_SHORT_NAME.put("ender", "EN");
+ ID_2_SHORT_NAME.put("ignition", "I");
+ ID_2_SHORT_NAME.put("life_recovery", "LR");
+ ID_2_SHORT_NAME.put("mana_steal", "MS");
+ ID_2_SHORT_NAME.put("midas_touch", "MT");
+ ID_2_SHORT_NAME.put("undead", "U");
+
+ //Swords & Bows
+ ID_2_SHORT_NAME.put("warrior", "W");
+ ID_2_SHORT_NAME.put("deadeye", "DE");
+
+ //Armor or Equipment
+ ID_2_SHORT_NAME.put("arachno_resistance", "AR");
+ ID_2_SHORT_NAME.put("blazing_resistance", "BR");
+ ID_2_SHORT_NAME.put("breeze", "B");
+ ID_2_SHORT_NAME.put("dominance", "D");
+ ID_2_SHORT_NAME.put("ender_resistance", "ER");
+ ID_2_SHORT_NAME.put("experience", "XP");
+ ID_2_SHORT_NAME.put("fortitude", "F");
+ ID_2_SHORT_NAME.put("life_regeneration", "HR"); //Health regeneration
+ ID_2_SHORT_NAME.put("lifeline", "L");
+ ID_2_SHORT_NAME.put("magic_find", "MF");
+ ID_2_SHORT_NAME.put("mana_pool", "MP");
+ ID_2_SHORT_NAME.put("mana_regeneration", "MR");
+ ID_2_SHORT_NAME.put("mending", "V"); //Vitality
+ ID_2_SHORT_NAME.put("speed", "S");
+ ID_2_SHORT_NAME.put("undead_resistance", "UR");
+ ID_2_SHORT_NAME.put("veteran", "V");
+
+ //Fishing Gear
+ ID_2_SHORT_NAME.put("blazing_fortune", "BF");
+ ID_2_SHORT_NAME.put("fishing_experience", "FE");
+ ID_2_SHORT_NAME.put("infection", "IF");
+ ID_2_SHORT_NAME.put("double_hook", "DH");
+ ID_2_SHORT_NAME.put("fisherman", "FM");
+ ID_2_SHORT_NAME.put("fishing_speed", "FS");
+ ID_2_SHORT_NAME.put("hunter", "H");
+ ID_2_SHORT_NAME.put("trophy_hunter", "TH");
+
+ }
+
+ public static String getShortName(String id) {
+ return ID_2_SHORT_NAME.getOrDefault(id, "");
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java
new file mode 100644
index 00000000..122ffe9b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java
@@ -0,0 +1,235 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.fabricmc.loader.api.FabricLoader;
+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.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.util.Identifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+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(SkyblockerMod.NAMESPACE, "textures/gui/inventory_background.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 Inventory[] storage = new Inventory[STORAGE_SIZE];
+ private static final boolean[] dirty = new boolean[STORAGE_SIZE];
+
+ private static String loaded = ""; // uuid + sb profile currently loaded
+ private static Path save_dir = null;
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ if (screen instanceof HandledScreen<?> handledScreen) {
+ updateStorage(handledScreen);
+ }
+ });
+ }
+
+ public static void tick() {
+ Utils.update(); // force update isOnSkyblock to prevent crash on disconnect
+ if (Utils.isOnSkyblock()) {
+ // save all dirty storages
+ saveStorage();
+ // update save dir based on uuid and sb profile
+ String uuid = MinecraftClient.getInstance().getSession().getUuidOrNull().toString().replaceAll("-", "");
+ String profile = Utils.getProfile();
+ if (profile != null && !profile.isEmpty()) {
+ save_dir = FabricLoader.getInstance().getConfigDir().resolve("skyblocker/backpack-preview/" + uuid + "/" + profile);
+ save_dir.toFile().mkdirs();
+ if (loaded.equals(uuid + "/" + profile)) {
+ // mark currently opened storage as dirty
+ if (MinecraftClient.getInstance().currentScreen != null) {
+ String title = MinecraftClient.getInstance().currentScreen.getTitle().getString();
+ int index = getStorageIndexFromTitle(title);
+ if (index != -1) dirty[index] = true;
+ }
+ } else {
+ // load storage again because uuid/profile changed
+ loaded = uuid + "/" + profile;
+ loadStorage();
+ }
+ }
+ }
+ }
+
+ public static void loadStorage() {
+ assert (save_dir != null);
+ for (int index = 0; index < STORAGE_SIZE; ++index) {
+ storage[index] = null;
+ dirty[index] = false;
+ File file = save_dir.resolve(index + ".nbt").toFile();
+ if (file.isFile()) {
+ try {
+ NbtCompound root = NbtIo.read(file);
+ storage[index] = new DummyInventory(root);
+ } catch (Exception e) {
+ LOGGER.error("Failed to load backpack preview file: " + file.getName(), e);
+ }
+ }
+ }
+ }
+
+ private static void saveStorage() {
+ assert (save_dir != null);
+ for (int index = 0; index < STORAGE_SIZE; ++index) {
+ if (dirty[index]) {
+ if (storage[index] != null) {
+ try {
+ NbtCompound root = new NbtCompound();
+ NbtList list = new NbtList();
+ for (int i = 9; i < storage[index].size(); ++i) {
+ ItemStack stack = storage[index].getStack(i);
+ NbtCompound item = new NbtCompound();
+ if (stack.isEmpty()) {
+ item.put("id", NbtString.of("minecraft:air"));
+ item.put("Count", NbtInt.of(1));
+ } else {
+ item.put("id", NbtString.of(stack.getItem().toString()));
+ item.put("Count", NbtInt.of(stack.getCount()));
+ item.put("tag", stack.getNbt());
+ }
+ list.add(item);
+ }
+ root.put("list", list);
+ root.put("size", NbtInt.of(storage[index].size() - 9));
+ NbtIo.write(root, save_dir.resolve(index + ".nbt").toFile());
+ dirty[index] = false;
+ } catch (Exception e) {
+ LOGGER.error("Failed to save backpack preview file: " + index + ".nbt", e);
+ }
+ }
+ }
+ }
+ }
+
+ public static void updateStorage(HandledScreen<?> screen) {
+ String title = screen.getTitle().getString();
+ int index = getStorageIndexFromTitle(title);
+ if (index != -1) {
+ storage[index] = screen.getScreenHandler().slots.get(0).inventory;
+ dirty[index] = true;
+ }
+ }
+
+ public static boolean renderPreview(DrawContext context, 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 (storage[index] == null) return false;
+ int rows = (storage[index].size() - 9) / 9;
+
+ Screen screen = MinecraftClient.getInstance().currentScreen;
+ if (screen == null) return false;
+ int x = mouseX + 184 >= screen.width ? mouseX - 188 : mouseX + 8;
+ int y = Math.max(0, mouseY - 16);
+
+ RenderSystem.disableDepthTest();
+ RenderSystem.setShaderTexture(0, TEXTURE);
+ context.drawTexture(TEXTURE, x, y, 0, 0, 176, 7);
+ for (int i = 0; i < rows; ++i) {
+ context.drawTexture(TEXTURE, x, y + i * 18 + 7, 0, 7, 176, 18);
+ }
+ context.drawTexture(TEXTURE, x, y + rows * 18 + 7, 0, 25, 176, 7);
+ RenderSystem.enableDepthTest();
+
+ MatrixStack matrices = context.getMatrices();
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ for (int i = 9; i < storage[index].size(); ++i) {
+ int itemX = x + (i - 9) % 9 * 18 + 8;
+ int itemY = y + (i - 9) / 9 * 18 + 8;
+ matrices.push();
+ matrices.translate(0, 0, 200);
+ context.drawItem(storage[index].getStack(i), itemX, itemY);
+ context.drawItemInSlot(textRenderer, storage[index].getStack(i), 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;
+ }
+}
+
+class DummyInventory implements Inventory {
+ private final List<ItemStack> stacks;
+
+ public DummyInventory(NbtCompound root) {
+ stacks = new ArrayList<>(root.getInt("size") + 9);
+ for (int i = 0; i < 9; ++i) stacks.add(ItemStack.EMPTY);
+ root.getList("list", NbtCompound.COMPOUND_TYPE).forEach(item ->
+ stacks.add(ItemStack.fromNbt((NbtCompound) item))
+ );
+ }
+
+ @Override
+ public int size() {
+ return stacks.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public ItemStack getStack(int slot) {
+ return stacks.get(slot);
+ }
+
+ @Override
+ public ItemStack removeStack(int slot, int amount) {
+ return null;
+ }
+
+ @Override
+ public ItemStack removeStack(int slot) {
+ return null;
+ }
+
+ @Override
+ public void setStack(int slot, ItemStack stack) {
+ stacks.set(slot, stack);
+ }
+
+ @Override
+ public void markDirty() {
+ }
+
+ @Override
+ public boolean canPlayerUse(PlayerEntity player) {
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java
new file mode 100644
index 00000000..2a6551c7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java
@@ -0,0 +1,92 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.mixin.accessor.DrawContextInvoker;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.ints.IntObjectPair;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+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 nbt = stack.getNbt();
+ if (nbt == null || !nbt.contains("ExtraAttributes", 10)) {
+ return false;
+ }
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ // 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)), ItemRegistry.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/CompactorPreviewTooltipComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java
new file mode 100644
index 00000000..f3634548
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.skyblock.item;
+
+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) {
+ 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/CustomArmorDyeColors.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
new file mode 100644
index 00000000..76042a81
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
@@ -0,0 +1,82 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.item.DyeableItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+
+public class CustomArmorDyeColors {
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(CustomArmorDyeColors::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("dyeColor")
+ .executes(context -> customizeDyeColor(context.getSource(), null))
+ .then(ClientCommandManager.argument("hexCode", StringArgumentType.string())
+ .executes(context -> customizeDyeColor(context.getSource(), StringArgumentType.getString(context, "hexCode")))))));
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int customizeDyeColor(FabricClientCommandSource source, String hex) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (hex != null && !isHexadecimalColor(hex)) {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.invalidHex"));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ if (Utils.isOnSkyblock() && heldItem != null) {
+ if (heldItem.getItem() instanceof DyeableItem) {
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2IntOpenHashMap<String> customDyeColors = SkyblockerConfigManager.get().general.customDyeColors;
+
+ if (hex == null) {
+ if (customDyeColors.containsKey(itemUuid)) {
+ customDyeColors.removeInt(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.neverHad"));
+ }
+ } else {
+ customDyeColors.put(itemUuid, Integer.decode("0x" + hex.replace("#", "")).intValue());
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.noItemUuid"));
+ }
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.notDyeable"));
+ return Command.SINGLE_SUCCESS;
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.unableToSetColor"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static boolean isHexadecimalColor(String s) {
+ return s.replace("#", "").chars().allMatch(c -> "0123456789ABCDEFabcdef".indexOf(c) >= 0) && s.replace("#", "").length() == 6;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
new file mode 100644
index 00000000..b8fa0797
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
@@ -0,0 +1,154 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Utils;
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
+import it.unimi.dsi.fastutil.Pair;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.command.CommandSource;
+import net.minecraft.command.argument.IdentifierArgumentType;
+import net.minecraft.item.ArmorItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.trim.ArmorTrim;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.registry.*;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+public class CustomArmorTrims {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CustomArmorTrims.class);
+ public static final Object2ObjectOpenHashMap<ArmorTrimId, Optional<ArmorTrim>> TRIMS_CACHE = new Object2ObjectOpenHashMap<>();
+ private static boolean trimsInitialized = false;
+
+ public static void init() {
+ SkyblockEvents.JOIN.register(CustomArmorTrims::initializeTrimCache);
+ ClientCommandRegistrationCallback.EVENT.register(CustomArmorTrims::registerCommand);
+ }
+
+ private static void initializeTrimCache() {
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (trimsInitialized || player == null) {
+ return;
+ }
+ try {
+ TRIMS_CACHE.clear();
+ DynamicRegistryManager registryManager = player.networkHandler.getRegistryManager();
+ for (Identifier material : registryManager.get(RegistryKeys.TRIM_MATERIAL).getIds()) {
+ for (Identifier pattern : registryManager.get(RegistryKeys.TRIM_PATTERN).getIds()) {
+ NbtCompound compound = new NbtCompound();
+ compound.putString("material", material.toString());
+ compound.putString("pattern", pattern.toString());
+
+ ArmorTrim trim = ArmorTrim.CODEC.parse(RegistryOps.of(NbtOps.INSTANCE, registryManager), compound).resultOrPartial(LOGGER::error).orElse(null);
+
+ // Something went terribly wrong
+ if (trim == null) throw new IllegalStateException("Trim shouldn't be null! [" + "\"" + material + "\",\"" + pattern + "\"]");
+
+ TRIMS_CACHE.put(new ArmorTrimId(material, pattern), Optional.of(trim));
+ }
+ }
+
+ LOGGER.info("[Skyblocker] Successfully cached all armor trims!");
+ trimsInitialized = true;
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an exception while caching armor trims", e);
+ }
+ }
+
+ private static void registerCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("armorTrim")
+ .executes(context -> customizeTrim(context.getSource(), null, null))
+ .then(ClientCommandManager.argument("material", IdentifierArgumentType.identifier())
+ .suggests(getIdSuggestionProvider(RegistryKeys.TRIM_MATERIAL))
+ .executes(context -> customizeTrim(context.getSource(), context.getArgument("material", Identifier.class), null))
+ .then(ClientCommandManager.argument("pattern", IdentifierArgumentType.identifier())
+ .suggests(getIdSuggestionProvider(RegistryKeys.TRIM_PATTERN))
+ .executes(context -> customizeTrim(context.getSource(), context.getArgument("material", Identifier.class), context.getArgument("pattern", Identifier.class))))))));
+ }
+
+ @NotNull
+ private static SuggestionProvider<FabricClientCommandSource> getIdSuggestionProvider(RegistryKey<? extends Registry<?>> registryKey) {
+ return (context, builder) -> context.getSource().listIdSuggestions(registryKey, CommandSource.SuggestedIdType.ELEMENTS, builder, context);
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int customizeTrim(FabricClientCommandSource source, Identifier material, Identifier pattern) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && heldItem != null) {
+ if (heldItem.getItem() instanceof ArmorItem) {
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2ObjectOpenHashMap<String, ArmorTrimId> customArmorTrims = SkyblockerConfigManager.get().general.customArmorTrims;
+
+ if (material == null && pattern == null) {
+ if (customArmorTrims.containsKey(itemUuid)) {
+ customArmorTrims.remove(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.neverHad"));
+ }
+ } else {
+ // Ensure that the material & trim are valid
+ ArmorTrimId trimId = new ArmorTrimId(material, pattern);
+ if (TRIMS_CACHE.get(trimId) == null) {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.invalidMaterialOrPattern"));
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ customArmorTrims.put(itemUuid, trimId);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.noItemUuid"));
+ }
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.notAnArmorPiece"));
+ return Command.SINGLE_SUCCESS;
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.unableToSetTrim"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ public record ArmorTrimId(@SerialEntry Identifier material, @SerialEntry Identifier pattern) implements Pair<Identifier, Identifier> {
+ @Override
+ public Identifier left() {
+ return material();
+ }
+
+ @Override
+ public Identifier right() {
+ return pattern();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java
new file mode 100644
index 00000000..5fbff253
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java
@@ -0,0 +1,74 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.command.argument.TextArgumentType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+
+public class CustomItemNames {
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(CustomItemNames::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("renameItem")
+ .executes(context -> renameItem(context.getSource(), null))
+ .then(ClientCommandManager.argument("textComponent", TextArgumentType.text())
+ .executes(context -> renameItem(context.getSource(), context.getArgument("textComponent", Text.class)))))));
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int renameItem(FabricClientCommandSource source, Text text) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2ObjectOpenHashMap<String, Text> customItemNames = SkyblockerConfigManager.get().general.customItemNames;
+
+ if (text == null) {
+ if (customItemNames.containsKey(itemUuid)) {
+ //Remove custom item name when the text argument isn't passed
+ customItemNames.remove(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.neverHad"));
+ }
+ } else {
+ //If the text is provided then set the item's custom name to it
+
+ //Set italic to false if it hasn't been changed (or was already false)
+ Style currentStyle = text.getStyle();
+ ((MutableText) text).setStyle(currentStyle.withItalic((currentStyle.isItalic() ? true : false)));
+
+ customItemNames.put(itemUuid, text);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customItemNames.noItemUuid"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customItemNames.unableToSetName"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
new file mode 100644
index 00000000..9c1fa6ad
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
@@ -0,0 +1,115 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.google.common.collect.ImmutableList;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.ClientPlayerBlockBreakEvent;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.fabricmc.fabric.api.event.player.UseItemCallback;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.registry.tag.BlockTags;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ItemCooldowns {
+ private static final String JUNGLE_AXE_ID = "JUNGLE_AXE";
+ private static final String TREECAPITATOR_ID = "TREECAPITATOR_AXE";
+ private static final String GRAPPLING_HOOK_ID = "GRAPPLING_HOOK";
+ private static final ImmutableList<String> BAT_ARMOR_IDS = ImmutableList.of("BAT_PERSON_HELMET", "BAT_PERSON_CHESTPLATE", "BAT_PERSON_LEGGINGS", "BAT_PERSON_BOOTS");
+
+ private static final Map<String, CooldownEntry> ITEM_COOLDOWNS = new HashMap<>();
+
+ public static void init() {
+ ClientPlayerBlockBreakEvent.AFTER.register(ItemCooldowns::afterBlockBreak);
+ UseItemCallback.EVENT.register(ItemCooldowns::onItemInteract);
+ }
+
+ public static void afterBlockBreak(World world, PlayerEntity player, BlockPos pos, BlockState state) {
+ if (!SkyblockerConfigManager.get().general.itemCooldown.enableItemCooldowns) return;
+
+ String usedItemId = ItemUtils.getItemId(player.getMainHandStack());
+ if (usedItemId == null) return;
+
+ if (state.isIn(BlockTags.LOGS)) {
+ if (usedItemId.equals(JUNGLE_AXE_ID)) {
+ if (!isOnCooldown(JUNGLE_AXE_ID)) {
+ ITEM_COOLDOWNS.put(JUNGLE_AXE_ID, new CooldownEntry(2000));
+ }
+ } else if (usedItemId.equals(TREECAPITATOR_ID)) {
+ if (!isOnCooldown(TREECAPITATOR_ID)) {
+ ITEM_COOLDOWNS.put(TREECAPITATOR_ID, new CooldownEntry(2000));
+ }
+ }
+ }
+ }
+
+ private static TypedActionResult<ItemStack> onItemInteract(PlayerEntity player, World world, Hand hand) {
+ if (!SkyblockerConfigManager.get().general.itemCooldown.enableItemCooldowns) return TypedActionResult.pass(ItemStack.EMPTY);
+
+ String usedItemId = ItemUtils.getItemId(player.getMainHandStack());
+ if (usedItemId != null && usedItemId.equals(GRAPPLING_HOOK_ID) && player.fishHook != null) {
+ if (!isOnCooldown(GRAPPLING_HOOK_ID) && !isWearingBatArmor(player)) {
+ ITEM_COOLDOWNS.put(GRAPPLING_HOOK_ID, new CooldownEntry(2000));
+ }
+ }
+
+ return TypedActionResult.pass(ItemStack.EMPTY);
+ }
+
+ public static boolean isOnCooldown(ItemStack itemStack) {
+ return isOnCooldown(ItemUtils.getItemId(itemStack));
+ }
+
+ private static boolean isOnCooldown(String itemId) {
+ if (ITEM_COOLDOWNS.containsKey(itemId)) {
+ CooldownEntry cooldownEntry = ITEM_COOLDOWNS.get(itemId);
+ if (cooldownEntry.isOnCooldown()) {
+ return true;
+ } else {
+ ITEM_COOLDOWNS.remove(itemId);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ public static CooldownEntry getItemCooldownEntry(ItemStack itemStack) {
+ return ITEM_COOLDOWNS.get(ItemUtils.getItemId(itemStack));
+ }
+
+ private static boolean isWearingBatArmor(PlayerEntity player) {
+ for (ItemStack stack : player.getArmorItems()) {
+ String itemId = ItemUtils.getItemId(stack);
+ if (!BAT_ARMOR_IDS.contains(itemId)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public record CooldownEntry(int cooldown, long startTime) {
+ public CooldownEntry(int cooldown) {
+ this(cooldown, System.currentTimeMillis());
+ }
+
+ public boolean isOnCooldown() {
+ return (this.startTime + this.cooldown) > System.currentTimeMillis();
+ }
+
+ public long getRemainingCooldown() {
+ long time = (this.startTime + this.cooldown) - System.currentTimeMillis();
+ return Math.max(time, 0);
+ }
+
+ public float getRemainingCooldownPercent() {
+ return this.isOnCooldown() ? (float) this.getRemainingCooldown() / cooldown : 0.0f;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java
new file mode 100644
index 00000000..d73e1545
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+
+public class ItemProtection {
+
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(ItemProtection::registerCommand);
+ }
+
+ public static boolean isItemProtected(ItemStack stack) {
+ if (stack == null || stack.isEmpty()) return false;
+
+ NbtCompound nbt = stack.getNbt();
+
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : "";
+
+ return SkyblockerConfigManager.get().general.protectedItems.contains(itemUuid);
+ }
+
+ return false;
+ }
+
+ private static void registerCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("protectItem")
+ .executes(context -> protectMyItem(context.getSource()))));
+ }
+
+ private static int protectMyItem(FabricClientCommandSource source) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ ObjectOpenHashSet<String> protectedItems = SkyblockerConfigManager.get().general.protectedItems;
+
+ if (!protectedItems.contains(itemUuid)) {
+ protectedItems.add(itemUuid);
+ SkyblockerConfigManager.save();
+
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.added", heldItem.getName()));
+ } else {
+ protectedItems.remove(itemUuid);
+ SkyblockerConfigManager.save();
+
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.removed", heldItem.getName()));
+ }
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.noItemUuid"));
+ }
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.unableToProtect"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java
new file mode 100644
index 00000000..0af64bd9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java
@@ -0,0 +1,109 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.google.common.collect.ImmutableMap;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class ItemRarityBackgrounds {
+ private static final Identifier RARITY_BG_TEX = new Identifier(SkyblockerMod.NAMESPACE, "item_rarity_background");
+ private static final Supplier<Sprite> SPRITE = () -> MinecraftClient.getInstance().getGuiAtlasManager().getSprite(RARITY_BG_TEX);
+ private static final ImmutableMap<String, SkyblockItemRarity> LORE_RARITIES = ImmutableMap.ofEntries(
+ Map.entry("ADMIN", SkyblockItemRarity.ADMIN),
+ Map.entry("SPECIAL", SkyblockItemRarity.SPECIAL), //Very special is the same color so this will cover it
+ Map.entry("DIVINE", SkyblockItemRarity.DIVINE),
+ Map.entry("MYTHIC", SkyblockItemRarity.MYTHIC),
+ Map.entry("LEGENDARY", SkyblockItemRarity.LEGENDARY),
+ Map.entry("LEGENJERRY", SkyblockItemRarity.LEGENDARY),
+ Map.entry("EPIC", SkyblockItemRarity.EPIC),
+ Map.entry("RARE", SkyblockItemRarity.RARE),
+ Map.entry("UNCOMMON", SkyblockItemRarity.UNCOMMON),
+ Map.entry("COMMON", SkyblockItemRarity.COMMON)
+ );
+ private static final Int2ReferenceOpenHashMap<SkyblockItemRarity> CACHE = new Int2ReferenceOpenHashMap<>();
+
+ public static void init() {
+ //Clear the cache every 5 minutes, ints are very compact!
+ Scheduler.INSTANCE.scheduleCyclic(CACHE::clear, 4800);
+
+ //Clear cache after a screen where items can be upgraded in rarity closes
+ ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ String title = screen.getTitle().getString();
+
+ if (Utils.isOnSkyblock() && (title.equals("The Hex") || title.equals("Craft Item") || title.equals("Anvil") || title.equals("Reforge Anvil"))) {
+ ScreenEvents.remove(screen).register(screen1 -> CACHE.clear());
+ }
+ });
+ }
+
+ public static void tryDraw(ItemStack stack, DrawContext context, int x, int y) {
+ MinecraftClient client = MinecraftClient.getInstance();
+
+ if (client.player != null) {
+ SkyblockItemRarity itemRarity = getItemRarity(stack, client.player);
+
+ if (itemRarity != null) draw(context, x, y, itemRarity);
+ }
+ }
+
+ private static SkyblockItemRarity getItemRarity(ItemStack stack, ClientPlayerEntity player) {
+ if (stack == null || stack.isEmpty()) return null;
+
+ int hashCode = 0;
+ NbtCompound nbt = stack.getNbt();
+
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.getString("uuid");
+
+ //If the item has an uuid, then use the hash code of the uuid otherwise use the identity hash code of the stack
+ hashCode = itemUuid.isEmpty() ? System.identityHashCode(stack) : itemUuid.hashCode();
+ }
+
+ if (CACHE.containsKey(hashCode)) return CACHE.get(hashCode);
+
+ List<Text> tooltip = stack.getTooltip(player, TooltipContext.BASIC);
+ String[] stringifiedTooltip = tooltip.stream().map(Text::getString).toArray(String[]::new);
+
+ for (String rarityString : LORE_RARITIES.keySet()) {
+ if (Arrays.stream(stringifiedTooltip).anyMatch(line -> line.contains(rarityString))) {
+ SkyblockItemRarity rarity = LORE_RARITIES.get(rarityString);
+
+ CACHE.put(hashCode, rarity);
+ return rarity;
+ }
+ }
+
+ CACHE.put(hashCode, null);
+ return null;
+ }
+
+ private static void draw(DrawContext context, int x, int y, SkyblockItemRarity rarity) {
+ //Enable blending to handle HUD translucency
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+
+ context.drawSprite(x, y, 0, 16, 16, SPRITE.get(), rarity.r, rarity.g, rarity.b, SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgroundsOpacity);
+
+ RenderSystem.disableBlend();
+ }
+}
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
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java
new file mode 100644
index 00000000..08cc5377
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import net.minecraft.util.Formatting;
+
+public enum SkyblockItemRarity {
+ ADMIN(Formatting.DARK_RED),
+ VERY_SPECIAL(Formatting.RED),
+ SPECIAL(Formatting.RED),
+ DIVINE(Formatting.AQUA),
+ MYTHIC(Formatting.LIGHT_PURPLE),
+ LEGENDARY(Formatting.GOLD),
+ EPIC(Formatting.DARK_PURPLE),
+ RARE(Formatting.BLUE),
+ UNCOMMON(Formatting.GREEN),
+ COMMON(Formatting.WHITE);
+
+ public final float r;
+ public final float g;
+ public final float b;
+
+ SkyblockItemRarity(Formatting formatting) {
+ @SuppressWarnings("DataFlowIssue")
+ int rgb = formatting.getColorValue();
+
+ this.r = ((rgb >> 16) & 0xFF) / 255f;
+ this.g = ((rgb >> 8) & 0xFF) / 255f;
+ this.b = (rgb & 0xFF) / 255f;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java b/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java
new file mode 100644
index 00000000..3ab478d0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java
@@ -0,0 +1,56 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.option.KeyBinding;
+import net.minecraft.client.util.InputUtil;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Util;
+import org.lwjgl.glfw.GLFW;
+
+import java.util.concurrent.CompletableFuture;
+
+public class WikiLookup {
+ public static KeyBinding wikiLookup;
+ static final MinecraftClient client = MinecraftClient.getInstance();
+ static String id;
+
+ public static void init() {
+ wikiLookup = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ "key.wikiLookup",
+ InputUtil.Type.KEYSYM,
+ GLFW.GLFW_KEY_F4,
+ "key.categories.skyblocker"
+ ));
+ }
+
+ public static String getSkyblockId(Slot slot) {
+ //Grabbing the skyblock NBT data
+ ItemStack selectedStack = slot.getStack();
+ NbtCompound nbt = selectedStack.getSubNbt("ExtraAttributes");
+ if (nbt != null) {
+ id = nbt.getString("id");
+ }
+ return id;
+ }
+
+ public static void openWiki(Slot slot) {
+ if (Utils.isOnSkyblock()) {
+ id = getSkyblockId(slot);
+ try {
+ String wikiLink = ItemRegistry.getWikiLink(id);
+ CompletableFuture.runAsync(() -> Util.getOperatingSystem().open(wikiLink));
+ } catch (IndexOutOfBoundsException | IllegalStateException e) {
+ e.printStackTrace();
+ if (client.player != null)
+ client.player.sendMessage(Text.of("Error while retrieving wiki article..."), false);
+ }
+ }
+ }
+
+}