From e558b9ac7997780f5409556dba17afa870ecaca4 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:19:38 +0530 Subject: [PATCH 1/8] Add ExcellentShop integration + build fixes - Add ExcellentShopProvider: scans VirtualShop products, caches best sell price per Material (lazy, invalidatable), returns 0 for non-item or non-sellable products - Wire into ShopIntegrationManager auto-detect and specific config cases - Add su.nightexpress.excellentshop api+Core as compileOnly dependencies - Add mavenLocal() to repository list for locally built ExcellentShop - Add content filter to nightexpress repo (su.nightexpress.* only) to prevent timeouts when resolving unrelated dependencies - Bump targetJavaVersion: 25 kept as-is (requires JDK 25 to build) --- build.gradle.kts | 4 + core/build.gradle.kts | 2 + .../economy/shops/ShopIntegrationManager.java | 11 ++ .../excellentshop/ExcellentShopProvider.java | 104 ++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java diff --git a/build.gradle.kts b/build.gradle.kts index 33549936..b202b4d7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ allprojects { version = "1.6.8" repositories { + mavenLocal() mavenCentral() maven { name = "papermc-repo" @@ -49,6 +50,9 @@ allprojects { maven { name = "nightexpress-releases" url = uri("https://repo.nightexpressdev.com/releases") + content { + includeGroupByRegex("su\\.nightexpress.*") + } } maven { name = "iridiumdevelopment" diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 025052b9..169ed8fb 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -30,6 +30,8 @@ dependencies { compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") compileOnly("su.nightexpress.excellenteconomy:ExcellentEconomy:2.8.0") compileOnly("su.nightexpress.nightcore:main:2.15.3") + compileOnly("su.nightexpress.excellentshop:api:5.1.1") + compileOnly("su.nightexpress.excellentshop:Core:5.1.1") compileOnly("com.github.Gypopo:EconomyShopGUI-API:1.10.0") compileOnly("world.bentobox:bentobox:3.16.2") compileOnly("dev.aurelium:auraskills-api-bukkit:2.3.12") diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java index 8fb2079a..a9dd843e 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java @@ -6,6 +6,7 @@ import github.nighter.smartspawner.hooks.economy.shops.providers.economyshopgui.ESGUICompatibilityHandler; import github.nighter.smartspawner.hooks.economy.shops.providers.shopguiplus.ShopGuiPlusProvider; import github.nighter.smartspawner.hooks.economy.shops.providers.shopguiplus.SpawnerHook; +import github.nighter.smartspawner.hooks.economy.shops.providers.excellentshop.ExcellentShopProvider; import github.nighter.smartspawner.hooks.economy.shops.providers.zshop.ZShopProvider; import lombok.RequiredArgsConstructor; import org.bukkit.Material; @@ -81,6 +82,10 @@ private void detectAndRegisterActiveProviders() { } // registerProviderIfAvailable("ZShop", () -> new ZShopProvider(plugin)); + + if (isPluginAvailable("ExcellentShop")) { + registerProviderIfAvailable("ExcellentShop", () -> new ExcellentShopProvider(plugin)); + } } private boolean tryRegisterSpecificProvider(String providerName) { @@ -125,6 +130,12 @@ private boolean tryRegisterSpecificProvider(String providerName) { return !availableProviders.isEmpty(); } break; + case "excellentshop": + if (isPluginAvailable("ExcellentShop")) { + registerProviderIfAvailable("ExcellentShop", () -> new ExcellentShopProvider(plugin)); + return !availableProviders.isEmpty(); + } + break; } } catch (Exception e) { plugin.debug("Failed to load specific provider " + providerName + ": " + e.getMessage()); diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java new file mode 100644 index 00000000..8ba228a2 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java @@ -0,0 +1,104 @@ +package github.nighter.smartspawner.hooks.economy.shops.providers.excellentshop; + +import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.hooks.economy.shops.providers.ShopProvider; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.plugin.Plugin; +import su.nightexpress.excellentshop.ShopAPI; +import su.nightexpress.excellentshop.api.product.ContentType; +import su.nightexpress.excellentshop.api.product.Product; +import su.nightexpress.excellentshop.virtualshop.VirtualShopModule; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public class ExcellentShopProvider implements ShopProvider { + + private final SmartSpawner plugin; + + // Cache: Material -> best sell price across all virtual shops + private final Map priceCache = new HashMap<>(); + private boolean cacheBuilt = false; + + @Override + public String getPluginName() { + return "ExcellentShop"; + } + + @Override + public boolean isAvailable() { + try { + Plugin shopPlugin = Bukkit.getPluginManager().getPlugin("ExcellentShop"); + if (shopPlugin == null || !shopPlugin.isEnabled()) return false; + + Class.forName("su.nightexpress.excellentshop.ShopAPI"); + Class.forName("su.nightexpress.excellentshop.virtualshop.VirtualShopModule"); + return ShopAPI.isInitialized(); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + plugin.debug("ExcellentShop API not found: " + e.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Error initializing ExcellentShop integration: " + e.getMessage()); + } + return false; + } + + @Override + public double getSellPrice(Material material) { + if (material == null || material.isAir()) return 0.0; + + try { + if (!cacheBuilt) buildCache(); + return priceCache.getOrDefault(material, 0.0); + } catch (Exception e) { + plugin.debug("Error getting sell price for " + material + " from ExcellentShop: " + e.getMessage()); + return 0.0; + } + } + + /** + * Scans all virtual shop products and caches the best (highest) sell price + * per Material. Only considers sellable, item-type products. + * Called lazily on first price lookup so the shop data is fully loaded. + */ + private void buildCache() { + priceCache.clear(); + cacheBuilt = true; + + try { + VirtualShopModule virtualShop = ShopAPI.getVirtualShop(); + if (virtualShop == null) return; + + virtualShop.getShops().forEach(shop -> + shop.getProductMap().values().forEach(product -> { + if (!product.isSellable()) return; + if (product.getContent().type() != ContentType.ITEM) return; + + Material material = product.getPreview().getType(); + if (material.isAir()) return; + + double sellPrice = product.getSellPrice(); + if (sellPrice <= 0) return; + + // Keep the highest sell price if the same material appears in multiple shops + priceCache.merge(material, sellPrice, Math::max); + }) + ); + + plugin.getLogger().info("[ExcellentShop] Price cache built: " + priceCache.size() + " materials indexed."); + } catch (Exception e) { + plugin.getLogger().warning("Failed to build ExcellentShop price cache: " + e.getMessage()); + } + } + + /** + * Invalidates the price cache. Call this when shop products may have changed + * (e.g. after a plugin reload). + */ + public void invalidateCache() { + priceCache.clear(); + cacheBuilt = false; + } +} From fb9d07ac0a47485a284b99fdf11de86ef530639c Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:28:19 +0530 Subject: [PATCH 2/8] Fix ExcellentShop not detected: add to softdepend + fix isAvailable check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ExcellentShop to plugin.yml softdepend so Bukkit loads it before SmartSpawner, ensuring ShopAPI is initialized when isAvailable() runs - Remove ShopAPI.isInitialized() guard from isAvailable() since plugin being enabled is sufficient — the API is always ready at that point --- .../shops/providers/excellentshop/ExcellentShopProvider.java | 3 ++- core/src/main/resources/plugin.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java index 8ba228a2..96d0188e 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java @@ -36,7 +36,8 @@ public boolean isAvailable() { Class.forName("su.nightexpress.excellentshop.ShopAPI"); Class.forName("su.nightexpress.excellentshop.virtualshop.VirtualShopModule"); - return ShopAPI.isInitialized(); + // Plugin is enabled — API will be initialized by the time we need it + return true; } catch (ClassNotFoundException | NoClassDefFoundError e) { plugin.debug("ExcellentShop API not found: " + e.getMessage()); } catch (Exception e) { diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 287a9c8b..80802dfb 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -14,6 +14,7 @@ softdepend: - EconomyShopGUI-Premium - ShopGUIPlus - zShop + - ExcellentShop # Utility/Economy Plugins - ExcellentEconomy From 6e1bf38082a2138f3dacaf65b022e06414118a49 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:40:35 +0530 Subject: [PATCH 3/8] Fix ExcellentShop price cache: timing, thread safety, empty cache - Cache now builds on ServerLoadEvent instead of lazily on first call, ensuring ExcellentShop's async data load is complete before reading prices - AtomicBoolean for cacheBuilt prevents double-build race condition on Folia - ConcurrentHashMap for thread-safe reads from multiple region threads - Fix sellPrice filter: was dropping sellPrice=0 (valid price), now only drops sellPrice<0 (-1 = disabled) - Allow cache retry if build fails (cacheBuilt reset on exception) - Register as Listener inside isAvailable() for ServerLoadEvent - Detailed logging at each failure point --- .../excellentshop/ExcellentShopProvider.java | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java index 96d0188e..e043a692 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/excellentshop/ExcellentShopProvider.java @@ -5,23 +5,26 @@ import lombok.RequiredArgsConstructor; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.plugin.Plugin; import su.nightexpress.excellentshop.ShopAPI; import su.nightexpress.excellentshop.api.product.ContentType; -import su.nightexpress.excellentshop.api.product.Product; import su.nightexpress.excellentshop.virtualshop.VirtualShopModule; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; @RequiredArgsConstructor -public class ExcellentShopProvider implements ShopProvider { +public class ExcellentShopProvider implements ShopProvider, Listener { private final SmartSpawner plugin; - // Cache: Material -> best sell price across all virtual shops - private final Map priceCache = new HashMap<>(); - private boolean cacheBuilt = false; + // ConcurrentHashMap for Folia thread safety + private final Map priceCache = new ConcurrentHashMap<>(); + private final AtomicBoolean cacheBuilt = new AtomicBoolean(false); @Override public String getPluginName() { @@ -32,26 +35,47 @@ public String getPluginName() { public boolean isAvailable() { try { Plugin shopPlugin = Bukkit.getPluginManager().getPlugin("ExcellentShop"); - if (shopPlugin == null || !shopPlugin.isEnabled()) return false; + if (shopPlugin == null) { + plugin.getLogger().info("[ExcellentShop] Plugin not found on server."); + return false; + } + if (!shopPlugin.isEnabled()) { + plugin.getLogger().info("[ExcellentShop] Plugin found but not enabled."); + return false; + } Class.forName("su.nightexpress.excellentshop.ShopAPI"); Class.forName("su.nightexpress.excellentshop.virtualshop.VirtualShopModule"); - // Plugin is enabled — API will be initialized by the time we need it + + // Register ServerLoadEvent listener to build cache after full startup + Bukkit.getPluginManager().registerEvents(this, plugin); + + plugin.getLogger().info("[ExcellentShop] Integration ready. Price cache will build after server finishes loading."); return true; } catch (ClassNotFoundException | NoClassDefFoundError e) { - plugin.debug("ExcellentShop API not found: " + e.getMessage()); + plugin.getLogger().warning("[ExcellentShop] API classes not found: " + e.getMessage()); } catch (Exception e) { - plugin.getLogger().warning("Error initializing ExcellentShop integration: " + e.getMessage()); + plugin.getLogger().warning("[ExcellentShop] Error initializing: " + e.getMessage()); } return false; } + /** + * Build the cache after the server has fully started so ExcellentShop's + * async data load is complete and all prices are populated. + */ + @EventHandler + public void onServerLoad(ServerLoadEvent event) { + buildCache(); + } + @Override public double getSellPrice(Material material) { if (material == null || material.isAir()) return 0.0; try { - if (!cacheBuilt) buildCache(); + // If cache hasn't been built yet (e.g. first call before ServerLoadEvent), build it now + if (!cacheBuilt.get()) buildCache(); return priceCache.getOrDefault(material, 0.0); } catch (Exception e) { plugin.debug("Error getting sell price for " + material + " from ExcellentShop: " + e.getMessage()); @@ -62,15 +86,19 @@ public double getSellPrice(Material material) { /** * Scans all virtual shop products and caches the best (highest) sell price * per Material. Only considers sellable, item-type products. - * Called lazily on first price lookup so the shop data is fully loaded. */ private void buildCache() { + if (!cacheBuilt.compareAndSet(false, true)) return; // Only build once + priceCache.clear(); - cacheBuilt = true; try { VirtualShopModule virtualShop = ShopAPI.getVirtualShop(); - if (virtualShop == null) return; + if (virtualShop == null) { + plugin.getLogger().warning("[ExcellentShop] VirtualShop module not available."); + cacheBuilt.set(false); // Allow retry + return; + } virtualShop.getShops().forEach(shop -> shop.getProductMap().values().forEach(product -> { @@ -81,25 +109,28 @@ private void buildCache() { if (material.isAir()) return; double sellPrice = product.getSellPrice(); - if (sellPrice <= 0) return; + if (sellPrice < 0) return; // -1 = disabled; 0 is valid - // Keep the highest sell price if the same material appears in multiple shops + // Keep the highest sell price if the same material is in multiple shops priceCache.merge(material, sellPrice, Math::max); }) ); plugin.getLogger().info("[ExcellentShop] Price cache built: " + priceCache.size() + " materials indexed."); + if (priceCache.isEmpty()) { + plugin.getLogger().warning("[ExcellentShop] Cache is empty — check that your virtual shops have sellable products."); + } } catch (Exception e) { - plugin.getLogger().warning("Failed to build ExcellentShop price cache: " + e.getMessage()); + plugin.getLogger().warning("[ExcellentShop] Failed to build price cache: " + e.getMessage()); + cacheBuilt.set(false); // Allow retry on next call } } /** - * Invalidates the price cache. Call this when shop products may have changed - * (e.g. after a plugin reload). + * Invalidates the price cache — call after shop prices change. */ public void invalidateCache() { priceCache.clear(); - cacheBuilt = false; + cacheBuilt.set(false); } } From 6693981dcd19b53f7e6e97d38a1b1fbd8344d698 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:45:18 +0530 Subject: [PATCH 4/8] Fix load order: add ExcellentShop to paper-plugin.yml dependencies softdepend in plugin.yml has no effect for Paper plugins - load order between Paper plugins and Bukkit plugins is controlled by paper-plugin.yml. ExcellentShop (Bukkit plugin) must load BEFORE SmartSpawner (Paper plugin) so the API is initialized when isAvailable() runs. --- core/src/main/resources/paper-plugin.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/resources/paper-plugin.yml b/core/src/main/resources/paper-plugin.yml index d776cfe9..3a352647 100644 --- a/core/src/main/resources/paper-plugin.yml +++ b/core/src/main/resources/paper-plugin.yml @@ -42,6 +42,10 @@ dependencies: load: BEFORE required: false join-classpath: true + ExcellentShop: + load: BEFORE + required: false + join-classpath: true # Economy Plugins ExcellentEconomy: From 7ab0e5f38878592bf5016618f634b33b79b63129 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:55:07 +0530 Subject: [PATCH 5/8] Add /ss priceanalysis command Shows all sellable items across all spawners grouped by spawner type, with per-item price source breakdown: - [Shop] price sourced from ExcellentShop (green) - [Custom] price from item_prices.yml (yellow) - [Fallback] secondary source used due to priority mode (yellow) - [Not Configured] no price found in any source (red) Fully respects price_source_mode (SHOP_PRIORITY, CUSTOM_PRIORITY, SHOP_ONLY, CUSTOM_ONLY). Summary at bottom shows counts per category with a warning if any items are unconfigured. Exposed getShopPriceFor(), getCustomPriceFor(), getPriceSourceMode() on ItemPriceManager for analysis use. Permission: smartspawner.command.priceanalysis (default: op) --- .../smartspawner/commands/MainCommand.java | 4 +- .../PriceAnalysisSubCommand.java | 196 ++++++++++++++++++ .../hooks/economy/ItemPriceManager.java | 14 ++ core/src/main/resources/paper-plugin.yml | 4 + 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java diff --git a/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java index 2199435e..77543ce7 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java @@ -9,6 +9,7 @@ import github.nighter.smartspawner.commands.list.ListSubCommand; import github.nighter.smartspawner.commands.near.NearSubCommand; import github.nighter.smartspawner.commands.prices.PricesSubCommand; +import github.nighter.smartspawner.commands.priceanalysis.PriceAnalysisSubCommand; import github.nighter.smartspawner.commands.reload.ReloadSubCommand; import github.nighter.smartspawner.commands.set.SetSubCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; @@ -36,7 +37,8 @@ public MainCommand(SmartSpawner plugin) { new PricesSubCommand(plugin), new ClearSubCommand(plugin), new NearSubCommand(plugin, plugin.getSpawnerHighlightManager()), - new SetSubCommand(plugin) + new SetSubCommand(plugin), + new PriceAnalysisSubCommand(plugin) ); } diff --git a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java new file mode 100644 index 00000000..ee67d679 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java @@ -0,0 +1,196 @@ +package github.nighter.smartspawner.commands.priceanalysis; + +import com.mojang.brigadier.context.CommandContext; +import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.commands.BaseSubCommand; +import github.nighter.smartspawner.hooks.economy.ItemPriceManager; +import github.nighter.smartspawner.hooks.economy.ItemPriceManager.PriceSourceMode; +import github.nighter.smartspawner.spawner.data.SpawnerManager; +import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; +import github.nighter.smartspawner.spawner.properties.SpawnerData; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.command.CommandSender; +import org.jspecify.annotations.NullMarked; + +import java.util.*; + +@NullMarked +public class PriceAnalysisSubCommand extends BaseSubCommand { + + public PriceAnalysisSubCommand(SmartSpawner plugin) { + super(plugin); + } + + @Override + public String getName() { + return "priceanalysis"; + } + + @Override + public String getPermission() { + return "smartspawner.command.priceanalysis"; + } + + @Override + public String getDescription() { + return "Show price analysis for all sellable items across all spawners"; + } + + @Override + public int execute(CommandContext context) { + CommandSender sender = context.getSource().getSender(); + + ItemPriceManager priceManager = plugin.getItemPriceManager(); + SpawnerManager spawnerManager = plugin.getSpawnerManager(); + + // Collect unique materials across all spawners, grouped by spawner type + // Map: EntityType -> Set + Map> materialsByType = new LinkedHashMap<>(); + + for (SpawnerData spawner : spawnerManager.getAllSpawners()) { + EntityType type = spawner.getEntityType(); + List lootItems = spawner.getValidLootItems(); + + if (lootItems.isEmpty()) continue; + + Set materials = materialsByType.computeIfAbsent(type, k -> new LinkedHashSet<>()); + for (LootItem loot : lootItems) { + materials.add(loot.material()); + } + } + + if (materialsByType.isEmpty()) { + sender.sendMessage(ChatColor.RED + "No spawners found or no sellable items configured."); + return 0; + } + + // --- Header --- + sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ SmartSpawner Price Analysis ════"); + sender.sendMessage(ChatColor.GRAY + "Mode: " + ChatColor.YELLOW + priceManager.getPriceSourceMode().name() + + ChatColor.GRAY + " Source: " + ChatColor.AQUA + getActiveSourceLabel(priceManager)); + sender.sendMessage(""); + + int totalItems = 0; + int shopPriced = 0; + int customPriced = 0; + int notConfigured = 0; + + // --- Per spawner type breakdown --- + for (Map.Entry> entry : materialsByType.entrySet()) { + EntityType entityType = entry.getKey(); + Set materials = entry.getValue(); + + sender.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD + + formatEntityName(entityType) + ChatColor.GRAY + " (" + materials.size() + " items)"); + + for (Material material : materials) { + totalItems++; + + double finalPrice = priceManager.getPrice(material); + PriceSource source = resolveSource(priceManager, material); + + String priceStr; + String sourceLabel; + ChatColor lineColor; + + switch (source) { + case SHOP -> { + shopPriced++; + lineColor = ChatColor.GREEN; + sourceLabel = ChatColor.GREEN + "[Shop]"; + priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); + } + case CUSTOM -> { + customPriced++; + lineColor = ChatColor.YELLOW; + sourceLabel = ChatColor.YELLOW + "[Custom]"; + priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); + } + case FALLBACK -> { + customPriced++; + lineColor = ChatColor.YELLOW; + sourceLabel = ChatColor.YELLOW + "[Fallback]"; + priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); + } + default -> { + notConfigured++; + lineColor = ChatColor.RED; + sourceLabel = ChatColor.RED + "[Not Configured]"; + priceStr = ChatColor.RED + "N/A"; + } + } + + sender.sendMessage(ChatColor.GRAY + " " + lineColor + formatMaterialName(material) + + ChatColor.DARK_GRAY + " → " + priceStr + " " + sourceLabel); + } + sender.sendMessage(""); + } + + // --- Summary --- + sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ Summary ════"); + sender.sendMessage(ChatColor.GRAY + "Total items: " + ChatColor.WHITE + totalItems); + sender.sendMessage(ChatColor.GREEN + "From shop: " + ChatColor.WHITE + shopPriced); + sender.sendMessage(ChatColor.YELLOW + "From custom/fallback: " + ChatColor.WHITE + customPriced); + sender.sendMessage(ChatColor.RED + "Not configured: " + ChatColor.WHITE + notConfigured); + + if (notConfigured > 0) { + sender.sendMessage(""); + sender.sendMessage(ChatColor.RED + "" + ChatColor.ITALIC + + "⚠ " + notConfigured + " item(s) have no price. Add them to item_prices.yml or your shop."); + } + + return 1; + } + + /** + * Determines exactly which source provided the final price for a material, + * respecting the configured price_source_mode. + */ + private PriceSource resolveSource(ItemPriceManager pm, Material material) { + PriceSourceMode mode = pm.getPriceSourceMode(); + double shopPrice = pm.getShopPriceFor(material); + double customPrice = pm.getCustomPriceFor(material); + + return switch (mode) { + case SHOP_ONLY -> shopPrice > 0 ? PriceSource.SHOP : PriceSource.NONE; + case CUSTOM_ONLY -> customPrice > 0 ? PriceSource.CUSTOM : PriceSource.NONE; + case SHOP_PRIORITY -> { + if (shopPrice > 0) yield PriceSource.SHOP; + if (customPrice > 0) yield PriceSource.FALLBACK; + yield PriceSource.NONE; + } + case CUSTOM_PRIORITY -> { + if (customPrice > 0) yield PriceSource.CUSTOM; + if (shopPrice > 0) yield PriceSource.FALLBACK; + yield PriceSource.NONE; + } + }; + } + + private String getActiveSourceLabel(ItemPriceManager pm) { + boolean hasShop = pm.getShopIntegrationManager() != null && pm.getShopIntegrationManager().hasActiveProvider(); + boolean hasCustom = pm.customPricesEnabled; + + if (hasShop && hasCustom) return "Shop + Custom"; + if (hasShop) return pm.getShopIntegrationManager().getActiveShopPlugin(); + if (hasCustom) return "Custom prices"; + return "None"; + } + + private String formatEntityName(EntityType type) { + return Arrays.stream(type.name().split("_")) + .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) + .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); + } + + private String formatMaterialName(Material material) { + return Arrays.stream(material.name().split("_")) + .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) + .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); + } + + private enum PriceSource { SHOP, CUSTOM, FALLBACK, NONE } +} diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java index f003e347..86288e84 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java @@ -143,6 +143,20 @@ private void loadPrices() { } } + public PriceSourceMode getPriceSourceMode() { + return priceSourceMode; + } + + /** Raw shop price for this material (0 if unavailable). Used by price analysis. */ + public double getShopPriceFor(Material material) { + return getShopPrice(material); + } + + /** Raw custom price for this material (0 if not configured). Used by price analysis. */ + public double getCustomPriceFor(Material material) { + return getCustomPrice(material); + } + public double getPrice(Material material) { if (material == null || !economyEnabled) return 0.0; diff --git a/core/src/main/resources/paper-plugin.yml b/core/src/main/resources/paper-plugin.yml index 3a352647..ddc9703b 100644 --- a/core/src/main/resources/paper-plugin.yml +++ b/core/src/main/resources/paper-plugin.yml @@ -161,6 +161,10 @@ permissions: description: "Allow viewing sell prices of spawner items" default: true + smartspawner.command.priceanalysis: + description: "Allow viewing price analysis across all spawners" + default: op + smartspawner.command.clear: description: "Allow clearing holograms and ghost spawners" default: op From 6c35c85134bc8860b04b2c03fccb09a4f5c82444 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 13:57:19 +0530 Subject: [PATCH 6/8] Add optional spawner type argument with tab completion to priceanalysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /ss priceanalysis — all spawners (existing behavior) /ss priceanalysis — autocompletes only entity types that have active spawners loaded on the server /ss priceanalysis pig — individual lookup for that spawner type Tab completion is dynamic: queries actual loaded spawners so it only suggests types that exist, not the full EntityType enum. --- .../PriceAnalysisSubCommand.java | 157 +++++++++++++----- 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java index ee67d679..35fba3c0 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java @@ -1,6 +1,9 @@ package github.nighter.smartspawner.commands.priceanalysis; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.commands.BaseSubCommand; import github.nighter.smartspawner.hooks.economy.ItemPriceManager; @@ -9,6 +12,7 @@ import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; import github.nighter.smartspawner.spawner.properties.SpawnerData; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.entity.EntityType; @@ -16,44 +20,98 @@ import org.jspecify.annotations.NullMarked; import java.util.*; +import java.util.stream.Collectors; @NullMarked public class PriceAnalysisSubCommand extends BaseSubCommand { + private static final String ARG_SPAWNER_TYPE = "spawnerType"; + public PriceAnalysisSubCommand(SmartSpawner plugin) { super(plugin); } @Override - public String getName() { - return "priceanalysis"; - } + public String getName() { return "priceanalysis"; } @Override - public String getPermission() { - return "smartspawner.command.priceanalysis"; - } + public String getPermission() { return "smartspawner.command.priceanalysis"; } + + @Override + public String getDescription() { return "Show price analysis for sellable items across spawners"; } + // Override build() to wire optional spawner type argument with tab completion @Override - public String getDescription() { - return "Show price analysis for all sellable items across all spawners"; + public LiteralArgumentBuilder build() { + LiteralArgumentBuilder builder = Commands.literal(getName()); + builder.requires(source -> hasPermission(source.getSender())); + + // /ss priceanalysis — all spawners + builder.executes(context -> { + logCommandExecution(context); + return runAnalysis(context, null); + }); + + // /ss priceanalysis — specific spawner type + builder.then(Commands.argument(ARG_SPAWNER_TYPE, StringArgumentType.word()) + .suggests(createSpawnerTypeSuggestions()) + .executes(context -> { + logCommandExecution(context); + String arg = StringArgumentType.getString(context, ARG_SPAWNER_TYPE); + return runAnalysis(context, arg.toUpperCase()); + }) + ); + + return builder; } @Override public int execute(CommandContext context) { - CommandSender sender = context.getSource().getSender(); + return runAnalysis(context, null); + } + + /** + * Suggests entity types that actually have spawners on this server. + */ + private SuggestionProvider createSpawnerTypeSuggestions() { + return (context, builder) -> { + String input = builder.getRemaining().toLowerCase(); + getActiveSpawnerTypes().stream() + .map(e -> e.name().toLowerCase()) + .filter(name -> name.startsWith(input)) + .sorted() + .forEach(builder::suggest); + return builder.buildFuture(); + }; + } + + /** + * Returns the set of EntityTypes that have at least one spawner loaded. + */ + private Set getActiveSpawnerTypes() { + Set types = new LinkedHashSet<>(); + for (SpawnerData spawner : plugin.getSpawnerManager().getAllSpawners()) { + if (!spawner.getValidLootItems().isEmpty()) { + types.add(spawner.getEntityType()); + } + } + return types; + } + private int runAnalysis(CommandContext context, String filterType) { + CommandSender sender = context.getSource().getSender(); ItemPriceManager priceManager = plugin.getItemPriceManager(); SpawnerManager spawnerManager = plugin.getSpawnerManager(); - // Collect unique materials across all spawners, grouped by spawner type - // Map: EntityType -> Set + // Build map: EntityType -> Set Map> materialsByType = new LinkedHashMap<>(); - for (SpawnerData spawner : spawnerManager.getAllSpawners()) { EntityType type = spawner.getEntityType(); - List lootItems = spawner.getValidLootItems(); + // Filter to specific type if argument provided + if (filterType != null && !type.name().equalsIgnoreCase(filterType)) continue; + + List lootItems = spawner.getValidLootItems(); if (lootItems.isEmpty()) continue; Set materials = materialsByType.computeIfAbsent(type, k -> new LinkedHashSet<>()); @@ -63,32 +121,38 @@ public int execute(CommandContext context) { } if (materialsByType.isEmpty()) { - sender.sendMessage(ChatColor.RED + "No spawners found or no sellable items configured."); + if (filterType != null) { + sender.sendMessage(ChatColor.RED + "No spawners found for type: " + ChatColor.YELLOW + filterType + + ChatColor.RED + ". Use /ss priceanalysis for the full list."); + } else { + sender.sendMessage(ChatColor.RED + "No spawners found or no sellable items configured."); + } return 0; } - // --- Header --- - sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ SmartSpawner Price Analysis ════"); + // Header + String scope = filterType != null + ? formatEntityName(filterType) + : "All Spawners"; + sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ Price Analysis: " + scope + " ════"); sender.sendMessage(ChatColor.GRAY + "Mode: " + ChatColor.YELLOW + priceManager.getPriceSourceMode().name() + ChatColor.GRAY + " Source: " + ChatColor.AQUA + getActiveSourceLabel(priceManager)); sender.sendMessage(""); - int totalItems = 0; - int shopPriced = 0; - int customPriced = 0; - int notConfigured = 0; + int totalItems = 0, shopPriced = 0, customPriced = 0, notConfigured = 0; - // --- Per spawner type breakdown --- for (Map.Entry> entry : materialsByType.entrySet()) { - EntityType entityType = entry.getKey(); Set materials = entry.getValue(); - sender.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD - + formatEntityName(entityType) + ChatColor.GRAY + " (" + materials.size() + " items)"); + // Show spawner type header only in "all" mode (single type is already in the title) + if (filterType == null) { + sender.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD + + formatEntityName(entry.getKey().name()) + + ChatColor.GRAY + " (" + materials.size() + " items)"); + } for (Material material : materials) { totalItems++; - double finalPrice = priceManager.getPrice(material); PriceSource source = resolveSource(priceManager, material); @@ -123,18 +187,20 @@ public int execute(CommandContext context) { } } - sender.sendMessage(ChatColor.GRAY + " " + lineColor + formatMaterialName(material) + String indent = filterType != null ? "" : " "; + sender.sendMessage(ChatColor.GRAY + indent + lineColor + formatMaterialName(material) + ChatColor.DARK_GRAY + " → " + priceStr + " " + sourceLabel); } - sender.sendMessage(""); + if (filterType == null) sender.sendMessage(""); } - // --- Summary --- + // Summary + sender.sendMessage(""); sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ Summary ════"); - sender.sendMessage(ChatColor.GRAY + "Total items: " + ChatColor.WHITE + totalItems); - sender.sendMessage(ChatColor.GREEN + "From shop: " + ChatColor.WHITE + shopPriced); - sender.sendMessage(ChatColor.YELLOW + "From custom/fallback: " + ChatColor.WHITE + customPriced); - sender.sendMessage(ChatColor.RED + "Not configured: " + ChatColor.WHITE + notConfigured); + sender.sendMessage(ChatColor.GRAY + "Total items: " + ChatColor.WHITE + totalItems); + sender.sendMessage(ChatColor.GREEN + "From shop: " + ChatColor.WHITE + shopPriced); + sender.sendMessage(ChatColor.YELLOW + "From custom/fallback: " + ChatColor.WHITE + customPriced); + sender.sendMessage(ChatColor.RED + "Not configured: " + ChatColor.WHITE + notConfigured); if (notConfigured > 0) { sender.sendMessage(""); @@ -145,26 +211,22 @@ public int execute(CommandContext context) { return 1; } - /** - * Determines exactly which source provided the final price for a material, - * respecting the configured price_source_mode. - */ private PriceSource resolveSource(ItemPriceManager pm, Material material) { PriceSourceMode mode = pm.getPriceSourceMode(); - double shopPrice = pm.getShopPriceFor(material); + double shopPrice = pm.getShopPriceFor(material); double customPrice = pm.getCustomPriceFor(material); return switch (mode) { - case SHOP_ONLY -> shopPrice > 0 ? PriceSource.SHOP : PriceSource.NONE; - case CUSTOM_ONLY -> customPrice > 0 ? PriceSource.CUSTOM : PriceSource.NONE; + case SHOP_ONLY -> shopPrice > 0 ? PriceSource.SHOP : PriceSource.NONE; + case CUSTOM_ONLY -> customPrice > 0 ? PriceSource.CUSTOM : PriceSource.NONE; case SHOP_PRIORITY -> { - if (shopPrice > 0) yield PriceSource.SHOP; + if (shopPrice > 0) yield PriceSource.SHOP; if (customPrice > 0) yield PriceSource.FALLBACK; yield PriceSource.NONE; } case CUSTOM_PRIORITY -> { if (customPrice > 0) yield PriceSource.CUSTOM; - if (shopPrice > 0) yield PriceSource.FALLBACK; + if (shopPrice > 0) yield PriceSource.FALLBACK; yield PriceSource.NONE; } }; @@ -173,23 +235,26 @@ private PriceSource resolveSource(ItemPriceManager pm, Material material) { private String getActiveSourceLabel(ItemPriceManager pm) { boolean hasShop = pm.getShopIntegrationManager() != null && pm.getShopIntegrationManager().hasActiveProvider(); boolean hasCustom = pm.customPricesEnabled; - if (hasShop && hasCustom) return "Shop + Custom"; if (hasShop) return pm.getShopIntegrationManager().getActiveShopPlugin(); if (hasCustom) return "Custom prices"; return "None"; } - private String formatEntityName(EntityType type) { - return Arrays.stream(type.name().split("_")) + private String formatEntityName(String name) { + return Arrays.stream(name.split("_")) .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) - .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); + .collect(Collectors.joining(" ")); + } + + private String formatEntityName(EntityType type) { + return formatEntityName(type.name()); } private String formatMaterialName(Material material) { return Arrays.stream(material.name().split("_")) .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) - .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); + .collect(Collectors.joining(" ")); } private enum PriceSource { SHOP, CUSTOM, FALLBACK, NONE } From 5d389905e2adbdc039de35788f61693e61bfec84 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 14:01:12 +0530 Subject: [PATCH 7/8] Fix priceanalysis warning: make hint mode-aware SHOP_ONLY -> only mention shop plugin CUSTOM_ONLY -> only mention item_prices.yml SHOP_PRIORITY / CUSTOM_PRIORITY -> mention both with correct preference order --- .../priceanalysis/PriceAnalysisSubCommand.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java index 35fba3c0..bf6274cf 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java @@ -205,12 +205,21 @@ private int runAnalysis(CommandContext context, String filte if (notConfigured > 0) { sender.sendMessage(""); sender.sendMessage(ChatColor.RED + "" + ChatColor.ITALIC - + "⚠ " + notConfigured + " item(s) have no price. Add them to item_prices.yml or your shop."); + + "⚠ " + notConfigured + " item(s) have no price. " + getMissingPriceHint(priceManager.getPriceSourceMode())); } return 1; } + private String getMissingPriceHint(PriceSourceMode mode) { + return switch (mode) { + case SHOP_ONLY -> "Add them to your shop plugin — custom prices are ignored in SHOP_ONLY mode."; + case CUSTOM_ONLY -> "Add them to item_prices.yml — shop prices are ignored in CUSTOM_ONLY mode."; + case SHOP_PRIORITY -> "Add them to your shop plugin (preferred) or item_prices.yml as fallback."; + case CUSTOM_PRIORITY -> "Add them to item_prices.yml (preferred) or your shop plugin as fallback."; + }; + } + private PriceSource resolveSource(ItemPriceManager pm, Material material) { PriceSourceMode mode = pm.getPriceSourceMode(); double shopPrice = pm.getShopPriceFor(material); From df9c047bc40f94124a133a2cad8d28e07a03ebc5 Mon Sep 17 00:00:00 2001 From: Jayesh Kambli Date: Wed, 3 Jun 2026 17:31:30 +0530 Subject: [PATCH 8/8] Remove priceanalysis command (kept ExcellentShop integration only) --- .../smartspawner/commands/MainCommand.java | 4 +- .../PriceAnalysisSubCommand.java | 270 ------------------ .../hooks/economy/ItemPriceManager.java | 14 - core/src/main/resources/paper-plugin.yml | 4 - 4 files changed, 1 insertion(+), 291 deletions(-) delete mode 100644 core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java diff --git a/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java index 77543ce7..2199435e 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/MainCommand.java @@ -9,7 +9,6 @@ import github.nighter.smartspawner.commands.list.ListSubCommand; import github.nighter.smartspawner.commands.near.NearSubCommand; import github.nighter.smartspawner.commands.prices.PricesSubCommand; -import github.nighter.smartspawner.commands.priceanalysis.PriceAnalysisSubCommand; import github.nighter.smartspawner.commands.reload.ReloadSubCommand; import github.nighter.smartspawner.commands.set.SetSubCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; @@ -37,8 +36,7 @@ public MainCommand(SmartSpawner plugin) { new PricesSubCommand(plugin), new ClearSubCommand(plugin), new NearSubCommand(plugin, plugin.getSpawnerHighlightManager()), - new SetSubCommand(plugin), - new PriceAnalysisSubCommand(plugin) + new SetSubCommand(plugin) ); } diff --git a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java deleted file mode 100644 index bf6274cf..00000000 --- a/core/src/main/java/github/nighter/smartspawner/commands/priceanalysis/PriceAnalysisSubCommand.java +++ /dev/null @@ -1,270 +0,0 @@ -package github.nighter.smartspawner.commands.priceanalysis; - -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.brigadier.suggestion.SuggestionProvider; -import github.nighter.smartspawner.SmartSpawner; -import github.nighter.smartspawner.commands.BaseSubCommand; -import github.nighter.smartspawner.hooks.economy.ItemPriceManager; -import github.nighter.smartspawner.hooks.economy.ItemPriceManager.PriceSourceMode; -import github.nighter.smartspawner.spawner.data.SpawnerManager; -import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; -import github.nighter.smartspawner.spawner.properties.SpawnerData; -import io.papermc.paper.command.brigadier.CommandSourceStack; -import io.papermc.paper.command.brigadier.Commands; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.entity.EntityType; -import org.bukkit.command.CommandSender; -import org.jspecify.annotations.NullMarked; - -import java.util.*; -import java.util.stream.Collectors; - -@NullMarked -public class PriceAnalysisSubCommand extends BaseSubCommand { - - private static final String ARG_SPAWNER_TYPE = "spawnerType"; - - public PriceAnalysisSubCommand(SmartSpawner plugin) { - super(plugin); - } - - @Override - public String getName() { return "priceanalysis"; } - - @Override - public String getPermission() { return "smartspawner.command.priceanalysis"; } - - @Override - public String getDescription() { return "Show price analysis for sellable items across spawners"; } - - // Override build() to wire optional spawner type argument with tab completion - @Override - public LiteralArgumentBuilder build() { - LiteralArgumentBuilder builder = Commands.literal(getName()); - builder.requires(source -> hasPermission(source.getSender())); - - // /ss priceanalysis — all spawners - builder.executes(context -> { - logCommandExecution(context); - return runAnalysis(context, null); - }); - - // /ss priceanalysis — specific spawner type - builder.then(Commands.argument(ARG_SPAWNER_TYPE, StringArgumentType.word()) - .suggests(createSpawnerTypeSuggestions()) - .executes(context -> { - logCommandExecution(context); - String arg = StringArgumentType.getString(context, ARG_SPAWNER_TYPE); - return runAnalysis(context, arg.toUpperCase()); - }) - ); - - return builder; - } - - @Override - public int execute(CommandContext context) { - return runAnalysis(context, null); - } - - /** - * Suggests entity types that actually have spawners on this server. - */ - private SuggestionProvider createSpawnerTypeSuggestions() { - return (context, builder) -> { - String input = builder.getRemaining().toLowerCase(); - getActiveSpawnerTypes().stream() - .map(e -> e.name().toLowerCase()) - .filter(name -> name.startsWith(input)) - .sorted() - .forEach(builder::suggest); - return builder.buildFuture(); - }; - } - - /** - * Returns the set of EntityTypes that have at least one spawner loaded. - */ - private Set getActiveSpawnerTypes() { - Set types = new LinkedHashSet<>(); - for (SpawnerData spawner : plugin.getSpawnerManager().getAllSpawners()) { - if (!spawner.getValidLootItems().isEmpty()) { - types.add(spawner.getEntityType()); - } - } - return types; - } - - private int runAnalysis(CommandContext context, String filterType) { - CommandSender sender = context.getSource().getSender(); - ItemPriceManager priceManager = plugin.getItemPriceManager(); - SpawnerManager spawnerManager = plugin.getSpawnerManager(); - - // Build map: EntityType -> Set - Map> materialsByType = new LinkedHashMap<>(); - for (SpawnerData spawner : spawnerManager.getAllSpawners()) { - EntityType type = spawner.getEntityType(); - - // Filter to specific type if argument provided - if (filterType != null && !type.name().equalsIgnoreCase(filterType)) continue; - - List lootItems = spawner.getValidLootItems(); - if (lootItems.isEmpty()) continue; - - Set materials = materialsByType.computeIfAbsent(type, k -> new LinkedHashSet<>()); - for (LootItem loot : lootItems) { - materials.add(loot.material()); - } - } - - if (materialsByType.isEmpty()) { - if (filterType != null) { - sender.sendMessage(ChatColor.RED + "No spawners found for type: " + ChatColor.YELLOW + filterType - + ChatColor.RED + ". Use /ss priceanalysis for the full list."); - } else { - sender.sendMessage(ChatColor.RED + "No spawners found or no sellable items configured."); - } - return 0; - } - - // Header - String scope = filterType != null - ? formatEntityName(filterType) - : "All Spawners"; - sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ Price Analysis: " + scope + " ════"); - sender.sendMessage(ChatColor.GRAY + "Mode: " + ChatColor.YELLOW + priceManager.getPriceSourceMode().name() - + ChatColor.GRAY + " Source: " + ChatColor.AQUA + getActiveSourceLabel(priceManager)); - sender.sendMessage(""); - - int totalItems = 0, shopPriced = 0, customPriced = 0, notConfigured = 0; - - for (Map.Entry> entry : materialsByType.entrySet()) { - Set materials = entry.getValue(); - - // Show spawner type header only in "all" mode (single type is already in the title) - if (filterType == null) { - sender.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD - + formatEntityName(entry.getKey().name()) - + ChatColor.GRAY + " (" + materials.size() + " items)"); - } - - for (Material material : materials) { - totalItems++; - double finalPrice = priceManager.getPrice(material); - PriceSource source = resolveSource(priceManager, material); - - String priceStr; - String sourceLabel; - ChatColor lineColor; - - switch (source) { - case SHOP -> { - shopPriced++; - lineColor = ChatColor.GREEN; - sourceLabel = ChatColor.GREEN + "[Shop]"; - priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); - } - case CUSTOM -> { - customPriced++; - lineColor = ChatColor.YELLOW; - sourceLabel = ChatColor.YELLOW + "[Custom]"; - priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); - } - case FALLBACK -> { - customPriced++; - lineColor = ChatColor.YELLOW; - sourceLabel = ChatColor.YELLOW + "[Fallback]"; - priceStr = ChatColor.WHITE + String.format("$%.2f", finalPrice); - } - default -> { - notConfigured++; - lineColor = ChatColor.RED; - sourceLabel = ChatColor.RED + "[Not Configured]"; - priceStr = ChatColor.RED + "N/A"; - } - } - - String indent = filterType != null ? "" : " "; - sender.sendMessage(ChatColor.GRAY + indent + lineColor + formatMaterialName(material) - + ChatColor.DARK_GRAY + " → " + priceStr + " " + sourceLabel); - } - if (filterType == null) sender.sendMessage(""); - } - - // Summary - sender.sendMessage(""); - sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "════ Summary ════"); - sender.sendMessage(ChatColor.GRAY + "Total items: " + ChatColor.WHITE + totalItems); - sender.sendMessage(ChatColor.GREEN + "From shop: " + ChatColor.WHITE + shopPriced); - sender.sendMessage(ChatColor.YELLOW + "From custom/fallback: " + ChatColor.WHITE + customPriced); - sender.sendMessage(ChatColor.RED + "Not configured: " + ChatColor.WHITE + notConfigured); - - if (notConfigured > 0) { - sender.sendMessage(""); - sender.sendMessage(ChatColor.RED + "" + ChatColor.ITALIC - + "⚠ " + notConfigured + " item(s) have no price. " + getMissingPriceHint(priceManager.getPriceSourceMode())); - } - - return 1; - } - - private String getMissingPriceHint(PriceSourceMode mode) { - return switch (mode) { - case SHOP_ONLY -> "Add them to your shop plugin — custom prices are ignored in SHOP_ONLY mode."; - case CUSTOM_ONLY -> "Add them to item_prices.yml — shop prices are ignored in CUSTOM_ONLY mode."; - case SHOP_PRIORITY -> "Add them to your shop plugin (preferred) or item_prices.yml as fallback."; - case CUSTOM_PRIORITY -> "Add them to item_prices.yml (preferred) or your shop plugin as fallback."; - }; - } - - private PriceSource resolveSource(ItemPriceManager pm, Material material) { - PriceSourceMode mode = pm.getPriceSourceMode(); - double shopPrice = pm.getShopPriceFor(material); - double customPrice = pm.getCustomPriceFor(material); - - return switch (mode) { - case SHOP_ONLY -> shopPrice > 0 ? PriceSource.SHOP : PriceSource.NONE; - case CUSTOM_ONLY -> customPrice > 0 ? PriceSource.CUSTOM : PriceSource.NONE; - case SHOP_PRIORITY -> { - if (shopPrice > 0) yield PriceSource.SHOP; - if (customPrice > 0) yield PriceSource.FALLBACK; - yield PriceSource.NONE; - } - case CUSTOM_PRIORITY -> { - if (customPrice > 0) yield PriceSource.CUSTOM; - if (shopPrice > 0) yield PriceSource.FALLBACK; - yield PriceSource.NONE; - } - }; - } - - private String getActiveSourceLabel(ItemPriceManager pm) { - boolean hasShop = pm.getShopIntegrationManager() != null && pm.getShopIntegrationManager().hasActiveProvider(); - boolean hasCustom = pm.customPricesEnabled; - if (hasShop && hasCustom) return "Shop + Custom"; - if (hasShop) return pm.getShopIntegrationManager().getActiveShopPlugin(); - if (hasCustom) return "Custom prices"; - return "None"; - } - - private String formatEntityName(String name) { - return Arrays.stream(name.split("_")) - .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) - .collect(Collectors.joining(" ")); - } - - private String formatEntityName(EntityType type) { - return formatEntityName(type.name()); - } - - private String formatMaterialName(Material material) { - return Arrays.stream(material.name().split("_")) - .map(w -> w.charAt(0) + w.substring(1).toLowerCase()) - .collect(Collectors.joining(" ")); - } - - private enum PriceSource { SHOP, CUSTOM, FALLBACK, NONE } -} diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java index 86288e84..f003e347 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/ItemPriceManager.java @@ -143,20 +143,6 @@ private void loadPrices() { } } - public PriceSourceMode getPriceSourceMode() { - return priceSourceMode; - } - - /** Raw shop price for this material (0 if unavailable). Used by price analysis. */ - public double getShopPriceFor(Material material) { - return getShopPrice(material); - } - - /** Raw custom price for this material (0 if not configured). Used by price analysis. */ - public double getCustomPriceFor(Material material) { - return getCustomPrice(material); - } - public double getPrice(Material material) { if (material == null || !economyEnabled) return 0.0; diff --git a/core/src/main/resources/paper-plugin.yml b/core/src/main/resources/paper-plugin.yml index ddc9703b..3a352647 100644 --- a/core/src/main/resources/paper-plugin.yml +++ b/core/src/main/resources/paper-plugin.yml @@ -161,10 +161,6 @@ permissions: description: "Allow viewing sell prices of spawner items" default: true - smartspawner.command.priceanalysis: - description: "Allow viewing price analysis across all spawners" - default: op - smartspawner.command.clear: description: "Allow clearing holograms and ghost spawners" default: op