From 0d0aae6b88b34d7f85ba397eea2469fb4a95a51e Mon Sep 17 00:00:00 2001 From: "upfault.lol" Date: Sun, 2 Mar 2025 22:08:45 -0600 Subject: Garden Tweaks & Features (#1118) * Sum visitors with alike crops, fix Farming XP/h regex, and improve precision for XP/h and Blocks/s * Added Show Stacks in Visitor Helper * Re-added the drawItemEntry into drawScreen * VacuumSolver, VisitorHelper, PestHighlighter * Forgot to fix highlight flicker in VacuumSolver * removed imports from MBB * Updated Regex & MayorUtils catch block * Forgot to add the visitor head and name coloring back into the render method during some tests * Refactor visitor package name * Clean up visitor helper and garden * Clean up visitor helper some more * Fix visitor requirements * Fix block breaks * Use primitive types and fixes * Missed a primitive * Remove vacuum solver * Fix amount parsing --------- Co-authored-by: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> --- .../config/categories/CrimsonIsleCategory.java | 4 +- .../config/categories/FarmingCategory.java | 44 +++- .../skyblocker/config/configs/FarmingConfig.java | 18 +- .../skyblocker/mixins/HandledScreenMixin.java | 21 +- .../skyblock/entity/MobBoundingBoxes.java | 2 +- .../hysky/skyblocker/skyblock/entity/MobGlow.java | 39 +++- .../skyblocker/skyblock/garden/FarmingHud.java | 241 ++++++++++--------- .../skyblock/garden/FarmingHudWidget.java | 6 +- .../skyblocker/skyblock/garden/VisitorHelper.java | 224 ------------------ .../skyblock/garden/visitor/Visitor.java | 16 ++ .../skyblock/garden/visitor/VisitorHelper.java | 256 +++++++++++++++++++++ .../java/de/hysky/skyblocker/utils/Formatters.java | 30 ++- .../java/de/hysky/skyblocker/utils/ItemUtils.java | 2 +- src/main/java/de/hysky/skyblocker/utils/Utils.java | 5 +- .../hysky/skyblocker/utils/mayor/MayorUtils.java | 17 +- .../skyblocker/utils/render/RenderHelper.java | 2 +- 16 files changed, 544 insertions(+), 383 deletions(-) delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/Visitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/VisitorHelper.java (limited to 'src/main/java/de') diff --git a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java index 242bfd24..5a210017 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java @@ -36,8 +36,8 @@ public class CrimsonIsleCategory { .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.crimsonIsle.kuudra.suppliesAndFuelWaypointType")) - .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.secretWaypoints.waypointType.@Tooltip"), - Text.translatable("skyblocker.config.dungeons.secretWaypoints.waypointType.generalNote"))) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType.@Tooltip"), + Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType.generalNote"))) .binding(defaults.crimsonIsle.kuudra.suppliesAndFuelWaypointType, () -> config.crimsonIsle.kuudra.suppliesAndFuelWaypointType, newValue -> config.crimsonIsle.kuudra.suppliesAndFuelWaypointType = newValue) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/FarmingCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/FarmingCategory.java index 929256c6..9b90c096 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/FarmingCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/FarmingCategory.java @@ -38,13 +38,14 @@ public class FarmingCategory { newValue -> config.farming.garden.dicerTitlePrevent = newValue) .controller(ConfigUtils::createBooleanController) .build()) - .option(Option.createBuilder() - .name(Text.translatable("skyblocker.config.farming.garden.visitorHelper")) - .binding(defaults.farming.garden.visitorHelper, - () -> config.farming.garden.visitorHelper, - newValue -> config.farming.garden.visitorHelper = newValue) - .controller(ConfigUtils::createBooleanController) - .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.farming.garden.pestHighlighter")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.farming.garden.pestHighlighter.@Tooltip"))) + .binding(defaults.farming.garden.pestHighlighter, + () -> config.farming.garden.pestHighlighter, + newValue -> config.farming.garden.pestHighlighter = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.farming.garden.lockMouseTool")) .binding(defaults.farming.garden.lockMouseTool, @@ -76,6 +77,35 @@ public class FarmingCategory { .controller(ConfigUtils::createBooleanController) .build()) .build()) + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.farming.visitorHelper")) + .collapsed(false) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.farming.visitorHelper.visitorHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.farming.visitorHelper.visitorHelper.@Tooltip"))) + .binding(defaults.farming.visitorHelper.visitorHelper, + () -> config.farming.visitorHelper.visitorHelper, + newValue -> config.farming.visitorHelper.visitorHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.farming.visitorHelper.visitorHelperGardenOnly")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.farming.visitorHelper.visitorHelperGardenOnly.@Tooltip"))) + .binding(defaults.farming.visitorHelper.visitorHelperGardenOnly, + () -> config.farming.visitorHelper.visitorHelperGardenOnly, + newValue -> config.farming.visitorHelper.visitorHelperGardenOnly = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.farming.visitorHelper.showStacksInVisitorHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.farming.visitorHelper.showStacksInVisitorHelper.@Tooltip"))) + .binding(defaults.farming.visitorHelper.showStacksInVisitorHelper, + () -> config.farming.visitorHelper.showStacksInVisitorHelper, + newValue -> config.farming.visitorHelper.showStacksInVisitorHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) .build(); + } } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/FarmingConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/FarmingConfig.java index 568165a7..88506bfe 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/FarmingConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/FarmingConfig.java @@ -7,6 +7,9 @@ public class FarmingConfig { @SerialEntry public Garden garden = new Garden(); + @SerialEntry + public VisitorHelper visitorHelper = new VisitorHelper(); + public static class Garden { @SerialEntry public FarmingHud farmingHud = new FarmingHud(); @@ -14,8 +17,8 @@ public class FarmingConfig { @SerialEntry public boolean dicerTitlePrevent = true; - @SerialEntry - public boolean visitorHelper = true; + @SerialEntry + public boolean pestHighlighter = true; @SerialEntry public boolean lockMouseTool = false; @@ -30,6 +33,17 @@ public class FarmingConfig { public boolean closeScreenOnPlotClick = false; } + public static class VisitorHelper { + @SerialEntry + public boolean visitorHelper = true; + + @SerialEntry + public boolean visitorHelperGardenOnly = true; + + @SerialEntry + public boolean showStacksInVisitorHelper = false; + } + public static class FarmingHud { @SerialEntry public boolean enableHud = true; diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java index 426f45df..1b6842f6 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java @@ -2,7 +2,6 @@ package de.hysky.skyblocker.mixins; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Local; - import com.mojang.blaze3d.systems.RenderSystem; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; @@ -11,7 +10,7 @@ import de.hysky.skyblocker.skyblock.PetCache; import de.hysky.skyblocker.skyblock.experiment.ExperimentSolver; import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; -import de.hysky.skyblocker.skyblock.garden.VisitorHelper; +import de.hysky.skyblocker.skyblock.garden.visitor.VisitorHelper; import de.hysky.skyblocker.skyblock.item.*; import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager; import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; @@ -136,13 +135,6 @@ public abstract class HandledScreenMixin extends Screen } } - @Inject(at = @At("HEAD"), method = "mouseClicked") - public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable 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); - } - } - @ModifyExpressionValue(method = "mouseClicked", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;mouseClicked(DDI)Z")) public boolean skyblocker$passThroughSearchFieldUnfocusedClicks(boolean superClicked, double mouseX, double mouseY, int button) { //Handle Search Field clicks - as of 1.21.4 the game will only send clicks to the selected element rather than trying to send one to each and stopping when the first returns true (if any). @@ -224,6 +216,7 @@ public abstract class HandledScreenMixin extends Screen /** * Avoids getting currentSolver again when it's already in the scope for some usages of this method. + * * @see #skyblocker$experimentSolvers$getStack(Slot, ItemStack, ContainerSolver) */ @Unique @@ -297,8 +290,7 @@ public abstract class HandledScreenMixin extends Screen switch (this.handler) { case GenericContainerScreenHandler genericContainerScreenHandler when genericContainerScreenHandler.getRows() == 6 -> { - VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack()); - + VisitorHelper.onSlotClick(slot, slotId, title); // 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) { @@ -329,6 +321,13 @@ public abstract class HandledScreenMixin extends Screen } } + @Inject(at = @At("HEAD"), method = "mouseClicked") + public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + if (VisitorHelper.shouldRender()) { + VisitorHelper.handleMouseClick(mouseX, mouseY, button, this.textRenderer); + } + } + @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V")) private void skyblocker$drawOnItem(DrawContext context, Slot slot, CallbackInfo ci) { if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java index 65ad6918..d5081668 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java @@ -50,7 +50,7 @@ public class MobBoundingBoxes { return false; } - + public static float[] getBoxColor(Entity entity) { int color = MobGlow.getMobGlow(entity); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java index 91469c38..76930bd8 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java @@ -33,7 +33,7 @@ import net.minecraft.util.Formatting; import net.minecraft.util.math.Box; import net.minecraft.world.World; -import java.util.List; +import java.util.*; public class MobGlow { public static final int NO_GLOW = 0; @@ -42,6 +42,31 @@ public class MobGlow { */ private static final String NUKEKUBI_HEAD_TEXTURE = "eyJ0aW1lc3RhbXAiOjE1MzQ5NjM0MzU5NjIsInByb2ZpbGVJZCI6ImQzNGFhMmI4MzFkYTRkMjY5NjU1ZTMzYzE0M2YwOTZjIiwicHJvZmlsZU5hbWUiOiJFbmRlckRyYWdvbiIsInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWIwNzU5NGUyZGYyNzM5MjFhNzdjMTAxZDBiZmRmYTExMTVhYmVkNWI5YjIwMjllYjQ5NmNlYmE5YmRiYjRiMyJ9fX0="; private static final String FEL_HEAD_TEXTURE = "ewogICJ0aW1lc3RhbXAiIDogMTcyMDAyNTQ4Njg2MywKICAicHJvZmlsZUlkIiA6ICIzZDIxZTYyMTk2NzQ0Y2QwYjM3NjNkNTU3MWNlNGJlZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJTcl83MUJsYWNrYmlyZCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9jMjg2ZGFjYjBmMjE0NGQ3YTQxODdiZTM2YmJhYmU4YTk4ODI4ZjdjNzlkZmY1Y2UwMTM2OGI2MzAwMTU1NjYzIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0="; + private static final Set PEST_HEAD_TEXTURES = Set.of( + // Mosquito + "ewogICJ0aW1lc3RhbXAiIDogMTY5Njk0NTAyOTQ2MSwKICAicHJvZmlsZUlkIiA6ICI3NTE0NDQ4MTkxZTY0NTQ2OGM5NzM5YTZlMzk1N2JlYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJUaGFua3NNb2phbmciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTJhOWZlMDViYzY2M2VmY2QxMmU1NmEzY2NjNWVjMDM1YmY1NzdiNzg3MDg1NDhiNmY0ZmZjZjFkMzBlY2NmZSIKICAgIH0KICB9Cn0=", + // Rat + "ewogICJ0aW1lc3RhbXAiIDogMTYxODQxOTcwMTc1MywKICAicHJvZmlsZUlkIiA6ICI3MzgyZGRmYmU0ODU0NTVjODI1ZjkwMGY4OGZkMzJmOCIsCiAgInByb2ZpbGVOYW1lIiA6ICJCdUlJZXQiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYThhYmI0NzFkYjBhYjc4NzAzMDExOTc5ZGM4YjQwNzk4YTk0MWYzYTRkZWMzZWM2MWNiZWVjMmFmOGNmZmU4IiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=", + // Locust + "ewogICJ0aW1lc3RhbXAiIDogMTY5NzU1NzA3NzAzNywKICAicHJvZmlsZUlkIiA6ICI0YjJlMGM1ODliZjU0ZTk1OWM1ZmJlMzg5MjQ1MzQzZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJfTmVvdHJvbl8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGIyNGE0ODJhMzJkYjFlYTc4ZmI5ODA2MGIwYzJmYTRhMzczY2JkMThhNjhlZGRkZWI3NDE5NDU1YTU5Y2RhOSIKICAgIH0KICB9Cn0=", + // Cricket + "ewogICJ0aW1lc3RhbXAiIDogMTcyMzE3OTgxMTI2NCwKICAicHJvZmlsZUlkIiA6ICJjZjc4YzFkZjE3ZTI0Y2Q5YTIxYmU4NWQ0NDk5ZWE4ZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNYXR0c0FybW9yU3RhbmRzIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2EyNGM2OWY5NmNlNTU2MjIxZTE5NWM4ZWYyYmZhZDcxZWJmN2Y5NWY1YWU5MTRhNDg0YThkMGVjMjE2NzI2NzQiCiAgICB9CiAgfQp9", + // Fly + "ewogICJ0aW1lc3RhbXAiIDogMTY5Njk0NTA2MzI4MSwKICAicHJvZmlsZUlkIiA6ICJjN2FmMWNkNjNiNTE0Y2YzOGY4NWQ2ZDUxNzhjYThlNCIsCiAgInByb2ZpbGVOYW1lIiA6ICJtb25zdGVyZ2FtZXIzMTUiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ5MGU3Nzc4MjZhNTI0NjEzNjhlMjZkMWIyZTE5YmZhMWJhNTgyZDYwMjQ4M2U1NDVmNDEyNGQwZjczMTg0MiIKICAgIH0KICB9Cn0=", + // Beetle + "ewogICJ0aW1lc3RhbXAiIDogMTcyMzE3OTc4OTkzNCwKICAicHJvZmlsZUlkIiA6ICJlMjc5NjliODYyNWY0NDg1YjkyNmM5NTBhMDljMWMwMSIsCiAgInByb2ZpbGVOYW1lIiA6ICJLRVZJTktFTE9LRSIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS83MGExZTgzNmJmMTk2OGIyZWFhNDgzNzIyN2ExOTIwNGYxNzI5NWQ4NzBlZTllNzU0YmQ2YjZkNjBkZGJlZDNjIgogICAgfQogIH0KfQ==", + // Slug + "ewogICJ0aW1lc3RhbXAiIDogMTY5NzQ3MDQ0MzA4MiwKICAicHJvZmlsZUlkIiA6ICJkOGNkMTNjZGRmNGU0Y2IzODJmYWZiYWIwOGIyNzQ4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICJaYWNoeVphY2giLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2E3OWQwZmQ2NzdiNTQ1MzA5NjExMTdlZjg0YWRjMjA2ZTJjYzUwNDVjMTM0NGQ2MWQ3NzZiZjhhYzJmZTFiYSIKICAgIH0KICB9Cn0=", + // Moth + "ewogICJ0aW1lc3RhbXAiIDogMTY5Njg3MDQwNTk1NCwKICAicHJvZmlsZUlkIiA6ICJiMTUyZDlhZTE1MTM0OWNmOWM2NmI0Y2RjMTA5NTZjOCIsCiAgInByb2ZpbGVOYW1lIiA6ICJNaXNxdW90aCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS82NTQ4NWM0YjM0ZTViNTQ3MGJlOTRkZTEwMGU2MWY3ODE2ZjgxYmM1YTExZGZkZjBlY2NmODkwMTcyZGE1ZDBhIgogICAgfQogIH0KfQ==", + // Mite + "ewogICJ0aW1lc3RhbXAiIDogMTY5Njg3MDQxOTcyNSwKICAicHJvZmlsZUlkIiA6ICJkYjYzNWE3MWI4N2U0MzQ5YThhYTgwOTMwOWFhODA3NyIsCiAgInByb2ZpbGVOYW1lIiA6ICJFbmdlbHMxNzQiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmU2YmFmNjQzMWE5ZGFhMmNhNjA0ZDVhM2MyNmU5YTc2MWQ1OTUyZjA4MTcxNzRhNGZlMGI3NjQ2MTZlMjFmZiIKICAgIH0KICB9Cn0=", + // Earthworm + "ewogICJ0aW1lc3RhbXAiIDogMTY5NzQ3MDQ1OTc0NywKICAicHJvZmlsZUlkIiA6ICIyNTBlNzc5MjZkNDM0ZDIyYWM2MTQ4N2EyY2M3YzAwNCIsCiAgInByb2ZpbGVOYW1lIiA6ICJMdW5hMTIxMDUiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjQwM2JhNDAyN2EzMzNkOGQyZmQzMmFiNTlkMWNmZGJhYTdkOTA4ZDgwZDIzODFkYjJhNjljYmU2NTQ1MGFkOCIKICAgIH0KICB9Cn0=", + // Field Mouse + "ewogICJ0aW1lc3RhbXAiIDogMTcyNzkwNDc5NzQ1OSwKICAicHJvZmlsZUlkIiA6ICI0MmIwOTMyZDUwMWI0MWQ1YTM4YjEwOTcxYTYwYmYxMyIsCiAgInByb2ZpbGVOYW1lIiA6ICJBaXJib2x0MDc4IiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2YzNzllMDkyNTI4MTczMTRiZDBiNjk0ZjdkNTNiNDhhZjJjN2ZhODQ5OTEwOTgwMmE0MWJiMjk0ZDJmOTNlM2UiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==" + ); + /** * Cache for mob glow. Absence means the entity does not have custom glow. * If an entity is in the cache, it must have custom glow. @@ -141,6 +166,9 @@ public class MobGlow { // Enderman Slayer's Nukekubi Skulls case ArmorStandEntity armorStand when SkyblockerConfigManager.get().slayers.endermanSlayer.highlightNukekubiHeads && Utils.isInTheEnd() && armorStand.isMarker() && SlayerManager.isInSlayer() && isNukekubiHead(armorStand) -> 0x990099; + // Pests + case ArmorStandEntity armorStand when SkyblockerConfigManager.get().farming.garden.pestHighlighter && Utils.isInGarden() && isPestHead(armorStand) -> 0xb62f00; + // Blaze Slayer's Demonic minions case WitherSkeletonEntity e when SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerManager.isInSlayerType(SlayerType.DEMONLORD) && e.distanceTo(MinecraftClient.getInstance().player) <= 15 -> AttunementColors.getColor(e); case ZombifiedPiglinEntity e when SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerManager.isInSlayerType(SlayerType.DEMONLORD) && e.distanceTo(MinecraftClient.getInstance().player) <= 15 -> AttunementColors.getColor(e); @@ -192,4 +220,13 @@ public class MobGlow { private static boolean isNukekubiHead(ArmorStandEntity entity) { return Streams.stream(entity.getArmorItems()).map(ItemUtils::getHeadTexture).anyMatch(headTexture -> headTexture.contains(NUKEKUBI_HEAD_TEXTURE)); } + + /** + * Compares the armor items of an armor stand to the Pest head texture to determine if it is a Pest head. + */ + private static boolean isPestHead(ArmorStandEntity entity) { + return Streams.stream(entity.getArmorItems()) + .map(ItemUtils::getHeadTexture) + .anyMatch(PEST_HEAD_TEXTURES::contains); + } } 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 eea3a3b7..ab1cfe2d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java @@ -20,6 +20,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; +import net.minecraft.util.Formatting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,119 +36,129 @@ import java.util.regex.Pattern; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; 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 FARMING_XP = Pattern.compile("ยง3\\+(?\\d+.?\\d*) Farming \\((?[\\d,]+.?\\d*)%\\)"); - private static final MinecraftClient client = MinecraftClient.getInstance(); - private static CounterType counterType = CounterType.NONE; - private static final Deque counter = new ArrayDeque<>(); - private static final LongPriorityQueue blockBreaks = new LongArrayFIFOQueue(); - private static final Queue farmingXp = new ArrayDeque<>(); - private static float farmingXpPercentProgress; - - @Init - public static void init() { - HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> { - if (shouldRender()) { - if (!counter.isEmpty() && counter.peek().rightLong() + 5000 < System.currentTimeMillis()) { - counter.poll(); - } - if (!blockBreaks.isEmpty() && blockBreaks.firstLong() + 1000 < System.currentTimeMillis()) { - blockBreaks.dequeueLong(); - } - if (!farmingXp.isEmpty() && farmingXp.peek().rightLong() + 1000 < System.currentTimeMillis()) { - farmingXp.poll(); - } - - ItemStack stack = client.player.getMainHandStack(); - if (stack == null || !tryGetCounter(stack, CounterType.CULTIVATING) && !tryGetCounter(stack, CounterType.COUNTER)) { - counterType = CounterType.NONE; - } - } - }); - ClientPlayerBlockBreakEvents.AFTER.register((world, player, pos, state) -> { - if (shouldRender()) { - blockBreaks.enqueue(System.currentTimeMillis()); - } - }); - ClientReceiveMessageEvents.GAME.register((message, overlay) -> { - if (shouldRender() && overlay) { - Matcher matcher = FARMING_XP.matcher(message.getString()); - if (matcher.matches()) { - try { - farmingXp.offer(FloatLongPair.of(NUMBER_FORMAT.parse(matcher.group("xp")).floatValue(), System.currentTimeMillis())); - farmingXpPercentProgress = NUMBER_FORMAT.parse(matcher.group("percent")).floatValue(); - } catch (ParseException e) { - LOGGER.error("[Skyblocker Farming HUD] Failed to parse farming xp", e); - } - } - } - }); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("hud").then(literal("farming") - .executes(Scheduler.queueOpenScreenCommand(() -> new WidgetsConfigurationScreen(Location.GARDEN, "hud_garden", 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 && client.player != null && Utils.getLocation() == Location.GARDEN; - } - - public static String counterText() { - return counterType.text; - } - - public static int counter() { - return counter.isEmpty() ? 0 : counter.peekLast().leftInt(); - } - - public static float cropsPerMinute() { - if (counter.isEmpty()) { - return 0; - } - IntLongPair first = counter.peek(); - IntLongPair last = counter.peekLast(); - return (float) (last.leftInt() - first.leftInt()) / (last.rightLong() - first.rightLong()) * 60_000f; - } - - public static int blockBreaks() { - return blockBreaks.size(); - } - - public static float farmingXpPercentProgress() { - return farmingXpPercentProgress; - } - - 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; - } - public boolean matchesText(String textToMatch) { - return this.text.equals(textToMatch); - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(FarmingHud.class); + public static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); + private static final Pattern FARMING_XP = Pattern.compile("\\+(?\\d+(?:\\.\\d+)?) Farming \\((?[\\d,]+(?:\\.\\d+)?%|[\\d,]+/[\\d,]+)\\)"); + private static final MinecraftClient client = MinecraftClient.getInstance(); + private static CounterType counterType = CounterType.NONE; + private static final Deque counter = new ArrayDeque<>(); + private static final LongPriorityQueue blockBreaks = new LongArrayFIFOQueue(); + private static final Queue farmingXp = new ArrayDeque<>(); + private static float farmingXpPercentProgress; + + @Init + public static void init() { + HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> { + if (shouldRender()) { + if (!counter.isEmpty() && counter.peek().rightLong() + 5000 < System.currentTimeMillis()) { + counter.poll(); + } + if (!blockBreaks.isEmpty() && blockBreaks.firstLong() + 1000 < System.currentTimeMillis()) { + blockBreaks.dequeueLong(); + } + if (!farmingXp.isEmpty() && farmingXp.peek().rightLong() + 1000 < System.currentTimeMillis()) { + farmingXp.poll(); + } + + assert client.player != null; + ItemStack stack = client.player.getMainHandStack(); + if (stack == null || tryGetCounter(stack, CounterType.CULTIVATING) && tryGetCounter(stack, CounterType.COUNTER)) { + counterType = CounterType.NONE; + } + } + }); + ClientPlayerBlockBreakEvents.AFTER.register((world, player, pos, state) -> { + if (shouldRender()) { + blockBreaks.enqueue(System.currentTimeMillis()); + } + }); + ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + if (shouldRender() && overlay) { + Matcher matcher = FARMING_XP.matcher(Formatting.strip(message.getString())); + if (matcher.matches()) { + try { + farmingXp.offer(FloatLongPair.of(NUMBER_FORMAT.parse(matcher.group("xp")).floatValue(), System.currentTimeMillis())); + farmingXpPercentProgress = NUMBER_FORMAT.parse(matcher.group("percent")).floatValue(); + } catch (ParseException e) { + LOGGER.error("[Skyblocker Farming HUD] Failed to parse farming xp", e); + } + } + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("hud").then(literal("farming") + .executes(Scheduler.queueOpenScreenCommand(() -> new WidgetsConfigurationScreen(Location.GARDEN, "hud_garden", null))))))); + } + + private static boolean tryGetCounter(ItemStack stack, CounterType counterType) { + NbtCompound customData = ItemUtils.getCustomData(stack); + if (customData.isEmpty() || !customData.contains(counterType.nbtKey, NbtElement.NUMBER_TYPE)) return true; + 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 false; + } + + private static boolean shouldRender() { + 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() { + return counter.isEmpty() ? 0 : counter.peekLast().leftInt(); + } + + public static float cropsPerMinute() { + if (counter.isEmpty()) { + return 0; + } + IntLongPair first = counter.peek(); + IntLongPair last = counter.peekLast(); + return (float) (last.leftInt() - first.leftInt()) / (last.rightLong() - first.rightLong()) * 60_000f; + } + + public static double blockBreaks() { + if (blockBreaks.isEmpty()) { + return 0; + } + long firstTimestamp = blockBreaks.firstLong(); + long lastTimestamp = blockBreaks.lastLong(); + return Math.round((blockBreaks.size() - 1) / (double) (lastTimestamp - firstTimestamp) * 10000) / 10d; + } + + public static float farmingXpPercentProgress() { + return Math.clamp(farmingXpPercentProgress, 0, 100); + } + + public static double farmingXpPerHour() { + if (farmingXp.isEmpty()) { + return 0; + } + return Math.round(farmingXp.peek().leftFloat() * blockBreaks() * 3600 * 10) / 10d; + } + + 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; + } + + public boolean matchesText(String textToMatch) { + return this.text.equals(textToMatch); + } + } } 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 ab37080f..efe67cc4 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java @@ -94,10 +94,10 @@ public class FarmingHudWidget extends ComponentBasedWidget { float cropsPerMinute = FarmingHud.cropsPerMinute(); addSimpleIcoText(cropStack, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) cropsPerMinute / 10 * 10)); addSimpleIcoText(Ico.GOLD, "Coins/h: ", Formatting.GOLD, getPriceText(cropItemId, cropsPerMinute)); - addSimpleIcoText(cropStack, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks())); + addSimpleIcoText(cropStack, "Blocks/s: ", Formatting.YELLOW, Double.toString(FarmingHud.blockBreaks())); //noinspection DataFlowIssue - addComponent(Components.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())); + addComponent(Components.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(FarmingHud.farmingXpPerHour())); Entity cameraEntity = client.getCameraEntity(); String yaw = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getYaw())); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java deleted file mode 100644 index db946317..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java +++ /dev/null @@ -1,224 +0,0 @@ -package de.hysky.skyblocker.skyblock.garden; - -import de.hysky.skyblocker.annotations.Init; -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; -import de.hysky.skyblocker.utils.scheduler.MessageScheduler; -import io.github.moulberry.repo.data.NEUItem; -import it.unimi.dsi.fastutil.Pair; -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; -import net.minecraft.component.DataComponentTypes; -import net.minecraft.item.ItemStack; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.NumberFormat; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -//TODO: check inventory items, sum all repeated items into one -public class VisitorHelper { - private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Visitor Helper"); - private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); - - // The pair contains the name of the visitor and the texture if the icon is a player head - private static final Map, Object2IntMap> itemMap = new HashMap<>(); - private static final Map itemCache = new HashMap<>(); - private static final int TEXT_START_X = 4; - private static final int TEXT_START_Y = 4; - private static final int ENTRY_INDENT = 8; - private static final int ITEM_INDENT = 20; - private static final int LINE_SPACING = 3; - - private static boolean shouldProcessVisitorItems = true; - - @Init - public static void init() { - ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - String title = screen.getTitle().getString(); - if (SkyblockerConfigManager.get().farming.garden.visitorHelper && screen instanceof HandledScreen handledScreen && (Utils.getLocationRaw().equals("garden") && !title.contains("Logbook") || title.startsWith("Bazaar"))) { - ScreenEvents.afterRender(screen).register((screen_, context, mouseX, mouseY, delta) -> renderScreen(title, context, client.textRenderer, handledScreen.getScreenHandler(), mouseX, mouseY)); - ScreenEvents.remove(screen).register(screen_ -> shouldProcessVisitorItems = true); - } - }); - } - - public static void renderScreen(String title, DrawContext context, TextRenderer textRenderer, ScreenHandler handler, int mouseX, int mouseY) { - if (handler.getCursorStack() == ItemStack.EMPTY && shouldProcessVisitorItems) processVisitorItem(title, handler); - drawScreenItems(context, textRenderer, mouseX, mouseY); - } - - public static void onMouseClicked(double mouseX, double mouseY, int mouseButton, TextRenderer textRenderer) { - int yPosition = TEXT_START_Y; - for (Map.Entry, Object2IntMap> visitorEntry : itemMap.entrySet()) { - yPosition += LINE_SPACING + textRenderer.fontHeight; - - for (Object2IntMap.Entry itemEntry : visitorEntry.getValue().object2IntEntrySet()) { - String itemText = itemEntry.getKey(); - int textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue()); - - // 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, true); - return; - } - - // 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; - } - } - } - - public static void onSlotClick(Slot slot, int slotId, String title, ItemStack visitorHeadStack) { - if ((slotId == 29 || slotId == 13 || slotId == 33) && slot.hasStack() && ItemUtils.getLoreLineIf(slot.getStack(), s -> s.equals("Click to give!") || s.equals("Click to refuse!")) != null) { - itemMap.remove(new ObjectObjectImmutablePair<>(title, getTextureOrNull(visitorHeadStack))); - shouldProcessVisitorItems = false; - } - } - - private static void processVisitorItem(String visitorName, ScreenHandler handler) { - ItemStack visitorItem = handler.getSlot(13).getStack(); - if (visitorItem == null || !visitorItem.contains(DataComponentTypes.LORE) || ItemUtils.getLoreLineIf(visitorItem, t -> t.contains("Times Visited")) == null) return; - ItemStack acceptButton = handler.getSlot(29).getStack(); - if (acceptButton == null) return; - processLore(visitorName, getTextureOrNull(visitorItem), ItemUtils.getLore(acceptButton)); - } - - private static @Nullable String getTextureOrNull(ItemStack stack) { - String texture = ItemUtils.getHeadTexture(stack); - - return texture.isEmpty() ? null : texture; - } - - private static void processLore(String visitorName, @Nullable String visitorTexture, List loreList) { - boolean saveRequiredItems = false; - for (Text text : loreList) { - String lore = text.getString(); - if (lore.contains("Items Required")) - saveRequiredItems = true; - else if (lore.contains("Rewards")) - break; - else if (saveRequiredItems) - updateItemMap(visitorName, visitorTexture, text); - } - } - - 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; - try { - int amount = splitItemText.length == 2 ? NUMBER_FORMAT.parse(splitItemText[1].trim()).intValue() : 1; - Pair key = Pair.of(visitorName, visitorTexture); - Object2IntMap visitorMap = itemMap.computeIfAbsent(key, _key -> new Object2IntOpenHashMap<>()); - visitorMap.put(itemName, amount); - } catch (Exception e) { - LOGGER.error("[Skyblocker Visitor Helper] Failed to parse item: {}", lore.getString(), e); - } - } - - private static void drawScreenItems(DrawContext context, TextRenderer textRenderer, int mouseX, int mouseY) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 200); - int index = 0; - for (Map.Entry, Object2IntMap> visitorEntry : itemMap.entrySet()) { - Pair visitorName = visitorEntry.getKey(); - drawTextWithOptionalUnderline(context, textRenderer, Text.literal(visitorName.left()), TEXT_START_X, TEXT_START_Y + index * (LINE_SPACING + textRenderer.fontHeight), mouseX, mouseY); - index++; - - for (Object2IntMap.Entry itemEntry : visitorEntry.getValue().object2IntEntrySet()) { - index = drawItemEntryWithHover(context, textRenderer, itemEntry, index, mouseX, mouseY); - } - } - context.getMatrices().pop(); - } - - private static int drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, Object2IntMap.Entry itemEntry, int index, int mouseX, int mouseY) { - String itemName = itemEntry.getKey(); - int amount = itemEntry.getIntValue(); - ItemStack stack = getCachedItem(itemName); - drawItemEntryWithHover(context, textRenderer, stack, itemName, amount, index, mouseX, mouseY); - return index + 1; - } - - private static ItemStack getCachedItem(String displayName) { - String strippedName = Formatting.strip(displayName); - ItemStack cachedStack = itemCache.get(strippedName); - if (cachedStack != null) return cachedStack; - if (NEURepoManager.isLoading() || !ItemRepository.filesImported()) return null; // Item repo might be taking its sweet time doing things and cause concurrent modification error - Map items = NEURepoManager.NEU_REPO.getItems().getItems(); - if (items == null) return null; - ItemStack stack = items.values().stream() - .filter(i -> Formatting.strip(i.getDisplayName()).equals(strippedName)) - .findFirst() - .map(NEUItem::getSkyblockItemId) - .map(ItemRepository::getItemStack) - .orElse(null); - if (stack == null) return null; - itemCache.put(strippedName, stack); - return stack; - } - - /** - * 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 + ENTRY_INDENT, y - textRenderer.fontHeight + 5); - } - } - - private static void drawTextWithOptionalUnderline(DrawContext context, TextRenderer textRenderer, Text text, int x, int y, int mouseX, int mouseY) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 150); // This also puts our z at 350 - context.drawText(textRenderer, text, x, y, -1, true); - if (isMouseOverText(mouseX, mouseY, x, y, textRenderer.getWidth(text), textRenderer.fontHeight)) { - context.drawHorizontalLine(x, x + textRenderer.getWidth(text), y + textRenderer.fontHeight, -1); - } - context.getMatrices().pop(); - } - - private static boolean isMouseOverText(double mouseX, double mouseY, int x, int y, int width, int height) { - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/Visitor.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/Visitor.java new file mode 100644 index 00000000..875ef8e3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/Visitor.java @@ -0,0 +1,16 @@ +package de.hysky.skyblocker.skyblock.garden.visitor; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; + +public record Visitor(Text name, ItemStack head, Object2IntMap requiredItems) { + public Visitor(Text name, ItemStack head) { + this(name, head, new Object2IntOpenHashMap<>()); + } + + public void addRequiredItem(Text item, int amount) { + requiredItems.put(item, requiredItems.getOrDefault(item, 0) + amount); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/VisitorHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/VisitorHelper.java new file mode 100644 index 00000000..bb9d8ac6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/visitor/VisitorHelper.java @@ -0,0 +1,256 @@ +package de.hysky.skyblocker.skyblock.garden.visitor; + +import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.utils.*; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import io.github.moulberry.repo.data.NEUItem; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +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; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.*; + +public class VisitorHelper { + private static final Set activeVisitors = new HashSet<>(); + private static final Map cachedItems = new HashMap<>(); + // Map of grouped items with their total amount and associated visitors + private static final Object2IntMap groupedItems = new Object2IntOpenHashMap<>(); + private static final Map> visitorsByItem = new LinkedHashMap<>(); + private static final int X_OFFSET = 4; + private static final int Y_OFFSET = 4; + private static final int ICON_SIZE = 16; + private static final int LINE_HEIGHT = 3; + + @Init + public static void initialize() { + ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (!(screen instanceof HandledScreen handledScreen) || !shouldRender()) return; + + ScreenEvents.afterTick(screen).register(_screen -> updateVisitors(handledScreen.getScreenHandler())); + ScreenEvents.afterRender(screen).register((_screen, context, _x, _y, _d) -> renderVisitorHelper(context, client.textRenderer)); + }); + } + + public static boolean shouldRender() { + boolean isHelperEnabled = SkyblockerConfigManager.get().farming.visitorHelper.visitorHelper; + boolean isGardenMode = SkyblockerConfigManager.get().farming.visitorHelper.visitorHelperGardenOnly; + return isHelperEnabled && (!isGardenMode || Utils.isInGarden() || Utils.getIslandArea().contains("Bazaar")); + } + + /** + * Updates the current visitors and their required items. + */ + private static void updateVisitors(ScreenHandler handler) { + ItemStack visitorHead = handler.getSlot(13).getStack(); + if (visitorHead == null || !visitorHead.contains(DataComponentTypes.LORE) || ItemUtils.getLoreLineIf(visitorHead, t -> t.contains("Times Visited")) == null) return; + + Text visitorName = visitorHead.getName(); + if (activeVisitors.stream().map(Visitor::name).anyMatch(visitorName::equals)) return; + + Visitor newVisitor = new Visitor(visitorName, visitorHead.copy()); + extractRequiredItems(handler, newVisitor); + + if (!newVisitor.requiredItems().isEmpty()) { + activeVisitors.add(newVisitor); + } + + updateItems(); + } + + /** + * Extracts the required items for the given visitor. + */ + private static void extractRequiredItems(ScreenHandler handler, Visitor visitor) { + ItemStack acceptButton = handler.getSlot(29).getStack(); + if (acceptButton == null || ItemUtils.getLoreLineIf(acceptButton, t -> t.contains("Items Required")) == null) return; + + ItemUtils.getLore(acceptButton).stream() + .map(Text::getString) + .map(String::trim) + .takeWhile(lore -> !lore.contains("Rewards")) + .filter(lore -> lore.contains(" x")) + .map(lore -> lore.split(" x")) + .forEach(parts -> visitor.addRequiredItem(Text.literal(parts[0].trim()), Formatters.parseNumber(parts[1].trim()).intValue())); + } + + private static void updateItems() { + groupedItems.clear(); + visitorsByItem.clear(); + + // Group items by their name and accumulate their counts + for (Visitor visitor : activeVisitors) { + for (Object2IntMap.Entry entry : visitor.requiredItems().object2IntEntrySet()) { + Text itemName = entry.getKey(); + int amount = entry.getIntValue(); + + groupedItems.put(itemName, groupedItems.getOrDefault(itemName, 0) + amount); + visitorsByItem.computeIfAbsent(itemName, k -> new LinkedList<>()).add(visitor); + } + } + } + + /** + * Retrieves a cached ItemStack or fetches it if not already cached. + */ + private static ItemStack getCachedItem(String itemName) { + String cleanName = Formatting.strip(itemName); + return cachedItems.computeIfAbsent(cleanName, name -> { + if (NEURepoManager.isLoading() || !ItemRepository.filesImported()) return null; + + return NEURepoManager.NEU_REPO.getItems().getItems() + .values().stream() + .filter(item -> Formatting.strip(item.getDisplayName()).equals(name)) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .orElse(null); + }); + } + + /** + * Draws the visitor items and their associated information. + */ + private static void renderVisitorHelper(DrawContext context, TextRenderer textRenderer) { + int index = 0; + + context.getMatrices().push(); + context.getMatrices().translate(0, 0, 500); + + for (Object2IntMap.Entry entry : groupedItems.object2IntEntrySet()) { + Text itemName = entry.getKey(); + int totalAmount = entry.getIntValue(); + List visitors = visitorsByItem.get(itemName); + + if (visitors == null || visitors.isEmpty()) continue; + + // Render visitors' heads for the shared item + for (Visitor visitor : visitors) { + int yPosition = Y_OFFSET + index * (LINE_HEIGHT + textRenderer.fontHeight); + + context.getMatrices().push(); + context.getMatrices().translate(X_OFFSET, yPosition + (float) textRenderer.fontHeight / 2 - ICON_SIZE * 0.95f / 2, 0); + context.getMatrices().scale(0.95f, 0.95f, 1.0f); + context.drawItem(visitor.head(), 0, 0); + context.getMatrices().pop(); + + context.drawText(textRenderer, visitor.name(), X_OFFSET + (int) (ICON_SIZE * 0.95f) + 4, yPosition, -1, true); + + index++; + } + + // Render the shared item with the total amount + int iconX = X_OFFSET + 12; + int textX = iconX + (int) (ICON_SIZE * 0.95f) + 4; + int yPosition = Y_OFFSET + index * (LINE_HEIGHT + textRenderer.fontHeight); + + ItemStack cachedStack = getCachedItem(itemName.getString()); + if (cachedStack != null) { + context.getMatrices().push(); + context.getMatrices().translate(iconX, yPosition + (float) textRenderer.fontHeight / 2 - ICON_SIZE * 0.95f / 2, 0); + context.getMatrices().scale(0.95f, 0.95f, 1.0f); + context.drawItem(cachedStack, 0, 0); + context.getMatrices().pop(); + } + + Text itemText = SkyblockerConfigManager.get().farming.visitorHelper.showStacksInVisitorHelper + ? cachedStack.getName().copy() + .append(" x" + (totalAmount / 64) + " stacks + " + (totalAmount % 64)) + : cachedStack.getName().copy() + .append(" x" + totalAmount); + + int itemTextWidth = textRenderer.getWidth(itemText); + int copyTextX = textX + itemTextWidth; + + context.drawText(textRenderer, itemText, textX, yPosition, -1, true); + context.drawText(textRenderer, Text.literal(" [Copy Amount]").setStyle(Style.EMPTY.withColor(Formatting.YELLOW)), copyTextX, yPosition, -1, true); + + index++; + } + + context.getMatrices().pop(); + } + + + /** + * Handles mouse click events on the visitor UI. + */ + public static void handleMouseClick(double mouseX, double mouseY, int mouseButton, TextRenderer textRenderer) { + if (mouseButton != 0) return; + + int index = 0; + int yOffsetAdjustment = -5; + + for (Object2IntMap.Entry entry : groupedItems.object2IntEntrySet()) { + Text itemName = entry.getKey(); + int totalAmount = entry.getIntValue(); + List visitors = visitorsByItem.get(itemName); + + if (visitors != null && !visitors.isEmpty()) { + for (Visitor ignored : visitors) { + index++; + } + + int iconX = X_OFFSET + 12; + int textX = iconX + (int) (ICON_SIZE * 0.95f) + 4; + int yPosition = Y_OFFSET + index * (LINE_HEIGHT + textRenderer.fontHeight) - + (int) ((float) textRenderer.fontHeight / 2 - ICON_SIZE * 0.95f / 2) + yOffsetAdjustment; + + Text itemText = SkyblockerConfigManager.get().farming.visitorHelper.showStacksInVisitorHelper + ? itemName.copy() + .append(" x" + (totalAmount / 64) + " stacks + " + (totalAmount % 64)) + : itemName.copy() + .append(" x" + totalAmount); + + int itemTextWidth = textRenderer.getWidth(itemText); + int copyTextX = textX + itemTextWidth; + + if (isMouseOverText(mouseX, mouseY, textX, yPosition, itemTextWidth, textRenderer.fontHeight)) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/bz " + itemName.getString(), true); + return; + } + + if (isMouseOverText(mouseX, mouseY, copyTextX, yPosition, textRenderer.getWidth(" [Copy Amount]"), textRenderer.fontHeight)) { + MinecraftClient.getInstance().keyboard.setClipboard(String.valueOf(totalAmount)); + MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append("Copied amount successfully"), false); + return; + } + + index++; + } + } + } + + /** + * Handles slot clicks to remove a visitor when certain conditions are met. + * + * @param title The visitor's name to match for removal. + */ + public static void onSlotClick(Slot slot, int slotId, String title) { + if ((slotId == 29 || slotId == 13 || slotId == 33) && slot.hasStack() && + ItemUtils.getLoreLineIf(slot.getStack(), s -> s.equals("Click to give!") || s.equals("Click to refuse!")) != null) { + activeVisitors.removeIf(entry -> entry.name().getString().equals(title)); + } + + updateItems(); + } + + /** + * Checks if the mouse is over a specific rectangular region. + */ + private static boolean isMouseOverText(double mouseX, double mouseY, int x, int y, int width, int height) { + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Formatters.java b/src/main/java/de/hysky/skyblocker/utils/Formatters.java index 539f9d67..eb063f36 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Formatters.java +++ b/src/main/java/de/hysky/skyblocker/utils/Formatters.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.utils; import java.text.NumberFormat; +import java.text.ParseException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; @@ -19,41 +20,52 @@ import net.minecraft.util.Util; public class Formatters { /** * Formats numbers as integers with commas. - * + * * Example: 100,000,000 */ public static final NumberFormat INTEGER_NUMBERS = NumberFormat.getIntegerInstance(Locale.US); /** * Formats numbers as floats with up to two digits of precision. - * + * * Example: 100,000.15 */ public static final NumberFormat DOUBLE_NUMBERS = Util.make(NumberFormat.getInstance(Locale.US), nf -> nf.setMaximumFractionDigits(2)); /** * Formats numbers as floats with up to one digit of precision. - * + * * Example: 100,000.1 */ public static final NumberFormat FLOAT_NUMBERS = Util.make(NumberFormat.getInstance(Locale.US), nf -> nf.setMaximumFractionDigits(1)); /** * Formats integer numbers in a short format. - * + * * Examples: 10B, 1M, and 5K. */ public static final NumberFormat SHORT_INTEGER_NUMBERS = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); /** * Formats float numbers in a short format. - * + * * Examples: 17.3B, 1.5M, and 10.8K. */ public static final NumberFormat SHORT_FLOAT_NUMBERS = Util.make(NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT), nf -> nf.setMinimumFractionDigits(1)); /** * Formats dates to a standard format. - * + * * Examples: Thu Jan 30 2025 2:00:10 PM, Thu Jan 30 2025 14:00:10 */ public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("E MMM d yyyy " + getTimeFormat(), Locale.US).withZone(ZoneId.systemDefault()); + /** + * Parses a number from a string, allowing commas as thousands separators and periods as decimal points. + */ + public static Number parseNumber(String number) throws NumberFormatException { + try { + return DOUBLE_NUMBERS.parse(number); + } catch (ParseException e) { + throw new NumberFormatException("For input string: \"" + number + "\""); + } + } + /** * Returns the formatting for the time, always returns 12 hour in development environments for testing purposes. */ @@ -63,12 +75,12 @@ public class Formatters { /** * Determines whether to use the 12 or 24 hour clock for formatting time.

- * + * * On macOS this reads the preference for the system clock's time format which accounts for whether a user * chooses 12 or 24 hour time in the System Settings.

- * + * * On other platforms, the time format follows the default for the user's current locale. - * + * * @see NSDateFormatter * @see Unicode Locale Data Markup Language (LDML) */ diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java index 385e1fd1..8c3c3c6b 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -460,4 +460,4 @@ public final class ItemUtils { } return stringBuilder.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index c6266cb7..53af1148 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -6,7 +6,6 @@ import com.mojang.util.UndashedUuid; import de.hysky.skyblocker.annotations.Init; 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.slayers.SlayerManager; import de.hysky.skyblocker.utils.purse.PurseChangeCause; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; @@ -136,6 +135,10 @@ public class Utils { return location == Location.THE_RIFT; } + public static boolean isInGarden() { + return location == Location.GARDEN; + } + /** * @return if the player is in the end island */ diff --git a/src/main/java/de/hysky/skyblocker/utils/mayor/MayorUtils.java b/src/main/java/de/hysky/skyblocker/utils/mayor/MayorUtils.java index ffe3594c..6cf75a81 100644 --- a/src/main/java/de/hysky/skyblocker/utils/mayor/MayorUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/mayor/MayorUtils.java @@ -92,12 +92,19 @@ public class MayorUtils { } try { JsonObject ministerObject = result.getAsJsonObject("minister"); - JsonObject ministerPerk = ministerObject.getAsJsonObject("perk"); - minister = new Minister(ministerObject.get("key").getAsString(), - ministerObject.get("name").getAsString(), - new Perk(ministerPerk.get("name").getAsString(), ministerPerk.get("description").getAsString())); + if (ministerObject != null) { // Check if ministerObject is not null stops NPE caused by Derpy + JsonObject ministerPerk = ministerObject.getAsJsonObject("perk"); + minister = new Minister( + ministerObject.get("key").getAsString(), + ministerOb