From f6736848feed926026a4a6045bd31de91f503b82 Mon Sep 17 00:00:00 2001 From: Rime <81419447+Emirlol@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:15:30 +0300 Subject: Fix chest value not accounting for item count in sacks & stashes (#1337) * Fix chest value not accounting for item count in sacks * Fix item count being ignored in the stash * Add some `@NotNull` annotations * Fix gemstone sack value not working --- .../de/hysky/skyblocker/skyblock/ChestValue.java | 94 +++++++++++++++++----- .../item/tooltip/adders/BazaarPriceTooltip.java | 5 +- .../item/tooltip/adders/CraftPriceTooltip.java | 5 +- .../tooltip/adders/EstimatedItemValueTooltip.java | 9 +-- .../item/tooltip/adders/NpcPriceTooltip.java | 5 +- .../java/de/hysky/skyblocker/utils/ItemUtils.java | 38 ++++++++- 6 files changed, 116 insertions(+), 40 deletions(-) (limited to 'src/main/java/de') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java index 26f64257..975edceb 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java @@ -25,6 +25,7 @@ import net.minecraft.screen.GenericContainerScreenHandler; import net.minecraft.screen.slot.Slot; import net.minecraft.text.Style; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import net.minecraft.util.Util; import net.minecraft.util.math.MathHelper; import org.apache.commons.lang3.StringUtils; @@ -34,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.DecimalFormat; +import java.text.NumberFormat; import java.text.ParseException; import java.util.List; import java.util.Set; @@ -43,7 +45,7 @@ import java.util.regex.Pattern; public class ChestValue { private static final Logger LOGGER = LoggerFactory.getLogger(ChestValue.class); private static final Set DUNGEON_CHESTS = Set.of("Wood Chest", "Gold Chest", "Diamond Chest", "Emerald Chest", "Obsidian Chest", "Bedrock Chest"); - private static final Pattern ESSENCE_PATTERN = Pattern.compile("(?[A-Za-z]+) Essence x(?[0-9]+)"); + private static final Pattern ESSENCE_PATTERN = Pattern.compile("(?[A-Za-z]+) Essence x(?\\d+)"); private static final Pattern MINION_PATTERN = Pattern.compile("Minion (I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$"); private static final DecimalFormat FORMATTER = new DecimalFormat("#,###"); @@ -53,6 +55,7 @@ public class ChestValue { if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) { Text title = screen.getTitle(); String titleString = title.getString(); + if (DUNGEON_CHESTS.contains(titleString)) { if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator) { ScreenEvents.afterTick(screen).register(ignored -> { @@ -62,20 +65,19 @@ public class ChestValue { }); } } else if (SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue && !titleString.equals("SkyBlock Menu")) { - boolean minion = MINION_PATTERN.matcher(title.getString().trim()).find(); + ScreenType screenType = determineScreenType(titleString); Screens.getButtons(screen).add(ButtonWidget .builder(Text.literal("$"), buttonWidget -> { Screens.getButtons(screen).remove(buttonWidget); ScreenEvents.afterTick(screen).register(ignored -> { - Text chestValue = getChestValue(genericContainerScreen.getScreenHandler(), minion); + Text chestValue = getChestValue(genericContainerScreen.getScreenHandler(), screenType); if (chestValue != null) { addValueToContainer(genericContainerScreen, chestValue, title); } }); - }) .dimensions(((HandledScreenAccessor) genericContainerScreen).getX() + ((HandledScreenAccessor) genericContainerScreen).getBackgroundWidth() - 16, ((HandledScreenAccessor) genericContainerScreen).getY() + 4, 12, 12) - .tooltip(minion ? Tooltip.of(Text.translatable("skyblocker.config.general.minionValue.@Tooltip")) : Tooltip.of(Text.translatable("skyblocker.config.general.chestValue.@Tooltip"))) + .tooltip(Tooltip.of(getButtonTooltipText(screenType))) .build() ); } @@ -138,7 +140,7 @@ public class ChestValue { //Incase we're searching the free chest if (!StringUtils.isBlank(foundString)) { - profit -= Integer.parseInt(foundString.replaceAll("[^0-9]", "")); + profit -= Integer.parseInt(foundString.replaceAll("\\D", "")); } continue; @@ -166,36 +168,52 @@ public class ChestValue { return null; } - private static @Nullable Text getChestValue(GenericContainerScreenHandler handler, boolean minion) { + private static @Nullable Text getChestValue(GenericContainerScreenHandler handler, @NotNull ScreenType screenType) { try { double value = 0; boolean hasIncompleteData = false; - List slots = minion ? getMinionSlots(handler) : handler.slots.subList(0, handler.getRows() * 9); + + List slots = switch (screenType) { + case MINION -> getMinionSlots(handler); + case SACK -> handler.slots.subList(10, (handler.getRows() * 9) - 10); // Skip the glass pane rows so we don't have to iterate over them + case STASH -> handler.slots.subList(0, (handler.getRows() - 1) * 9); // Stash uses the bottom row for the menu, so we skip it + case OTHER -> handler.slots.subList(0, handler.getRows() * 9); + }; for (Slot slot : slots) { ItemStack stack = slot.getStack(); - if (stack.isEmpty()) { - continue; - } + if (stack.isEmpty()) continue; + String coinsLine; - if (minion && slot.id == 28 && stack.isOf(Items.HOPPER) && (coinsLine = ItemUtils.getLoreLineIf(stack, s -> s.contains("Held Coins:"))) != null) { + if (screenType == ScreenType.MINION && slot.id == 28 && stack.isOf(Items.HOPPER) && (coinsLine = ItemUtils.getLoreLineIf(stack, s -> s.contains("Held Coins:"))) != null) { String source = coinsLine.split(":")[1]; try { - value += DecimalFormat.getNumberInstance(java.util.Locale.US).parse(source.trim()).doubleValue(); + value += NumberFormat.getNumberInstance(java.util.Locale.US).parse(source.trim()).doubleValue(); } catch (ParseException e) { - LOGGER.warn("[Skyblocker] Failed to parse {}", source); + LOGGER.warn("[Skyblocker] Failed to parse `{}`", source); } continue; } String id = stack.getSkyblockApiId(); + int count = switch (screenType) { + case SACK -> { + List lines = ItemUtils.getLore(stack); + yield ItemUtils.getItemCountInSack(stack, lines, true).orElse(0); // If this is in a sack and the item is not a stored item, we can just skip it + } + case STASH -> ItemUtils.getItemCountInStash(stack).orElse(0); + case OTHER, MINION -> stack.getCount(); + }; + + if (count == 0) continue; + if (!id.isEmpty()) { DoubleBooleanPair priceData = ItemUtils.getItemPrice(id); if (!priceData.rightBoolean()) hasIncompleteData = true; - value += NetworthCalculator.getItemNetworth(stack).price(); + value += NetworthCalculator.getItemNetworth(stack, count).price(); } } @@ -223,17 +241,26 @@ public class ChestValue { } static Text getProfitText(long profit, boolean hasIncompleteData) { + return Text.literal((profit > 0 ? " +" : ' ') + FORMATTER.format(profit) + " Coins").formatted(getProfitColor(hasIncompleteData, profit)); + } + + @NotNull + static Formatting getProfitColor(boolean hasIncompleteData, long profit) { DungeonsConfig.DungeonChestProfit config = SkyblockerConfigManager.get().dungeons.dungeonChestProfit; - return Text.literal((profit > 0 ? " +" : ' ') + FORMATTER.format(profit) + " Coins").formatted(hasIncompleteData ? config.incompleteColor : (Math.abs(profit) < config.neutralThreshold) ? config.neutralColor : (profit > 0) ? config.profitColor : config.lossColor); + if (hasIncompleteData) return config.incompleteColor; + if (Math.abs(profit) < config.neutralThreshold) return config.neutralColor; + if (profit > 0) return config.profitColor; + return config.lossColor; } + @NotNull static Text getValueText(long value, boolean hasIncompleteData) { UIAndVisualsConfig.ChestValue config = SkyblockerConfigManager.get().uiAndVisuals.chestValue; return Text.literal(' ' + FORMATTER.format(value) + " Coins").formatted(hasIncompleteData ? config.incompleteColor : config.color); } private static void addValueToContainer(GenericContainerScreen genericContainerScreen, Text chestValue, Text title) { - Screens.getButtons(genericContainerScreen).removeIf(clickableWidget -> clickableWidget instanceof ChestValueTextWidget); + Screens.getButtons(genericContainerScreen).removeIf(ChestValueTextWidget.class::isInstance); int backgroundWidth = ((HandledScreenAccessor) genericContainerScreen).getBackgroundWidth(); int y = ((HandledScreenAccessor) genericContainerScreen).getY(); int x = ((HandledScreenAccessor) genericContainerScreen).getX(); @@ -250,6 +277,24 @@ public class ChestValue { Screens.getButtons(genericContainerScreen).add(chestTitleWidget); } + @NotNull + private static ScreenType determineScreenType(String rawTitleString) { + if (StringUtils.containsIgnoreCase(rawTitleString, "sack")) return ScreenType.SACK; + if (MINION_PATTERN.matcher(rawTitleString.trim()).find()) return ScreenType.MINION; + if (StringUtils.equalsIgnoreCase(rawTitleString, "View Stash")) return ScreenType.STASH; + return ScreenType.OTHER; + } + + @NotNull + private static Text getButtonTooltipText(ScreenType screenType) { + return switch (screenType) { + case MINION -> Text.translatable("skyblocker.config.general.minionValue.@Tooltip"); + case OTHER -> Text.translatable("skyblocker.config.general.chestValue.@Tooltip"); + case STASH -> Text.translatable("skyblocker.config.general.stashValue.@Tooltip"); + case SACK -> Text.translatable("skyblocker.config.general.sackValue.@Tooltip"); + }; + } + private static class ChestValueTextWidget extends TextWidget { public boolean shadow = false; @@ -264,15 +309,13 @@ public class ChestValue { } // Yoinked from ClickableWidget - protected void draw( - DrawContext context, TextRenderer textRenderer, Text text, int startX, int endX - ) { + protected void draw(DrawContext context, TextRenderer textRenderer, Text text, int startX, int endX) { int i = textRenderer.getWidth(text); int k = endX - startX; if (i > k) { int l = i - k; - double d = (double) Util.getMeasuringTimeMs() / 600.0; - double e = Math.max((double) l * 0.5, 3.0); + double d = Util.getMeasuringTimeMs() / 600.0; + double e = Math.max(l * 0.5, 3.0); double f = Math.sin((Math.PI / 2) * Math.cos((Math.PI * 2) * d / e)) / 2.0 + 0.5; double g = MathHelper.lerp(f, 0.0, l); context.enableScissor(startX, getY(), endX, getY() + textRenderer.fontHeight); @@ -283,4 +326,11 @@ public class ChestValue { } } } + + private enum ScreenType { + MINION, + SACK, + STASH, + OTHER + } } 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 index ec2f10cf..c07fa401 100644 --- 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 @@ -12,7 +12,6 @@ import net.minecraft.util.Formatting; import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.OptionalInt; public class BazaarPriceTooltip extends SimpleTooltipAdder { public BazaarPriceTooltip(int priority) { @@ -24,9 +23,7 @@ public class BazaarPriceTooltip extends SimpleTooltipAdder { String skyblockApiId = stack.getSkyblockApiId(); if (TooltipInfoType.BAZAAR.hasOrNullWarning(skyblockApiId)) { - OptionalInt optCount = ItemUtils.getItemCountInSack(stack, lines); - // This clamp is here to ensure that the tooltip doesn't show a useless price of 0 coins if the item count is 0. - int count = optCount.isPresent() ? Math.max(optCount.getAsInt(), 1) : stack.getCount(); + int count = Math.max(ItemUtils.getItemCountInSack(stack, lines).orElse(ItemUtils.getItemCountInStash(lines.getFirst()).orElse(stack.getCount())), 1); BazaarProduct product = TooltipInfoType.BAZAAR.getData().get(skyblockApiId); lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:")) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java index e42e76bb..35171251 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; -import java.util.OptionalInt; import java.util.concurrent.ConcurrentHashMap; public class CraftPriceTooltip extends SimpleTooltipAdder { @@ -53,9 +52,7 @@ public class CraftPriceTooltip extends SimpleTooltipAdder { if (totalCraftCost == 0) return; - OptionalInt optCount = ItemUtils.getItemCountInSack(stack, lines); - // This clamp is here to ensure that the tooltip doesn't show a useless price of 0 coins if the item count is 0. - int count = optCount.isPresent() ? Math.max(optCount.getAsInt(), 1) : stack.getCount(); + int count = Math.max(ItemUtils.getItemCountInSack(stack, lines).orElse(ItemUtils.getItemCountInStash(lines.getFirst()).orElse(stack.getCount())), 1); neuRecipes.getFirst().getAllOutputs().stream().findFirst().ifPresent(outputIngredient -> lines.add(Text.literal(String.format("%-20s", "Crafting Price:")).formatted(Formatting.GOLD) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/EstimatedItemValueTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/EstimatedItemValueTooltip.java index a2509a82..10924e55 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/EstimatedItemValueTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/EstimatedItemValueTooltip.java @@ -1,9 +1,5 @@ package de.hysky.skyblocker.skyblock.item.tooltip.adders; -import java.util.List; - -import org.jetbrains.annotations.Nullable; - import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.skyblock.item.tooltip.SimpleTooltipAdder; import de.hysky.skyblocker.skyblock.item.tooltip.info.TooltipInfoType; @@ -14,6 +10,9 @@ 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 EstimatedItemValueTooltip extends SimpleTooltipAdder { @@ -23,7 +22,7 @@ public class EstimatedItemValueTooltip extends SimpleTooltipAdder { @Override public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List lines) { - int count = Math.max(ItemUtils.getItemCountInSack(stack, lines).orElse(stack.getCount()), 1); + int count = Math.max(ItemUtils.getItemCountInSack(stack, lines).orElse(ItemUtils.getItemCountInStash(lines.getFirst()).orElse(stack.getCount())), 1); NetworthResult result = NetworthCalculator.getItemNetworth(stack, count); 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 index 13e2b213..8abd9487 100644 --- 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 @@ -11,7 +11,6 @@ import net.minecraft.util.Formatting; import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.OptionalInt; public class NpcPriceTooltip extends SimpleTooltipAdder { @@ -35,9 +34,7 @@ public class NpcPriceTooltip extends SimpleTooltipAdder { double price = TooltipInfoType.NPC.getData().getOrDefault(internalID, -1); // The original default return value of 0 can be an actual price, so we use a value that can't be a price if (price < 0) return; - OptionalInt optCount = ItemUtils.getItemCountInSack(stack, lines); - // This clamp is here to ensure that the tooltip doesn't show a useless price of 0 coins if the item count is 0. - int count = optCount.isPresent() ? Math.max(optCount.getAsInt(), 1) : stack.getCount(); + int count = Math.max(ItemUtils.getItemCountInSack(stack, lines).orElse(ItemUtils.getItemCountInStash(lines.getFirst()).orElse(stack.getCount())), 1); lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:")) .formatted(Formatting.YELLOW) diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java index c64a6b5c..d6742a51 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -40,6 +40,7 @@ import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Util; import net.minecraft.util.dynamic.Codecs; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -65,6 +66,7 @@ public final class ItemUtils { ).apply(instance, ItemStack::new))); private static final Logger LOGGER = LoggerFactory.getLogger(ItemUtils.class); private static final Pattern STORED_PATTERN = Pattern.compile("Stored: ([\\d,]+)/\\S+"); + private static final Pattern STASH_COUNT_PATTERN = Pattern.compile("x([\\d,]+)$"); // This is used with Matcher#find, not #matches private static final short LOG_INTERVAL = 1000; private static long lastLog = Util.getMeasuringTimeMs(); @@ -484,8 +486,22 @@ public final class ItemUtils { * @param lines The tooltip lines to search in. This isn't equivalent to the item's lore. * @return An {@link OptionalInt} containing the number of items stored in the sack, or an empty {@link OptionalInt} if the item is not a sack or the amount could not be found. */ + @NotNull public static OptionalInt getItemCountInSack(@NotNull ItemStack itemStack, @NotNull List lines) { - if (lines.size() >= 2 && lines.get(1).getString().endsWith("Sack")) { + return getItemCountInSack(itemStack, lines, false); + } + + /** + * Finds the number of items stored in a sack from a list of texts. + * @param itemStack The item stack this list of texts belong to. This is used for logging purposes. + * @param lines A list of text lines that represent the tooltip of the item stack. + * @param isLore Whether the lines are from the item's lore or not. This is useful to figure out which line to look at, as lore and tooltip lines are different due to the first line being the item's name. + * @return An {@link OptionalInt} containing the number of items stored in the sack, or an empty {@link OptionalInt} if the item is not a sack or the amount could not be found. + */ + @NotNull + public static OptionalInt getItemCountInSack(@NotNull ItemStack itemStack, @NotNull List lines, boolean isLore) { + // Gemstone sack is a special case, it has a different 2nd line. + if (lines.size() >= 2 && StringUtils.endsWithAny(lines.get(isLore ? 0 : 1).getString(), "Sack", "Gemstones")) { // 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}]] // Which equals: `Stored: 0/20k` Matcher matcher = TextUtils.matchInList(lines, STORED_PATTERN); @@ -500,4 +516,24 @@ public final class ItemUtils { } return OptionalInt.empty(); } + + /** + * Finds the number of items stored in a stash based on item's name. + * @param itemStack The item stack. + * @return An {@link OptionalInt} containing the number of items stored in the stash, or an empty {@link OptionalInt} if the item is not a stash or the amount could not be found. + */ + @NotNull + public static OptionalInt getItemCountInStash(@NotNull ItemStack itemStack) { + return getItemCountInStash(itemStack.getName()); + } + + /** + * Finds the number of items stored in a stash based on item's name. + * @param itemName The name of the item to look in. + * @return An {@link OptionalInt} containing the number of items stored in the stash, or an empty {@link OptionalInt} if the item is not a stash or the amount could not be found. + */ + @NotNull + public static OptionalInt getItemCountInStash(@NotNull Text itemName) { + return RegexUtils.findIntFromMatcher(STASH_COUNT_PATTERN.matcher(itemName.getString())); + } } -- cgit