Compare commits

...

1 Commits

Author SHA1 Message Date
Molzonas
796aaae5a9 PriceProvider changed to abstract & cache price
Needs refinement, but works kinda well

Took 3 hours 46 minutes
2025-08-26 20:48:08 +02:00
9 changed files with 168 additions and 22 deletions

View File

@ -21,6 +21,7 @@ import java.util.logging.Level;
public final class PainfulLoss extends JavaPlugin {
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
public static final String DEFAULT_LOCALE_TAG = "en-US";
private static boolean debug = false;
@Getter private static PainfulLoss instance = null;
@Getter private static PriceProvider priceProvider = null;
@ -29,6 +30,7 @@ public final class PainfulLoss extends JavaPlugin {
PainfulLoss.instance = this;
Message.init(DEFAULT_LOCALE);
configurationInit();
PainfulLoss.debug = PropertiesEnum.DEBUG.getBoolean();
langInit();
registerCommands();
initPriceProvider();
@ -109,6 +111,7 @@ public final class PainfulLoss extends JavaPlugin {
public void reload() {
reloadConfig();
PainfulLoss.debug = PropertiesEnum.DEBUG.getBoolean();
Message.clearCache();
langInit();
initPriceProvider();
@ -121,4 +124,8 @@ public final class PainfulLoss extends JavaPlugin {
public static void info(String message) {
PainfulLoss.log(Level.INFO, message);
}
public static void debug(String message) {
if (PainfulLoss.debug) Bukkit.getLogger().log(Level.INFO, "[Painful Loss DEBUG] {0}", message);
}
}

View File

@ -76,8 +76,12 @@ public class PainfulLossCommand implements CommandExecutor, TabCompleter {
private boolean commandWorth(CommandSender commandSender, String[] args) {
if (args.length == 1 && commandSender instanceof Player p && commandSender.hasPermission("painfulloss.worth")) {
double t1 = System.currentTimeMillis();
PainfulLoss.debug("Starting clock for worth...");
commandSender.sendMessage(Message.of("command.worth", PriceCalculator.estimate(p)
+ (PropertiesEnum.ENCHANT_ENABLED.getBoolean() ? PriceCalculator.enchantEstimate(p) : 0)));
double t2 = System.currentTimeMillis();
PainfulLoss.debug("It took " + (t2-t1) + "ms to get the job done !");
} else if (args.length == 2 && commandSender.hasPermission("painfulloss.worth.other")) {
Player p = PainfulLoss.getInstance().getServer().getPlayer(args[1]);
if (p == null) {
@ -89,7 +93,7 @@ public class PainfulLossCommand implements CommandExecutor, TabCompleter {
} else {
return commandNotAutorised(commandSender);
}
return false;
return true;
}
@Override

View File

@ -7,19 +7,19 @@ import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
public class CountPriceProvider implements PriceProvider{
public class CountPriceProvider extends PriceProvider{
@Override
public String getName() {
return "Count";
}
@Override
public Optional<Double> getPrice(ItemStack stack, @Nullable Player player) {
public Optional<Double> getProviderPrice(ItemStack stack, @Nullable Player player) {
return Optional.of((double) stack.getAmount());
}
@Override
public Optional<Double> getPrices(List<ItemStack> stack, @org.jetbrains.annotations.Nullable Player player) {
public Optional<Double> getProviderPrices(List<ItemStack> stack, @org.jetbrains.annotations.Nullable Player player) {
return Optional.of(stack.stream().mapToDouble(ItemStack::getAmount).sum());
}

View File

@ -9,7 +9,7 @@ import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.util.Optional;
public class EssentialsPriceProvider implements PriceProvider {
public class EssentialsPriceProvider extends PriceProvider {
private final IEssentials plugin;
private final Worth worth;
private boolean ready = false;
@ -25,13 +25,13 @@ public class EssentialsPriceProvider implements PriceProvider {
}
@Override
public Optional<Double> getPrice(ItemStack stack, @Nullable Player player) {
public Optional<Double> getProviderPrice(ItemStack stack, @Nullable Player player) {
if (stack == null || stack.getAmount() <= 0) return Optional.empty();
try {
BigDecimal price = worth.getPrice(plugin, stack);
if (price == null) return Optional.empty();
if (price.signum() < 0) return Optional.empty();
if (price.doubleValue() <= 0) return Optional.empty();
return Optional.of(price.doubleValue());
} catch (Exception e) {
return Optional.empty();

View File

@ -1,20 +1,149 @@
package fr.molzonas.painfulloss.provider;
import fr.molzonas.painfulloss.PainfulLoss;
import fr.molzonas.painfulloss.utils.PropertiesEnum;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.*;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public interface PriceProvider {
String getName();
Optional<Double> getPrice(ItemStack stack, @Nullable Player player);
default Optional<Double> getPrices(List<ItemStack> stack, @Nullable Player player) {
public abstract class PriceProvider {
private final Map<Material, BigDecimal> cachedPrices = new ConcurrentHashMap<>();
private final Set<Material> tested = java.util.Collections.newSetFromMap(new ConcurrentHashMap<>());
public abstract String getName();
protected abstract Optional<Double> getProviderPrice(ItemStack stack, @Nullable Player player);
protected Optional<Double> getProviderPrices(List<ItemStack> stack, @Nullable Player player) {
if (stack.isEmpty()) return Optional.empty();
return Optional.of(stack.stream().mapToDouble(x -> this.getProviderPrice(x, player).orElse(0d)).sum());
}
public Optional<Double> getPrice(ItemStack item, @Nullable Player player) {
if (item == null || item.getAmount() <= 0 || item.getType().isAir()) return Optional.empty();
ItemStack stack = item.clone();
stack.setAmount(1);
if (cachedPrices.containsKey(stack.getType())) {
BigDecimal price = cachedPrices.get(stack.getType());
return price.doubleValue() > 0d ? Optional.of(price.doubleValue() * item.getAmount()) : Optional.empty();
}
PainfulLoss.debug("Item not cached : " + stack.getType().name() + " ! Searching for values.");
Optional<Double> priceFromProvider = getProviderPrice(stack, player);
if (priceFromProvider.isPresent() && priceFromProvider.orElse(-1d) > 0) {
cachedPrices.put(stack.getType(), BigDecimal.valueOf(priceFromProvider.get()));
PainfulLoss.debug("Found " + stack.getType().name() + " price in the " + this.getName() + " provider for "+ priceFromProvider.get() + "$ per unit.");
return Optional.of(priceFromProvider.orElse(0d) * item.getAmount());
}
if (PropertiesEnum.RECIPE_COST_IF_NO_PRICE_FOUND.getBoolean()) {
// Anti-loop guard - if the material was already tested, return empty
if (!tested.add(stack.getType())) return Optional.empty();
PainfulLoss.debug("Time to search components of " + stack.getType().name() + " !");
try {
RecipePrice recipePrice = new RecipePrice(stack.clone(), player);
double priceFromIngredients = recipePrice.getLowestRecipePrice();
if (priceFromIngredients > 0) {
PainfulLoss.debug("And the components total price for " + stack.getType().name() + " is " + priceFromIngredients + "$ !");
cachedPrices.put(stack.getType(), BigDecimal.valueOf(priceFromIngredients));
return Optional.of(priceFromIngredients * item.getAmount());
}
} finally {
PainfulLoss.debug("Let's get back from " + stack.getType().name() + " after this search.");
tested.remove(stack.getType());
}
}
// If nothing is found : the price is NULL
cachedPrices.put(stack.getType(), BigDecimal.valueOf(-1));
PainfulLoss.debug("No price found for " + item.getType().name() + ". Registering it to 0$ !");
return Optional.empty();
}
public Optional<Double> getPrices(List<ItemStack> stack, @Nullable Player player) {
if (stack.isEmpty()) return Optional.empty();
return Optional.of(stack.stream().mapToDouble(x -> this.getPrice(x, player).orElse(0d)).sum());
}
default boolean isReady() { return true; }
default boolean isCountBased() { return false; }
default void reload() { }
public boolean isReady() {
return true;
}
public boolean isCountBased() {
return false;
}
public void reload() {
this.cachedPrices.clear();
}
class RecipePrice {
final ItemStack item;
final Player player;
final int finalAmount;
RecipePrice(ItemStack item, @Nullable Player player) {
this.item = new ItemStack(item.getType(), 1);
this.player = player;
this.finalAmount = item.getAmount();
}
double getLowestRecipePrice() {
double lowest = 0d;
for (Recipe r : Bukkit.getRecipesFor(item)) {
double total = switch (r) {
case ShapedRecipe sr -> getShapedRecipePrice(sr);
case ShapelessRecipe sr -> getShapelessRecipePrice(sr);
case CookingRecipe<?> sr -> getRecipeChoicePrice(sr.getInputChoice());
default -> 0d;
};
if (total > 0d) {
double unit = total / Math.max(1, r.getResult().getAmount());
if (lowest == 0 || unit < lowest) lowest = unit;
}
}
PainfulLoss.debug("And the lowest price for the recipes was " + lowest + "$.");
return lowest;
}
double getShapedRecipePrice(ShapedRecipe sr) {
double sum = 0d;
Map<Character, RecipeChoice> map = sr.getChoiceMap();
Map<RecipeChoice, Integer> nbOfUnit = new HashMap<>();
for (String s : sr.getShape()) {
for (Character c : s.toCharArray()) {
RecipeChoice rc = map.getOrDefault(c, null);
if (c == ' ' || rc == null) continue;
nbOfUnit.put(rc, nbOfUnit.getOrDefault(rc, 0) + 1);
}
}
for (Map.Entry<RecipeChoice, Integer> entry : nbOfUnit.entrySet()) {
double price = getRecipeChoicePrice(entry.getKey()) * entry.getValue();
sum += price;
}
return sum;
}
double getShapelessRecipePrice(ShapelessRecipe sr) {
return sr.getChoiceList().stream().mapToDouble(this::getRecipeChoicePrice).sum();
}
double getRecipeChoicePrice(RecipeChoice choice) {
if (choice == null) return 0d;
double lowest = 0d;
List<ItemStack> stacks = new ArrayList<>();
if (choice instanceof RecipeChoice.MaterialChoice c) {
stacks = c.getChoices().stream().map(x -> new ItemStack(x, 1)).toList();
}
if (choice instanceof RecipeChoice.ExactChoice c) {
stacks = c.getChoices();
}
for (ItemStack stack : stacks) {
double price = getPrice(stack, player).orElse(0d);
if (price > 0 && (lowest == 0 || lowest > price)) lowest = price;
}
return lowest;
}
}
}

View File

@ -13,7 +13,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
public class UltimateShopPriceProvider implements PriceProvider {
public class UltimateShopPriceProvider extends PriceProvider {
private final Reflect reflect;
private String economyProvider;
private boolean ready = true;
@ -39,9 +39,10 @@ public class UltimateShopPriceProvider implements PriceProvider {
}
@Override
public Optional<Double> getPrice(ItemStack stack, @Nullable Player player) {
public Optional<Double> getProviderPrice(ItemStack stack, @Nullable Player player) {
try {
return Optional.of(reflect.priceFor(stack, player, economyProvider));
double price = reflect.priceFor(stack, player, economyProvider);
return price > 0 ? Optional.of(price) : Optional.empty();
} catch (Exception e) {
return Optional.empty();
}

View File

@ -13,7 +13,8 @@ public enum PropertiesEnum {
PRICE_PROVIDER("priceProvider"),
ENCHANT_ENABLED("estimation.enchant.enabled"),
ENCHANT_DEFAULT_PER_LEVEL("estimation.enchant.default_per_level"),
ECONOMY_PROVIDER("economyProvider", "Vault")
ECONOMY_PROVIDER("economyProvider", "Vault"),
RECIPE_COST_IF_NO_PRICE_FOUND("recipeCostIfNoPriceFound", "false")
;
@Getter private final String path;

View File

@ -11,6 +11,10 @@ priceProvider:
# Used to do estimations with UltimateShop, can only be Vault... for now.
economyProvider: Vault
# If the product price isn't found, use the components of this product to get the price
# Disable this if you already defined every item or if the plugin is laggy
recipeCostIfNoPriceFound: true
estimation:
# Is enchants on items used in worth calculation ?
enchant:

View File

@ -2,7 +2,7 @@ name: PainfulLoss
version: '1.0'
main: fr.molzonas.painfulloss.PainfulLoss
api-version: '1.20'
prefix: PainfulLoss
prefix: Painful Loss
softdepend: [Essentials, UltimateShop]
author: Molzonas
commands: