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