diff options
author | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
---|---|---|
committer | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
commit | bd3f0329d0e391bd84b5f9e3ff207d9dd9815853 (patch) | |
tree | 2fd1d1ef625f57acc2e4916c967d8d2393844798 /src/main/java/de/hysky/skyblocker/skyblock/item | |
parent | 2315b90da8117f28f66348927afdb621ee4fc815 (diff) | |
download | Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.gz Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.bz2 Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.zip |
new pr because fixing merge conflict would take too long
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/item')
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); + } + } + } + +} |