diff --git a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/BuyOrderAmountHelper.java b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/BuyOrderAmountHelper.java index 9c3df99c9..f35422217 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/BuyOrderAmountHelper.java +++ b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/BuyOrderAmountHelper.java @@ -6,6 +6,9 @@ import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenMatcher; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenType; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarSlots; +import com.github.mkram17.bazaarutils.utils.bazaar.gui.layouts.TransactionPageLayout; +import com.github.mkram17.bazaarutils.utils.bazaar.market.order.OrderUtil; +import com.github.mkram17.bazaarutils.utils.bazaar.market.price.PricingPosition; import com.github.mkram17.bazaarutils.utils.minecraft.components.CustomDataComponents; import com.github.mkram17.bazaarutils.utils.bazaar.market.order.TransactionType; import com.github.mkram17.bazaarutils.utils.minecraft.gui.ScreenMatcher; @@ -18,6 +21,8 @@ import lombok.Getter; import net.minecraft.network.chat.Component; +import java.util.OptionalDouble; +import java.util.OptionalInt; import java.util.stream.IntStream; @Getter @@ -100,6 +105,20 @@ protected int computeFixedValue(TransactionState state) { return getFixedAmount(); } + @Override + protected OptionalInt computeMaxValue(TransactionAmount.TransactionState state) { + OptionalDouble price = OrderUtil.getPriceForPositionOptional(state.productId(), PricingPosition.COMPETITIVE, getTransactionType()); + + // A missing or non-positive price divides into an amount that is nonsense rather than large. + if (price.isEmpty() || price.getAsDouble() <= 0) return OptionalInt.empty(); + + int amountCanAfford = (int) (state.purse() / price.getAsDouble()); + + return OptionalInt.of(TransactionPageLayout.findBuyOrderAmountLimit(state.inputSign().itemStack()) + .map(limit -> Math.min(amountCanAfford, limit)) + .orElse(amountCanAfford)); + } + @Override protected Component getButtonItemText(TransactionState state) { return Component.nullToEmpty("Order " + getButtonItemStackSize(state) + " items."); diff --git a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/InstantBuyAmountHelper.java b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/InstantBuyAmountHelper.java index 1738dd248..8e9c73078 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/InstantBuyAmountHelper.java +++ b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/InstantBuyAmountHelper.java @@ -3,10 +3,14 @@ import com.github.mkram17.bazaarutils.config.util.api.SlotProviders; import com.github.mkram17.bazaarutils.config.util.api.annotations.ContainerSlot; import com.github.mkram17.bazaarutils.utils.bazaar.SignInputHelper; +import com.github.mkram17.bazaarutils.utils.bazaar.data.BazaarDataUtil; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenMatcher; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenType; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarSlots; +import com.github.mkram17.bazaarutils.utils.bazaar.gui.layouts.TransactionPageLayout; import com.github.mkram17.bazaarutils.utils.bazaar.market.order.TransactionType; +import com.github.mkram17.bazaarutils.utils.minecraft.ItemInfo; +import com.github.mkram17.bazaarutils.utils.minecraft.SlotLookup; import com.github.mkram17.bazaarutils.utils.minecraft.components.CustomDataComponents; import com.github.mkram17.bazaarutils.utils.minecraft.gui.ScreenMatcher; import com.github.mkram17.bazaarutils.utils.minecraft.item.ItemRef; @@ -18,6 +22,8 @@ import lombok.Getter; import net.minecraft.network.chat.Component; +import java.util.Optional; +import java.util.OptionalInt; import java.util.stream.IntStream; @Getter @@ -100,9 +106,34 @@ protected int computeFixedValue(TransactionState state) { return getFixedAmount(); } + @Override + protected OptionalInt computeMaxValue(TransactionAmount.TransactionState state) { + return OptionalInt.of(SlotLookup.getInventoryItem(state.container(), BazaarSlots.INSTANT_BUY.INPUT_FILLING_AMOUNT.slot) + .map(ItemInfo::itemStack) + .flatMap(TransactionPageLayout::findOptionAmount) + .map(value -> (int) Math.floor(value)) + .orElse(state.playerInventory() + .getNonEquipmentItems() + .stream() + .mapToInt(stack -> { + int maxStackSize = state.productItem().itemStack().getMaxStackSize(); + if (stack.isEmpty()) return maxStackSize; + + boolean isSameItem = Optional.ofNullable(stack.getCustomName()) + .map(Component::getString) + .flatMap(BazaarDataUtil::findProductIdOptional) + .map(id -> id.equals(state.productId())) + .orElse(false); + + return isSameItem ? maxStackSize - stack.getCount() : 0; + }) + .sum() + )); + } + @Override protected Component getButtonItemText(TransactionState state) { - return Component.nullToEmpty("Offer " + getButtonItemStackSize(state) + " items."); + return Component.nullToEmpty("Purchase " + getButtonItemStackSize(state) + " items."); } @Override diff --git a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/SellOfferAmountHelper.java b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/SellOfferAmountHelper.java index 21da5a9fb..426a4f9da 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/SellOfferAmountHelper.java +++ b/src/main/java/com/github/mkram17/bazaarutils/features/gui/buttons/inputhelper/amount/SellOfferAmountHelper.java @@ -3,6 +3,7 @@ import com.github.mkram17.bazaarutils.config.util.api.SlotProviders; import com.github.mkram17.bazaarutils.config.util.api.annotations.ContainerSlot; import com.github.mkram17.bazaarutils.utils.bazaar.SignInputHelper; +import com.github.mkram17.bazaarutils.utils.bazaar.data.BazaarDataUtil; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenMatcher; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenType; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarSlots; @@ -17,7 +18,10 @@ import com.teamresourceful.resourcefulconfig.api.types.info.ListEntryInfoProvider; import lombok.Getter; import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import java.util.Optional; +import java.util.OptionalInt; import java.util.stream.IntStream; @Getter @@ -100,6 +104,19 @@ protected int computeFixedValue(TransactionState state) { return getFixedAmount(); } + @Override + protected OptionalInt computeMaxValue(TransactionState state) { + return OptionalInt.of(state.playerInventory().getNonEquipmentItems().stream() + .filter(stack -> !stack.isEmpty()) + .filter(stack -> Optional.ofNullable(stack.getCustomName()) + .map(Component::getString) + .flatMap(BazaarDataUtil::findProductIdOptional) + .map(productId -> productId.equals(state.productId())) + .orElse(false)) + .mapToInt(ItemStack::getCount) + .sum()); + } + @Override protected Component getButtonItemText(TransactionState state) { return Component.nullToEmpty("Offer " + getButtonItemStackSize(state) + " items."); diff --git a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/InputHelper.java b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/InputHelper.java index 82a21bcf9..4bbde8b1a 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/InputHelper.java +++ b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/InputHelper.java @@ -80,8 +80,7 @@ public Result onButtonClicked(int button) { SoundUtil.playSound(BUTTON_SOUND, BUTTON_VOLUME); - handleAction(state.get()); - resetState(); + handleAction(state.get(), this::resetState); return Result.CONSUMED; } @@ -94,5 +93,5 @@ public Result onButtonClicked(int button) { // Action stuff - protected abstract void handleAction(T state); + protected abstract void handleAction(T state, Runnable callback); } \ No newline at end of file diff --git a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/SignInputHelper.java b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/SignInputHelper.java index c86267466..9622bf848 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/SignInputHelper.java +++ b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/SignInputHelper.java @@ -1,10 +1,9 @@ package com.github.mkram17.bazaarutils.utils.bazaar; import com.github.mkram17.bazaarutils.events.minecraft.ContainerLoadedEvent; +import com.github.mkram17.bazaarutils.utils.Result; import com.github.mkram17.bazaarutils.utils.bazaar.gui.layouts.ProductPageLayout; -import com.github.mkram17.bazaarutils.utils.bazaar.gui.layouts.TransactionPageLayout; import com.github.mkram17.bazaarutils.utils.Util; -import com.github.mkram17.bazaarutils.utils.bazaar.data.BazaarDataUtil; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarScreenType; import com.github.mkram17.bazaarutils.utils.bazaar.gui.BazaarSlots; import com.github.mkram17.bazaarutils.utils.bazaar.market.order.Order; @@ -14,30 +13,26 @@ import com.github.mkram17.bazaarutils.utils.bazaar.market.price.PriceInfo; import com.github.mkram17.bazaarutils.utils.bazaar.market.price.PricingPosition; import com.github.mkram17.bazaarutils.utils.minecraft.ItemInfo; -import com.github.mkram17.bazaarutils.utils.minecraft.SlotLookup; import com.github.mkram17.bazaarutils.utils.minecraft.components.LoreParser; import com.github.mkram17.bazaarutils.utils.minecraft.gui.ScreenManager; import com.github.mkram17.bazaarutils.utils.minecraft.gui.container.ContainerManager; import com.github.mkram17.bazaarutils.utils.minecraft.gui.sign.SignManager; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.Getter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.player.LocalPlayer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.Container; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.scores.ScoreHolder; -import net.minecraft.world.scores.DisplaySlot; -import net.minecraft.world.scores.Objective; -import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.inventory.ChestMenu; import net.minecraft.network.chat.Component; -import net.minecraft.ChatFormatting; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tech.thatgravyboat.skyblockapi.api.profile.currency.CurrencyAPI; import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -51,6 +46,7 @@ public sealed interface ResolvedInput permits ResolvedInput.Value, ResolvedInput record Value(Number amount) implements ResolvedInput { public String format() { double d = amount.doubleValue(); + return d == (long) d ? String.valueOf((long) d) : String.valueOf(Util.truncateNum(d)); @@ -66,13 +62,40 @@ public String format() { String format(); } + /** + * Wraps the lazily-computed, memoized result of {@link #resolveInput}. + * + *

On first {@link #get()} call {@code resolveInput} fires and, if it resolved, the result + * is cached for the lifetime of this container load. A failed resolution is deliberately not + * cached: market data can land after the first render, and freezing that miss would leave the + * button showing — and the sign filled with — a value that never recovers. + */ + protected class WorkingValue { + private final T state; + + @Nullable + private ResolvedInput resolved; + + WorkingValue(T state) { + this.state = state; + } + + public Optional get() { + if (resolved == null) resolved = resolveInput(state).orElse(null); + + return Optional.ofNullable(resolved); + } + } + @Getter @NotNull protected BazaarSlots.BazaarSlot inputSignRef; + @Nullable + private WorkingValue workingValue; + public SignInputHelper(@NotNull String name, @NotNull BazaarSlots.BazaarSlot inputSignRef) { super(name); - this.inputSignRef = inputSignRef; } @@ -80,20 +103,64 @@ protected Optional getInputSign(Container inventory) { return inputSignRef.query(inventory).first(inventory); } + /** + * Returns the live {@link WorkingValue} for this container load, creating it on first access. + * All render and action paths read through here so {@link #resolveInput} is called at most once. + */ + protected WorkingValue getWorkingValue(T state) { + if (workingValue == null) workingValue = createWorkingValue(state); + + return workingValue; + } + + /** + * Factory method for subclasses that need custom step/clamp logic for scroll. + * The default implementation returns a plain {@link WorkingValue} backed by + * {@link #resolveInput}. + */ + protected WorkingValue createWorkingValue(T state) { + return new WorkingValue(state); + } + + /** + * The working value as the button's stack-size overlay, or blank while it cannot be resolved — + * a button showing nothing reads better than one showing a placeholder that isn't a real amount. + */ + protected String formatWorkingValue(T state) { + return getWorkingValue(state).get().map(ResolvedInput::format).orElse(""); + } + + @Override + protected void resetState() { + workingValue = null; + super.resetState(); + } + @Override - protected void handleAction(T state) { + protected void handleAction(T state, Runnable resetState) { + Optional input = getWorkingValue(state).get(); + + // Opening a sign we have nothing to type into it just strands the player on it. + if (input.isEmpty()) { + Util.logMessage("Cannot handle action for " + name + ", input could not be resolved."); + + return; + } + ContainerManager.clickSlot(state.inputSign().slotIndex(), 0); - ResolvedInput input = resolveInput(state); + SignManager.runOnNextSignOpen(event -> { + SignManager.setSignText(input.get().format(), true); + + resetState.run(); - SignManager.runOnNextSignOpen(event -> SignManager.setSignText(input.format(), true)); + return Result.CONSUMED; + }); } - protected abstract ResolvedInput resolveInput(T state); + protected abstract Optional resolveInput(T state); public abstract static class TransactionAmount extends SignInputHelper { - private static final Pattern PURSE_PATTERN = Pattern.compile("(Purse|Piggy): (?[0-9,.]+)"); - public record TransactionState( @NotNull Double purse, @@ -147,53 +214,7 @@ protected Optional makeState(ContainerLoadedEvent event) { if (productId.isEmpty()) return Optional.empty(); - Optional purse = Optional.of(Minecraft.getInstance()) - .flatMap(client -> Optional.ofNullable(client.level)) - .flatMap(world -> Optional.of(world.getScoreboard())) - .flatMap(scoreboard -> { - Objective objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); - - if (objective == null) { - return Optional.empty(); - } - - ObjectArrayList scoreboardLines = new ObjectArrayList<>(); - - for (ScoreHolder scoreHolder : scoreboard.getTrackedPlayers()) { - if (scoreboard.listPlayerScores(scoreHolder).containsKey(objective)) { - PlayerTeam team = scoreboard.getPlayersTeam(scoreHolder.getScoreboardName()); - - if (team != null) { - String line = team.getPlayerPrefix().getString() + team.getPlayerSuffix().getString(); - - if (!line.trim().isEmpty()) { - scoreboardLines.add(ChatFormatting.stripFormatting(line)); - } - } - } - } - - return Optional.of(scoreboardLines); - }) - .flatMap(lines -> { - for (String line : lines) { - if (line.contains("Purse:") || line.contains("Piggy:")) { - Matcher matcher = PURSE_PATTERN.matcher(line); - - if (matcher.find()) { - try { - return Optional.of(Double.parseDouble(matcher.group("purse").replace(",", ""))); - } catch (NumberFormatException e) { - Util.notifyError("Failed to parse purse from scoreboard", e); - } - } - } - } - - return Optional.empty(); - }); - - if (purse.isEmpty()) return Optional.empty(); + double purse = CurrencyAPI.INSTANCE.getPurse(); Optional playerInventory = Optional.of(Minecraft.getInstance()) .flatMap(client -> Optional.ofNullable(client.player)) @@ -201,7 +222,7 @@ protected Optional makeState(ContainerLoadedEvent event) { if (playerInventory.isEmpty()) return Optional.empty(); - return Optional.of(new TransactionState(purse.get(), productId.get(), productItem.get(), inputSign.get(), playerInventory.get(), container, event.getScreen())); + return Optional.of(new TransactionState(purse, productId.get(), productItem.get(), inputSign.get(), playerInventory.get(), container, event.getScreen())); } public TransactionAmount(@NotNull String name, @NotNull BazaarSlots.BazaarSlot inputSignRef) { @@ -210,62 +231,24 @@ public TransactionAmount(@NotNull String name, @NotNull BazaarSlots.BazaarSlot i @Override protected String getButtonItemStackSize(TransactionState state) { - ResolvedInput input = resolveInput(state); - - return input.format(); + return formatWorkingValue(state); } @Override - protected ResolvedInput resolveInput(TransactionState state) { - int amount = switch (getAmountStrategy()) { + protected Optional resolveInput(TransactionState state) { + OptionalInt amount = switch (getAmountStrategy()) { case MAX -> computeMaxValue(state); - case FIXED -> computeFixedValue(state); + case FIXED -> OptionalInt.of(computeFixedValue(state)); }; - return new ResolvedInput.Value(amount); + return amount.isPresent() + ? Optional.of(new ResolvedInput.Value(amount.getAsInt())) + : Optional.empty(); } protected abstract int computeFixedValue(TransactionState state); - protected int computeMaxValue(TransactionState state) { - return switch (getTransactionType().getMethod()) { - case INSTANT -> { - if (getTransactionType().isBuy()) { - yield SlotLookup.getInventoryItem(state.container(), BazaarSlots.INSTANT_BUY.INPUT_FILLING_AMOUNT.slot) - .map(ItemInfo::itemStack) - .flatMap(TransactionPageLayout::findOptionAmount) - .map(value -> (int) Math.floor(value)) - .orElse((int) state.playerInventory() - .getNonEquipmentItems() - .stream() - .filter(ItemStack::isEmpty) - .count() - ); - } - // Should be impossible to reach, as there is no sign to input a custom amount on items to instant sell. - // TODO: consider refactors needed for this case not to exist - yield 0; - } - case ORDER -> { - if (getTransactionType().isBuy()) { - int amountCanAfford = (int) (state.purse() / OrderUtil.getPriceForPosition(state.productId(), PricingPosition.COMPETITIVE, getTransactionType())); - - yield TransactionPageLayout.findBuyOrderAmountLimit(state.inputSign().itemStack()) - .map(limit -> Math.min(amountCanAfford, limit)) - .orElse(amountCanAfford); - } - yield state.playerInventory().getNonEquipmentItems().stream() - .filter(stack -> !stack.isEmpty()) - .filter(stack -> Optional.ofNullable(stack.getCustomName()) - .map(Component::getString) - .flatMap(BazaarDataUtil::findProductIdOptional) - .map(productId -> productId.equals(state.productId())) - .orElse(false)) - .mapToInt(ItemStack::getCount) - .sum(); - } - }; - } + protected abstract OptionalInt computeMaxValue(TransactionState state); } public abstract static class TransactionCost extends SignInputHelper { @@ -300,11 +283,9 @@ protected Optional makeState(ContainerLoadedEvent event) { Container container = event.getContainer(); Optional inputSign = getInputSign(container); - if (inputSign.isEmpty()) return Optional.empty(); Optional productId = getItemProductId(inputSign.get()); - if (productId.isEmpty()) return Optional.empty(); return Optional.of(new TransactionState(productId.get(), inputSign.get(), container, event.getScreen())); @@ -316,14 +297,16 @@ public TransactionCost(@NotNull String name, @NotNull BazaarSlots.BazaarSlot inp @Override protected String getButtonItemStackSize(TransactionState state) { - ResolvedInput input = resolveInput(state); - - return input.format(); + return formatWorkingValue(state); } @Override - protected ResolvedInput resolveInput(TransactionState state) { - return new ResolvedInput.Value(OrderUtil.getPriceForPosition(state.productId(), getPricingPosition(), getTransactionType())); + protected Optional resolveInput(TransactionState state) { + OptionalDouble price = OrderUtil.getPriceForPositionOptional(state.productId(), getPricingPosition(), getTransactionType()); + + return price.isPresent() + ? Optional.of(new ResolvedInput.Value(price.getAsDouble())) + : Optional.empty(); } } diff --git a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/market/order/OrderUtil.java b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/market/order/OrderUtil.java index b34be3fb0..baa113f5b 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/market/order/OrderUtil.java +++ b/src/main/java/com/github/mkram17/bazaarutils/utils/bazaar/market/order/OrderUtil.java @@ -99,9 +99,18 @@ public static void trackUserOrder(Order order) { } public static double getPriceForPosition(String productID, PricingPosition pricingPosition, TransactionType transactionType) { + return getPriceForPositionOptional(productID, pricingPosition, transactionType).orElse(-1); + } + + /** + * Same resolution as {@link #getPriceForPosition}, but reports an unresolvable price as an + * empty result instead of the {@code -1} sentinel — so callers that must not present or act on + * a bogus price can tell the two apart. + */ + public static OptionalDouble getPriceForPositionOptional(String productID, PricingPosition pricingPosition, TransactionType transactionType) { if (productID == null || pricingPosition == null || transactionType == null) { Util.notifyError("Call to OrderUtil.getPriceForPosition contained a null param", new Exception("Price resolution error")); - return -1; + return OptionalDouble.empty(); } OptionalDouble marketSellPriceOpt = BazaarDataUtil.findItemPriceOptional(productID, TransactionType.of(TransactionType.Side.SELL, TransactionType.Method.ORDER)); @@ -109,13 +118,13 @@ public static double getPriceForPosition(String productID, PricingPosition prici if(marketBuyPriceOpt.isEmpty() || marketSellPriceOpt.isEmpty()) { Util.notifyError("Could not resolve market prices for " + productID + " when calculating price for position. Buy price present: " + marketBuyPriceOpt.isPresent() + " Sell price present: " + marketSellPriceOpt.isPresent(), new Exception("Price resolution error")); - return -1; + return OptionalDouble.empty(); } double marketBuyPrice = marketBuyPriceOpt.getAsDouble(); double marketSellPrice = marketSellPriceOpt.getAsDouble(); - return switch (transactionType.getPriceType()) { + return OptionalDouble.of(switch (transactionType.getPriceType()) { case PriceType.INSTABUY -> switch (pricingPosition) { case COMPETITIVE -> marketSellPrice - 0.1; case MATCHED -> marketSellPrice; @@ -126,6 +135,6 @@ public static double getPriceForPosition(String productID, PricingPosition prici case MATCHED -> marketBuyPrice; case OUTBID -> marketBuyPrice - 0.1; }; - }; + }); } } diff --git a/src/main/java/com/github/mkram17/bazaarutils/utils/minecraft/gui/sign/SignManager.java b/src/main/java/com/github/mkram17/bazaarutils/utils/minecraft/gui/sign/SignManager.java index 0a456c259..b90a60f47 100644 --- a/src/main/java/com/github/mkram17/bazaarutils/utils/minecraft/gui/sign/SignManager.java +++ b/src/main/java/com/github/mkram17/bazaarutils/utils/minecraft/gui/sign/SignManager.java @@ -1,12 +1,14 @@ package com.github.mkram17.bazaarutils.utils.minecraft.gui.sign; -import com.github.mkram17.bazaarutils.BazaarUtils; +import com.github.mkram17.bazaarutils.events.BUListener; import com.github.mkram17.bazaarutils.events.minecraft.SignOpenEvent; import com.github.mkram17.bazaarutils.misc.NotificationType; import com.github.mkram17.bazaarutils.mixin.AccessorSignEditScreen; import com.github.mkram17.bazaarutils.utils.PlayerActionUtil; import com.github.mkram17.bazaarutils.utils.Priority; +import com.github.mkram17.bazaarutils.utils.Result; import com.github.mkram17.bazaarutils.utils.Util; +import com.github.mkram17.bazaarutils.utils.annotations.modules.PreInitModule; import com.github.mkram17.bazaarutils.utils.minecraft.gui.ScreenContext; import com.github.mkram17.bazaarutils.utils.minecraft.gui.ScreenManager; import net.minecraft.client.Minecraft; @@ -16,29 +18,60 @@ import java.util.Optional; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Consumer; +import java.util.function.Function; public class SignManager { - /** One-shot callbacks to run the next time a sign opens; drained by {@link SignOpenDispatcher}. */ - private static final Queue> PENDING = new ConcurrentLinkedQueue<>(); + /** + * How long a queued handler stays eligible for the next sign. + * + *

Expiry rather than clearing on container close: the chest closing is the same server-side + * transition that opens the sign, so a close-triggered clear races the very sign open it is + * meant to serve. SkyblockAPI posts {@code ContainerCloseEvent} a tick behind the clientbound + * close packet, which usually puts it after the sign open — but only usually. + */ + private static final long HANDLER_TTL_MILLIS = 5_000L; + + private record PendingHandler(Function handler, long expiresAtMillis) { + static PendingHandler of(Function handler) { + return new PendingHandler(handler, System.currentTimeMillis() + HANDLER_TTL_MILLIS); + } - static { - BazaarUtils.EVENT_BUS.register(new SignOpenDispatcher()); + boolean isExpired() { + return System.currentTimeMillis() > expiresAtMillis; + } } - private static final class SignOpenDispatcher { + /** Handlers queued for the next sign open; drained by {@link SignQueueDispatcher} in order. */ + private static final Queue PENDING = new ConcurrentLinkedQueue<>(); + + @PreInitModule + public static final class SignQueueDispatcher extends BUListener { @Subscription(priority = Priority.FIRST) private void onSignOpen(SignOpenEvent event) { - Consumer action; + PendingHandler pending; - while ((action = PENDING.poll()) != null) { - action.accept(event); + while ((pending = PENDING.poll()) != null) { + if (pending.isExpired()) continue; + + if (!pending.handler().apply(event).propagate()) { + // Claiming the sign also discards the rest: they were queued for a different one. + PENDING.clear(); + + break; + } } } } - public static void runOnNextSignOpen(Consumer action) { - PENDING.add(action); + /** + * Queues a handler for the next sign that opens. Handlers run in order against a single + * {@link SignOpenEvent}; return {@link Result#CONSUMED} once you've claimed the sign so + * nothing else queued fires on it. A handler whose sign never opens expires after + * {@link #HANDLER_TTL_MILLIS}, so it can't fire on some later, unrelated one. + */ + public static void runOnNextSignOpen(Function handler) { + PENDING.removeIf(PendingHandler::isExpired); + PENDING.add(PendingHandler.of(handler)); } public static void setSignText(String text, boolean closeAfter) { diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 909446fac..64e4857ed 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -1,4 +1,4 @@ plugins { id("dev.kikugie.stonecutter") } -stonecutter active "26.2" \ No newline at end of file +stonecutter active "26.2" diff --git a/versions/26.1.2/gradle.properties b/versions/26.1.2/gradle.properties new file mode 100644 index 000000000..de3df9cdd --- /dev/null +++ b/versions/26.1.2/gradle.properties @@ -0,0 +1,14 @@ +deps.core.mcVersion=26.1.2 +deps.core.maxMcVersion=26.1.2 + +deps.fabric_api=0.155.2+26.1.2 +deps.api.architectury=20.0.12 +deps.resourcefulconfig_version=26.1:4.0.1 +deps.modmenu_version=18.0.0 +deps.skyblock_api_version=4.2.22 +deps.skyblock_api_platform=26.1 +deps.skyblocker_version=6.5.3+26.1.2 +deps.owo_version=0.13.1+26.1 +deps.hypixel_mod_api_version=1.0.2+build.1+mc26.1 + +loom.platform=fabric \ No newline at end of file