diff --git a/src/main/java/fr/molzonas/painfulloss/PainfulLoss.java b/src/main/java/fr/molzonas/painfulloss/PainfulLoss.java index 412cee5..8933e58 100644 --- a/src/main/java/fr/molzonas/painfulloss/PainfulLoss.java +++ b/src/main/java/fr/molzonas/painfulloss/PainfulLoss.java @@ -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); + } } diff --git a/src/main/java/fr/molzonas/painfulloss/commands/PainfulLossCommand.java b/src/main/java/fr/molzonas/painfulloss/commands/PainfulLossCommand.java index 9d6967f..8fe9fea 100644 --- a/src/main/java/fr/molzonas/painfulloss/commands/PainfulLossCommand.java +++ b/src/main/java/fr/molzonas/painfulloss/commands/PainfulLossCommand.java @@ -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 diff --git a/src/main/java/fr/molzonas/painfulloss/provider/CountPriceProvider.java b/src/main/java/fr/molzonas/painfulloss/provider/CountPriceProvider.java index a1002b4..f09cc35 100644 --- a/src/main/java/fr/molzonas/painfulloss/provider/CountPriceProvider.java +++ b/src/main/java/fr/molzonas/painfulloss/provider/CountPriceProvider.java @@ -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 getPrice(ItemStack stack, @Nullable Player player) { + public Optional getProviderPrice(ItemStack stack, @Nullable Player player) { return Optional.of((double) stack.getAmount()); } @Override - public Optional getPrices(List stack, @org.jetbrains.annotations.Nullable Player player) { + public Optional getProviderPrices(List stack, @org.jetbrains.annotations.Nullable Player player) { return Optional.of(stack.stream().mapToDouble(ItemStack::getAmount).sum()); } diff --git a/src/main/java/fr/molzonas/painfulloss/provider/EssentialsPriceProvider.java b/src/main/java/fr/molzonas/painfulloss/provider/EssentialsPriceProvider.java index 7596307..0c19da4 100644 --- a/src/main/java/fr/molzonas/painfulloss/provider/EssentialsPriceProvider.java +++ b/src/main/java/fr/molzonas/painfulloss/provider/EssentialsPriceProvider.java @@ -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 getPrice(ItemStack stack, @Nullable Player player) { + public Optional 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(); diff --git a/src/main/java/fr/molzonas/painfulloss/provider/PriceProvider.java b/src/main/java/fr/molzonas/painfulloss/provider/PriceProvider.java index 0c6397e..ceedab1 100644 --- a/src/main/java/fr/molzonas/painfulloss/provider/PriceProvider.java +++ b/src/main/java/fr/molzonas/painfulloss/provider/PriceProvider.java @@ -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 getPrice(ItemStack stack, @Nullable Player player); - default Optional getPrices(List stack, @Nullable Player player) { +public abstract class PriceProvider { + private final Map cachedPrices = new ConcurrentHashMap<>(); + private final Set tested = java.util.Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public abstract String getName(); + protected abstract Optional getProviderPrice(ItemStack stack, @Nullable Player player); + protected Optional getProviderPrices(List 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 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 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 getPrices(List 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 map = sr.getChoiceMap(); + Map 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 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 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; + } + } } diff --git a/src/main/java/fr/molzonas/painfulloss/provider/UltimateShopPriceProvider.java b/src/main/java/fr/molzonas/painfulloss/provider/UltimateShopPriceProvider.java index 4782f85..af5fdb1 100644 --- a/src/main/java/fr/molzonas/painfulloss/provider/UltimateShopPriceProvider.java +++ b/src/main/java/fr/molzonas/painfulloss/provider/UltimateShopPriceProvider.java @@ -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 getPrice(ItemStack stack, @Nullable Player player) { + public Optional 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(); } diff --git a/src/main/java/fr/molzonas/painfulloss/utils/PropertiesEnum.java b/src/main/java/fr/molzonas/painfulloss/utils/PropertiesEnum.java index dc9c3a8..6fdbec4 100644 --- a/src/main/java/fr/molzonas/painfulloss/utils/PropertiesEnum.java +++ b/src/main/java/fr/molzonas/painfulloss/utils/PropertiesEnum.java @@ -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; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 689c3d7..8a6dcf2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index aa0e04f..d00bb70 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -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: