diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker')
77 files changed, 3621 insertions, 1250 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index bdb1766c..eff88783 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -2,7 +2,6 @@ package de.hysky.skyblocker; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import de.hysky.skyblocker.config.ImageRepoLoader; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.datafixer.ConfigDataFixer; import de.hysky.skyblocker.debug.Debug; @@ -26,15 +25,17 @@ import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.entity.MobBoundingBoxes; import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars; +import de.hysky.skyblocker.skyblock.events.EventNotifications; import de.hysky.skyblocker.skyblock.garden.FarmingHud; import de.hysky.skyblocker.skyblock.garden.LowerSensitivity; import de.hysky.skyblocker.skyblock.garden.VisitorHelper; import de.hysky.skyblocker.skyblock.item.*; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager; import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper; import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; -import de.hysky.skyblocker.skyblock.quicknav.QuickNav; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; @@ -106,7 +107,7 @@ public class SkyblockerMod implements ClientModInitializer { SkyblockerScreen.initClass(); Tips.init(); NEURepoManager.init(); - ImageRepoLoader.init(); + //ImageRepoLoader.init(); ItemRepository.init(); PlayerHeadHashCache.init(); HotbarSlotLock.init(); @@ -179,6 +180,7 @@ public class SkyblockerMod implements ClientModInitializer { Kuudra.init(); RenderHelper.init(); FancyStatusBars.init(); + EventNotifications.init(); containerSolverManager.init(); statusBarTracker.init(); BeaconHighlighter.init(); @@ -187,6 +189,8 @@ public class SkyblockerMod implements ClientModInitializer { EggFinder.init(); TimeTowerReminder.init(); SkyblockTime.init(); + TooltipManager.init(); + SlotTextManager.init(); Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200); diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 9c495382..c9246599 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -44,5 +44,8 @@ public class SkyblockerConfig { public QuickNavigationConfig quickNav = new QuickNavigationConfig(); @SerialEntry + public EventNotificationsConfig eventNotifications = new EventNotificationsConfig(); + + @SerialEntry public MiscConfig misc = new MiscConfig(); } diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java index dd406b8a..f519473c 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java @@ -83,6 +83,7 @@ public class SkyblockerConfigManager { .category(SlayersCategory.create(defaults, config)) .category(ChatCategory.create(defaults, config)) .category(QuickNavigationCategory.create(defaults, config)) + .category(EventNotificationsCategory.create(defaults, config)) .category(MiscCategory.create(defaults, config))).generateScreen(parent); } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java new file mode 100644 index 00000000..6fd01cf8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java @@ -0,0 +1,68 @@ +package de.hysky.skyblocker.config.categories; + +import de.hysky.skyblocker.config.ConfigUtils; +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.configs.EventNotificationsConfig; +import de.hysky.skyblocker.skyblock.events.EventNotifications; +import de.hysky.skyblocker.utils.config.DurationController; +import dev.isxander.yacl3.api.*; +import it.unimi.dsi.fastutil.ints.IntImmutableList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class EventNotificationsCategory { + + private static boolean shouldPlaySound = false; + + public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) { + shouldPlaySound = false; + return ConfigCategory.createBuilder() + .name(Text.translatable("skyblocker.config.eventNotifications")) + .option(Option.<EventNotificationsConfig.Sound>createBuilder() + .binding(defaults.eventNotifications.reminderSound, + () -> config.eventNotifications.reminderSound, + sound -> config.eventNotifications.reminderSound = sound) + .controller(ConfigUtils::createEnumCyclingListController) + .name(Text.translatable("skyblocker.config.eventNotifications.notificationSound")) + .listener((soundOption, sound) -> { + if (!shouldPlaySound) { + shouldPlaySound = true; + return; + } + if (sound.getSoundEvent() != null) + MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(sound.getSoundEvent(), 1f, 1f)); + }) + .build()) + .groups(createGroups(config)) + .build(); + + } + + private static List<OptionGroup> createGroups(SkyblockerConfig config) { + Map<String, IntList> eventsReminderTimes = config.eventNotifications.eventsReminderTimes; + List<OptionGroup> groups = new ArrayList<>(eventsReminderTimes.size()); + if (eventsReminderTimes.isEmpty()) return List.of(OptionGroup.createBuilder().option(LabelOption.create(Text.translatable("skyblocker.config.eventNotifications.monologue"))).build()); + for (Map.Entry<String, IntList> entry : eventsReminderTimes.entrySet()) { + groups.add(ListOption.<Integer>createBuilder() + .name(Text.literal(entry.getKey())) + .binding(EventNotifications.DEFAULT_REMINDERS, entry::getValue, integers -> entry.setValue(new IntImmutableList(integers))) + .controller(option -> () -> new DurationController(option)) // yea + .description(OptionDescription.of(Text.translatable("skyblocker.config.eventNotifications.@Tooltip[0]"), + Text.empty(), + Text.translatable("skyblocker.config.eventNotifications.@Tooltip[1]"), + Text.empty(), + Text.translatable("skyblocker.config.eventNotifications.@Tooltip[2]", entry.getKey()))) + .initial(60) + .collapsed(true) + .build() + ); + } + return groups; + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java index 1477d669..fa87be3d 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java @@ -211,6 +211,14 @@ public class GeneralCategory { .name(Text.translatable("skyblocker.config.general.itemInfoDisplay")) .collapsed(true) .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText.@Tooltip"))) + .binding(defaults.general.itemInfoDisplay.slotText, + () -> config.general.itemInfoDisplay.slotText, + newValue -> config.general.itemInfoDisplay.slotText = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo")) .description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo.@Tooltip"))) .binding(defaults.general.itemInfoDisplay.attributeShardInfo, diff --git a/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java new file mode 100644 index 00000000..c43ae7a6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java @@ -0,0 +1,35 @@ +package de.hysky.skyblocker.config.configs; + +import dev.isxander.yacl3.config.v2.api.SerialEntry; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; + +import java.util.HashMap; +import java.util.Map; + +public class EventNotificationsConfig { + + @SerialEntry + public Sound reminderSound = Sound.PLING; + + @SerialEntry + public Map<String, IntList> eventsReminderTimes = new HashMap<>(); + + public enum Sound { + NONE(null), + BELL(SoundEvents.BLOCK_BELL_USE), + DING(SoundEvents.ENTITY_ARROW_HIT_PLAYER), + PLING(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()), + GOAT(SoundEvents.GOAT_HORN_SOUNDS.getFirst().value()); + + public SoundEvent getSoundEvent() { + return soundEvent; + } + + final SoundEvent soundEvent; + Sound(SoundEvent soundEvent) { + this.soundEvent = soundEvent; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java index 56e110b6..aa7566a0 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java @@ -141,6 +141,9 @@ public class GeneralConfig { public static class ItemInfoDisplay { @SerialEntry + public boolean slotText = true; + + @SerialEntry public boolean attributeShardInfo = true; @SerialEntry diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java index d642ca5b..16d91635 100644 --- a/src/main/java/de/hysky/skyblocker/debug/Debug.java +++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java @@ -16,6 +16,7 @@ import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.item.ItemStack; import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.screen.slot.Slot; import net.minecraft.text.Text; import org.lwjgl.glfw.GLFW; @@ -48,8 +49,9 @@ public class Debug { ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { if (screen instanceof HandledScreen<?> handledScreen) { ScreenKeyboardEvents.afterKeyPress(screen).register((_screen, key, scancode, modifier) -> { - if (key == GLFW.GLFW_KEY_U && client.player != null) { - client.player.sendMessage(Text.literal("[Skyblocker Debug] Hovered Item: " + SkyblockerMod.GSON_COMPACT.toJson(ItemStack.CODEC.encodeStart(JsonOps.INSTANCE, ((HandledScreenAccessor) handledScreen).getFocusedSlot().getStack())))); + Slot focusedSlot = ((HandledScreenAccessor) handledScreen).getFocusedSlot(); + if (key == GLFW.GLFW_KEY_U && client.player != null && focusedSlot != null && focusedSlot.hasStack()) { + client.player.sendMessage(Text.literal("[Skyblocker Debug] Hovered Item: " + SkyblockerMod.GSON_COMPACT.toJson(ItemStack.CODEC.encodeStart(JsonOps.INSTANCE, focusedSlot.getStack())))); } }); } diff --git a/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java new file mode 100644 index 00000000..2f54917b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java @@ -0,0 +1,20 @@ +package de.hysky.skyblocker.injected; + +import org.jetbrains.annotations.Nullable; + +public interface SkyblockerStack { + @Nullable + default String getSkyblockId() { + return ""; + } + + @Nullable + default String getSkyblockApiId() { + return ""; + } + + @Nullable + default String getNeuName() { + return ""; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java index 7964b114..9f09c37a 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java @@ -2,67 +2,15 @@ package de.hysky.skyblocker.mixins; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.AttributeShards; import de.hysky.skyblocker.skyblock.item.ItemCooldowns; -import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.util.math.MatrixStack; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.util.Formatting; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(DrawContext.class) public abstract class DrawContextMixin { - @Shadow - @Final - private MatrixStack matrices; - - @Shadow - public abstract int drawText(TextRenderer textRenderer, @Nullable String text, int x, int y, int color, boolean shadow); - - @Inject(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", at = @At("HEAD")) - private void skyblocker$renderAttributeShardDisplay(CallbackInfo ci, @Local(argsOnly = true) TextRenderer textRenderer, @Local(argsOnly = true) ItemStack stack, @Local(argsOnly = true, ordinal = 0) int x, @Local(argsOnly = true, ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) { - if (!SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo) return; - - if (Utils.isOnSkyblock()) { - NbtCompound customData = ItemUtils.getCustomData(stack); - - if (ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) { - NbtCompound attributesTag = customData.getCompound("attributes"); - String[] attributes = attributesTag.getKeys().toArray(String[]::new); - - if (attributes.length != 0) { - String attributeId = attributes[0]; - int attributeLevel = attributesTag.getInt(attributeId); - - //Set item count - countOverride.set(Integer.toString(attributeLevel)); - - //Draw the attribute name - this.matrices.push(); - this.matrices.translate(0f, 0f, 200f); - - String attributeInitials = AttributeShards.getShortName(attributeId); - - this.drawText(textRenderer, attributeInitials, x, y, Formatting.AQUA.getColorValue(), true); - - this.matrices.pop(); - } - } - } - } - @ModifyExpressionValue(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/ItemCooldownManager;getCooldownProgress(Lnet/minecraft/item/Item;F)F")) private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(argsOnly = true) ItemStack stack) { diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java index f662be7c..1a97c471 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java @@ -12,6 +12,8 @@ import de.hysky.skyblocker.skyblock.garden.VisitorHelper; import de.hysky.skyblocker.skyblock.item.ItemProtection; import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds; import de.hysky.skyblocker.skyblock.item.WikiLookup; +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager; import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; import de.hysky.skyblocker.skyblock.item.tooltip.CompactorDeletorPreview; import de.hysky.skyblocker.skyblock.quicknav.QuickNav; @@ -22,6 +24,7 @@ import de.hysky.skyblocker.utils.render.gui.ContainerSolver; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; import net.minecraft.inventory.SimpleInventory; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -50,251 +53,284 @@ import java.util.regex.Matcher; @Mixin(HandledScreen.class) public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen { - /** - * This is the slot id returned for when a click is outside the screen's bounds - */ - @Unique - private static final int OUT_OF_BOUNDS_SLOT = -999; - - @Unique - private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png"); - - @Unique - private static final Set<String> FILLER_ITEMS = Set.of( - " ", // Empty menu item - "Locked Page", - "Quick Crafting Slot", - "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax - "Locked Backpack Slot 3", - "Locked Backpack Slot 4", - "Locked Backpack Slot 5", - "Locked Backpack Slot 6", - "Locked Backpack Slot 7", - "Locked Backpack Slot 8", - "Locked Backpack Slot 9", - "Locked Backpack Slot 10", - "Locked Backpack Slot 11", - "Locked Backpack Slot 12", - "Locked Backpack Slot 13", - "Locked Backpack Slot 14", - "Locked Backpack Slot 15", - "Locked Backpack Slot 16", - "Locked Backpack Slot 17", - "Locked Backpack Slot 18", - "Preparing" - ); - - @Shadow - @Nullable - protected Slot focusedSlot; - - @Shadow - @Final - protected T handler; - - @Unique - private List<QuickNavButton> quickNavButtons; - - protected HandledScreenMixin(Text title) { - super(title); - } - - @Inject(method = "init", at = @At("RETURN")) - private void skyblocker$initQuickNav(CallbackInfo ci) { - if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) { - for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) { - addSelectableChild(quickNavButton); - } - } - } - - @Inject(at = @At("HEAD"), method = "keyPressed") - public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) { - if (this.client != null && this.focusedSlot != null && keyCode != 256) { - //wiki lookup - if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) { - WikiLookup.openWiki(this.focusedSlot, client.player); - } - //item protection - if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) { - ItemProtection.handleKeyPressed(this.focusedSlot.getStack()); - } - } - } - - @Inject(at = @At("HEAD"), method = "mouseClicked") - public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) { - if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) { - VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer); - } - } - - /** - * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs - */ - @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V")) - private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { - if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) { - if (!quickNavButton.toggled()) { - quickNavButton.render(context, mouseX, mouseY, delta); - } - } - } - - /** - * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs - */ - @Inject(method = "renderBackground", at = @At("RETURN")) - private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { - if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) { - if (quickNavButton.toggled()) { - quickNavButton.render(context, mouseX, mouseY, delta); - } - } - } - - @SuppressWarnings("DataFlowIssue") - // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method. - @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true) - public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) { - if (!Utils.isOnSkyblock()) return; - - // Hide Empty Tooltips - if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) { - ci.cancel(); - } - - // Backpack Preview - boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown(); - if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) { - ci.cancel(); - } - - // Compactor Preview - if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) { - Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack)); - if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, matcher.group("type"), matcher.group("size"), x, y)) { - ci.cancel(); - } - } - } - - @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0)) - private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) { - return skyblocker$experimentSolvers$getStack(focusedSlot, stack); - } - - @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0) - private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) { - return skyblocker$experimentSolvers$getStack(slot, stack); - } - - /** - * Redirects getStack calls to account for different stacks in experiment solvers. - */ - @Unique - private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) { - ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver(); - if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) { - ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex()); - return itemStack == null ? stack : itemStack; - } - return stack; - } - - /** - * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks) - * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item - * - * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)} - */ - @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true) - private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) { - if (!Utils.isOnSkyblock()) return; - - // Item Protection - // When you try and drop the item by picking it up then clicking outside the screen - if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) { - ci.cancel(); - return; - } - - if (slot == null) return; - String title = getTitle().getString(); - ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack()); - ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver(); - - // Prevent clicks on filler items - if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) && - // Allow clicks in Ultrasequencer and Superpairs - (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) { - ci.cancel(); - return; - } - // Item Protection - // When you click your drop key while hovering over an item - if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) { - ci.cancel(); - return; - } - // Prevent salvaging - if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) { - ci.cancel(); - return; - } - if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) { - VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack()); - - // Prevent selling to NPC shops - ItemStack sellStack = this.handler.slots.get(49).getStack(); - if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) { - if (slotId != 49 && ItemProtection.isItemProtected(stack)) { - ci.cancel(); - return; - } - } - } - - if (currentSolver != null) { - boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack); - - if (disallowed) ci.cancel(); - } - - // Experiment Solvers - if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) { - switch (experimentSolver) { - case ChronomatronSolver chronomatronSolver -> { - Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal()); - if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) { - chronomatronSolver.setState(ExperimentSolver.State.END); - } - } - - case SuperpairsSolver superpairsSolver -> { - superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex()); - superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY); - } - - case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> { - int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1; - ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END)); - } - - default -> { /*Do Nothing*/ } - } - } - } - - @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V")) - private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) { - if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) - ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y); - // Item protection - if (ItemProtection.isItemProtected(slot.getStack())) { - RenderSystem.enableBlend(); - context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16); - RenderSystem.disableBlend(); - } - } + /** + * This is the slot id returned for when a click is outside the screen's bounds + */ + @Unique + private static final int OUT_OF_BOUNDS_SLOT = -999; + + @Unique + private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png"); + + @Unique + private static final Set<String> FILLER_ITEMS = Set.of( + " ", // Empty menu item + "Locked Page", + "Quick Crafting Slot", + "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax + "Locked Backpack Slot 3", + "Locked Backpack Slot 4", + "Locked Backpack Slot 5", + "Locked Backpack Slot 6", + "Locked Backpack Slot 7", + "Locked Backpack Slot 8", + "Locked Backpack Slot 9", + "Locked Backpack Slot 10", + "Locked Backpack Slot 11", + "Locked Backpack Slot 12", + "Locked Backpack Slot 13", + "Locked Backpack Slot 14", + "Locked Backpack Slot 15", + "Locked Backpack Slot 16", + "Locked Backpack Slot 17", + "Locked Backpack Slot 18", + "Preparing" + ); + + @Shadow + @Nullable + protected Slot focusedSlot; + + @Shadow + @Final + protected T handler; + + @Shadow + protected abstract List<Text> getTooltipFromItem(ItemStack stack); + + @Unique + private List<QuickNavButton> quickNavButtons; + + protected HandledScreenMixin(Text title) { + super(title); + } + + @Inject(method = "init", at = @At("RETURN")) + private void skyblocker$initQuickNav(CallbackInfo ci) { + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) { + for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) { + addSelectableChild(quickNavButton); + } + } + } + + @Inject(at = @At("HEAD"), method = "keyPressed") + public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) { + if (this.client != null && this.focusedSlot != null && keyCode != 256) { + //wiki lookup + if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) { + WikiLookup.openWiki(this.focusedSlot, client.player); + } + //item protection + if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) { + ItemProtection.handleKeyPressed(this.focusedSlot.getStack()); + } + } + } + + @Inject(at = @At("HEAD"), method = "mouseClicked") + public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) { + if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) { + VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer); + } + } + + /** + * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs + */ + @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V")) + private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) { + if (!quickNavButton.toggled()) { + quickNavButton.render(context, mouseX, mouseY, delta); + } + } + } + + /** + * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs + */ + @Inject(method = "renderBackground", at = @At("RETURN")) + private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) { + if (quickNavButton.toggled()) { + quickNavButton.render(context, mouseX, mouseY, delta); + } + } + } + + @SuppressWarnings("DataFlowIssue") + // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method. + @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true) + public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) { + if (!Utils.isOnSkyblock()) return; + + // Hide Empty Tooltips + if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) { + ci.cancel(); + } + + // Backpack Preview + boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown(); + if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) { + ci.cancel(); + } + + // Compactor Preview + if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) { + Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack)); + if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, getTooltipFromItem(stack), matcher.group("type"), matcher.group("size"), x, y)) { + ci.cancel(); + } + } + } + + @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0)) + private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) { + return skyblocker$experimentSolvers$getStack(focusedSlot, stack); + } + + @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0) + private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) { + return skyblocker$experimentSolvers$getStack(slot, stack); + } + + /** + * Redirects getStack calls to account for different stacks in experiment solvers. + */ + @Unique + private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) { + ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver(); + if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) { + ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex()); + return itemStack == null ? stack : itemStack; + } + return stack; + } + + /** + * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks) + * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item + * + * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)} + */ + @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true) + private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) { + if (!Utils.isOnSkyblock()) return; + + // Item Protection + // When you try and drop the item by picking it up then clicking outside the screen + if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) { + ci.cancel(); + return; + } + + if (slot == null) return; + String title = getTitle().getString(); + ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack()); + ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver(); + + // Prevent clicks on filler items + if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) && + // Allow clicks in Ultrasequencer and Superpairs + (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) { + ci.cancel(); + return; + } + // Item Protection + // When you click your drop key while hovering over an item + if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) { + ci.cancel(); + return; + } + // Prevent salvaging + if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) { + ci.cancel(); + return; + } + if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) { + VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack()); + + // Prevent selling to NPC shops + ItemStack sellStack = this.handler.slots.get(49).getStack(); + if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) { + if (slotId != 49 && ItemProtection.isItemProtected(stack)) { + ci.cancel(); + return; + } + } + } + + if (currentSolver != null) { + boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack); + + if (disallowed) ci.cancel(); + } + + // Experiment Solvers + if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) { + switch (experimentSolver) { + case ChronomatronSolver chronomatronSolver -> { + Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal()); + if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) { + chronomatronSolver.setState(ExperimentSolver.State.END); + } + } + + case SuperpairsSolver superpairsSolver -> { + superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex()); + superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY); + } + + case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> { + int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1; + ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END)); + } + + default -> { /*Do Nothing*/ } + } + } + } + + @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V")) + private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) { + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) + ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y); + // Item protection + if (ItemProtection.isItemProtected(slot.getStack())) { + RenderSystem.enableBlend(); + context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16); + RenderSystem.disableBlend(); + } + } + + @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V")) + private void skyblocker$drawSlotText(DrawContext context, Slot slot, CallbackInfo ci) { + List<SlotText> textList = SlotTextManager.getText(slot); + if (textList.isEmpty()) return; + MatrixStack matrices = context.getMatrices(); + + for (SlotText slotText : textList) { + matrices.push(); + matrices.translate(0.0f, 0.0f, 200.0f); + int length = textRenderer.getWidth(slotText.text()); + if (length > 16) { + matrices.scale(16.0f / length, 16.0f / length, 1.0f); //Make them fit in the slot. FYI, a slot is sized 16×16. + final float x = (slot.x * length / 16.0f) - slot.x; //Save in a variable to not recalculate + switch (slotText.position()) { + case TOP_LEFT, TOP_RIGHT -> matrices.translate(x, (slot.y * length / 16.0f) - slot.y, 0.0f); + case BOTTOM_LEFT, BOTTOM_RIGHT -> matrices.translate(x, ((slot.y + 16f - textRenderer.fontHeight + 2f + 0.7f) * length / 16.0f) - slot.y, 0.0f); + } + } else { + switch (slotText.position()) { + case TOP_LEFT -> { /*Do Nothing*/ } + case TOP_RIGHT -> matrices.translate(16f - length, 0.0f, 0.0f); + case BOTTOM_LEFT -> matrices.translate(0.0f, 16f - textRenderer.fontHeight + 2f, 0.0f); + case BOTTOM_RIGHT -> matrices.translate(16f - length, 16f - textRenderer.fontHeight + 2f, 0.0f); + } + } + context.drawText(textRenderer, slotText.text(), slot.x, slot.y, 0xFFFFFF, true); + matrices.pop(); + } + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java index 878a93ac..6797cb61 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java @@ -1,14 +1,22 @@ package de.hysky.skyblocker.mixins; +import com.google.gson.JsonObject; import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.injected.SkyblockerStack; +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.component.ComponentHolder; import net.minecraft.component.type.ItemEnchantmentsComponent; import net.minecraft.item.ItemStack; import net.minecraft.item.TooltipAppender; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -17,8 +25,11 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Locale; +import java.util.Optional; + @Mixin(ItemStack.class) -public abstract class ItemStackMixin { +public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack { @Shadow public abstract int getDamage(); @@ -29,10 +40,19 @@ public abstract class ItemStackMixin { @Unique private int maxDamage; + @Unique + private String skyblockId; + + @Unique + private String skyblockApiId; + + @Unique + private String neuName; + @ModifyReturnValue(method = "getName", at = @At("RETURN")) private Text skyblocker$customItemNames(Text original) { if (Utils.isOnSkyblock()) { - return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid((ItemStack) (Object) this), original); + return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid(this), original); } return original; @@ -103,4 +123,93 @@ public abstract class ItemStackMixin { setDamage(durability.rightInt() - durability.leftInt()); return true; } + + @Override + @Nullable + public String getSkyblockId() { + if (skyblockId != null && !skyblockId.isEmpty()) return skyblockId; + return skyblockId = skyblocker$getSkyblockId(true); + } + + @Override + @Nullable + public String getSkyblockApiId() { + if (skyblockApiId != null && !skyblockApiId.isEmpty()) return skyblockApiId; + return skyblockApiId = skyblocker$getSkyblockId(false); + } + + @Override + @Nullable + public String getNeuName() { + if (neuName != null && !neuName.isEmpty()) return neuName; + String apiId = getSkyblockApiId(); + String id = getSkyblockId(); + if (apiId == null || id == null) return null; + + if (apiId.startsWith("ISSHINY_")) apiId = id; + + return neuName = ItemTooltip.getNeuName(id, apiId); + } + + @Unique + private String skyblocker$getSkyblockId(boolean internalIDOnly) { + NbtCompound customData = ItemUtils.getCustomData((ItemStack) (Object) this); + + if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) { + return null; + } + String customDataString = customData.getString(ItemUtils.ID); + + if (internalIDOnly) { + return customDataString; + } + + // Transformation to API format. + if (customData.contains("is_shiny")) { + return "ISSHINY_" + customDataString; + } + + switch (customDataString) { + case "ENCHANTED_BOOK" -> { + if (customData.contains("enchantments")) { + NbtCompound enchants = customData.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 (customData.contains("petInfo")) { + JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class); + return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString(); + } + } + case "POTION" -> { + String enhanced = customData.contains("enhanced") ? "_ENHANCED" : ""; + String extended = customData.contains("extended") ? "_EXTENDED" : ""; + String splash = customData.contains("splash") ? "_SPLASH" : ""; + if (customData.contains("potion") && customData.contains("potion_level")) { + return (customData.getString("potion") + "_" + customDataString + "_" + customData.getInt("potion_level") + + enhanced + extended + splash).toUpperCase(Locale.ENGLISH); + } + } + case "RUNE" -> { + if (customData.contains("runes")) { + NbtCompound runes = customData.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 (customData.contains("attributes")) { + NbtCompound shards = customData.getCompound("attributes"); + Optional<String> firstShards = shards.getKeys().stream().findFirst(); + String shard = firstShards.orElse(""); + return customDataString + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard); + } + } + } + return customDataString; + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java index 7f8b2c71..908525e1 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java @@ -7,13 +7,11 @@ import de.hysky.skyblocker.config.configs.UIAndVisualsConfig; import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; import de.hysky.skyblocker.mixins.accessors.ScreenAccessor; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; -import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import it.unimi.dsi.fastutil.longs.LongBooleanPair; +import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.fabric.api.client.screen.v1.Screens; -import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; @@ -45,7 +43,7 @@ public class ChestValue { if (DUNGEON_CHESTS.contains(titleString)) { if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator) { ScreenEvents.afterTick(screen).register(screen_ -> - ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString, client)) + ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString)) ); } } else if (SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue && !titleString.equals("SkyBlock Menu")) { @@ -65,9 +63,9 @@ public class ChestValue { }); } - private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString, MinecraftClient client) { + private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString) { try { - long profit = 0; + double profit = 0; boolean hasIncompleteData = false, usedKismet = false; List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9); @@ -81,16 +79,16 @@ public class ChestValue { } String name = stack.getName().getString(); - String id = ItemTooltip.getInternalNameFromNBT(stack, false); + String id = stack.getSkyblockApiId(); //Regular item price if (id != null) { - LongBooleanPair priceData = getItemPrice(id); + DoubleBooleanPair priceData = ItemUtils.getItemPrice(id); if (!priceData.rightBoolean()) hasIncompleteData = true; //Add the item price to the profit - profit += priceData.leftLong() * stack.getCount(); + profit += priceData.leftDouble() * stack.getCount(); continue; } @@ -103,12 +101,12 @@ public class ChestValue { String type = matcher.group("type"); int amount = Integer.parseInt(matcher.group("amount")); - LongBooleanPair priceData = getItemPrice(("ESSENCE_" + type).toUpperCase()); + DoubleBooleanPair priceData = ItemUtils.getItemPrice(("ESSENCE_" + type).toUpperCase()); if (!priceData.rightBoolean()) hasIncompleteData = true; //Add the price of the essence to the profit - profit += priceData.leftLong() * amount; + profit += priceData.leftDouble() * amount; continue; } @@ -116,7 +114,7 @@ public class ChestValue { //Determine the cost of the chest if (name.contains("Open Reward Chest")) { - String foundString = searchLoreFor(stack, client, "Coins"); + String foundString = searchLoreFor(stack, "Coins"); //Incase we're searching the free chest if (!StringUtils.isBlank(foundString)) { @@ -128,19 +126,19 @@ public class ChestValue { //Determine if a kismet was used or not if (name.contains("Reroll Chest")) { - usedKismet = !StringUtils.isBlank(searchLoreFor(stack, client, "You already rerolled a chest!")); + usedKismet = !StringUtils.isBlank(searchLoreFor(stack, "You already rerolled a chest!")); } } if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.includeKismet && usedKismet) { - LongBooleanPair kismetPriceData = getItemPrice("KISMET_FEATHER"); + DoubleBooleanPair kismetPriceData = ItemUtils.getItemPrice("KISMET_FEATHER"); if (!kismetPriceData.rightBoolean()) hasIncompleteData = true; - profit -= kismetPriceData.leftLong(); + profit -= kismetPriceData.leftDouble(); } - return Text.literal(titleString).append(getProfitText(profit, hasIncompleteData)); + return Text.literal(titleString).append(getProfitText((long) profit, hasIncompleteData)); } catch (Exception e) { LOGGER.error("[Skyblocker Profit Calculator] Failed to calculate dungeon chest profit! ", e); } @@ -150,7 +148,7 @@ public class ChestValue { private static Text getChestValue(GenericContainerScreenHandler handler, Text title, String titleString) { try { - long value = 0; + double value = 0; boolean hasIncompleteData = false; List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9); @@ -160,18 +158,18 @@ public class ChestValue { continue; } - String id = ItemTooltip.getInternalNameFromNBT(stack, false); + String id = stack.getSkyblockApiId(); if (id != null) { - LongBooleanPair priceData = getItemPrice(id); + DoubleBooleanPair priceData = ItemUtils.getItemPrice(id); if (!priceData.rightBoolean()) hasIncompleteData = true; - value += priceData.leftLong() * stack.getCount(); + value += priceData.leftDouble() * stack.getCount(); } } - return Text.literal(titleString).append(getValueText(value, hasIncompleteData)); + return Text.literal(titleString).append(getValueText((long) value, hasIncompleteData)); } catch (Exception e) { LOGGER.error("[Skyblocker Value Calculator] Failed to calculate dungeon chest value! ", e); } @@ -180,33 +178,9 @@ public class ChestValue { } /** - * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, and the {@code right boolean} indicating if the price - * was based on complete data. - */ - private static LongBooleanPair getItemPrice(String id) { - JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData(); - JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData(); - - if (bazaarPrices == null || lbinPrices == null) return LongBooleanPair.of(0L, false); - - if (bazaarPrices.has(id)) { - JsonObject item = bazaarPrices.get(id).getAsJsonObject(); - boolean isPriceNull = item.get("sellPrice").isJsonNull(); - - return LongBooleanPair.of(isPriceNull ? 0L : (long) item.get("sellPrice").getAsDouble(), !isPriceNull); - } - - if (lbinPrices.has(id)) { - return LongBooleanPair.of((long) lbinPrices.get(id).getAsDouble(), true); - } - - return LongBooleanPair.of(0L, false); - } - - /** * Searches for a specific string of characters in the name and lore of an item */ - private static String searchLoreFor(ItemStack stack, MinecraftClient client, String searchString) { + private static String searchLoreFor(ItemStack stack, String searchString) { return ItemUtils.getLoreLineIf(stack, line -> line.contains(searchString)); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java index 042b126b..a8155b43 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java @@ -1,7 +1,6 @@ package de.hysky.skyblocker.skyblock; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; @@ -27,7 +26,7 @@ public class TeleportOverlay { private static void render(WorldRenderContext wrc) { if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.teleportOverlay.enableTeleportOverlays && client.player != null && client.world != null) { ItemStack heldItem = client.player.getMainHandStack(); - String itemId = ItemTooltip.getInternalNameFromNBT(heldItem, true); + String itemId = heldItem.getSkyblockId(); NbtCompound customData = ItemUtils.getCustomData(heldItem); if (itemId != null) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java index fd69d886..557cb6c9 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java @@ -295,8 +295,8 @@ public class AuctionBrowserScreen extends AbstractCustomHypixelGUI<AuctionHouseS String coins = split[1].replace(",", "").replace("coins", "").trim(); try { long parsed = Long.parseLong(coins); - String name = ItemTooltip.getInternalNameFromNBT(stack, false); - String internalID = ItemTooltip.getInternalNameFromNBT(stack, true); + String name = stack.getSkyblockApiId(); + String internalID = stack.getSkyblockId(); String neuName = name; if (name == null || internalID == null) break; if (name.startsWith("ISSHINY_")) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java index 02dbc132..a0b5f0b9 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java @@ -1,42 +1,26 @@ package de.hysky.skyblocker.skyblock.auction.widgets; import de.hysky.skyblocker.skyblock.auction.SlotClickHandler; +import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.ButtonTextures; -import net.minecraft.client.gui.widget.ToggleButtonWidget; import net.minecraft.client.item.TooltipType; import net.minecraft.item.Item.TooltipContext; import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; import org.jetbrains.annotations.NotNull; -public class CategoryTabWidget extends ToggleButtonWidget { - private static final ButtonTextures TEXTURES = new ButtonTextures(new Identifier("recipe_book/tab"), new Identifier("recipe_book/tab_selected")); - - public void setIcon(@NotNull ItemStack icon) { - this.icon = icon.copy(); - } - - private @NotNull ItemStack icon; +public class CategoryTabWidget extends SideTabButtonWidget { private final SlotClickHandler slotClick; private int slotId = -1; public CategoryTabWidget(@NotNull ItemStack icon, SlotClickHandler slotClick) { - super(0, 0, 35, 27, false); - this.icon = icon.copy(); // copy prevents item disappearing on click + super(0, 0, false, icon); this.slotClick = slotClick; - setTextures(TEXTURES); } @Override public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { - if (textures == null) return; - Identifier identifier = textures.get(true, this.toggled); - int x = getX(); - if (toggled) x -= 2; - context.drawGuiTexture(identifier, x, this.getY(), this.width, this.height); - context.drawItem(icon, x + 9, getY() + 5); + super.renderWidget(context, mouseX, mouseY, delta); if (isMouseOver(mouseX, mouseY)) { context.getMatrices().push(); @@ -52,8 +36,8 @@ public class CategoryTabWidget extends ToggleButtonWidget { @Override public void onClick(double mouseX, double mouseY) { - if (this.toggled || slotId == -1) return; + if (isToggled() || slotId == -1) return; + super.onClick(mouseX, mouseY); slotClick.click(slotId); - this.setToggled(true); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java index 2d530b6d..69d2e532 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java @@ -1,25 +1,24 @@ package de.hysky.skyblocker.skyblock.chocolatefactory; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.LineSmoothener; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.RegexUtils; +import de.hysky.skyblocker.utils.RomanNumerals; import de.hysky.skyblocker.utils.render.gui.ColorHighlight; import de.hysky.skyblocker.utils.render.gui.ContainerSolver; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.client.item.TooltipType; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.Nullable; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -28,6 +27,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class ChocolateFactorySolver extends ContainerSolver { + //Patterns private static final Pattern CPS_PATTERN = Pattern.compile("([\\d,.]+) Chocolate per second"); private static final Pattern CPS_INCREASE_PATTERN = Pattern.compile("\\+([\\d,]+) Chocolate per second"); private static final Pattern COST_PATTERN = Pattern.compile("Cost ([\\d,]+) Chocolate"); @@ -36,20 +36,51 @@ public class ChocolateFactorySolver extends ContainerSolver { private static final Pattern CHOCOLATE_PATTERN = Pattern.compile("^([\\d,]+) Chocolate$"); private static final Pattern PRESTIGE_REQUIREMENT_PATTERN = Pattern.compile("Chocolate this Prestige: ([\\d,]+) +Requires (\\S+) Chocolate this Prestige!"); private static final Pattern TIME_TOWER_STATUS_PATTERN = Pattern.compile("Status: (ACTIVE|INACTIVE)"); - private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(6); + private static final Pattern TIME_TOWER_MULTIPLIER_PATTERN = Pattern.compile("by \\+([\\d.]+)x for \\dh\\."); + + private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(8); private static long totalChocolate = -1L; private static double totalCps = -1.0; private static double totalCpsMultiplier = -1.0; private static long requiredUntilNextPrestige = -1L; + private static boolean canPrestige = false; + private static boolean reachedMaxPrestige = false; private static double timeTowerMultiplier = -1.0; + private static boolean isTimeTowerMaxed = false; private static boolean isTimeTowerActive = false; + private static int bestUpgrade = -1; + private static int bestAffordableUpgrade = -1; private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); - private static ItemStack bestUpgrade = null; - private static ItemStack bestAffordableUpgrade = null; + + @Override + protected void reset() { + cpsIncreaseFactors.clear(); + totalChocolate = -1L; + totalCps = -1.0; + totalCpsMultiplier = -1.0; + requiredUntilNextPrestige = -1L; + canPrestige = false; + reachedMaxPrestige = false; + timeTowerMultiplier = -1.0; + isTimeTowerMaxed = false; + isTimeTowerActive = false; + bestUpgrade = -1; + bestAffordableUpgrade = -1; + } + + //Slots, for ease of maintenance rather than using magic numbers everywhere. + private static final byte RABBITS_START = 28; + private static final byte RABBITS_END = 34; + private static final byte COACH_SLOT = 42; + private static final byte CHOCOLATE_SLOT = 13; + private static final byte CPS_SLOT = 45; + private static final byte PRESTIGE_SLOT = 27; + private static final byte TIME_TOWER_SLOT = 39; + private static final byte STRAY_RABBIT_START = 0; + private static final byte STRAY_RABBIT_END = 26; public ChocolateFactorySolver() { - super("^Chocolate Factory$"); - ItemTooltipCallback.EVENT.register(ChocolateFactorySolver::handleTooltip); + super("^Chocolate Factory$"); //There are multiple screens that fit the pattern `^Chocolate Factory`, so the $ is required } @Override @@ -62,11 +93,12 @@ public class ChocolateFactorySolver extends ContainerSolver { updateFactoryInfo(slots); List<ColorHighlight> highlights = new ArrayList<>(); - getPrestigeHighlight(slots.get(28)).ifPresent(highlights::add); + getPrestigeHighlight().ifPresent(highlights::add); + highlights.addAll(getStrayRabbitHighlight(slots)); if (totalChocolate <= 0 || cpsIncreaseFactors.isEmpty()) return highlights; //Something went wrong or there's nothing we can afford. Rabbit bestRabbit = cpsIncreaseFactors.getFirst(); - bestUpgrade = bestRabbit.itemStack; + bestUpgrade = bestRabbit.slot; if (bestRabbit.cost <= totalChocolate) { highlights.add(ColorHighlight.green(bestRabbit.slot)); return highlights; @@ -75,7 +107,7 @@ public class ChocolateFactorySolver extends ContainerSolver { for (Rabbit rabbit : cpsIncreaseFactors.subList(1, cpsIncreaseFactors.size())) { if (rabbit.cost <= totalChocolate) { - bestAffordableUpgrade = rabbit.itemStack; + bestAffordableUpgrade = rabbit.slot; highlights.add(ColorHighlight.green(rabbit.slot)); break; } @@ -87,7 +119,7 @@ public class ChocolateFactorySolver extends ContainerSolver { private static void updateFactoryInfo(Int2ObjectMap<ItemStack> slots) { cpsIncreaseFactors.clear(); - for (int i = 29; i <= 33; i++) { // The 5 rabbits slots are in 29, 30, 31, 32 and 33. + for (int i = RABBITS_START; i <= RABBITS_END; i++) { // The 7 rabbits slots are in 28, 29, 30, 31, 32, 33 and 34. ItemStack item = slots.get(i); if (item.isOf(Items.PLAYER_HEAD)) { getRabbit(item, i).ifPresent(cpsIncreaseFactors::add); @@ -95,20 +127,21 @@ public class ChocolateFactorySolver extends ContainerSolver { } //Coach is in slot 42 - getCoach(slots.get(42)).ifPresent(cpsIncreaseFactors::add); + getCoach(slots.get(COACH_SLOT)).ifPresent(cpsIncreaseFactors::add); //The clickable chocolate is in slot 13, holds the total chocolate - RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(13).getName().getString())).ifPresent(l -> totalChocolate = l); + RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(CHOCOLATE_SLOT).getName().getString())).ifPresent(l -> totalChocolate = l); //Cps item (cocoa bean) is in slot 45 - String cpsItemLore = getConcatenatedLore(slots.get(45)); + String cpsItemLore = getConcatenatedLore(slots.get(CPS_SLOT)); Matcher cpsMatcher = CPS_PATTERN.matcher(cpsItemLore); RegexUtils.getDoubleFromMatcher(cpsMatcher).ifPresent(d -> totalCps = d); Matcher multiplierMatcher = TOTAL_MULTIPLIER_PATTERN.matcher(cpsItemLore); RegexUtils.getDoubleFromMatcher(multiplierMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0).ifPresent(d -> totalCpsMultiplier = d); //Prestige item is in slot 28 - Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(getConcatenatedLore(slots.get(28))); + String prestigeLore = getConcatenatedLore(slots.get(PRESTIGE_SLOT)); + Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(prestigeLore); OptionalLong currentChocolate = RegexUtils.getLongFromMatcher(prestigeMatcher); if (currentChocolate.isPresent()) { String requirement = prestigeMatcher.group(2); //If the first one matched, we can assume the 2nd one is also matched since it's one whole regex @@ -117,12 +150,22 @@ public class ChocolateFactorySolver extends ContainerSolver { if (NumberUtils.isParsable(amountString)) { requiredUntilNextPrestige = Long.parseLong(amountString) - currentChocolate.getAsLong(); } + canPrestige = false; + } else if (prestigeLore.endsWith("Click to prestige!")) { + canPrestige = true; + reachedMaxPrestige = false; + } else if (prestigeLore.endsWith("You have reached max prestige!")) { + canPrestige = false; + reachedMaxPrestige = true; } //Time Tower is in slot 39 - timeTowerMultiplier = romanToDecimal(StringUtils.substringAfterLast(slots.get(39).getName().getString(), ' ')) / 10.0; //The name holds the level, which is multiplier * 10 in roman numerals - Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(getConcatenatedLore(slots.get(39))); - if (timeTowerStatusMatcher.find()) { + isTimeTowerMaxed = StringUtils.substringAfterLast(slots.get(TIME_TOWER_SLOT).getName().getString(), ' ').equals("XV"); + String timeTowerLore = getConcatenatedLore(slots.get(TIME_TOWER_SLOT)); + Matcher timeTowerMultiplierMatcher = TIME_TOWER_MULTIPLIER_PATTERN.matcher(timeTowerLore); + RegexUtils.getDoubleFromMatcher(timeTowerMultiplierMatcher).ifPresent(d -> timeTowerMultiplier = d); + Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(timeTowerLore); + if (timeTowerStatusMatcher.find(timeTowerMultiplierMatcher.hasMatch() ? timeTowerMultiplierMatcher.end() : 0)) { isTimeTowerActive = timeTowerStatusMatcher.group(1).equals("ACTIVE"); } @@ -130,128 +173,6 @@ public class ChocolateFactorySolver extends ContainerSolver { cpsIncreaseFactors.sort(Comparator.comparingDouble(rabbit -> rabbit.cost() / rabbit.cpsIncrease())); //Ascending order, lower = better } - private static void handleTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) { - if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper) return; - if (!(MinecraftClient.getInstance().currentScreen instanceof GenericContainerScreen screen) || !screen.getTitle().getString().equals("Chocolate Factory")) return; - - int lineIndex = lines.size(); - //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip. - //It should be set to true if there's any information added, false otherwise. - boolean shouldAddLine = false; - - String lore = concatenateLore(lines); - Matcher costMatcher = COST_PATTERN.matcher(lore); - OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher); - //Available on all items with a chocolate cost - if (cost.isPresent()) shouldAddLine = addUpgradeTimerToLore(lines, cost.getAsLong()); - - //Prestige item - if (stack.isOf(Items.DROPPER) && requiredUntilNextPrestige != -1L) { - shouldAddLine = addPrestigeTimerToLore(lines) || shouldAddLine; - } - //Time tower - else if (stack.isOf(Items.CLOCK)) { - shouldAddLine = addTimeTowerStatsToLore(lines) || shouldAddLine; - } - //Rabbits - else if (stack.isOf(Items.PLAYER_HEAD)) { - shouldAddLine = addRabbitStatsToLore(lines, stack) || shouldAddLine; - } - - //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of - if (shouldAddLine) lines.add(lineIndex, ItemTooltip.createSmoothLine()); - } - - private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) { - if (totalChocolate < 0L || totalCps < 0.0) return false; - lines.add(Text.empty() - .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY)) - .append(formatTime((cost - totalChocolate) / totalCps))); - return true; - } - - private static boolean addPrestigeTimerToLore(List<Text> lines) { - if (requiredUntilNextPrestige == -1L || totalCps == -1.0) return false; - lines.add(Text.empty() - .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD))); - lines.add(Text.empty() - .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY)) - .append(formatTime(requiredUntilNextPrestige / totalCps))); - return true; - } - - private static boolean addTimeTowerStatsToLore(List<Text> lines) { - if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false; - lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY)); - lines.add(Text.empty() - .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD))); - lines.add(Text.empty() - .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD))); - if (timeTowerMultiplier < 1.5) { - lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY)); - lines.add(Text.empty() - .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD))); - lines.add(Text.empty() - .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD))); - } - return true; - } - - private static boolean addRabbitStatsToLore(List<Text> lines, ItemStack stack) { - if (cpsIncreaseFactors.isEmpty()) return false; - boolean changed = false; - for (Rabbit rabbit : cpsIncreaseFactors) { - if (rabbit.itemStack != stack) continue; - changed = true; - lines.add(Text.empty() - .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD))); - - lines.add(Text.empty() - .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY)) - .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD))); - - if (rabbit.itemStack == bestUpgrade) { - if (rabbit.cost <= totalChocolate) { - lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN)); - } else { - lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW)); - } - } else if (rabbit.itemStack == bestAffordableUpgrade && rabbit.cost <= totalChocolate) { - lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN)); - } - } - return changed; - } - - private static MutableText formatTime(double seconds) { - seconds = Math.ceil(seconds); - if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN); - - StringBuilder builder = new StringBuilder(); - if (seconds >= 86400) { - builder.append((int) (seconds / 86400)).append("d "); - seconds %= 86400; - } - if (seconds >= 3600) { - builder.append((int) (seconds / 3600)).append("h "); - seconds %= 3600; - } - if (seconds >= 60) { - builder.append((int) (seconds / 60)).append("m "); - seconds %= 60; - } - if (seconds >= 1) { - builder.append((int) seconds).append("s"); - } - return Text.literal(builder.toString()).formatted(Formatting.GOLD); - } - /** * Utility method. */ @@ -292,7 +213,7 @@ public class ChocolateFactorySolver extends ContainerSolver { OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), 42, coachItem)); + return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), COACH_SLOT)); } private static Optional<Rabbit> getRabbit(ItemStack item, int slot) { @@ -309,40 +230,159 @@ public class ChocolateFactorySolver extends ContainerSolver { Matcher costMatcher = COST_PATTERN.matcher(lore); OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit(nextCps.getAsInt() - currentCps.getAsInt(), cost.getAsInt(), slot, item)); + return Optional.of(new Rabbit(nextCps.getAsInt() - currentCps.getAsInt(), cost.getAsInt(), slot)); } - private static Optional<ColorHighlight> getPrestigeHighlight(ItemStack item) { - List<Text> loreList = ItemUtils.getLore(item); - if (loreList.isEmpty()) return Optional.empty(); - - String lore = loreList.getLast().getString(); //The last line holds the text we're looking for - if (lore.equals("Click to prestige!")) return Optional.of(ColorHighlight.green(28)); - return Optional.of(ColorHighlight.red(28)); + private static Optional<ColorHighlight> getPrestigeHighlight() { + if (reachedMaxPrestige) return Optional.empty(); + if (canPrestige) return Optional.of(ColorHighlight.green(PRESTIGE_SLOT)); + return Optional.of(ColorHighlight.red(PRESTIGE_SLOT)); } - private record Rabbit(double cpsIncrease, int cost, int slot, ItemStack itemStack) { + private static List<ColorHighlight> getStrayRabbitHighlight(Int2ObjectMap<ItemStack> slots) { + final List<ColorHighlight> highlights = new ArrayList<>(); + for (byte i = STRAY_RABBIT_START; i <= STRAY_RABBIT_END; i++) { + ItemStack item = slots.get(i); + if (!item.isOf(Items.PLAYER_HEAD)) continue; + String name = item.getName().getString(); + if (name.equals("CLICK ME!") || name.startsWith("GOLDEN RABBIT")) { + highlights.add(ColorHighlight.green(i)); + } + } + return highlights; } - //Perhaps the part below can go to a separate file later on, but I couldn't find a proper name for the class, so they're staying here. - private static final Map<Character, Integer> romanMap = Map.of( - 'I', 1, - 'V', 5, - 'X', 10, - 'L', 50, - 'C', 100, - 'D', 500, - 'M', 1000 - ); - - public static int romanToDecimal(String romanNumeral) { - int decimal = 0; - int lastNumber = 0; - for (int i = romanNumeral.length() - 1; i >= 0; i--) { - char ch = romanNumeral.charAt(i); - decimal = romanMap.get(ch) >= lastNumber ? decimal + romanMap.get(ch) : decimal - romanMap.get(ch); - lastNumber = romanMap.get(ch); + private record Rabbit(double cpsIncrease, int cost, int slot) { } + + public static final class Tooltip extends TooltipAdder { + public Tooltip() { + super("^Chocolate Factory$", 0); //The priority doesn't really matter here as this is the only tooltip adder for the Chocolate Factory. + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper || focusedSlot == null) return; + + int lineIndex = lines.size(); + //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip. + //It should be set to true if there's any information added, false otherwise. + boolean shouldAddLine = false; + + String lore = concatenateLore(lines); + Matcher costMatcher = COST_PATTERN.matcher(lore); + OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher); + //Available on all items with a chocolate cost + if (cost.isPresent()) shouldAddLine |= addUpgradeTimerToLore(lines, cost.getAsLong()); + + int index = focusedSlot.id; + + //Prestige item + if (index == PRESTIGE_SLOT) { + shouldAddLine |= addPrestigeTimerToLore(lines); + } + //Time tower + else if (index == TIME_TOWER_SLOT) { + shouldAddLine |= addTimeTowerStatsToLore(lines); + } + //Rabbits + else if (index == COACH_SLOT || (index >= RABBITS_START && index <= RABBITS_END)) { + shouldAddLine |= addRabbitStatsToLore(lines, index); + } + + //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of + if (shouldAddLine) lines.add(lineIndex, LineSmoothener.createSmoothLine()); + } + + private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) { + if (totalChocolate < 0L || totalCps < 0.0) return false; + lines.add(Text.empty() + .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY)) + .append(formatTime((cost - totalChocolate) / totalCps))); + return true; + } + + private static boolean addPrestigeTimerToLore(List<Text> lines) { + if (totalCps < 0.0 || reachedMaxPrestige) return false; + if (requiredUntilNextPrestige > 0 && !canPrestige) { + lines.add(Text.empty() + .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD))); + } + lines.add(Text.empty() //Keep this outside of the `if` to match the format of the upgrade tooltips, that say "Time until upgrade: Now" when it's possible + .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY)) + .append(formatTime(requiredUntilNextPrestige / totalCps))); + return true; + } + + private static boolean addTimeTowerStatsToLore(List<Text> lines) { + if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false; + lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY)); + lines.add(Text.empty() + .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD))); + lines.add(Text.empty() + .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD))); + if (!isTimeTowerMaxed) { + lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY)); + lines.add(Text.empty() + .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD))); + lines.add(Text.empty() + .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD))); + } + return true; + } + + private static boolean addRabbitStatsToLore(List<Text> lines, int slot) { + if (cpsIncreaseFactors.isEmpty()) return false; + for (Rabbit rabbit : cpsIncreaseFactors) { + if (rabbit.slot == slot) { + lines.add(Text.empty() + .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD))); + + lines.add(Text.empty() + .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY)) + .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD))); + + if (rabbit.slot == bestUpgrade) { + if (rabbit.cost <= totalChocolate) { + lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN)); + } else { + lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW)); + } + } else if (rabbit.slot == bestAffordableUpgrade && rabbit.cost <= totalChocolate) { + lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN)); + } + return true; + } + } + return false; + } + + private static MutableText formatTime(double seconds) { + seconds = Math.ceil(seconds); + if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN); + + StringBuilder builder = new StringBuilder(); + if (seconds >= 86400) { + builder.append((int) (seconds / 86400)).append("d "); + seconds %= 86400; + } + if (seconds >= 3600) { + builder.append((int) (seconds / 3600)).append("h "); + seconds %= 3600; + } + if (seconds >= 60) { + builder.append((int) (seconds / 60)).append("m "); + seconds %= 60; + } + if (seconds >= 1) { + builder.append((int) seconds).append("s"); + } + return Text.literal(builder.toString()).formatted(Formatting.GOLD); } - return decimal; } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java index c4fd7d4d..c3a76632 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java @@ -1,10 +1,17 @@ package de.hysky.skyblocker.skyblock.chocolatefactory; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.*; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.waypoint.Waypoint; import it.unimi.dsi.fastutil.objects.ObjectImmutableList; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; @@ -13,6 +20,8 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.item.ItemStack; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.apache.commons.lang3.mutable.MutableObject; @@ -25,10 +34,9 @@ import java.util.regex.Pattern; public class EggFinder { private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)"); - private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$"); private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder"); private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>(); - private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE}; + private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE, Location.DEEP_CAVERNS, Location.SPIDERS_DEN, Location.THE_FARMING_ISLAND}; private static boolean isLocationCorrect = false; private EggFinder() { @@ -39,6 +47,17 @@ public class EggFinder { SkyblockEvents.LOCATION_CHANGE.register(EggFinder::handleLocationChange); ClientReceiveMessageEvents.GAME.register(EggFinder::onChatMessage); WorldRenderEvents.AFTER_TRANSLUCENT.register(EggFinder::renderWaypoints); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE) + .then(ClientCommandManager.literal("eggFinder") + .then(ClientCommandManager.literal("shareLocation") + .then(ClientCommandManager.argument("x", IntegerArgumentType.integer()) + .then(ClientCommandManager.argument("y", IntegerArgumentType.integer()) + .then(ClientCommandManager.argument("z", IntegerArgumentType.integer()) + .then(ClientCommandManager.argument("eggType", StringArgumentType.word()) + .executes(context -> { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("[Skyblocker] Chocolate " + context.getArgument("eggType", String.class) + " Egg found at " + context.getArgument("x", Integer.class) + " " + context.getArgument("y", Integer.class) + " " + context.getArgument("z", Integer.class) + "!"); + return Command.SINGLE_SUCCESS; + }))))))))); } private static void handleLocationChange(Location location) { @@ -102,7 +121,9 @@ public class EggFinder { .append("Found a ") .append(Text.literal("Chocolate " + eggType + " Egg") .withColor(eggType.color)) - .append(" at " + entity.getBlockPos().up(2).toShortString() + "!")); + .append(" at " + entity.getBlockPos().up(2).toShortString() + "!") + .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + entity.getBlockY() + 2 + " " + entity.getBlockZ() + " " + eggType)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN))))); } private static void renderWaypoints(WorldRenderContext context) { @@ -124,16 +145,6 @@ public class EggFinder { logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e); } } - - //There's only one egg of the same type at any given time, so we can set the changed egg to null - matcher = newEggPattern.matcher(text.getString()); - if (matcher.find()) { - try { - EggType.valueOf(matcher.group(1).toUpperCase()).egg.setValue(null); - } catch (IllegalArgumentException e) { - logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e); - } - } } record Egg(ArmorStandEntity entity, Waypoint waypoint) { } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java index 72cbeb2a..6ed11676 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.chocolatefactory; import com.mojang.brigadier.Message; import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; @@ -49,7 +50,7 @@ public class TimeTowerReminder { } try (FileWriter writer = new FileWriter(tempFile)) { - writer.write(String.valueOf(System.currentTimeMillis())); + writer.write(String.valueOf(System.currentTimeMillis())); //Overwrites the file so no need to handle case where the file already exists and has text } catch (IOException e) { LOGGER.error("[Skyblocker Time Tower Reminder] Failed to write to temp file for Time Tower Reminder!", e); } @@ -57,8 +58,9 @@ public class TimeTowerReminder { private static void sendMessage() { if (MinecraftClient.getInstance().player == null || !Utils.isOnSkyblock()) return; - MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED))); - + if (SkyblockerConfigManager.get().helpers.chocolateFactory.enableTimeTowerReminder) { + MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED))); + } File tempFile = SkyblockerMod.CONFIG_DIR.resolve(TIME_TOWER_FILE).toFile(); try { scheduled = false; @@ -81,6 +83,9 @@ public class TimeTowerReminder { } if (System.currentTimeMillis() - time >= 60 * 60 * 1000) sendMessage(); - else Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick + else { + Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick + scheduled = true; + } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java index 70e785cb..0f459c9d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java @@ -35,13 +35,13 @@ public class CroesusProfit extends ContainerSolver { protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) { List<ColorHighlight> highlights = new ArrayList<>(); ItemStack bestChest = null, secondBestChest = null; - long bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question - long dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle + double bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question + double dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) { ItemStack stack = entry.getValue(); if (stack.getName().getString().contains("Chest")) { - long value = valueChest(stack); + double value = valueChest(stack); if (value > bestValue) { secondBestChest = bestChest; secondBestValue = bestValue; @@ -68,8 +68,8 @@ public class CroesusProfit extends ContainerSolver { } - private long valueChest(@NotNull ItemStack chest) { - long chestValue = 0; + private double valueChest(@NotNull ItemStack chest) { + double chestValue = 0; int chestPrice = 0; List<String> chestItems = new ArrayList<>(); @@ -107,22 +107,8 @@ public class CroesusProfit extends ContainerSolver { } - private long getItemPrice(String itemDisplayName) { - JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData(); - JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData(); - long itemValue = 0; - String id = dungeonDropsNameToId.get(itemDisplayName); - - if (bazaarPrices == null || lbinPrices == null) return 0; - - if (bazaarPrices.has(id)) { - JsonObject item = bazaarPrices.get(id).getAsJsonObject(); - boolean isPriceNull = item.get("sellPrice").isJsonNull(); - return (isPriceNull ? 0L : item.get("sellPrice").getAsLong()); - } else if (lbinPrices.has(id)) { - return lbinPrices.get(id).getAsLong(); - } - return itemValue; + private double getItemPrice(String itemDisplayName) { + return ItemUtils.getItemPrice(dungeonDropsNameToId.get(itemDisplayName)).leftDouble(); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java new file mode 100644 index 00000000..0fd41969 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java @@ -0,0 +1,174 @@ +package de.hysky.skyblocker.skyblock.events; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.logging.LogUtils; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.ints.IntList; +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.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.sound.SoundEvent; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class EventNotifications { + private static final Logger LOGGER = LogUtils.getLogger(); + + private static long currentTime = System.currentTimeMillis() / 1000; + + public static final String JACOBS = "Jacob's Farming Contest"; + + public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5); + + public static final Map<String, ItemStack> eventIcons = new Object2ObjectOpenHashMap<>(); + + static { + eventIcons.put("Dark Auction", new ItemStack(Items.NETHER_BRICK)); + eventIcons.put("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)); + eventIcons.put("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)); + eventIcons.put(JACOBS, new ItemStack(Items.IRON_HOE)); + eventIcons.put("New Year Celebration", new ItemStack(Items.CAKE)); + eventIcons.put("Election Over!", new ItemStack(Items.JUKEBOX)); + eventIcons.put("Election Booth Opens", new ItemStack(Items.JUKEBOX)); + eventIcons.put("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)); + eventIcons.put("Season of Jerry", new ItemStack(Items.SNOWBALL)); + eventIcons.put("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)); + eventIcons.put("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)); // change to the custom head one day + } + + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(EventNotifications::timeUpdate, 20); + + SkyblockEvents.JOIN.register(EventNotifications::refreshEvents); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register( + ClientCommandManager.literal("skyblocker").then( + ClientCommandManager.literal("debug").then( + ClientCommandManager.literal("toasts").then( + ClientCommandManager.argument("time", IntegerArgumentType.integer(0)) + .then(ClientCommandManager.argument("jacob", BoolArgumentType.bool()).executes(context -> { + long time = System.currentTimeMillis() / 1000 + context.getArgument("time", int.class); + if (context.getArgument("jacob", Boolean.class)) { + MinecraftClient.getInstance().getToastManager().add( + new JacobEventToast(time, "Jacob's farming contest", new String[]{"Cactus", "Cocoa Beans", "Pumpkin"}) + ); + } else { + MinecraftClient.getInstance().getToastManager().add( + new EventToast(time, "Jacob's or something idk", new ItemStack(Items.PAPER)) + ); + } + return 0; + } + ) + ) + ) + ) + ) + )); + } + + private static final Map<String, LinkedList<SkyblockEvent>> events = new Object2ObjectOpenHashMap<>(); + + public static Map<String, LinkedList<SkyblockEvent>> getEvents() { + return events; + } + + public static void refreshEvents() { + CompletableFuture.supplyAsync(() -> { + try { + JsonArray jsonElements = SkyblockerMod.GSON.fromJson(Http.sendGetRequest("https://hysky.de/api/calendar"), JsonArray.class); + return jsonElements.asList().stream().map(JsonElement::getAsJsonObject).toList(); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to download events list", e); + } + return List.<JsonObject>of(); + }).thenAccept(eventsList -> { + events.clear(); + for (JsonObject object : eventsList) { + if (object.get("timestamp").getAsLong() + object.get("duration").getAsInt() < currentTime) continue; + SkyblockEvent skyblockEvent = SkyblockEvent.of(object); + events.computeIfAbsent(object.get("event").getAsString(), s -> new LinkedList<>()).add(skyblockEvent); + } + + for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) { + entry.getValue().sort(Comparator.comparingLong(SkyblockEvent::start)); // Sort just in case it's not in order for some reason in API + //LOGGER.info("Next {} is at {}", entry.getKey(), entry.getValue().peekFirst()); + } + + for (String s : events.keySet()) { + SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.computeIfAbsent(s, s1 -> DEFAULT_REMINDERS); + } + }).exceptionally(EventNotifications::itBorked); + } + + private static Void itBorked(Throwable throwable) { + LOGGER.error("[Skyblocker] Event loading borked, sowwy :(", throwable); + return null; + } + + + private static void timeUpdate() { + + long newTime = System.currentTimeMillis() / 1000; + for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) { + LinkedList<SkyblockEvent> nextEvents = entry.getValue(); + SkyblockEvent skyblockEvent = nextEvents.peekFirst(); + if (skyblockEvent == null) continue; + + // Remove finished event + if (newTime > skyblockEvent.start() + skyblockEvent.duration()) { + nextEvents.pollFirst(); + skyblockEvent = nextEvents.peekFirst(); + if (skyblockEvent == null) continue; + } + String eventName = entry.getKey(); + List<Integer> reminderTimes = SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.getOrDefault(eventName, DEFAULT_REMINDERS); + if (reminderTimes.isEmpty()) continue; + + for (Integer reminderTime : reminderTimes) { + if (currentTime + reminderTime < skyblockEvent.start() && newTime + reminderTime >= skyblockEvent.start()) { + MinecraftClient instance = MinecraftClient.getInstance(); + if (eventName.equals(JACOBS)) { + instance.getToastManager().add( + new JacobEventToast(skyblockEvent.start(), eventName, skyblockEvent.extras()) + ); + } else { + instance.getToastManager().add( + new EventToast(skyblockEvent.start(), eventName, eventIcons.getOrDefault(eventName, new ItemStack(Items.PAPER))) + ); + } + SoundEvent soundEvent = SkyblockerConfigManager.get().eventNotifications.reminderSound.getSoundEvent(); + if (soundEvent != null) + instance.getSoundManager().play(PositionedSoundInstance.master(soundEvent, 1f, 1f)); + break; + } + } + } + currentTime = newTime; + } + + public record SkyblockEvent(long start, int duration, String[] extras, @Nullable String warpCommand) { + public static SkyblockEvent of(JsonObject jsonObject) { + String location = jsonObject.get("location").getAsString(); + location = location.isBlank() ? null : location; + return new SkyblockEvent(jsonObject.get("timestamp").getAsLong(), + jsonObject.get("duration").getAsInt(), + jsonObject.get("extras").getAsJsonArray().asList().stream().map(JsonElement::getAsString).toArray(String[]::new), + location); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java new file mode 100644 index 00000000..567c800a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.events; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.toast.Toast; +import net.minecraft.client.toast.ToastManager; +import net.minecraft.item.ItemStack; +import net.minecraft.text.MutableText; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class EventToast implements Toast { + protected static final Identifier TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "notification"); + + private final long eventStartTime; + + protected final List<OrderedText> message; + protected final List<OrderedText> messageNow; + protected final int messageWidth; + protected final int messageNowWidth; + protected final ItemStack icon; + + protected boolean started; + + public EventToast(long eventStartTime, String name, ItemStack icon) { + this.eventStartTime = eventStartTime; + MutableText formatted = Text.translatable("skyblocker.events.startsSoon", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE); + TextRenderer renderer = MinecraftClient.getInstance().textRenderer; + message = renderer.wrapLines(formatted, 150); + messageWidth = message.stream().mapToInt(renderer::getWidth).max().orElse(150); + + MutableText formattedNow = Text.translatable("skyblocker.events.startsNow", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE); + messageNow = renderer.wrapLines(formattedNow, 150); + messageNowWidth = messageNow.stream().mapToInt(renderer::getWidth).max().orElse(150); + this.icon = icon; + this.started = eventStartTime - System.currentTimeMillis() / 1000 < 0; + + } + @Override + public Visibility draw(DrawContext context, ToastManager manager, long startTime) { + context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight()); + + int y = (getHeight() - getInnerContentsHeight())/2; + y = 2 + drawMessage(context, 30, y, Colors.WHITE); + drawTimer(context, 30, y); + + context.drawItemWithoutEntity(icon, 8, getHeight()/2 - 8); + return startTime > 5_000 ? Visibility.HIDE: Visibility.SHOW; + } + + protected int drawMessage(DrawContext context, int x, int y, int color) { + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + for (OrderedText orderedText : started ? messageNow: message) { + context.drawText(textRenderer, orderedText, x, y, color, false); + y += textRenderer.fontHeight; + } + return y; + } + + protected void drawTimer(DrawContext context, int x, int y) { + long currentTime = System.currentTimeMillis() / 1000; + int timeTillEvent = (int) (eventStartTime - currentTime); + started = timeTillEvent < 0; + if (started) return; + + Text time = Utils.getDurationText(timeTillEvent); + + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, time, x, y, Colors.LIGHT_YELLOW, false); + } + + @Override + public int getWidth() { + return (started ? messageNowWidth: messageWidth) + 30 + 6; + } + + protected int getInnerContentsHeight() { + return message.size() * 9 + (started ? 0 : 9); + } + + @Override + public int getHeight() { + return Math.max(getInnerContentsHeight() + 12 + 2, 32); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java new file mode 100644 index 00000000..43ed7d12 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java @@ -0,0 +1,60 @@ +package de.hysky.skyblocker.skyblock.events; + +import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.toast.ToastManager; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.Colors; +import net.minecraft.util.math.MathHelper; + +public class JacobEventToast extends EventToast { + + private final String[] crops; + + private static final ItemStack DEFAULT_ITEM = new ItemStack(Items.IRON_HOE); + + public JacobEventToast(long eventStartTime, String name, String[] crops) { + super(eventStartTime, name, new ItemStack(Items.IRON_HOE)); + this.crops = crops; + } + + @Override + public Visibility draw(DrawContext context, ToastManager manager, long startTime) { + context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight()); + + int y = (getHeight() - getInnerContentsHeight()) / 2; + TextRenderer textRenderer = manager.getClient().textRenderer; + MatrixStack matrices = context.getMatrices(); + if (startTime < 3_000) { + int k = MathHelper.floor(Math.clamp((3_000 - startTime) / 200.0f, 0.0f, 1.0f) * 255.0f) << 24 | 0x4000000; + y = 2 + drawMessage(context, 30, y, 0xFFFFFF | k); + } else { + int k = (~MathHelper.floor(Math.clamp((startTime - 3_000) / 200.0f, 0.0f, 1.0f) * 255.0f)) << 24 | 0x4000000; + + + String s = "Crops:"; + int x = 30 + textRenderer.getWidth(s) + 4; + context.drawText(textRenderer, s, 30, 7 + (16 - textRenderer.fontHeight) / 2, Colors.WHITE, false); + for (int i = 0; i < crops.length; i++) { + context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crops[i], DEFAULT_ITEM), x + i * (16 + 8), 7); + } + // IDK how to make the items transparent, so I just redraw the texture on top + matrices.push(); + matrices.translate(0, 0, 400f); + RenderHelper.renderNineSliceColored(context, TEXTURE, 0, 0, getWidth(), getHeight(), 1f, 1f, 1f, (k >> 24) / 255f); + matrices.pop(); + y += textRenderer.fontHeight * message.size(); + } + matrices.push(); + matrices.translate(0, 0, 400f); + drawTimer(context, 30, y); + + context.drawItemWithoutEntity(icon, 8, getHeight() / 2 - 8); + matrices.pop(); + return startTime > 5_000 ? Visibility.HIDE : Visibility.SHOW; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java index 201e6716..b845e07a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java @@ -16,6 +16,8 @@ import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,8 +35,9 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit public class FarmingHud { private static final Logger LOGGER = LoggerFactory.getLogger(FarmingHud.class); public static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); - private static final Pattern COUNTER = Pattern.compile("Counter: (?<count>[\\d,]+) .+"); private static final Pattern FARMING_XP = Pattern.compile("§3\\+(?<xp>\\d+.?\\d*) Farming \\((?<percent>[\\d,]+.?\\d*)%\\)"); + private static final MinecraftClient client = MinecraftClient.getInstance(); + private static CounterType counterType = CounterType.NONE; private static final Deque<IntLongPair> counter = new ArrayDeque<>(); private static final LongPriorityQueue blockBreaks = new LongArrayFIFOQueue(); private static final Queue<FloatLongPair> farmingXp = new ArrayDeque<>(); @@ -43,7 +46,7 @@ public class FarmingHud { public static void init() { HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> { if (shouldRender()) { - if (!counter.isEmpty() && counter.peek().rightLong() + 10_000 < System.currentTimeMillis()) { + if (!counter.isEmpty() && counter.peek().rightLong() + 5000 < System.currentTimeMillis()) { counter.poll(); } if (!blockBreaks.isEmpty() && blockBreaks.firstLong() + 1000 < System.currentTimeMillis()) { @@ -53,17 +56,9 @@ public class FarmingHud { farmingXp.poll(); } - ItemStack stack = MinecraftClient.getInstance().player.getMainHandStack(); - Matcher matcher = ItemUtils.getLoreLineIfMatch(stack, FarmingHud.COUNTER); - if (matcher != null) { - try { - int count = NUMBER_FORMAT.parse(matcher.group("count")).intValue(); - if (counter.isEmpty() || counter.peekLast().leftInt() != count) { - counter.offer(IntLongPair.of(count, System.currentTimeMillis())); - } - } catch (ParseException e) { - LOGGER.error("[Skyblocker Farming HUD] Failed to parse counter", e); - } + ItemStack stack = client.player.getMainHandStack(); + if (stack == null || !tryGetCounter(stack, CounterType.CULTIVATING) && !tryGetCounter(stack, CounterType.COUNTER)) { + counterType = CounterType.NONE; } FarmingHudWidget.INSTANCE.update(); @@ -92,8 +87,26 @@ public class FarmingHud { .executes(Scheduler.queueOpenScreenCommand(() -> new FarmingHudConfigScreen(null))))))); } + private static boolean tryGetCounter(ItemStack stack, CounterType counterType) { + NbtCompound customData = ItemUtils.getCustomData(stack); + if (customData == null || !customData.contains(counterType.nbtKey, NbtElement.NUMBER_TYPE)) return false; + int count = customData.getInt(counterType.nbtKey); + if (FarmingHud.counterType != counterType) { + counter.clear(); + FarmingHud.counterType = counterType; + } + if (counter.isEmpty() || counter.peekLast().leftInt() != count) { + counter.offer(IntLongPair.of(count, System.currentTimeMillis())); + } + return true; + } + private static boolean shouldRender() { - return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && Utils.getLocation() == Location.GARDEN; + return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && client.player != null && Utils.getLocation() == Location.GARDEN; + } + + public static String counterText() { + return counterType.text; } public static int counter() { @@ -120,4 +133,18 @@ public class FarmingHud { public static double farmingXpPerHour() { return farmingXp.stream().mapToDouble(FloatLongPair::leftFloat).sum() * blockBreaks() * 1800; // Hypixel only sends xp updates around every half a second } + + public enum CounterType { + NONE("", "No Counter: "), + COUNTER("mined_crops", "Counter: "), + CULTIVATING("farmed_cultivating", "Cultivating Counter: "); + + private final String nbtKey; + private final String text; + + CounterType(String nbtKey, String text) { + this.nbtKey = nbtKey; + this.text = text; + } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java index f12f6fa8..eb444813 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java @@ -1,11 +1,13 @@ package de.hysky.skyblocker.skyblock.garden; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; import de.hysky.skyblocker.skyblock.tabhud.util.Ico; import de.hysky.skyblocker.skyblock.tabhud.widget.Widget; import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent; import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent; import de.hysky.skyblocker.utils.ItemUtils; +import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.item.ItemStack; @@ -18,31 +20,31 @@ import java.util.Map; public class FarmingHudWidget extends Widget { private static final MutableText TITLE = Text.literal("Farming").formatted(Formatting.YELLOW, Formatting.BOLD); - public static final Map<String, ItemStack> FARMING_TOOLS = Map.ofEntries( - Map.entry("THEORETICAL_HOE_WHEAT_1", Ico.WHEAT), - Map.entry("THEORETICAL_HOE_WHEAT_2", Ico.WHEAT), - Map.entry("THEORETICAL_HOE_WHEAT_3", Ico.WHEAT), - Map.entry("THEORETICAL_HOE_CARROT_1", Ico.CARROT), - Map.entry("THEORETICAL_HOE_CARROT_2", Ico.CARROT), - Map.entry("THEORETICAL_HOE_CARROT_3", Ico.CARROT), - Map.entry("THEORETICAL_HOE_POTATO_1", Ico.POTATO), - Map.entry("THEORETICAL_HOE_POTATO_2", Ico.POTATO), - Map.entry("THEORETICAL_HOE_POTATO_3", Ico.POTATO), - Map.entry("THEORETICAL_HOE_CANE_1", Ico.SUGAR_CANE), - Map.entry("THEORETICAL_HOE_CANE_2", Ico.SUGAR_CANE), - Map.entry("THEORETICAL_HOE_CANE_3", Ico.SUGAR_CANE), - Map.entry("THEORETICAL_HOE_WARTS_1", Ico.NETHER_WART), - Map.entry("THEORETICAL_HOE_WARTS_2", Ico.NETHER_WART), - Map.entry("THEORETICAL_HOE_WARTS_3", Ico.NETHER_WART), - Map.entry("FUNGI_CUTTER", Ico.MUSHROOM), - Map.entry("CACTUS_KNIFE", Ico.CACTUS), - Map.entry("MELON_DICER", Ico.MELON), - Map.entry("MELON_DICER_2", Ico.MELON), - Map.entry("MELON_DICER_3", Ico.MELON), - Map.entry("PUMPKIN_DICER", Ico.PUMPKIN), - Map.entry("PUMPKIN_DICER_2", Ico.PUMPKIN), - Map.entry("PUMPKIN_DICER_3", Ico.PUMPKIN), - Map.entry("COCO_CHOPPER", Ico.COCOA_BEANS) + public static final Map<String, String> FARMING_TOOLS = Map.ofEntries( + Map.entry("THEORETICAL_HOE_WHEAT_1", "WHEAT"), + Map.entry("THEORETICAL_HOE_WHEAT_2", "WHEAT"), + Map.entry("THEORETICAL_HOE_WHEAT_3", "WHEAT"), + Map.entry("THEORETICAL_HOE_CARROT_1", "CARROT_ITEM"), + Map.entry("THEORETICAL_HOE_CARROT_2", "CARROT_ITEM"), + Map.entry("THEORETICAL_HOE_CARROT_3", "CARROT_ITEM"), + Map.entry("THEORETICAL_HOE_POTATO_1", "POTATO_ITEM"), + Map.entry("THEORETICAL_HOE_POTATO_2", "POTATO_ITEM"), + Map.entry("THEORETICAL_HOE_POTATO_3", "POTATO_ITEM"), + Map.entry("THEORETICAL_HOE_CANE_1", "SUGAR_CANE"), + Map.entry("THEORETICAL_HOE_CANE_2", "SUGAR_CANE"), + Map.entry("THEORETICAL_HOE_CANE_3", "SUGAR_CANE"), + Map.entry("THEORETICAL_HOE_WARTS_1", "NETHER_STALK"), + Map.entry("THEORETICAL_HOE_WARTS_2", "NETHER_STALK"), + Map.entry("THEORETICAL_HOE_WARTS_3", "NETHER_STALK"), + Map.entry("FUNGI_CUTTER", "RED_MUSHROOM"), + Map.entry("CACTUS_KNIFE", "CACTUS"), + Map.entry("MELON_DICER", "MELON"), + Map.entry("MELON_DICER_2", "MELON"), + Map.entry("MELON_DICER_3", "MELON"), + Map.entry("PUMPKIN_DICER", "PUMPKIN"), + Map.entry("PUMPKIN_DICER_2", "PUMPKIN"), + Map.entry("PUMPKIN_DICER_3", "PUMPKIN"), + Map.entry("COCO_CHOPPER", "INK_SACK:3") ); public static final FarmingHudWidget INSTANCE = new FarmingHudWidget(); private final MinecraftClient client = MinecraftClient.getInstance(); @@ -56,19 +58,28 @@ public class FarmingHudWidget extends Widget { @Override public void updateContent() { - ItemStack icon = client.player == null ? Ico.HOE : FARMING_TOOLS.getOrDefault(ItemUtils.getItemId(client.player.getMainHandStack()), Ico.HOE); - addSimpleIcoText(icon, "Counter: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter())); - addSimpleIcoText(icon, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.cropsPerMinute() / 100 * 100)); - addSimpleIcoText(icon, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks())); + if (client.player == null) return; + ItemStack farmingToolStack = client.player.getMainHandStack(); + if (farmingToolStack == null) return; + String cropItemId = FARMING_TOOLS.get(ItemUtils.getItemId(farmingToolStack)); + ItemStack cropStack = ItemRepository.getItemStack(cropItemId); + + addSimpleIcoText(cropStack, FarmingHud.counterText(), Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter())); + float cropsPerMinute = FarmingHud.cropsPerMinute(); + addSimpleIcoText(cropStack, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) cropsPerMinute / 10 * 10)); + DoubleBooleanPair itemPrice = ItemUtils.getItemPrice(cropItemId); + addSimpleIcoText(Ico.GOLD, "Coins/h: ", Formatting.GOLD, itemPrice.rightBoolean() ? FarmingHud.NUMBER_FORMAT.format((int) (itemPrice.leftDouble() * cropsPerMinute * 0.6) * 100) : "No Data"); // Multiply by 60 to convert to hourly and divide by 100 for rounding is combined into multiplying by 0.6 + + addSimpleIcoText(cropStack, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks())); //noinspection DataFlowIssue addComponent(new ProgressComponent(Ico.LANTERN, Text.literal("Farming Level: "), FarmingHud.farmingXpPercentProgress(), Formatting.GOLD.getColorValue())); addSimpleIcoText(Ico.LIME_DYE, "Farming XP/h: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.farmingXpPerHour())); Entity cameraEntity = client.getCameraEntity(); - double yaw = cameraEntity == null ? 0.0d : cameraEntity.getYaw(); - double pitch = cameraEntity == null ? 0.0d : cameraEntity.getPitch(); - addComponent(new PlainTextComponent(Text.literal("Yaw: " + String.format("%.3f", MathHelper.wrapDegrees(yaw))).formatted(Formatting.YELLOW))); - addComponent(new PlainTextComponent(Text.literal("Pitch: " + String.format("%.3f", MathHelper.wrapDegrees(pitch))).formatted(Formatting.YELLOW))); + String yaw = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getYaw())); + String pitch = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getPitch())); + addComponent(new PlainTextComponent(Text.literal("Yaw: " + yaw).formatted(Formatting.GOLD))); + addComponent(new PlainTextComponent(Text.literal("Pitch: " + pitch).formatted(Formatting.GOLD))); if (LowerSensitivity.isSensitivityLowered()) { addComponent(new PlainTextComponent(Text.translatable("skyblocker.garden.hud.mouseLocked").formatted(Formatting.ITALIC))); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java index ee857618..2bfb7fb2 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java @@ -9,28 +9,23 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.item.ItemStack; public class LowerSensitivity { - private static boolean sensitivityLowered = false; public static void init() { ClientTickEvents.END_WORLD_TICK.register(world -> { - if (!Utils.isOnSkyblock() || Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null) { + if (Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null || !SkyblockerConfigManager.get().farming.garden.lockMouseTool) { if (sensitivityLowered) lowerSensitivity(false); return; } - if (SkyblockerConfigManager.get().farming.garden.lockMouseTool) { - ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack(); - String itemId = ItemUtils.getItemId(mainHandStack); - boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround()); - if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true); - else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false); - - } + ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack(); + String itemId = ItemUtils.getItemId(mainHandStack); + boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround()); + if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true); + else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false); }); } public static void lowerSensitivity(boolean lowerSensitivity) { - if (sensitivityLowered == lowerSensitivity) return; sensitivityLowered = lowerSensitivity; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java index 682933f4..d8f4dad7 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.garden; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.NEURepoManager; import de.hysky.skyblocker.utils.Utils; @@ -12,6 +13,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.ingame.HandledScreen; @@ -40,7 +42,8 @@ public class VisitorHelper { private static final Map<String, ItemStack> itemCache = new HashMap<>(); private static final int TEXT_START_X = 4; private static final int TEXT_START_Y = 4; - private static final int TEXT_INDENT = 8; + private static final int ENTRY_INDENT = 8; + private static final int ITEM_INDENT = 20; private static final int LINE_SPACING = 3; public static void init() { @@ -59,22 +62,32 @@ public class VisitorHelper { public static void onMouseClicked(double mouseX, double mouseY, int mouseButton, TextRenderer textRenderer) { int yPosition = TEXT_START_Y; - for (Map.Entry<Pair<String, String>, Object2IntMap<String>> visitorEntry : itemMap.entrySet()) { - int textWidth; - int textHeight = textRenderer.fontHeight; - - yPosition += LINE_SPACING + textHeight; + yPosition += LINE_SPACING + textRenderer.fontHeight; for (Object2IntMap.Entry<String> itemEntry : visitorEntry.getValue().object2IntEntrySet()) { String itemText = itemEntry.getKey(); - textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue()); + int textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue()); - if (isMouseOverText(mouseX, mouseY, TEXT_START_X + TEXT_INDENT, yPosition, textWidth, textHeight)) { + // Check if the mouse is over the item text + // The text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT` + if (isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, yPosition, textWidth, textRenderer.fontHeight)) { + // Send command to buy the item from the bazaar MessageScheduler.INSTANCE.sendMessageAfterCooldown("/bz " + itemText); return; } - yPosition += LINE_SPACING + textHeight; + + // Check if the mouse is over the copy amount text + // The copy amount text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth` + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null && isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth, yPosition, textRenderer.getWidth(" [Copy Amount]"), textRenderer.fontHeight)) { + // Copy the amount to the clipboard + client.keyboard.setClipboard(String.valueOf(itemEntry.getIntValue())); + client.player.sendMessage(Constants.PREFIX.get().append("Copied amount successfully"), false); + return; + } + + yPosition += LINE_SPACING + textRenderer.fontHeight; } } } @@ -112,7 +125,7 @@ public class VisitorHelper { } } - private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) { + private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) { String[] splitItemText = lore.getString().split(" x"); String itemName = splitItemText[0].trim(); if (itemName.isEmpty()) return; @@ -168,12 +181,23 @@ public class VisitorHelper { return stack; } - private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mousseY) { - Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount); - drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + TEXT_INDENT, TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)), mouseX, mousseY); + /** + * Draws the item entry, amount, and copy amount text with optional underline and the item icon + */ + private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mouseY) { + Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount); + Text copyAmount = Text.literal(" [Copy Amount]"); + + // Calculate the y position of the text with index as the line number + int y = TEXT_START_Y + index * (LINE_SPACING + textRenderer.fontHeight); + // Draw the item and amount text + drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, y, mouseX, mouseY); + // Draw the copy amount text separately after the item and amount text + drawTextWithOptionalUnderline(context, textRenderer, copyAmount, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textRenderer.getWidth(text), y, mouseX, mouseY); + // drawItem adds 150 to the z, which puts our z at 350, above the item in the slot (250) and their text (300) and below the cursor stack (382) and their text (432) if (stack != null) { - context.drawItem(stack, TEXT_START_X + TEXT_INDENT + 2 + textRenderer.getWidth(text), TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)) - textRenderer.fontHeight + 5); + context.drawItem(stack, TEXT_START_X + ENTRY_INDENT, y - textRenderer.fontHeight + 5); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java new file mode 100644 index 00000000..66c02ca1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java @@ -0,0 +1,21 @@ +package de.hysky.skyblocker.skyblock.item.slottext; + +import net.minecraft.text.Text; + +public record SlotText(Text text, TextPosition position) { + public static SlotText bottomLeft(Text text) { + return new SlotText(text, TextPosition.BOTTOM_LEFT); + } + + public static SlotText bottomRight(Text text) { + return new SlotText(text, TextPosition.BOTTOM_RIGHT); + } + + public static SlotText topLeft(Text text) { + return new SlotText(text, TextPosition.TOP_LEFT); + } + + public static SlotText topRight(Text text) { + return new SlotText(text, TextPosition.TOP_RIGHT); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java new file mode 100644 index 00000000..71d9aa30 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java @@ -0,0 +1,58 @@ +package de.hysky.skyblocker.skyblock.item.slottext; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher; +import net.minecraft.screen.slot.Slot; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Extend this class and add it to {@link SlotTextManager#adders} to add text to any arbitrary slot. + */ +public abstract class SlotTextAdder extends AbstractContainerMatcher { + /** + * Utility constructor that will compile the given string into a pattern. + * + * @see #SlotTextAdder(Pattern) + */ + protected SlotTextAdder(@NotNull @Language("RegExp") String titlePattern) { + super(titlePattern); + } + + /** + * Creates a SlotTextAdder that will be applied to screens with titles that match the given pattern. + * + * @param titlePattern The pattern to match the screen title against. + */ + protected SlotTextAdder(@NotNull Pattern titlePattern) { + super(titlePattern); + } + + /** + * Creates a SlotTextAdder that will be applied to all screens. + */ + protected SlotTextAdder() { + super(); + } + + /** + * This method will be called for each rendered slot. Consider using a switch statement on {@link Slot#id} if you wish to add different text to different slots. + * + * @return A list of positioned text to be rendered. Return {@link List#of()} if no text should be rendered. + * @implNote By minecraft's design, scaled text inexplicably moves around. + * So, limit your text to 3 characters (or roughly less than 20 width) if you want it to not look horrible. + */ + public abstract @NotNull List<SlotText> getText(Slot slot); + + /** + * Override this method to add conditions to enable or disable this adder. + * @return Whether this adder is enabled. + * @implNote The slot text adders only work while in skyblock, so no need to check for that again. + */ + public boolean isEnabled() { + return SkyblockerConfigManager.get().general.itemInfoDisplay.slotText; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java new file mode 100644 index 00000000..18131527 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java @@ -0,0 +1,71 @@ +package de.hysky.skyblocker.skyblock.item.slottext; + +import de.hysky.skyblocker.skyblock.item.slottext.adders.*; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class SlotTextManager { + private static final SlotTextAdder[] adders = new SlotTextAdder[]{ + new EnchantmentLevelAdder(), + new MinionLevelAdder(), + new PetLevelAdder(), + new SkyblockLevelAdder(), + new SkillLevelAdder(), + new CatacombsLevelAdder.Dungeoneering(), + new CatacombsLevelAdder.DungeonClasses(), + new CatacombsLevelAdder.ReadyUp(), + new RancherBootsSpeedAdder(), + new AttributeShardAdder(), + new PrehistoricEggAdder(), + new PotionLevelAdder(), + new CollectionAdder(), + new CommunityShopAdder() + }; + private static final ArrayList<SlotTextAdder> currentScreenAdders = new ArrayList<>(); + + private SlotTextManager() { + } + + public static void init() { + ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> { + if (screen instanceof HandledScreen<?> && Utils.isOnSkyblock()) { + onScreenChange(screen); + ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear()); + } + }); + } + + private static void onScreenChange(Screen screen) { + final String title = screen.getTitle().getString(); + for (SlotTextAdder adder : adders) { + if (!adder.isEnabled()) continue; + if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) { + currentScreenAdders.add(adder); + } + } + } + + /** + * The returned text is rendered on top of the slot. The text will be scaled if it doesn't fit in the slot, + * but 3 characters should be seen as the maximum to keep it readable and in place as it tends to move around when scaled. + * + * @implNote Only the first adder that returns a non-null text will be used. + * The order of the adders remains the same as they were added to the {@link SlotTextManager#adders} array. + */ + @NotNull + public static List<SlotText> getText(Slot slot) { + if (currentScreenAdders.isEmpty()) return List.of(); + for (SlotTextAdder adder : currentScreenAdders) { + List<SlotText> text = adder.getText(slot); + if (!text.isEmpty()) return text; + } + return List.of(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java new file mode 100644 index 00000000..052b228d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java @@ -0,0 +1,8 @@ +package de.hysky.skyblocker.skyblock.item.slottext; + +public enum TextPosition { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java index ed650e26..811677d7 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java @@ -1,9 +1,22 @@ -package de.hysky.skyblocker.skyblock.item; +package de.hysky.skyblocker.skyblock.item.slottext.adders; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +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.Formatting; +import org.jetbrains.annotations.NotNull; -public class AttributeShards { - private static final Object2ObjectOpenHashMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>(); +import java.util.List; + +public class AttributeShardAdder extends SlotTextAdder { + private static final Object2ObjectMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>(); static { //Weapons @@ -50,10 +63,35 @@ public class AttributeShards { ID_2_SHORT_NAME.put("fishing_speed", "FS"); ID_2_SHORT_NAME.put("hunter", "H"); ID_2_SHORT_NAME.put("trophy_hunter", "TH"); + } + + public AttributeShardAdder() { + super(); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack stack = slot.getStack(); + NbtCompound customData = ItemUtils.getCustomData(stack); + + if (!ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) return List.of(); + + NbtCompound attributesTag = customData.getCompound("attributes"); + String[] attributes = attributesTag.getKeys().toArray(String[]::new); + + if (attributes.length != 1) return List.of(); + String attributeId = attributes[0]; + int attributeLevel = attributesTag.getInt(attributeId); + String attributeInitials = ID_2_SHORT_NAME.getOrDefault(attributeId, ""); + return List.of( + SlotText.bottomRight(Text.literal(String.valueOf(attributeLevel)).withColor(0x34eb77)), + SlotText.topLeft(Text.literal(attributeInitials).formatted(Formatting.AQUA)) + ); } - public static String getShortName(String id) { - return ID_2_SHORT_NAME.getOrDefault(id, ""); + @Override + public boolean isEnabled() { + return SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo; } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java new file mode 100644 index 00000000..da12e867 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java @@ -0,0 +1,105 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +//This class is split into 3 inner classes as there are multiple screens for showing catacombs levels, each with different slot ids or different style of showing the level. +//It's still kept in 1 main class for organization purposes. +public class CatacombsLevelAdder { + private CatacombsLevelAdder() { + } + + public static class Dungeoneering extends SlotTextAdder { + private static final Pattern LEVEL_PATTERN = Pattern.compile(".*?(?:(?<arabic>\\d+)|(?<roman>\\S+))? ?✯?"); + public Dungeoneering() { + super("^Dungeoneering"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + switch (slot.id) { + case 12, 29, 30, 31, 32, 33 -> { + Matcher matcher = LEVEL_PATTERN.matcher(slot.getStack().getName().getString()); + if (!matcher.matches()) return List.of(); + String arabic = matcher.group("arabic"); + String roman = matcher.group("roman"); + if (arabic == null && roman == null) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.RED))); + String level; + if (arabic != null) { + if (!NumberUtils.isDigits(arabic)) return List.of(); //Sanity check + level = arabic; + } else { // roman != null + if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of(); //Sanity check + level = String.valueOf(RomanNumerals.romanToDecimal(roman)); + } + + return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED))); + } + default -> { + return List.of(); + } + } + } + } + + public static class DungeonClasses extends SlotTextAdder { + + public DungeonClasses() { + super("^Dungeon Classes"); //Applies to both screens as they are same in both the placement and the style of the level text. + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + switch (slot.id) { + case 11, 12, 13, 14, 15 -> { + String level = getBracketedLevelFromName(slot.getStack()); + if (!NumberUtils.isDigits(level)) return List.of(); + return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED))); + } + default -> { + return List.of(); + } + } + } + } + + public static class ReadyUp extends SlotTextAdder { + + public ReadyUp() { + super("^Ready Up"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + switch (slot.id) { + case 29, 30, 31, 32, 33 -> { + String level = getBracketedLevelFromName(slot.getStack()); + if (!NumberUtils.isDigits(level)) return List.of(); + return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED))); + } + default -> { + return List.of(); + } + } + } + } + + public static String getBracketedLevelFromName(ItemStack itemStack) { + String name = itemStack.getName().getString(); + if (!name.startsWith("[Lvl ")) return null; + int index = name.indexOf(']'); + if (index == -1) return null; + return name.substring(5, index); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java new file mode 100644 index 00000000..e577f0d8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java @@ -0,0 +1,33 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CollectionAdder extends SlotTextAdder { + private static final Pattern COLLECTION = Pattern.compile("^[\\w ]+ (?<level>[IVXLCDM]+)$"); + + public CollectionAdder() { + super("^\\w+ Collections"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack stack = slot.getStack(); + Matcher matcher = COLLECTION.matcher(stack.getName().getString()); + if (matcher.matches()) { + int level = RomanNumerals.romanToDecimal(matcher.group("level")); + return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).formatted(Formatting.YELLOW))); + } + return List.of(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java new file mode 100644 index 00000000..c7ea17dc --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java @@ -0,0 +1,60 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class CommunityShopAdder extends SlotTextAdder { + private static final byte CATEGORIES_START = 10; + private static final byte CATEGORIES_END = 14; //Inclusive + + public CommunityShopAdder() { + super("^Community Shop"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + for (byte i = CATEGORIES_START; i <= CATEGORIES_END; i++) { + if (slot.inventory.getStack(i).isOf(Items.LIME_STAINED_GLASS_PANE)) { //Only the selected category has a lime stained glass pane, the others have a gray one. + return switch (i) { //This is a switch to allow adding more categories easily in the future, if we ever add more. + case 11 -> getTextForUpgradesScreen(slot); + default -> List.of(); + }; + } + } + return List.of(); + } + + private static List<SlotText> getTextForUpgradesScreen(Slot slot) { + final ItemStack stack = slot.getStack(); + switch (slot.id) { + case 30, 31, 32, 33, 34, 38, 39, 40, 41, 42, 43, 44 -> { + String name = stack.getName().getString(); + int lastIndex = name.lastIndexOf(' '); + String roman = name.substring(lastIndex + 1); // + 1 as we don't want the space + if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of(); + + List<Text> lore = ItemUtils.getLore(stack); + if (lore.isEmpty()) return List.of(); + String lastLine = lore.getLast().getString(); + return List.of(SlotText.bottomLeft(switch (lastLine) { + case "Maxed out!" -> Text.literal("Max").withColor(0xfab387); + case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD); + case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD); + default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7); + })); + + } + } + return List.of(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java new file mode 100644 index 00000000..9c85ae61 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class EnchantmentLevelAdder extends SlotTextAdder { + public EnchantmentLevelAdder() { + super(); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack itemStack = slot.getStack(); + if (!itemStack.isOf(Items.ENCHANTED_BOOK)) return List.of(); + String name = itemStack.getName().getString(); + if (name.equals("Enchanted Book")) { + NbtCompound nbt = ItemUtils.getCustomData(itemStack); + if (nbt.isEmpty() || !nbt.contains("enchantments", NbtElement.COMPOUND_TYPE)) return List.of(); + NbtCompound enchantments = nbt.getCompound("enchantments"); + if (enchantments.getSize() != 1) return List.of(); //Only makes sense to display the level when there's one enchant. + int level = enchantments.getInt(enchantments.getKeys().iterator().next()); + return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).formatted(Formatting.GREEN))); + } else { //In bazaar, the books have the enchantment level in the name + int level = getEnchantLevelFromString(name); + if (level == 0) return List.of(); + return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).formatted(Formatting.GREEN))); + } + } + + private static int getEnchantLevelFromString(String str) { + String romanNumeral = str.substring(str.lastIndexOf(' ') + 1); //+1 because we don't need the space itself + return RomanNumerals.romanToDecimal(romanNumeral); //Temporary line. The method will be moved out later. + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java new file mode 100644 index 00000000..b9fe130f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MinionLevelAdder extends SlotTextAdder { + private static final Pattern MINION_PATTERN = Pattern.compile(".* Minion ([IVXLCDM]+)"); + public MinionLevelAdder() { + super(); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + ItemStack itemStack = slot.getStack(); + if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of(); + Matcher matcher = MINION_PATTERN.matcher(itemStack.getName().getString()); + if (!matcher.matches()) return List.of(); + String romanNumeral = matcher.group(1); + if (!RomanNumerals.isValidRomanNumeral(romanNumeral)) return List.of(); + int level = RomanNumerals.romanToDecimal(romanNumeral); + return List.of(SlotText.topRight(Text.literal(String.valueOf(level)).formatted(Formatting.AQUA))); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java new file mode 100644 index 00000000..3813563a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PetLevelAdder extends SlotTextAdder { + public PetLevelAdder() { + super(); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + ItemStack itemStack = slot.getStack(); + if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of(); + String level = CatacombsLevelAdder.getBracketedLevelFromName(itemStack); + if (!NumberUtils.isDigits(level)) return List.of(); + return List.of(SlotText.topLeft(Text.literal(level).formatted(Formatting.GOLD))); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java new file mode 100644 index 00000000..457d2964 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java @@ -0,0 +1,27 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PotionLevelAdder extends SlotTextAdder { + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack stack = slot.getStack(); + NbtCompound customData = ItemUtils.getCustomData(stack); + if (customData.contains("potion_level", NbtElement.INT_TYPE)) { + int level = customData.getInt("potion_level"); + return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).formatted(Formatting.AQUA))); + } + return List.of(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java new file mode 100644 index 00000000..a157efee --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PrehistoricEggAdder extends SlotTextAdder { + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack stack = slot.getStack(); + if (!stack.isOf(Items.PLAYER_HEAD) || !StringUtils.equals(stack.getSkyblockId(), "PREHISTORIC_EGG")) return List.of(); + NbtCompound nbt = ItemUtils.getCustomData(stack); + if (!nbt.contains("blocks_walked", NbtElement.INT_TYPE)) return List.of(); + int walked = nbt.getInt("blocks_walked"); + + String walkedstr; + if (walked < 1000) walkedstr = String.valueOf(walked); + else if (walked < 10000) walkedstr = String.format("%.1fk", walked/1000.0f); + else walkedstr = walked / 1000 + "k"; + + return List.of(SlotText.bottomLeft(Text.literal(walkedstr).formatted(Formatting.GOLD))); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java new file mode 100644 index 00000000..1f92fb8a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java @@ -0,0 +1,36 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RancherBootsSpeedAdder extends SlotTextAdder { + private static final Pattern SPEED_PATTERN = Pattern.compile("Current Speed Cap: (\\d+) ?(\\d+)?"); + + public RancherBootsSpeedAdder() { + super(); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + final ItemStack itemStack = slot.getStack(); + // V null-safe equals. + if (!itemStack.isOf(Items.LEATHER_BOOTS) && !StringUtils.equals(itemStack.getSkyblockId(), "RANCHERS_BOOTS")) return List.of(); + Matcher matcher = ItemUtils.getLoreLineIfMatch(slot.getStack(), SPEED_PATTERN); + if (matcher == null) return List.of(); + String speed = matcher.group(2); + if (speed == null) speed = matcher.group(1); //2nd group only matches when the speed cap is set to a number beyond the player's actual speed cap. + return List.of(SlotText.bottomLeft(Text.literal(speed).formatted(Formatting.GREEN))); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java new file mode 100644 index 00000000..095982af --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.RomanNumerals; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class SkillLevelAdder extends SlotTextAdder { + public SkillLevelAdder() { + super("^Your Skills"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + switch (slot.id) { + case 19, 20, 21, 22, 23, 24, 25, 29, 30, 31, 32 -> { //These are the slots that contain the skill items. Note that they aren't continuous, as there are 2 rows. + String name = slot.getStack().getName().getString(); + int lastIndex = name.lastIndexOf(' '); + if (lastIndex == -1) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.LIGHT_PURPLE))); //Skills without any levels don't display any roman numerals. Probably because 0 doesn't exist. + String romanNumeral = name.substring(lastIndex + 1); //+1 because we don't need the space itself + //The "romanNumeral" might be a latin numeral, too. There's a skyblock setting for this, so we have to do it this way V + return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(RomanNumerals.isValidRomanNumeral(romanNumeral) ? RomanNumerals.romanToDecimal(romanNumeral) : Integer.parseInt(romanNumeral))).formatted(Formatting.LIGHT_PURPLE))); + } + default -> { + return List.of(); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java new file mode 100644 index 00000000..8b528508 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java @@ -0,0 +1,29 @@ +package de.hysky.skyblocker.skyblock.item.slottext.adders; + +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class SkyblockLevelAdder extends SlotTextAdder { + public SkyblockLevelAdder() { + super("^SkyBlock Menu"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + if (slot.getIndex() != 22) return List.of(); + List<Text> lore = ItemUtils.getLore(slot.getStack()); + if (lore.isEmpty()) return List.of(); + List<Text> siblings = lore.getFirst().getSiblings(); + if (siblings.size() < 3) return List.of(); + Text levelText = siblings.get(2); //The 3rd child is the level text itself + if (!NumberUtils.isDigits(levelText.getString())) return List.of(); + return List.of(SlotText.bottomLeft(levelText)); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java index 8798a139..992206ad 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java @@ -108,7 +108,7 @@ public class AccessoriesHelper { .put(page, new ObjectOpenHashSet<>(accessoryIds)); } - static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) { + public static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) { if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null); Accessory accessory = ACCESSORY_DATA.get(accessoryId); @@ -208,7 +208,7 @@ public class AccessoriesHelper { } } - enum AccessoryReport { + public enum AccessoryReport { HAS_HIGHEST_TIER, //You've collected the highest tier - Collected IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade -- Shows you what tier this accessory is in its family HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java index c5279c61..b93ca77a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java @@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntIntPair; import it.unimi.dsi.fastutil.ints.IntObjectPair; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; import net.minecraft.client.gui.tooltip.TooltipComponent; import net.minecraft.item.ItemStack; @@ -34,8 +33,7 @@ public class CompactorDeletorPreview { 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); + public static boolean drawPreview(DrawContext context, ItemStack stack, List<Text> tooltips, String type, String size, int x, int y) { int targetIndex = getTargetIndex(tooltips); if (targetIndex == -1) return false; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java deleted file mode 100644 index 46babc8b..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.hysky.skyblocker.skyblock.item.tooltip; - -import de.hysky.skyblocker.utils.Constants; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.text.MutableText; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import net.minecraft.util.StringIdentifiable; - -public class ExoticTooltip { - public static String getExpectedHex(String id) { - String color = TooltipInfoType.COLOR.getData().get(id).getAsString(); - if (color != null) { - String[] RGBValues = color.split(","); - return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2])); - } else { - ItemTooltip.LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id); - return null; - } - } - - public static boolean isException(String id, String hex) { - if (id.startsWith("LEATHER") || id.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(id)) { - return true; - } - if (id.startsWith("RANCHER")) { - return Constants.RANCHERS.contains(hex); - } - if (id.contains("ADAPTIVE_CHESTPLATE")) { - return Constants.ADAPTIVE_CHEST.contains(hex); - } else if (id.contains("ADAPTIVE")) { - return Constants.ADAPTIVE.contains(hex); - } - if (id.startsWith("REAPER")) { - return Constants.REAPER.contains(hex); - } - if (id.startsWith("FAIRY")) { - return Constants.FAIRY_HEXES.contains(hex); - } - if (id.startsWith("CRYSTAL")) { - return Constants.CRYSTAL_HEXES.contains(hex); - } - if (id.contains("SPOOK")) { - return Constants.SPOOK.contains(hex); - } - return false; - } - - public static DyeType checkDyeType(String hex) { - if (Constants.CRYSTAL_HEXES.contains(hex)) { - return DyeType.CRYSTAL; - } - if (Constants.FAIRY_HEXES.contains(hex)) { - return DyeType.FAIRY; - } - if (Constants.OG_FAIRY_HEXES.contains(hex)) { - return DyeType.OG_FAIRY; - } - if (Constants.SPOOK.contains(hex)) { - return DyeType.SPOOK; - } - if (Constants.GLITCHED.contains(hex)) { - return DyeType.GLITCHED; - } - return DyeType.EXOTIC; - } - - public static boolean intendedDyed(NbtCompound customData) { - return customData.contains("dye_item"); - } - - public enum DyeType implements StringIdentifiable { - CRYSTAL("crystal", Formatting.AQUA), - FAIRY("fairy", Formatting.LIGHT_PURPLE), - OG_FAIRY("og_fairy", Formatting.DARK_PURPLE), - SPOOK("spook", Formatting.RED), - GLITCHED("glitched", Formatting.BLUE), - EXOTIC("exotic", Formatting.GOLD); - private final String name; - private final Formatting formatting; - - DyeType(String name, Formatting formatting) { - this.name = name; - this.formatting = formatting; - } - - @Override - public String asString() { - return name; - } - - public MutableText getTranslatedText() { - return Text.translatable("skyblocker.exotic." + name).formatted(formatting); - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java index 8c083e25..e6a364e4 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java @@ -1,259 +1,28 @@ package de.hysky.skyblocker.skyblock.item.tooltip; -import com.google.gson.JsonObject; -import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.configs.GeneralConfig; -import de.hysky.skyblocker.skyblock.item.MuseumItemCache; -import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper.AccessoryReport; import de.hysky.skyblocker.utils.Constants; -import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.Pair; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.item.TooltipType; -import net.minecraft.component.DataComponentTypes; -import net.minecraft.component.type.DyedColorComponent; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtElement; -import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; - import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; public class ItemTooltip { protected static final Logger LOGGER = LoggerFactory.getLogger(ItemTooltip.class.getName()); private static final MinecraftClient client = MinecraftClient.getInstance(); - protected static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip; + public static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip; private static volatile boolean sentNullWarning = false; - public static void getTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) { - if (!Utils.isOnSkyblock() || client.player == null) return; - - smoothenLines(lines); - - String name = getInternalNameFromNBT(stack, false); - String internalID = getInternalNameFromNBT(stack, true); - String neuName = name; - if (name == null || internalID == null) return; - - if (name.startsWith("ISSHINY_")) { - name = "SHINY_" + internalID; - neuName = internalID; - } - - if (lines.isEmpty()) { - return; - } - - int count = stack.getCount(); - boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:")); - - if (TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) { - lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:")) - .formatted(Formatting.YELLOW) - .append(getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), count))); - } - - boolean bazaarExist = false; - - if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened) { - JsonObject getItem = TooltipInfoType.BAZAAR.getData().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 (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened && !bazaarExist) { - lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:")) - .formatted(Formatting.GOLD) - .append(getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), count))); - lbinExist = true; - } - - if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) { - if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) { - nullWarning(); - } else { - /* - We are skipping check average prices for potions, runes - and enchanted books because there is no data for their in API. - */ - neuName = getNeuName(internalID, neuName); - - if (!neuName.isEmpty() && lbinExist) { - GeneralConfig.Average type = config.avg; - - // "No data" line because of API not keeping old data, it causes NullPointerException - if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) { - lines.add( - Text.literal(String.format("%-19s", "1 Day Avg. Price:")) - .formatted(Formatting.GOLD) - .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null - ? Text.literal("No data").formatted(Formatting.RED) - : getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count) - ) - ); - } - if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) { - lines.add( - Text.literal(String.format("%-19s", "3 Day Avg. Price:")) - .formatted(Formatting.GOLD) - .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null - ? Text.literal("No data").formatted(Formatting.RED) - : getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count) - ) - ); - } - } - } - } - - final Map<Integer, String> itemTierFloors = Map.of( - 1, "F1", - 2, "F2", - 3, "F3", - 4, "F4/M1", - 5, "F5/M2", - 6, "F6/M3", - 7, "F7/M4", - 8, "M5", - 9, "M6", - 10, "M7" - ); - - if (SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) { - NbtCompound customData = ItemUtils.getCustomData(stack); - if (customData != null && customData.contains("baseStatBoostPercentage")) { - int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage"); - boolean maxQuality = baseStatBoostPercentage == 50; - if (maxQuality) { - lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD)); - } else { - lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE)); - } - if (customData.contains("item_tier")) { // sometimes it just isn't here? - int itemTier = customData.getInt("item_tier"); - if (maxQuality) { - lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD)); - } else { - lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.BLUE)); - } - } - } - } - - if (TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) { - lines.add(Text.literal(String.format("%-20s", "Motes Price:")) - .formatted(Formatting.LIGHT_PURPLE) - .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), count))); - } - - if (TooltipInfoType.OBTAINED.isTooltipEnabled()) { - String timestamp = ItemUtils.getTimestamp(stack); - - if (!timestamp.isEmpty()) { - lines.add(Text.literal(String.format("%-21s", "Obtained: ")) - .formatted(Formatting.LIGHT_PURPLE) - .append(Text.literal(timestamp).formatted(Formatting.RED))); - } - } - - if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID) && !bazaarOpened) { - String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString(); - String format = switch (itemCategory) { - case "Weapons" -> "%-18s"; - case "Armor" -> "%-19s"; - default -> "%-20s"; - }; - - //Special case the special category so that it doesn't always display not donated - if (itemCategory.equals("Special")) { - lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")")) - .formatted(Formatting.LIGHT_PURPLE)); - } else { - NbtCompound customData = ItemUtils.getCustomData(stack); - boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID); - - Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED; - - lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):")) - .formatted(Formatting.LIGHT_PURPLE) - .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD)) - .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting))); - } - } - - if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) { - String uuid = ItemUtils.getItemUuid(stack); - boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid); - //DyedColorComponent#getColor returns ARGB so we mask out the alpha bits - int dyeColor = DyedColorComponent.getColor(stack, 0); - - // dyeColor will have alpha = 255 if it's dyed, and alpha = 0 if it's not dyed, - if (!hasCustomDye && dyeColor != 0) { - dyeColor = dyeColor & 0x00FFFFFF; - String colorHex = String.format("%06X", dyeColor); - String expectedHex = ExoticTooltip.getExpectedHex(internalID); - - boolean correctLine = false; - for (Text text : lines) { - String existingTooltip = text.getString() + " "; - if (existingTooltip.startsWith("Color: ")) { - correctLine = true; - - addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip); - break; - } - } - - if (!correctLine) { - addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, ""); - } - } - } - - if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) { - Pair<AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID); - - if (report.left() != AccessoryReport.INELIGIBLE) { - MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542); - - Text stateText = switch (report.left()) { - case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN); - case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff)); - case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff)); - case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff)); - case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff)); - - //Should never be the case - default -> Text.literal("? Unknown").formatted(Formatting.GRAY); - }; - - lines.add(title.append(stateText)); - } - } - } - @NotNull public static String getNeuName(String internalID, String neuName) { switch (internalID) { @@ -280,13 +49,6 @@ public class ItemTooltip { return neuName; } - private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) { - if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !ExoticTooltip.isException(internalID, colorHex) && !ExoticTooltip.intendedDyed(customData)) { - final ExoticTooltip.DyeType type = ExoticTooltip.checkDyeType(colorHex); - lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")")); - } - } - public static void nullWarning() { if (!sentNullWarning && client.player != null) { LOGGER.warn(Constants.PREFIX.get().append(Text.translatable("skyblocker.itemTooltip.nullMessage")).getString()); @@ -294,69 +56,7 @@ public class ItemTooltip { } } - // TODO What in the world is this? - public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) { - NbtCompound customData = ItemUtils.getCustomData(stack); - - if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) { - return null; - } - String internalName = customData.getString(ItemUtils.ID); - - if (internalIDOnly) { - return internalName; - } - - // Transformation to API format. - if (customData.contains("is_shiny")) { - return "ISSHINY_" + internalName; - } - - switch (internalName) { - case "ENCHANTED_BOOK" -> { - if (customData.contains("enchantments")) { - NbtCompound enchants = customData.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 (customData.contains("petInfo")) { - JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class); - return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString(); - } - } - case "POTION" -> { - String enhanced = customData.contains("enhanced") ? "_ENHANCED" : ""; - String extended = customData.contains("extended") ? "_EXTENDED" : ""; - String splash = customData.contains("splash") ? "_SPLASH" : ""; - if (customData.contains("potion") && customData.contains("potion_level")) { - return (customData.getString("potion") + "_" + internalName + "_" + customData.getInt("potion_level") - + enhanced + extended + splash).toUpperCase(Locale.ENGLISH); - } - } - case "RUNE" -> { - if (customData.contains("runes")) { - NbtCompound runes = customData.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 (customData.contains("attributes")) { - NbtCompound shards = customData.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) { + public static Text getCoinsMessage(double price, int count) { // Format the price string once String priceString = String.format(Locale.ENGLISH, "%1$,.1f", price); @@ -367,47 +67,9 @@ public class ItemTooltip { // 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().otherLocations.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; - } - - //This is static to not create a new text object for each line in every item - private static final Text BUMPY_LINE = Text.literal("-----------------").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH); - - private static void smoothenLines(List<Text> lines) { - for (int i = 0; i < lines.size(); i++) { - List<Text> lineSiblings = lines.get(i).getSiblings(); - //Compare the first sibling rather than the whole object as the style of the root object can change while visually staying the same - if (lineSiblings.size() == 1 && lineSiblings.getFirst().equals(BUMPY_LINE)) { - lines.set(i, createSmoothLine()); - } - } - } - public static Text createSmoothLine() { - return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD); + return Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA) + .append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY)); } // If these options is true beforehand, the client will get first data of these options while loading. diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java new file mode 100644 index 00000000..9bd63adc --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java @@ -0,0 +1,45 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Extend this class and add it to {@link TooltipManager#adders} to add additional text to tooltips. + */ +public abstract class TooltipAdder extends AbstractContainerMatcher { + /** + * The priority of this adder. Lower priority means it will be applied first. + * @apiNote Consider taking this value on your class' constructor and setting it from {@link TooltipManager#adders} to make it easy to read and maintain. + */ + public final int priority; + + protected TooltipAdder(String titlePattern, int priority) { + super(titlePattern); + this.priority = priority; + } + + protected TooltipAdder(Pattern titlePattern, int priority) { + super(titlePattern); + this.priority = priority; + } + + /** + * Creates a TooltipAdder that will be applied to all screens. + */ + protected TooltipAdder(int priority) { + super(); + this.priority = priority; + } + + /** + * @implNote The first element of the lines list holds the item's display name, + * as it's a list of all lines that will be displayed in the tooltip. + */ + public abstract void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines); +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java new file mode 100644 index 00000000..e3a2ef04 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java @@ -0,0 +1,87 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; +import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.*; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class TooltipManager { + private static final TooltipAdder[] adders = new TooltipAdder[]{ + new LineSmoothener(), // Applies before anything else + new SupercraftReminder(), + new ChocolateFactorySolver.Tooltip(), + new NpcPriceTooltip(1), + new BazaarPriceTooltip(2), + new LBinTooltip(3), + new AvgBinTooltip(4), + new DungeonQualityTooltip(5), + new MotesTooltip(6), + new ObtainedDateTooltip(7), + new MuseumTooltip(8), + new ColorTooltip(9), + new AccessoryTooltip(10), + }; + private static final ArrayList<TooltipAdder> currentScreenAdders = new ArrayList<>(); + + private TooltipManager() { + } + + public static void init() { + ItemTooltipCallback.EVENT.register((stack, tooltipContext, tooltipType, lines) -> { + if (MinecraftClient.getInstance().currentScreen instanceof HandledScreen<?> handledScreen) { + addToTooltip(((HandledScreenAccessor) handledScreen).getFocusedSlot(), stack, lines); + } else { + addToTooltip(null, stack, lines); + } + }); + ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> { + onScreenChange(screen); + ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear()); + }); + } + + private static void onScreenChange(Screen screen) { + final String title = screen.getTitle().getString(); + currentScreenAdders.clear(); + for (TooltipAdder adder : adders) { + if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) { + currentScreenAdders.add(adder); + } + } + currentScreenAdders.sort(Comparator.comparingInt(adder -> adder.priority)); + } + + /** + * <p>Adds additional text from all adders that are applicable to the current screen. + * This method is run on each tooltip render, so don't do any heavy calculations here.</p> + * + * <p>If you want to add info to the tooltips of multiple items, consider using a switch statement with {@code focusedSlot.getIndex()}</p> + * + * @param focusedSlot The slot that is currently focused by the cursor. + * @param stack The stack to render the tooltip for. + * @param lines The tooltip lines of the focused item. This includes the display name, as it's a part of the tooltip (at index 0). + * @return The lines list itself after all adders have added their text. + * @deprecated This method is public only for the sake of the mixin. Don't call directly, not that there is any point to it. + */ + @Deprecated + public static List<Text> addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (!Utils.isOnSkyblock()) return lines; + for (TooltipAdder adder : currentScreenAdders) { + adder.addToTooltip(focusedSlot, stack, lines); + } + return lines; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java new file mode 100644 index 00000000..caed0e0e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java @@ -0,0 +1,45 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import it.unimi.dsi.fastutil.Pair; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AccessoryTooltip extends TooltipAdder { + public AccessoryTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + final String internalID = stack.getSkyblockId(); + if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) { + Pair<AccessoriesHelper.AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID); + + if (report.left() != AccessoriesHelper.AccessoryReport.INELIGIBLE) { + MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542); + + Text stateText = switch (report.left()) { + case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN); + case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff)); + case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff)); + case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff)); + case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff)); + + //Should never be the case + default -> Text.literal("? Unknown").formatted(Formatting.GRAY); + }; + + lines.add(title.append(stateText)); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java new file mode 100644 index 00000000..d7a56b95 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java @@ -0,0 +1,63 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.GeneralConfig; +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AvgBinTooltip extends TooltipAdder { + public AvgBinTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + String neuName = stack.getNeuName(); + String internalID = stack.getSkyblockId(); + if (neuName == null || internalID == null) return; + + if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) { + if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) { + ItemTooltip.nullWarning(); + } else { + /* + We are skipping check average prices for potions, runes + and enchanted books because there is no data for their in API. + */ + if (!neuName.isEmpty() && LBinTooltip.lbinExist) { + GeneralConfig.Average type = ItemTooltip.config.avg; + + // "No data" line because of API not keeping old data, it causes NullPointerException + if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) { + lines.add( + Text.literal(String.format("%-19s", "1 Day Avg. Price:")) + .formatted(Formatting.GOLD) + .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : ItemTooltip.getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount()) + ) + ); + } + if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) { + lines.add( + Text.literal(String.format("%-19s", "3 Day Avg. Price:")) + .formatted(Formatting.GOLD) + .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null + ? Text.literal("No data").formatted(Formatting.RED) + : ItemTooltip.getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount()) + ) + ); + } + } + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java new file mode 100644 index 00000000..d2fa563b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java @@ -0,0 +1,57 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class BazaarPriceTooltip extends TooltipAdder { + public static boolean bazaarExist = false; + + public BazaarPriceTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + bazaarExist = false; + final String internalID = stack.getSkyblockId(); + if (internalID == null) return; + String name = stack.getSkyblockApiId(); + if (name == null) return; + + if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID; + + if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name)) { + int amount; + if (lines.get(1).getString().endsWith("Sack")) { + //The amount is in the 2nd sibling of the 3rd line of the lore. here V + //Example line: empty[style={color=dark_purple,!italic}, siblings=[literal{Stored: }[style={color=gray}], literal{0}[style={color=dark_gray}], literal{/20k}[style={color=gray}]] + String line = lines.get(3).getSiblings().get(1).getString().replace(",", ""); + amount = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount(); + } else { + amount = stack.getCount(); + } + JsonObject getItem = TooltipInfoType.BAZAAR.getData().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) + : ItemTooltip.getCoinsMessage(getItem.get("buyPrice").getAsDouble(), amount))); + 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) + : ItemTooltip.getCoinsMessage(getItem.get("sellPrice").getAsDouble(), amount))); + bazaarExist = true; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java new file mode 100644 index 00000000..26a040ec --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java @@ -0,0 +1,135 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.StringIdentifiable; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class ColorTooltip extends TooltipAdder { + private static final Logger LOGGER = LoggerFactory.getLogger(ColorTooltip.class); + + public ColorTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + final String internalID = stack.getSkyblockId(); + if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) { + String uuid = ItemUtils.getItemUuid(stack); + boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid); + //DyedColorComponent#getColor returns ARGB so we mask out the alpha bits + int dyeColor = DyedColorComponent.getColor(stack, 0); + + // dyeColor will have alpha = 255 if it's dyed, and alpha = 0 if it's not dyed, + if (!hasCustomDye && dyeColor != 0) { + dyeColor = dyeColor & 0x00FFFFFF; + String colorHex = String.format("%06X", dyeColor); + String expectedHex = getExpectedHex(internalID); + + boolean correctLine = false; + for (Text text : lines) { + String existingTooltip = text.getString() + " "; + if (existingTooltip.startsWith("Color: ")) { + correctLine = true; + + addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip); + break; + } + } + + if (!correctLine) { + addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, ""); + } + } + } + } + + private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) { + if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !isException(internalID, colorHex) && !intendedDyed(customData)) { + final DyeType type = checkDyeType(colorHex); + lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")")); + } + } + + public static String getExpectedHex(String id) { + String color = TooltipInfoType.COLOR.getData().get(id).getAsString(); + if (color != null) { + String[] RGBValues = color.split(","); + return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2])); + } else { + LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id); + return null; + } + } + + public static boolean isException(String id, String hex) { + return switch (id) { + case String it when it.startsWith("LEATHER") || it.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(it) -> true; + case String it when it.startsWith("RANCHER") -> Constants.RANCHERS.contains(hex); + case String it when it.contains("ADAPTIVE_CHESTPLATE") -> Constants.ADAPTIVE_CHEST.contains(hex); + case String it when it.contains("ADAPTIVE") -> Constants.ADAPTIVE.contains(hex); + case String it when it.contains("REAPER") -> Constants.REAPER.contains(hex); + case String it when it.contains("FAIRY") -> Constants.FAIRY_HEXES.contains(hex); + case String it when it.contains("CRYSTAL") -> Constants.CRYSTAL_HEXES.contains(hex); + case String it when it.contains("SPOOK") -> Constants.SPOOK.contains(hex); + default -> false; + }; + } + + public static DyeType checkDyeType(String hex) { + return switch (hex) { + case String it when Constants.CRYSTAL_HEXES.contains(it) -> DyeType.CRYSTAL; + case String it when Constants.FAIRY_HEXES.contains(it) -> DyeType.FAIRY; + case String it when Constants.OG_FAIRY_HEXES.contains(it) -> DyeType.OG_FAIRY; + case String it when Constants.SPOOK.contains(it) -> DyeType.SPOOK; + case String it when Constants.GLITCHED.contains(it) -> DyeType.GLITCHED; + default -> DyeType.EXOTIC; + }; + } + + public static boolean intendedDyed(NbtCompound customData) { + return customData.contains("dye_item"); + } + + public enum DyeType implements StringIdentifiable { + CRYSTAL("crystal", Formatting.AQUA), + FAIRY("fairy", Formatting.LIGHT_PURPLE), + OG_FAIRY("og_fairy", Formatting.DARK_PURPLE), + SPOOK("spook", Formatting.RED), + GLITCHED("glitched", Formatting.BLUE), + EXOTIC("exotic", Formatting.GOLD); + private final String name; + private final Formatting formatting; + + DyeType(String name, Formatting formatting) { + this.name = name; + this.formatting = formatting; + } + + @Override + public String asString() { + return name; + } + + public MutableText getTranslatedText() { + return Text.translatable("skyblocker.exotic." + name).formatted(formatting); + } + } + +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java new file mode 100644 index 00000000..0b1d993d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java @@ -0,0 +1,59 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.utils.ItemUtils; +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.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DungeonQualityTooltip extends TooltipAdder { + public DungeonQualityTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (!SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) return; + NbtCompound customData = ItemUtils.getCustomData(stack); + if (customData == null || !customData.contains("baseStatBoostPercentage")) return; + int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage"); + boolean maxQuality = baseStatBoostPercentage == 50; + if (maxQuality) { + lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD)); + } else { + lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE)); + } + + if (customData.contains("item_tier")) { // sometimes it just isn't here? + int itemTier = customData.getInt("item_tier"); + if (maxQuality) { + lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD)); + } else { + lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.BLUE)); + } + } + } + + final String getItemTierFloor(int tier) { + return switch (tier) { + case 0 -> "E"; + case 1 -> "F1"; + case 2 -> "F2"; + case 3 -> "F3"; + case 4 -> "F4/M1"; + case 5 -> "F5/M2"; + case 6 -> "F6/M3"; + case 7 -> "F7/M4"; + case 8 -> "M5"; + case 9 -> "M6"; + case 10 -> "M7"; + default -> "Unknown"; + }; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java new file mode 100644 index 00000000..e6930c32 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java @@ -0,0 +1,40 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class LBinTooltip extends TooltipAdder { + public static boolean lbinExist = false; + + public LBinTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + lbinExist = false; + final String internalID = stack.getSkyblockId(); + if (internalID == null) return; + String name = stack.getSkyblockApiId(); + if (name == null) return; + + if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID; + + // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api + + if (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !BazaarPriceTooltip.bazaarExist) { + lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:")) + .formatted(Formatting.GOLD) + .append(ItemTooltip.getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), stack.getCount()))); + lbinExist = true; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java new file mode 100644 index 00000000..0e997834 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class LineSmoothener extends TooltipAdder { + //This is static to not create a new text object for each line in every item + private static final Text BUMPY_LINE = Text.literal("-----------------").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH); + + public static Text createSmoothLine() { + return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD); + } + + public LineSmoothener() { + super(Integer.MIN_VALUE); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + for (int i = 0; i < lines.size(); i++) { + List<Text> lineSiblings = lines.get(i).getSiblings(); + //Compare the first sibling rather than the whole object as the style of the root object can change while visually staying the same + if (lineSiblings.size() == 1 && lineSiblings.getFirst().equals(BUMPY_LINE)) { + lines.set(i, createSmoothLine()); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java new file mode 100644 index 00000000..a0aa8d94 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java @@ -0,0 +1,50 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Locale; + +public class MotesTooltip extends TooltipAdder { + public MotesTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + final String internalID = stack.getSkyblockId(); + if (internalID != null && TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) { + lines.add(Text.literal(String.format("%-20s", "Motes Price:")) + .formatted(Formatting.LIGHT_PURPLE) + .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), stack.getCount()))); + } + } + + private static Text getMotesMessage(int price, int count) { + float motesMultiplier = SkyblockerConfigManager.get().otherLocations.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; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java new file mode 100644 index 00000000..5c21d2df --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java @@ -0,0 +1,49 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.MuseumItemCache; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.utils.ItemUtils; +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.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class MuseumTooltip extends TooltipAdder { + public MuseumTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + final String internalID = stack.getSkyblockId(); + if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID)) { + String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString(); + String format = switch (itemCategory) { + case "Weapons" -> "%-18s"; + case "Armor" -> "%-19s"; + default -> "%-20s"; + }; + + //Special case the special category so that it doesn't always display not donated + if (itemCategory.equals("Special")) { + lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")")) + .formatted(Formatting.LIGHT_PURPLE)); + } else { + NbtCompound customData = ItemUtils.getCustomData(stack); + boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID); + + Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED; + + lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):")) + .formatted(Formatting.LIGHT_PURPLE) + .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD)) + .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting))); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java new file mode 100644 index 00000000..672201d5 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class NpcPriceTooltip extends TooltipAdder { + public NpcPriceTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + final String internalID = stack.getSkyblockId(); + if (internalID != null && TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) { + lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:")) + .formatted(Formatting.YELLOW) + .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), stack.getCount()))); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java new file mode 100644 index 00000000..9f405c58 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.List; +import java.util.Locale; + +public class ObtainedDateTooltip extends TooltipAdder { + private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH); + private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH); + + public ObtainedDateTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (TooltipInfoType.OBTAINED.isTooltipEnabled()) { + String timestamp = getTimestamp(stack); + + if (!timestamp.isEmpty()) { + lines.add(Text.empty() + .append(Text.literal(String.format("%-21s", "Obtained: ")).formatted(Formatting.LIGHT_PURPLE)) + .append(Text.literal(timestamp).formatted(Formatting.RED))); + } + } + } + + /** + * This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum. + * Currently, there are two types of string 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. + * <p> + * This also handles the long timestamp format introduced in January 2024 where the timestamp is in epoch milliseconds. + * + * @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 customData = ItemUtils.getCustomData(stack); + + if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) { + Instant date = Instant.ofEpochMilli(customData.getLong("timestamp")); + return OBTAINED_DATE_FORMATTER.format(date); + } + + if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) { + TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp")); + return OBTAINED_DATE_FORMATTER.format(date); + } + + return ""; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java new file mode 100644 index 00000000..47d2bd48 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java @@ -0,0 +1,32 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.regex.Pattern; + +public class SupercraftReminder extends TooltipAdder { + private static final byte SUPERCRAFT_SLOT = 32; + private static final byte RECIPE_RESULT_SLOT = 25; + + public SupercraftReminder() { + super(Pattern.compile("^.+ Recipe$"), Integer.MIN_VALUE); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (focusedSlot == null || focusedSlot.id != SUPERCRAFT_SLOT || !stack.isOf(Items.GOLDEN_PICKAXE)) return; + String uuid = ItemUtils.getItemUuid(focusedSlot.inventory.getStack(RECIPE_RESULT_SLOT)); + if (!uuid.isEmpty()) return; //Items with UUID can't be stacked, and therefore the shift-click feature doesn't matter + int index = lines.size() - 1; + if (lines.get(lines.size() - 2).getString().equals("Recipe not unlocked!")) index--; //Place it right below the "Right-Click to set amount" line + lines.add(index, Text.literal("Shift-Click to maximize the amount!").formatted(Formatting.GOLD)); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java new file mode 100644 index 00000000..4109246d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java @@ -0,0 +1,89 @@ +package de.hysky.skyblocker.skyblock.itemlist; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.List; + +public class ItemListTab extends ItemListWidget.TabContainerWidget { + + private SearchResultsWidget results; + private final MinecraftClient client; + private TextFieldWidget searchField; + + public ItemListTab(int x, int y, MinecraftClient client, TextFieldWidget searchField) { + super(x, y, Text.literal("Item List Tab")); + this.client = client; + this.searchField = searchField; + if (ItemRepository.filesImported()) { + this.results = new SearchResultsWidget(this.client, x - 9, y - 9 ); + this.results.updateSearchResult(searchField == null ? "": this.searchField.getText()); + } + } + + @Override + public List<? extends Element> children() { + return List.of(results, searchField); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(0.0D, 0.0D, 100.0D); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int x = getX(); + int y = getY(); + + // all coordinates offseted -9 + if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) { + Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); + context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1); + } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) { + Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); + context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1); + } else { + this.searchField.render(context, mouseX, mouseY, delta); + } + if (ItemRepository.filesImported()) { + if (results == null) { + this.results = new SearchResultsWidget(this.client, x - 9, y - 9); + } + this.results.updateSearchResult(this.searchField.getText()); + this.results.render(context, mouseX, mouseY, delta); + } + matrices.pop(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setSearchField(TextFieldWidget searchField) { + this.searchField = searchField; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!visible) return false; + if (this.searchField.mouseClicked(mouseX, mouseY, button)) { + this.results.closeRecipeView(); + this.searchField.setFocused(true); + return true; + } else { + this.searchField.setFocused(false); + return this.results.mouseClicked(mouseX, mouseY, button); + } + } + + @Override + public void drawTooltip(DrawContext context, int mouseX, int mouseY) { + if (this.results != null) this.results.drawTooltip(context, mouseX, mouseY); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java index 6120528c..a618f4df 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java @@ -1,103 +1,135 @@ package de.hysky.skyblocker.skyblock.itemlist; -import com.mojang.blaze3d.systems.RenderSystem; - import de.hysky.skyblocker.mixins.accessors.RecipeBookWidgetAccessor; +import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ContainerWidget; import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; import net.minecraft.screen.AbstractRecipeScreenHandler; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; + +import java.util.ArrayList; +import java.util.List; @Environment(value = EnvType.CLIENT) public class ItemListWidget extends RecipeBookWidget { private int parentWidth; private int parentHeight; private int leftOffset; - private TextFieldWidget searchField; - private SearchResultsWidget results; + + private TabContainerWidget currentTabContent; + private final List<Pair<SideTabButtonWidget, TabContainerWidget>> tabs = new ArrayList<>(2); + private ItemListTab itemListTab; + + private static int currentTab = 0; public ItemListWidget() { super(); } - public void updateSearchResult() { - this.results.updateSearchResult(((RecipeBookWidgetAccessor) this).getSearchText()); - } - @Override public void initialize(int parentWidth, int parentHeight, MinecraftClient client, boolean narrow, AbstractRecipeScreenHandler<?> craftingScreenHandler) { super.initialize(parentWidth, parentHeight, client, narrow, craftingScreenHandler); this.parentWidth = parentWidth; this.parentHeight = parentHeight; this.leftOffset = narrow ? 0 : 86; - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); - int x = (this.parentWidth - 147) / 2 - this.leftOffset; - int y = (this.parentHeight - 166) / 2; - if (ItemRepository.filesImported()) { - this.results = new SearchResultsWidget(this.client, x, y); - this.updateSearchResult(); - } + TextFieldWidget searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); + int x = (parentWidth - 147) / 2 - leftOffset; + int y = (parentHeight - 166) / 2; + + // Init all the tabs, content and the tab button on the left + tabs.clear(); + + // Item List + itemListTab = new ItemListTab(x + 9, y + 9, this.client, searchField); + SideTabButtonWidget itemListTabButton = new SideTabButtonWidget(x - 30, y + 3, currentTab == 0, new ItemStack(Items.CRAFTING_TABLE)); + itemListTabButton.setTooltip(Tooltip.of(Text.literal("Item List"))); + if (currentTab == 0) currentTabContent = itemListTab; + tabs.add(new ObjectObjectImmutablePair<>( + itemListTabButton, + this.itemListTab)); + + // Upcoming Events + UpcomingEventsTab upcomingEventsTab = new UpcomingEventsTab(x + 9, y + 9, this.client); + SideTabButtonWidget eventsTabButtonWidget = new SideTabButtonWidget(x - 30, y + 3 + 27, currentTab == 1, new ItemStack(Items.CLOCK)); + eventsTabButtonWidget.setTooltip(Tooltip.of(Text.literal("Upcoming Events"))); + if (currentTab == 1) currentTabContent = upcomingEventsTab; + tabs.add(new ObjectObjectImmutablePair<>( + eventsTabButtonWidget, + upcomingEventsTab + )); + + } + + @Override + public void reset() { + super.reset(); + if (itemListTab != null) itemListTab.setSearchField(((RecipeBookWidgetAccessor) this).getSearchField()); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { if (this.isOpen()) { - MatrixStack matrices = context.getMatrices(); - matrices.push(); - matrices.translate(0.0D, 0.0D, 100.0D); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); int i = (this.parentWidth - 147) / 2 - this.leftOffset; int j = (this.parentHeight - 166) / 2; + // Draw the texture context.drawTexture(TEXTURE, i, j, 1, 1, 147, 166); - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); - - if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) { - Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); - context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1); - } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) { - Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); - context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1); - } else { - this.searchField.render(context, mouseX, mouseY, delta); - } - if (ItemRepository.filesImported()) { - if (results == null) { - int x = (this.parentWidth - 147) / 2 - this.leftOffset; - int y = (this.parentHeight - 166) / 2; - this.results = new SearchResultsWidget(this.client, x, y); - } - this.updateSearchResult(); - this.results.render(context, mouseX, mouseY, delta); + // Draw the tab's content + if (currentTabContent != null) currentTabContent.render(context, mouseX, mouseY, delta); + // Draw the tab buttons + for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) { + tab.left().render(context, mouseX, mouseY, delta); } - matrices.pop(); + } } @Override public void drawTooltip(DrawContext context, int x, int y, int mouseX, int mouseY) { - if (this.isOpen() && ItemRepository.filesImported() && results != null) { - this.results.drawTooltip(context, mouseX, mouseY); + if (this.isOpen() && currentTabContent != null) { + this.currentTabContent.drawTooltip(context, mouseX, mouseY); } } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator() && ItemRepository.filesImported() && this.searchField != null && results != null) { - if (this.searchField.mouseClicked(mouseX, mouseY, button)) { - this.results.closeRecipeView(); - this.searchField.setFocused(true); - return true; - } else { - this.searchField.setFocused(false); - return this.results.mouseClicked(mouseX, mouseY, button); + if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator()) { + // check if a tab is clicked + for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) { + if (tab.first().mouseClicked(mouseX, mouseY, button) && currentTabContent != tab.right()) { + for (Pair<SideTabButtonWidget, TabContainerWidget> tab2 : tabs) { + tab2.first().setToggled(false); + } + tab.first().setToggled(true); + currentTabContent = tab.right(); + currentTab = tabs.indexOf(tab); + return true; + } } + // click the tab content + if (currentTabContent != null) return currentTabContent.mouseClicked(mouseX, mouseY, button); + else return false; } else return false; } + + /** + * A container widget but with a fixed width and height and a drawTooltip method to implement + */ + public abstract static class TabContainerWidget extends ContainerWidget { + + public TabContainerWidget(int x, int y, Text text) { + super(x, y, 131, 150, text); + } + + public abstract void drawTooltip(DrawContext context, int mouseX, int mouseY); + } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java index d2d463c7..6af03f31 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java @@ -1,30 +1,27 @@ package de.hysky.skyblocker.skyblock.itemlist; -import java.util.List; -import java.util.ArrayList; - import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.text.OrderedText; import net.minecraft.text.Text; import net.minecraft.util.Identifier; +import java.util.List; + public class ResultButtonWidget extends ClickableWidget { private static final Identifier BACKGROUND_TEXTURE = new Identifier("recipe_book/slot_craftable"); protected ItemStack itemStack = null; public ResultButtonWidget(int x, int y) { - super(x, y, 25, 25, Text.of("")); + super(x, y, 25, 25, Text.literal("")); } protected void setItemStack(ItemStack itemStack) { - this.active = !itemStack.getItem().equals(Items.AIR); + this.active = !itemStack.isEmpty(); this.visible = true; this.itemStack = itemStack; } @@ -37,29 +34,18 @@ public class ResultButtonWidget extends ClickableWidget { @Override public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { MinecraftClient client = MinecraftClient.getInstance(); - // this.drawTexture(matrices, this.x, this.y, 29, 206, this.width, this.height); context.drawGuiTexture(BACKGROUND_TEXTURE, this.getX(), this.getY(), this.getWidth(), this.getHeight()); - // client.getItemRenderer().renderInGui(this.itemStack, this.x + 4, this.y + 4); context.drawItem(this.itemStack, this.getX() + 4, this.getY() + 4); - // client.getItemRenderer().renderGuiItemOverlay(client.textRenderer, itemStack, this.x + 4, this.y + 4); context.drawItemInSlot(client.textRenderer, itemStack, this.getX() + 4, this.getY() + 4); } public void renderTooltip(DrawContext context, int mouseX, int mouseY) { MinecraftClient client = MinecraftClient.getInstance(); + if (client.currentScreen == null) return; List<Text> tooltip = Screen.getTooltipFromItem(client, this.itemStack); - List<OrderedText> orderedTooltip = new ArrayList<>(); - - for(int i = 0; i < tooltip.size(); i++) { - orderedTooltip.add(tooltip.get(i).asOrderedText()); - } - - client.currentScreen.setTooltip(orderedTooltip); + client.currentScreen.setTooltip(tooltip.stream().map(Text::asOrderedText).toList()); } - @Override - protected void appendClickableNarrations(NarrationMessageBuilder builder) { - // TODO Auto-generated method stub - - } + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java index 1ef352e3..48d3a8f6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java @@ -6,6 +6,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; import net.minecraft.client.gui.screen.ButtonTextures; import net.minecraft.client.gui.widget.ToggleButtonWidget; import net.minecraft.component.DataComponentTypes; @@ -23,7 +24,7 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class SearchResultsWidget implements Drawable { +public class SearchResultsWidget implements Drawable, Element { private static final ButtonTextures PAGE_FORWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_forward"), new Identifier("recipe_book/page_forward_highlighted")); private static final ButtonTextures PAGE_BACKWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_backward"), new Identifier("recipe_book/page_backward_highlighted")); private static final int COLS = 5; @@ -225,4 +226,16 @@ public class SearchResultsWidget implements Drawable { return false; } + private boolean focused = false; + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean isFocused() { + return focused; + } + } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java new file mode 100644 index 00000000..9552ae87 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java @@ -0,0 +1,168 @@ +package de.hysky.skyblocker.skyblock.itemlist; + +import de.hysky.skyblocker.mixins.accessors.DrawContextInvoker; +import de.hysky.skyblocker.skyblock.events.EventNotifications; +import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.text.MutableText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class UpcomingEventsTab extends ItemListWidget.TabContainerWidget { + private static final ItemStack CLOCK = new ItemStack(Items.CLOCK); + private final MinecraftClient client; + private final List<EventRenderer> events; + + public UpcomingEventsTab(int x, int y, MinecraftClient client) { + super(x, y, Text.literal("Upcoming Events Tab")); + this.client = client; + events = EventNotifications.getEvents().entrySet() + .stream() + .sorted(Comparator.comparingLong(a -> a.getValue().isEmpty() ? Long.MAX_VALUE : a.getValue().peekFirst().start())) + .map(stringLinkedListEntry -> new EventRenderer(stringLinkedListEntry.getKey(), stringLinkedListEntry.getValue())) + .toList(); + } + + @Override + public void drawTooltip(DrawContext context, int mouseX, int mouseY) { + if (hovered != null) { + ((DrawContextInvoker) context).invokeDrawTooltip(this.client.textRenderer, hovered.getTooltip(), mouseX, mouseY, HoveredTooltipPositioner.INSTANCE); + } + } + + @Override + public List<? extends Element> children() { + return List.of(); + } + + private EventRenderer hovered = null; + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + int x = getX(); + int y = getY(); + context.enableScissor(x, y, getRight(), getBottom()); + context.drawItem(CLOCK, x, y + 4); + context.drawText(this.client.textRenderer, "Upcoming Events", x + 17, y + 7, -1, true); + + int eventsY = y + 7 + 24; + hovered = null; + for (EventRenderer eventRenderer : events) { + eventRenderer.render(context, x + 1, eventsY, mouseX, mouseY); + if (isMouseOver(mouseX, mouseY) && eventRenderer.isMouseOver(mouseX, mouseY, x+1, eventsY)) hovered = eventRenderer; + eventsY += eventRenderer.getHeight(); + + } + context.disableScissor(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (hovered != null && hovered.getWarpCommand() != null) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(hovered.getWarpCommand()); + return true; + } + return false; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public static class EventRenderer { + + private final LinkedList<EventNotifications.SkyblockEvent> events; + private final String eventName; + + public EventRenderer(String eventName, LinkedList<EventNotifications.SkyblockEvent> events) { + this.events = events; + this.eventName = eventName; + } + + public void render(DrawContext context, int x, int y, int mouseX, int mouseY) { + long time = System.currentTimeMillis() / 1000; + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, Text.literal(eventName).fillStyle(Style.EMPTY.withUnderline(isMouseOver(mouseX, mouseY, x, y))), x, y, -1, true); + if (events.isEmpty()) { + context.drawText(textRenderer, Text.literal(" ").append(Text.translatable("skyblocker.events.tab.noMore")), x, y + textRenderer.fontHeight, Colors.GRAY, false); + } else if (events.peekFirst().start() > time) { + MutableText formatted = Text.literal(" ").append(Text.translatable("skyblocker.events.tab.startsIn", Utils.getDurationText((int) (events.peekFirst().start() - time)))).formatted(Formatting.YELLOW); + context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true); + } else { + MutableText formatted = Text.literal(" ").append(Text.translatable( "skyblocker.events.tab.endsIn", Utils.getDurationText((int) (events.peekFirst().start() + events.peekFirst().duration() - time)))).formatted(Formatting.GREEN); + context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true); + } + + } + + public int getHeight() { + return 20; + } + + public boolean isMouseOver(int mouseX, int mouseY, int x, int y) { + return mouseX >= x && mouseX <= x + 131 && mouseY >= y && mouseY <= y+getHeight(); + } + + public List<TooltipComponent> getTooltip() { + List<TooltipComponent> components = new ArrayList<>(); + if (events.peekFirst() == null) return components; + if (eventName.equals(EventNotifications.JACOBS)) { + components.add(new JacobsTooltip(events.peekFirst().extras())); + } + //noinspection DataFlowIssue + if (events.peekFirst().warpCommand() != null) { + components.add(TooltipComponent.of(Text.translatable("skyblocker.events.tab.clickToWarp").formatted(Formatting.ITALIC).asOrderedText())); + } + + return components; + } + + public @Nullable String getWarpCommand() { + if (events.isEmpty()) return null; + return events.peek().warpCommand(); + } + } + + private record JacobsTooltip(String[] crops) implements TooltipComponent { + + private static final ItemStack BARRIER = new ItemStack(Items.BARRIER); + + @Override + public int getHeight() { + return 20; + } + + @Override + public int getWidth(TextRenderer textRenderer) { + return 16 * 3 + 4; + } + + @Override + public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) { + for (int i = 0; i < crops.length; i++) { + String crop = crops[i]; + context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crop, BARRIER), x + 18 * i, y + 2); + } + } + + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java index a03f3549..b227ff01 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java @@ -3,9 +3,11 @@ package de.hysky.skyblocker.skyblock.searchoverlay; import de.hysky.skyblocker.config.SkyblockerConfigManager; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.item.ItemStack; +import net.minecraft.text.MutableText; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -17,9 +19,12 @@ import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack; public class OverlayScreen extends Screen { protected static final Identifier SEARCH_ICON_TEXTURE = new Identifier("icon/search"); + private static final Identifier BACKGROUND_TEXTURE = new Identifier("social_interactions/background"); private static final int rowHeight = 20; private TextFieldWidget searchField; private ButtonWidget finishedButton; + private ButtonWidget maxPetButton; + private ButtonWidget dungeonStarButton; private ButtonWidget[] suggestionButtons; private ButtonWidget[] historyButtons; @@ -44,10 +49,11 @@ public class OverlayScreen extends Screen { searchField.setMaxLength(30); // finish buttons - finishedButton = ButtonWidget.builder(Text.literal("").setStyle(Style.EMPTY.withColor(Formatting.GREEN)), a -> close()) + finishedButton = ButtonWidget.builder(Text.literal(""), a -> close()) .position(startX + rowWidth - rowHeight, startY) .size(rowHeight, rowHeight).build(); + // suggested item buttons int rowOffset = rowHeight; int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions; @@ -80,6 +86,26 @@ public class OverlayScreen extends Screen { break; } } + //auction only elements + if (SearchOverManager.isAuction) { + //max pet level button + maxPetButton = ButtonWidget.builder(Text.literal(""), a -> { + SearchOverManager.maxPetLevel = !SearchOverManager.maxPetLevel; + updateMaxPetText(); + }) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.maxPet.@Tooltip"))) + .position(startX, startY - rowHeight - 8) + .size(rowWidth / 2, rowHeight).build(); + updateMaxPetText(); + + //dungeon star input + dungeonStarButton = ButtonWidget.builder(Text.literal("✪"), a -> updateStars()) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.starsTooltip"))) + .position(startX + (int) (rowWidth * 0.5), startY - rowHeight - 8) + .size(rowWidth / 2, rowHeight).build(); + + updateStars(); + } //add drawables in order to make tab navigation sensible addDrawableChild(searchField); @@ -93,11 +119,86 @@ public class OverlayScreen extends Screen { } addDrawableChild(finishedButton); + if (SearchOverManager.isAuction) { + addDrawableChild(maxPetButton); + addDrawableChild(dungeonStarButton); + } + //focus the search box this.setInitialFocus(searchField); } /** + * Finds if the mouse is clicked on the dungeon star button and if so works out what stars the user clicked on + * + * @param mouseX the X coordinate of the mouse + * @param mouseY the Y coordinate of the mouse + * @param button the mouse button number + * @return super + */ + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (SearchOverManager.isAuction && dungeonStarButton.isHovered() && client != null) { + double actualTextWidth = client.textRenderer.getWidth(dungeonStarButton.getMessage()); + double textOffset = (dungeonStarButton.getWidth() - actualTextWidth) / 2; + double offset = mouseX - (dungeonStarButton.getX() + textOffset); + int starCount = (int) ((offset / actualTextWidth) * 10); + starCount = Math.clamp(starCount + 1, 0, 10); + //if same as old value set stars to 0 else set to selected amount + if (starCount == SearchOverManager.dungeonStars) { + SearchOverManager.dungeonStars = 0; + } else { + SearchOverManager.dungeonStars = starCount; + } + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + /** + * Updates the text displayed on the max pet level button to represent the settings current state + */ + private void updateMaxPetText() { + if (SearchOverManager.maxPetLevel) { + maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ✔")).formatted(Formatting.GREEN)); + } else { + maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ❌")).formatted(Formatting.RED)); + } + } + + /** + * Updates stars in dungeon star input to represent the current star value + */ + private void updateStars() { + MutableText stars = Text.empty(); + for (int i = 0; i < SearchOverManager.dungeonStars; i++) { + stars.append(Text.literal("✪").formatted(i < 5 ? Formatting.YELLOW : Formatting.RED)); + } + for (int i = SearchOverManager.dungeonStars; i < 10; i++) { + stars.append(Text.literal("✪")); + } + dungeonStarButton.setMessage(stars); + } + + /** + * Renders the background for the search using the social interactions background + * @param context context + * @param mouseX mouseX + * @param mouseY mouseY + * @param delta delta + */ + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + super.renderBackground(context, mouseX, mouseY, delta); + //find max height + int maxHeight = rowHeight * (1 + suggestionButtons.length + historyButtons.length); + if (historyButtons.length > 0) { //add space for history label if it could exist + maxHeight += (int) (rowHeight * 0.75); + } + context.drawGuiTexture(BACKGROUND_TEXTURE, searchField.getX() - 8, searchField.getY() - 8, (int) (this.width * 0.4) + 16, maxHeight + 16); + } + + /** * Renders the search icon, label for the history and item Stacks for item names */ @Override diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java index 917a6aa0..bb1875ba 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java @@ -10,6 +10,7 @@ import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; import de.hysky.skyblocker.utils.NEURepoManager; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import io.github.moulberry.repo.data.NEUItem; +import io.github.moulberry.repo.util.NEUId; import it.unimi.dsi.fastutil.Pair; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -24,10 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -40,8 +38,7 @@ public class SearchOverManager { private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Search Overlay"); private static final Pattern BAZAAR_ENCHANTMENT_PATTERN = Pattern.compile("ENCHANTMENT_(\\D*)_(\\d+)"); - private static final Pattern AUCTION_PET_AND_RUNE_PATTERN = Pattern.compile("([A-Z0-9_]+);(\\d+)"); - + private static final String PET_NAME_START = "[Lvl {LVL}] "; /** * converts index (in array) +1 to a roman numeral */ @@ -52,14 +49,18 @@ public class SearchOverManager { private static @Nullable SignBlockEntity sign = null; private static boolean signFront = true; - private static boolean isAuction; + protected static boolean isAuction; private static boolean isCommand; protected static String search = ""; + protected static Boolean maxPetLevel = false; + protected static int dungeonStars = 0; // Use non-final variables and swap them to prevent concurrent modification private static HashSet<String> bazaarItems; private static HashSet<String> auctionItems; + private static HashSet<String> auctionPets; + private static HashSet<String> starableItems; private static HashMap<String, String> namesToId; public static String[] suggestionsArray = {}; @@ -91,6 +92,8 @@ public class SearchOverManager { private static void loadItems() { HashSet<String> bazaarItems = new HashSet<>(); HashSet<String> auctionItems = new HashSet<>(); + HashSet<String> auctionPets = new HashSet<>(); + HashSet<String> starableItems = new HashSet<>(); HashMap<String, String> namesToId = new HashMap<>(); //get bazaar items @@ -139,28 +142,28 @@ public class SearchOverManager { //get auction items try { + Set<@NEUId String> essenceCosts = NEURepoManager.NEU_REPO.getConstants().getEssenceCost().getCosts().keySet(); if (TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) { TooltipInfoType.THREE_DAY_AVERAGE.run(); } for (Map.Entry<String, JsonElement> entry : TooltipInfoType.THREE_DAY_AVERAGE.getData().entrySet()) { String id = entry.getKey(); - - Matcher matcher = AUCTION_PET_AND_RUNE_PATTERN.matcher(id); - if (matcher.find()) {//is a pet or rune convert id to name - String name = matcher.group(1).replace("_", " "); - name = capitalizeFully(name); - auctionItems.add(name); - namesToId.put(name, id); - continue; - } - //something else look up in NEU repo. + //look up in NEU repo. id = id.split("[+-]")[0]; NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(id); if (neuItem != null) { String name = Formatting.strip(neuItem.getDisplayName()); + //add names that are pets to the list of pets to work with the lvl 100 button + if (name != null && name.startsWith(PET_NAME_START)) { + name = name.replace(PET_NAME_START, ""); + auctionPets.add(name.toLowerCase()); + } + //if it has essence cost add to starable items + if (name != null && essenceCosts.contains(neuItem.getSkyblockItemId())) { + starableItems.add(name.toLowerCase()); + } auctionItems.add(name); namesToId.put(name, id); - continue; } } } catch (Exception e) { @@ -169,11 +172,14 @@ public class SearchOverManager { SearchOverManager.bazaarItems = bazaarItems; SearchOverManager.auctionItems = auctionItems; + SearchOverManager.auctionPets = auctionPets; + SearchOverManager.starableItems = starableItems; SearchOverManager.namesToId = namesToId; } /** * Capitalizes the first letter off every word in a string + * * @param str string to capitalize */ private static String capitalizeFully(String str) { @@ -188,8 +194,9 @@ public class SearchOverManager { /** * Receives data when a search is started and resets values - * @param sign the sign that is being edited - * @param front if it's the front of the sign + * + * @param sign the sign that is being edited + * @param front if it's the front of the sign * @param isAuction if the sign is loaded from the auction house menu or bazaar */ public static void updateSign(@NotNull SignBlockEntity sign, boolean front, boolean isAuction) { @@ -214,6 +221,7 @@ public class SearchOverManager { /** * Updates the search value and the suggestions based on that value. + * * @param newValue new search value */ protected static void updateSearch(String newValue) { @@ -221,11 +229,30 @@ public class SearchOverManager { //update the suggestion values int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions; if (newValue.isBlank() || totalSuggestions == 0) return; //do not search for empty value - suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new); + suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().sorted(Comparator.comparing(SearchOverManager::shouldFrontLoad, Comparator.reverseOrder())).filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new); + } + + /** + * determines if a value should be moved to the front of the search + * + * @param name name of the suggested item + * @return if the value should be at the front of the search queue + */ + private static boolean shouldFrontLoad(String name) { + if (!isAuction) { + return false; + } + //do nothing to non pets + if (!auctionPets.contains(name.toLowerCase())) { + return false; + } + //only front load pets when there is enough of the pet typed, so it does not spoil searching for other items + return (double) search.length() / name.length() > 0.5; } /** * Gets the suggestion in the suggestion array at the index + * * @param index index of suggestion */ protected static String getSuggestion(int index) { @@ -242,6 +269,7 @@ public class SearchOverManager { /** * Gets the item name in the history array at the index + * * @param index index of suggestion */ protected static String getHistory(int index) { @@ -286,13 +314,18 @@ public class SearchOverManager { } /** - *Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened + * Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened */ protected static void pushSearch() { //save to history if (!search.isEmpty()) { saveHistory(); } + //add pet level or dungeon starts if in ah + if (isAuction) { + addExtras(); + } + //push if (isCommand) { pushCommand(); } else { @@ -301,6 +334,45 @@ public class SearchOverManager { } /** + * Adds pet level 100 or necessary dungeon starts if needed + */ + private static void addExtras() { + // pet level + if (maxPetLevel) { + if (auctionPets.contains(search.toLowerCase())) { + if (search.equalsIgnoreCase("golden dragon")) { + search = "[Lvl 200] " + search; + } else { + search = "[Lvl 100] " + search; + } + } + } else { + // still filter for only pets + if (auctionPets.contains(search.toLowerCase())) { + // add bracket so only get pets + search = "] " + search; + } + } + + // dungeon stars + // check if it's a dungeon item and if so add correct stars + if (dungeonStars > 0 && starableItems.contains(search.toLowerCase())) { + StringBuilder starString = new StringBuilder(" "); + //add stars up to 5 + starString.append("✪".repeat(Math.max(0, Math.min(dungeonStars, 5)))); + //add number for other stars + switch (dungeonStars) { + case 6 -> starString.append("➊"); + case 7 -> starString.append("➋"); + case 8 -> starString.append("➌"); + case 9 -> starString.append("➍"); + case 10 -> starString.append("➎"); + } + search += starString.toString(); + } + } + + /** * runs the command to search for the value in ({@link SearchOverManager#search}) */ private static void pushCommand() { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java index 24dcc229..c28c8679 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java @@ -26,7 +26,7 @@ public class JacobsContestWidget extends Widget { //TODO Properly match the contest placement and display it private static final Pattern CROP_PATTERN = Pattern.compile("(?<fortune>[☘○]) (?<crop>[A-Za-z ]+).*"); - private static final Map<String, ItemStack> FARM_DATA = Map.ofEntries( + public static final Map<String, ItemStack> FARM_DATA = Map.ofEntries( entry("Wheat", new ItemStack(Items.WHEAT)), entry("Sugar Cane", new ItemStack(Items.SUGAR_CANE)), entry("Carrot", new ItemStack(Items.CARROT)), diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java index 13b28808..46950fc4 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -1,5 +1,7 @@ package de.hysky.skyblocker.utils; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.PropertyMap; @@ -8,10 +10,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.ObtainedDateTooltip; import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import it.unimi.dsi.fastutil.ints.IntIntPair; +import it.unimi.dsi.fastutil.longs.LongBooleanPair; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.component.ComponentChanges; +import net.minecraft.component.ComponentHolder; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.LoreComponent; import net.minecraft.component.type.NbtComponent; @@ -20,7 +27,6 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtElement; import net.minecraft.registry.Registries; import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.text.Text; @@ -29,13 +35,7 @@ import net.minecraft.util.dynamic.Codecs; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; -import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Matcher; @@ -46,8 +46,6 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit public class ItemUtils { public static final String ID = "id"; public static final String UUID = "uuid"; - private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH); - private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH); public static final Pattern NOT_DURABILITY = Pattern.compile("[^0-9 /]"); public static final Predicate<String> FUEL_PREDICATE = line -> line.contains("Fuel: "); private static final Codec<RegistryEntry<Item>> EMPTY_ALLOWING_ITEM_CODEC = Registries.ITEM.getEntryCodec(); @@ -65,7 +63,7 @@ public class ItemUtils { } @SuppressWarnings("deprecation") - public static NbtCompound getCustomData(@NotNull ItemStack stack) { + public static NbtCompound getCustomData(@NotNull ComponentHolder stack) { return stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).getNbt(); } @@ -75,7 +73,7 @@ public class ItemUtils { * @param stack the item stack to get the internal name from * @return an optional containing the internal name of the item stack */ - public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) { + public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); return customData.contains(ID) ? Optional.of(customData.getString(ID)) : Optional.empty(); } @@ -86,7 +84,7 @@ public class ItemUtils { * @param stack the item stack to get the internal name from * @return the internal name of the item stack, or an empty string if the item stack is null or does not have an internal name */ - public static String getItemId(@NotNull ItemStack stack) { + public static String getItemId(@NotNull ItemStack stack) { return getCustomData(stack).getString(ID); } @@ -96,7 +94,7 @@ public class ItemUtils { * @param stack the item stack to get the UUID from * @return an optional containing the UUID of the item stack */ - public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) { + public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); return customData.contains(UUID) ? Optional.of(customData.getString(UUID)) : Optional.empty(); } @@ -107,11 +105,46 @@ public class ItemUtils { * @param stack the item stack to get the UUID from * @return the UUID of the item stack, or an empty string if the item stack is null or does not have a UUID */ - public static String getItemUuid(@NotNull ItemStack stack) { + public static String getItemUuid(@NotNull ComponentHolder stack) { return getCustomData(stack).getString(UUID); } /** + * Gets the bazaar sell price or the lowest bin based on the id of the item stack. + * + * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, + * and the {@code right boolean} indicating if the price was based on complete data. + */ + public static DoubleBooleanPair getItemPrice(@NotNull ItemStack stack) { + return getItemPrice(getItemId(stack)); + } + + /** + * Gets the bazaar sell price or the lowest bin of the item with the specified id. + * + * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, + * and the {@code right boolean} indicating if the price was based on complete data. + */ + public static DoubleBooleanPair getItemPrice(@Nullable String id) { + JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData(); + JsonObject lowestBinPrices = TooltipInfoType.LOWEST_BINS.getData(); + + if (id == null || id.isEmpty() || bazaarPrices == null || lowestBinPrices == null) return DoubleBooleanPair.of(0, false); + + if (bazaarPrices.has(id)) { + JsonElement sellPrice = bazaarPrices.get(id).getAsJsonObject().get("sellPrice"); + boolean isPriceNull = sellPrice.isJsonNull(); + return DoubleBooleanPair.of(isPriceNull ? 0 : sellPrice.getAsDouble(), !isPriceNull); + } + + if (lowestBinPrices.has(id)) { + return DoubleBooleanPair.of(lowestBinPrices.get(id).getAsDouble(), true); + } + + return DoubleBooleanPair.of(0, false); + } + + /** * This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum. * Currently, there are two types of string 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 @@ -126,21 +159,12 @@ public class ItemUtils { * * @param stack the item under the pointer * @return if the item have a "Timestamp" it will be shown formated on the tooltip + * @deprecated use {@link ObtainedDateTooltip#getTimestamp(ItemStack)} instead */ - public static String getTimestamp(ItemStack stack) { + public static String getTimestamp(ItemStack stack) { NbtCompound customData = getCustomData(stack); - if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) { - Instant date = Instant.ofEpochMilli(customData.getLong("timestamp")); - return OBTAINED_DATE_FORMATTER.format(date); - } - - if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) { - TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp")); - return OBTAINED_DATE_FORMATTER.format(date); - } - - return ""; + return ObtainedDateTooltip.getTimestamp(stack); } public static boolean hasCustomDurability(@NotNull ItemStack stack) { @@ -198,7 +222,7 @@ public class ItemUtils { public static List<Text> getLore(ItemStack item) { return item.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).styledLines(); } - + public static PropertyMap propertyMapWithTexture(String textureValue) { return Codecs.GAME_PROFILE_PROPERTY_MAP.parse(JsonOps.INSTANCE, JsonParser.parseString("[{\"name\":\"textures\",\"value\":\"" + textureValue + "\"}]")).getOrThrow(); } @@ -207,12 +231,12 @@ public class ItemUtils { if (!stack.isOf(Items.PLAYER_HEAD) || !stack.contains(DataComponentTypes.PROFILE)) return ""; ProfileComponent profile = stack.get(DataComponentTypes.PROFILE); - String texture = profile.properties().get("textures").stream() + if (profile == null) return ""; + + return profile.properties().get("textures").stream() .map(Property::value) .findFirst() .orElse(""); - - return texture; } public static Optional<String> getHeadTextureOptional(ItemStack stack) { diff --git a/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java new file mode 100644 index 00000000..007cb0b1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java @@ -0,0 +1,54 @@ +package de.hysky.skyblocker.utils; + +public class RomanNumerals { + private RomanNumerals() { + } + private static int getDecimalValue(char romanChar) { + return switch (romanChar) { + case 'I' -> 1; + case 'V' -> 5; + case 'X' -> 10; + case 'L' -> 50; + case 'C' -> 100; + case 'D' -> 500; + case 'M' -> 1000; + default -> 0; + }; + } + + /** + * Checks if a string is a valid roman numeral. + * It's the caller's responsibility to clean up the string before calling this method (such as trimming it). + * @param romanNumeral The roman numeral to check. + * @return True if the string is a valid roman numeral, false otherwise. + * @implNote This will only check if the string contains valid roman numeral characters. It won't check if the numeral is well-formed. + */ + public static boolean isValidRomanNumeral(String romanNumeral) { + if (romanNumeral == null || romanNumeral.isEmpty()) return false; + for (int i = 0; i < romanNumeral.length(); i++) { + if (getDecimalValue(romanNumeral.charAt(i)) == 0) return false; + } + return true; + } + + /** + * Converts a roman numeral to a decimal number. + * + * @param romanNumeral The roman numeral to convert. + * @return The decimal number, or 0 if the roman numeral string has non-roman characters in it, or if the string is empty or null. + */ + public static int romanToDecimal(String romanNumeral) { + if (romanNumeral == null || romanNumeral.isEmpty()) return 0; + romanNumeral = romanNumeral.trim().toUpperCase(); + int decimal = 0; + int lastNumber = 0; + for (int i = romanNumeral.length() - 1; i >= 0; i--) { + char ch = romanNumeral.charAt(i); + int number = getDecimalValue(ch); + if (number == 0) return 0; //Malformed roman numeral + decimal = number >= lastNumber ? decimal + number : decimal - number; + lastNumber = number; + } + return decimal; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 62a3b897..925879b8 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -5,11 +5,9 @@ import com.google.gson.JsonParser; import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor; import de.hysky.skyblocker.skyblock.item.MuseumItemCache; -import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.networking.v1.PacketSender; @@ -19,6 +17,7 @@ import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.network.PlayerListEntry; import net.minecraft.scoreboard.*; +import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.jetbrains.annotations.NotNull; @@ -234,7 +233,6 @@ public class Utils { if (!isOnSkyblock) { if (!isInjected) { isInjected = true; - ItemTooltipCallback.EVENT.register(ItemTooltip::getTooltip); } isOnSkyblock = true; SkyblockEvents.JOIN.invoker().onSkyblockJoin(); @@ -355,6 +353,23 @@ public class Utils { } } + // TODO: Combine with `ChocolateFactorySolver.formatTime` and move into `SkyblockTime`. + public static Text getDurationText(int timeInSeconds) { + int seconds = timeInSeconds % 60; + int minutes = (timeInSeconds/60) % 60; + int hours = (timeInSeconds/3600); + + MutableText time = Text.empty(); + if (hours > 0) { + time.append(hours + "h").append(" "); + } + if (hours > 0 || minutes > 0) { + time.append(minutes + "m").append(" "); + } + time.append(seconds + "s"); + return time; + } + private static void updateFromPlayerList(MinecraftClient client) { if (client.getNetworkHandler() == null) { return; diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java new file mode 100644 index 00000000..09edcf3c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java @@ -0,0 +1,70 @@ +package de.hysky.skyblocker.utils.config; + +import de.hysky.skyblocker.utils.Utils; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.AbstractWidget; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.gui.controllers.string.IStringController; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public record DurationController(Option<Integer> option) implements IStringController<Integer> { + + private static final Pattern secondsPattern = Pattern.compile("(^|\\s)(\\d+)s(\\s|$)"); + private static final Pattern minutesPattern = Pattern.compile("(^|\\s)(\\d+)m(\\s|$)"); + private static final Pattern hoursPattern = Pattern.compile("(^|\\s)(\\d+)h(\\s|$)"); + + @Override + public String getString() { + return Utils.getDurationText(option.pendingValue()).getString(); + } + + + @Override + public void setFromString(String value) { + Matcher hoursMatcher = hoursPattern.matcher(value); + Matcher minutesMatcher = minutesPattern.matcher(value); + Matcher secondsMatcher = secondsPattern.matcher(value); + + int result = 0; + if (hoursMatcher.find()) { + result += Integer.parseInt(hoursMatcher.group(2)) * 3600; + } + if (minutesMatcher.find()) { + result += Integer.parseInt(minutesMatcher.group(2)) * 60; + } + if (secondsMatcher.find()) { + result += Integer.parseInt(secondsMatcher.group(2)); + } + option.requestSet(result); + } + + + @Override + public boolean isInputValid(String s) { + Matcher hoursMatcher = hoursPattern.matcher(s); + Matcher minutesMatcher = minutesPattern.matcher(s); + Matcher secondsMatcher = secondsPattern.matcher(s); + + int hoursCount = 0; + while (hoursMatcher.find()) hoursCount++; + int minutesCount = 0; + while (minutesMatcher.find()) minutesCount++; + int secondsCount = 0; + while (secondsMatcher.find()) secondsCount++; + + if (hoursCount == 0 && minutesCount == 0 && secondsCount == 0) return false; + if (hoursCount > 1 || minutesCount > 1 || secondsCount > 1) return false; + s = s.replaceAll(hoursPattern.pattern(), ""); + s = s.replaceAll(minutesPattern.pattern(), ""); + s = s.replaceAll(secondsPattern.pattern(), ""); + return s.isBlank(); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + return new DurationControllerWidget(this, screen, widgetDimension); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java new file mode 100644 index 00000000..f25cd088 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java @@ -0,0 +1,38 @@ +package de.hysky.skyblocker.utils.config; + +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.gui.controllers.string.IStringController; +import dev.isxander.yacl3.gui.controllers.string.StringControllerElement; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.function.Consumer; + +public class DurationControllerWidget extends StringControllerElement { + + public DurationControllerWidget(IStringController<?> control, YACLScreen screen, Dimension<Integer> dim) { + super(control, screen, dim, false); + } + + @Override + public void unfocus() { + if (control.isInputValid(inputField)) super.unfocus(); + else modifyInput(stringBuilder -> stringBuilder.replace(0, stringBuilder.length(), control.getString())); + } + + @Override + public boolean modifyInput(Consumer<StringBuilder> consumer) { + StringBuilder temp = new StringBuilder(inputField); + consumer.accept(temp); + inputField = temp.toString(); + return true; + } + + @Override + protected Text getValueText() { + Text valueText = super.getValueText(); + boolean inputValid = control.isInputValid(valueText.getString()); + return valueText.copy().formatted(inputValid ? Formatting.WHITE: Formatting.RED); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java new file mode 100644 index 00000000..bf255218 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java @@ -0,0 +1,29 @@ +package de.hysky.skyblocker.utils.render.gui; + +import de.hysky.skyblocker.skyblock.ChestValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Pattern; + +public abstract class AbstractContainerMatcher { + /** + * The title of the screen must match this pattern for this to be applied. Null means it will be applied to all screens. + * @implNote Don't end your regex with a {@code $} as {@link ChestValue} appends text to the end of the title, + * so the regex will stop matching if the player uses chest value. + */ + @Nullable + public final Pattern titlePattern; + + protected AbstractContainerMatcher() { + this((Pattern) null); + } + + protected AbstractContainerMatcher(@NotNull String titlePattern) { + this(Pattern.compile(titlePattern)); + } + + protected AbstractContainerMatcher(@Nullable Pattern titlePattern) { + this.titlePattern = titlePattern; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java index 0417dc3c..81c9ebec 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java @@ -11,17 +11,15 @@ import java.util.regex.Pattern; /** * Abstract class for gui solvers. Extend this class to add a new gui solver, like terminal solvers or experiment solvers. */ -public abstract class ContainerSolver { - private final Pattern containerName; - - protected ContainerSolver(String containerName) { - this.containerName = Pattern.compile(containerName); +public abstract class ContainerSolver extends AbstractContainerMatcher { + protected ContainerSolver(String titlePattern) { + super(titlePattern); } protected abstract boolean isEnabled(); public final Pattern getName() { - return containerName; + return titlePattern; } protected void start(GenericContainerScreen screen) { diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java new file mode 100644 index 00000000..87da0d36 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.utils.render.gui; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ButtonTextures; +import net.minecraft.client.gui.widget.ToggleButtonWidget; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; + +public class SideTabButtonWidget extends ToggleButtonWidget { + private static final ButtonTextures TEXTURES = new ButtonTextures(new Identifier("recipe_book/tab"), new Identifier("recipe_book/tab_selected")); + protected @NotNull ItemStack icon; + + public void setIcon(@NotNull ItemStack icon) { + this.icon = icon.copy(); + } + + public SideTabButtonWidget(int x, int y, boolean toggled, @NotNull ItemStack icon) { + super(x, y, 35, 27, toggled); + this.icon = icon.copy(); + setTextures(TEXTURES); + } + + @Override + public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + if (textures == null) return; + Identifier identifier = textures.get(true, this.toggled); + int x = getX(); + if (toggled) x -= 2; + context.drawGuiTexture(identifier, x, this.getY(), this.width, this.height); + context.drawItem(icon, x + 9, getY() + 5); + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + if (!isToggled()) this.setToggled(true); + } +} |